From 0ac16fbe142c08f884861109b93ebe5a138f6675 Mon Sep 17 00:00:00 2001 From: xanimo Date: Tue, 4 Aug 2026 20:08:03 +0000 Subject: [PATCH] chainparams, test: store pow_limit in internal byte order 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. --- src/chainparams.c | 6 +++--- test/block_tests.c | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/chainparams.c b/src/chainparams.c index d7183195e..1c0d48743 100644 --- a/src/chainparams.c +++ b/src/chainparams.c @@ -44,7 +44,7 @@ const dogecoin_chainparams dogecoin_chainparams_main = { {{"seed.multidoge.org"}, {{"seed2.multidoge.org"}}}, true, 0x0062, - {0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, // pow limit + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x00, 0x00}, // pow limit 0x00000fffff..ff, internal byte order {0x9b, 0xa4, 0x46, 0xf2, 0x6c, 0xa8, 0x2a, 0x3d, 0x99, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} }; @@ -63,7 +63,7 @@ const dogecoin_chainparams dogecoin_chainparams_test = { {{"testseed.jrn.me.uk"}, {{0}}}, false, 0x0062, - {0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, // pow limit + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x00, 0x00}, // pow limit 0x00000fffff..ff, internal byte order {0x26, 0x9a, 0xff, 0x62, 0x2f, 0x0f, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} }; @@ -82,7 +82,7 @@ const dogecoin_chainparams dogecoin_chainparams_regtest = { {{"testseed.jrn.me.uk"}, {{0}}}, true, 0x0062, - {0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, // pow limit + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, // pow limit {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} }; diff --git a/test/block_tests.c b/test/block_tests.c index 3d27c9e76..fe9d81075 100644 --- a/test/block_tests.c +++ b/test/block_tests.c @@ -78,6 +78,57 @@ static void test_check_merkle_branch() vector_free(merkle_branch, true); } +/* check_pow() must reject a target above the chain's pow_limit. This is the + fourth rejection condition in check_pow(), and it is the only one that + depends on the byte order of dogecoin_chainparams.pow_limit. + + uint256_cmp() treats index 31 as most significant, i.e. it reads its operands + as internal (little-endian) byte arrays, and the target it is handed comes + from arith_uint256.pn[], which is little-endian. Every other uint256_t in + chainparams -- genesisblockhash, genesisblockchainwork, minimumchainwork -- + is stored in that same internal order. pow_limit was stored in display order + instead, so the comparison read it as ~0xffff..ff0f0000, a value no real + target can exceed, and the bound never rejected anything. + + nbits 0x1e100000 decodes to 0x0000100000..00, which is above the mainnet + pow_limit of 0x00000fffff..ff by one byte at index 2. The hash is all zeroes + so it clears the hash-vs-target comparison that follows; the pow_limit bound + is therefore the only thing in check_pow() that can reject this input, and a + true return means the bound is not being enforced. */ +void test_check_pow_limit_bound() +{ + uint256_t hash = {0}; + arith_uint256 chainwork = {0}; + + /* above the limit: must be rejected */ + u_assert_int_eq(check_pow(&hash, 0x1e100000, &dogecoin_chainparams_main, &chainwork), 0); + + /* regtest carries a different limit (0x7fffff..ff); the same target is well + under it, so this must still be accepted -- the bound is chain-specific, + not a blanket rejection. */ + memset(hash, 0, sizeof(hash)); + u_assert_int_eq(check_pow(&hash, 0x1e100000, &dogecoin_chainparams_regtest, &chainwork), 1); + + /* at the limit exactly: mainnet pow_limit is 0x00000fffff..ff, and nbits + 0x1e0fffff decodes to 0x00000fffff00..00, the largest representable target + that does not exceed it. Must be accepted. */ + memset(hash, 0, sizeof(hash)); + u_assert_int_eq(check_pow(&hash, 0x1e0fffff, &dogecoin_chainparams_main, &chainwork), 1); + + /* Negative control for the zero hash. The cases above all pass an all-zero + hash, which clears the hash-vs-target comparison so that the pow_limit + bound is the only rejection path left. That is what makes them a clean + test of the bound -- but on its own it does not show the hash comparison + is still live, so an "accepted" result could mean the zero hash bypassed + something rather than that the target was in range. + + Same chain and same nbits as the accepted case above, with a hash above + the target: this must be rejected. If it is not, the acceptances above are + not evidence about pow_limit at all. */ + memset(hash, 0xff, sizeof(hash)); + u_assert_int_eq(check_pow(&hash, 0x1e0fffff, &dogecoin_chainparams_main, &chainwork), 0); +} + void test_block_header() { size_t outlen; @@ -223,6 +274,7 @@ void test_block_header() test_auxpow_deserialize_real_vector(); test_auxpow_deserialize_e2e(); test_auxpow_deserialize_merkle_count_bounds(); + test_check_pow_limit_bound(); } /* Real mainnet auxpow block: height 371338, hash