Skip to content

chainparams, test: store pow_limit in internal byte order - #398

Open
xanimo wants to merge 1 commit into
dogecoinfoundation:0.1.5-devfrom
xanimo:0.1.5-dev-pow-limit-byte-order
Open

chainparams, test: store pow_limit in internal byte order#398
xanimo wants to merge 1 commit into
dogecoinfoundation:0.1.5-devfrom
xanimo:0.1.5-dev-pow-limit-byte-order

Conversation

@xanimo

@xanimo xanimo commented Aug 4, 2026

Copy link
Copy Markdown
Member

Found while investigating the log output @edtubbs flagged (see #397). The
diagnostic fix there is what made this visible: once above_pow_limit could
actually be reported, it became clear it was never firing.

pow_limit was the only 32-byte chainparams field stored big-endian.
genesis_hash, genesis_chainwork and minimumchainwork are all little-endian
(internal) order. check_pow compares against it in internal order, so the
comparison was against a byte-reversed value and the bound never fired — the
check was dead.

Verified against Core

Each new value reverses to exactly what Core sets in chainparams.cpp:

chain Core line
main 0x00000fffffffffff…ff 88
test 0x00000fffffffffff…ff 248
regtest 0x7fffffffffffffff…ff 392

The guard is discriminating

test_check_pow_limit_bound() fails at line 104 on the parent commit and
passes here. It pins from both sides rather than only asserting the happy path:

  • an above-limit target is rejected on mainnet
  • the same target is accepted on regtest, where the limit is higher — so the
    test would catch a fix that simply rejected everything
  • 0x1e0fffff, the largest non-exceeding mainnet target, is accepted

Consensus-visible, and worth saying plainly

This takes the bound from dead to live. No chain has ever had a target above
powLimit, so nothing real is affected — genesis bits 0x1e0ffff0 decodes
below the limit either way, and the passing-test set is identical to baseline.
But it is a validation behaviour change and should be reviewed as one.

Kept off #397 deliberately so the diagnostic fix is not blocked behind this
review. They do not conflict: #397 touches pow.c and validation.c, this
touches chainparams.c and block_tests.c.

78/78.

Left alone, worth separate issues

  • check_pow calls swap_bytes on the caller's hash in place and does not
    swap back on the success path, so the caller's buffer is silently mutated.
    Harmless today because every call site passes a local, but a landmine for
    anyone reusing a hash across calls.
  • The strerror(errno) pattern in block.c's deserialize paths — same defect as
    pow, validation: report the condition that actually failed #397 fixed in check_auxpow, larger diff.

@xanimo

xanimo commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Amended: added a negative control for the zero hash.

Every case in the test passed an all-zero hash, which clears the hash-vs-target comparison so the pow_limit bound is the only rejection path left. That is what makes the cases a clean test of the bound — but on its own it does not demonstrate the hash comparison is still live, so an "accepted" result could have meant the zero hash bypassed something rather than that the target was in range.

The new arm uses the same chain and the same nbits as the accepted case, with a hash above the target, and requires rejection. If that ever passes, the acceptances above stop being evidence about pow_limit.

Worth noting what the test already did, since it is stronger than it looks:

  • the above-limit and regtest cases are a controlled experiment — identical nbits, identical hash, only the chain differs — so pow_limit is the only variable
  • 0x1e0fffff and 0x1e100000 are the tightest adjacent mantissa pair at that exponent, so it pins the exact boundary rather than a value comfortably either side
  • the regtest arm catches a regression that rejects everything; the at-limit arm catches one that rejects too much

Re-confirmed that the whole test still fails at line 104 against the old byte order, so the new arm has not masked the original guard.

78/78.

@edtubbs edtubbs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK, great catch and test to prove the check.

check_pow() rejects a target above the chain's pow_limit via
uint256_cmp(target_bytes, params->pow_limit). uint256_cmp() scans from
index 31 down and returns on the first differing byte, so it reads its
operands as internal (little-endian) byte arrays, and target_bytes comes
from arith_uint256.pn[], which is little-endian.

pow_limit was stored in display order. Mainnet held

  {0x00, 0x00, 0x0f, 0xff, ... 0xff}

which uint256_cmp() read as 0xffffff..ff0f0000 -- within one byte of the
256-bit maximum. No representable target exceeds that, so the comparison
was false for every input and the bound rejected nothing. Testnet carried
the same literal and regtest the equivalent 0x7f-prefixed one.

This is the only 32-byte field in dogecoin_chainparams stored that way.
genesisblockhash, genesisblockchainwork and minimumchainwork are all in
internal order; reversing genesisblockhash yields the value Core asserts
in chainparams.cpp, and pow_limit was the outlier.

Reverse the three literals. Each now reverses to the value Core sets for
the corresponding chain:

  main/test  0x00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
  regtest    0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

This changes validation behavior: a header whose nBits decode above the
chain's pow_limit is now rejected rather than accepted. No block on any
chain has such a target, and the existing suite is unaffected -- the
genesis chainwork vector and the spv header chains decode well under the
limit and pass unchanged.

Add test_check_pow_limit_bound() to pin it. nbits 0x1e100000 decodes to
0x0000100000..00, one byte above the mainnet limit, and is paired with a
zero hash so it clears the hash-vs-target comparison that follows: the
pow_limit bound is the only condition left that can reject it, and a true
return means the bound is not enforced. The test fails on the parent
commit and passes here. It also asserts the same target is accepted on
regtest, whose limit is higher, and that 0x1e0fffff -- the largest target
that does not exceed the mainnet limit -- is accepted, so the bound is
pinned from both sides rather than as a blanket rejection.
@xanimo
xanimo force-pushed the 0.1.5-dev-pow-limit-byte-order branch from a23cc9d to 0ac16fb Compare August 5, 2026 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants