From 121d84e3ec0a9a66610ad4aab7cef339d0e2bd9f Mon Sep 17 00:00:00 2001 From: bluezr Date: Tue, 4 Aug 2026 15:40:30 -0700 Subject: [PATCH] chainparams: sync the public struct with the internal one dogecoin_chainparams is declared twice by design: once in the public libdogecoin.h so the installed header is self-contained, once in the internal chainparams.h. The objects are defined once, in chainparams.c, against the internal declaration and read through the public one, so the two must describe the same layout. They had drifted. The public copy was missing genesisblockchainwork, strict_id, auxpow_id, pow_limit and minimumchainwork. Only the first of those matters for layout, and it matters a great deal: it sits in the middle rather than at the end, so this was not a harmless truncation. Every field after it landed 32 bytes early. A consumer reading dogecoin_chainparams_main.default_port through the public header read offset 84 while the object had it at 116, returning the low four bytes of the chainwork instead of 22556. dnsseeds shifted with it, so the seed list read into the middle of a hostname. sizeof reported 2136 against an object of 2236, so stack-allocating or copying a dogecoin_chainparams was short by 100 bytes. None of it warns. Adds the missing fields in the order chainparams.c defines them, and a comment on both declarations saying they move together. Adds test/chainparams_abi_tests.c, which includes only libdogecoin.h and reads the exported objects the way a consumer does. Including both headers to compare them is not possible -- C forbids redefining a struct tag even identically -- so the test checks values instead: they agree only if the declaration a consumer sees matches the one the objects were built with. Verified it fails on both drift modes. Removing a field fails at compile time; inserting one the internal copy lacks compiles clean and fails at runtime, with default_port reading 1684366707. The test asserts on minimumchainwork rather than pow_limit deliberately, so that it fails on layout drift only and not on the pow_limit byte order change under separate review. Registered in both build systems. 79/79 under CMake and autotools. --- CMakeLists.txt | 1 + Makefile.am | 1 + include/dogecoin/libdogecoin.h | 13 ++++ test/chainparams_abi_tests.c | 107 +++++++++++++++++++++++++++++++++ test/unittester.c | 2 + 5 files changed, 124 insertions(+) create mode 100644 test/chainparams_abi_tests.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 72dc941aa..075edd488 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -553,6 +553,7 @@ IF(USE_TESTS) test/block_tests.c test/buffer_tests.c test/chacha20_tests.c + test/chainparams_abi_tests.c test/context_tests.c test/cstr_tests.c test/ecc_tests.c diff --git a/Makefile.am b/Makefile.am index 14dc6d8ca..7def25d88 100644 --- a/Makefile.am +++ b/Makefile.am @@ -280,6 +280,7 @@ tests_SOURCES = \ test/block_tests.c \ test/buffer_tests.c \ test/chacha20_tests.c \ + test/chainparams_abi_tests.c \ test/context_tests.c \ test/cstr_tests.c \ test/ecc_tests.c \ diff --git a/include/dogecoin/libdogecoin.h b/include/dogecoin/libdogecoin.h index 89a3ba56b..dc5bc978b 100644 --- a/include/dogecoin/libdogecoin.h +++ b/include/dogecoin/libdogecoin.h @@ -45,6 +45,14 @@ typedef struct dogecoin_dns_seed_ { char domain[256]; } dogecoin_dns_seed; +/* Declared here and again in the internal chainparams.h, so that this header + stays self-contained. The objects are defined once, in chainparams.c, against + the internal copy and read through this one -- so the two must describe the + same layout, field for field and in order. A field added to one and not the + other shifts every field after it with no compile error and no warning; that + is what happened to genesisblockchainwork, which left consumers reading + default_port out of the middle of the chainwork. Change both, in step. + test/chainparams_abi_tests.c fails if they diverge again. */ typedef struct dogecoin_chainparams_ { char chainname[32]; uint8_t b58prefix_pubkey_address; @@ -55,8 +63,13 @@ typedef struct dogecoin_chainparams_ { uint32_t b58prefix_bip32_pubkey; const unsigned char netmagic[4]; uint256_t genesisblockhash; + uint256_t genesisblockchainwork; int default_port; dogecoin_dns_seed dnsseeds[8]; + dogecoin_bool strict_id; + dogecoin_bool auxpow_id; + uint256_t pow_limit; + uint256_t minimumchainwork; } dogecoin_chainparams; typedef struct dogecoin_checkpoint_ { diff --git a/test/chainparams_abi_tests.c b/test/chainparams_abi_tests.c new file mode 100644 index 000000000..c73efc1d2 --- /dev/null +++ b/test/chainparams_abi_tests.c @@ -0,0 +1,107 @@ +/* + + The MIT License (MIT) + + Copyright (c) 2026 bluezr + Copyright (c) 2026 The Dogecoin Foundation + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES + OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#include + +#include + +/* libdogecoin.h is deliberately the only header this file includes. + * + * dogecoin_chainparams is declared twice by design -- once in the public + * libdogecoin.h, once in the internal chainparams.h -- so that the installed + * header stays self-contained and a consumer needs exactly one include. The + * objects (dogecoin_chainparams_main and friends) are defined once, in + * chainparams.c, against the internal declaration and read through the public + * one. + * + * The cost of two declarations is that they can drift, and they did: the public + * copy was missing genesisblockchainwork. That field sits in the middle rather + * than at the end, so it was not a harmless truncation -- every field after it + * landed 32 bytes early. A consumer reading default_port through the public + * header read at offset 84 while the object had it at 116, landing in the first + * four bytes of the chainwork. No compile error, no warning. + * + * Including both headers here to compare them is not possible: C forbids + * redefining a struct tag even identically. So this file does the next best + * thing and reads the exported objects exactly as a consumer does, through the + * public declaration alone. The values below agree only if that declaration + * describes the same layout chainparams.c was compiled with. Drift shifts the + * offsets and these comparisons fail. */ +#include + +void test_chainparams_abi() +{ + /* Fields before the divergence. If these fail the symbol itself is wrong, + not the layout. */ + u_assert_str_eq(dogecoin_chainparams_main.chainname, "main"); + u_assert_int_eq(dogecoin_chainparams_main.b58prefix_pubkey_address, 0x1e); + u_assert_int_eq(dogecoin_chainparams_main.b58prefix_secret_address, 0x9e); + u_assert_uint32_eq(dogecoin_chainparams_main.b58prefix_bip32_pubkey, 0x02facafd); + + /* genesisblockchainwork, the field that was missing. Mainnet's is + 0x00100010 in the low word, stored little-endian. Reading it proves the + public declaration has the field rather than skipping past it. */ + u_assert_int_eq(dogecoin_chainparams_main.genesisblockchainwork[0], 0x10); + u_assert_int_eq(dogecoin_chainparams_main.genesisblockchainwork[2], 0x10); + + /* The field immediately after it. This is the assertion the old layout + failed, and the one a consumer would have hit first. */ + u_assert_int_eq(dogecoin_chainparams_main.default_port, 22556); + u_assert_int_eq(dogecoin_chainparams_test.default_port, 44556); + u_assert_int_eq(dogecoin_chainparams_regtest.default_port, 18332); + + /* Further past the shift: the seed list moves by the same 32 bytes, so the + old layout read into the middle of a hostname. */ + u_assert_str_eq(dogecoin_chainparams_main.dnsseeds[0].domain, "seed.multidoge.org"); + + /* Trailing fields the public header did not declare at all. + + strict_id and auxpow_id are both typed dogecoin_bool, but only the first + is a boolean: auxpow_id carries the AuxPoW chain ID, 0x62 for Dogecoin on + every network. dogecoin_bool is a uint8_t, so a chain ID above 255 would + truncate silently. Dogecoin's does not, and retyping it is a separate + change -- asserted here as the value it actually holds, not as true. */ + u_assert_int_eq(dogecoin_chainparams_main.strict_id, true); + u_assert_int_eq(dogecoin_chainparams_test.strict_id, false); + u_assert_int_eq(dogecoin_chainparams_main.auxpow_id, 0x62); + u_assert_int_eq(dogecoin_chainparams_regtest.auxpow_id, 0x62); + + /* minimumchainwork, the last field in the struct, so reading it correctly + means the whole extent lines up. Deliberately not pow_limit: its byte + order is being changed under separate review, and this test should fail + on layout drift only. */ + u_assert_int_eq(dogecoin_chainparams_main.minimumchainwork[0], 0x9b); + u_assert_int_eq(dogecoin_chainparams_main.minimumchainwork[1], 0xa4); + u_assert_int_eq(dogecoin_chainparams_regtest.minimumchainwork[0], 0x02); + + /* Whole-struct size and the two offsets that moved. A consumer that + stack-allocates or copies a dogecoin_chainparams gets these wrong when the + declaration is short, and the damage is silent. */ + u_assert_uint32_eq((uint32_t)sizeof(dogecoin_chainparams), 2236); + u_assert_uint32_eq((uint32_t)offsetof(dogecoin_chainparams, default_port), 116); + u_assert_uint32_eq((uint32_t)offsetof(dogecoin_chainparams, dnsseeds), 120); +} diff --git a/test/unittester.c b/test/unittester.c index 96dd35fae..34e5e17bb 100644 --- a/test/unittester.c +++ b/test/unittester.c @@ -48,6 +48,7 @@ extern void test_bip44(); extern void test_block_header(); extern void test_buffer(); extern void test_chacha20(); +extern void test_chainparams_abi(); extern void test_cstr(); extern void test_ecc(); extern void test_hash(); @@ -179,6 +180,7 @@ int main() u_run_test(test_block_header); u_run_test(test_buffer); u_run_test(test_chacha20); + u_run_test(test_chainparams_abi); u_run_test(test_cstr); u_run_test(test_ecc); u_run_test(test_hash);