From 0aecfb15f2786cabf67d984f2e1e613d341a3012 Mon Sep 17 00:00:00 2001 From: bluezr Date: Mon, 3 Aug 2026 16:56:53 -0700 Subject: [PATCH 1/3] block: let a header own its AuxPoW proof dogecoin_block_header carried an auxpow member holding only the validation hook -- check, ctx and is -- never the proof itself. The proof was built into a local dogecoin_auxpow_block inside dogecoin_block_header_deserialize and freed at cleanup, so by the time the caller had its header the parent coinbase, merkle branches and parent header were gone. dogecoin_block_header_copy made that worse by looking complete: it copied the three hook fields, so a copied merge-mined header was silently missing its proof with nothing to indicate it. Anything needing the proof after the parse therefore had to re-parse the wire bytes, or retain them separately. That is why BIP152 keeps a header_raw span on dogecoin_compact_block: not because a compact block wants raw bytes, but because the parsed header could not answer for itself. Add dogecoin_auxpow_payload, owned by the header. The deserializer moves the parsed fields onto it and nulls them on the scratch block, so ownership transfers rather than duplicating; _free releases it; _copy deep-copies it. The payload deliberately has no back-pointer to its header, unlike dogecoin_auxpow_block, which owns both its header and its parent_header and frees them. A header holding one of those would own the thing that owns it. The payload owns only parent_header, a plain 80-byte header whose own payload is NULL, so ownership terminates. auxpow.check / auxpow.ctx are untouched. That is a validation hook whose context the caller supplies at call time -- validation.c passes the block directly -- not a reference to this data. dogecoin_block_header_copy assigns the copied payload rather than freeing what dest held. Every other field there is a plain overwrite: the function treats dest as raw memory, and callers pass uninitialised stack headers to it -- net_tests.c does, through dogecoin_block_header_deserialize. Freeing dest->auxpow_payload dereferenced whatever the stack contained, which is a SEGV on eight platforms and the reason ASAN caught this and a local build did not: every local caller happened to use dogecoin_block_header_new, where the pointer is NULL. A dest that already owns a payload is the caller's to release, as with every other member. The test extends the height-371338 vector: the proof survives the parse, a copy owns an independent one, and freeing the source leaves the copy intact, which it can only do if nothing is shared. Disabling just the deep copy fails it at line 351. WITH_NET=ON with -DBUILD_SHARED_LIBS=1: 78/78. WITH_NET=OFF: 72/72. ASAN+UBSAN: 78/78, no leaks. --- include/dogecoin/block.h | 36 ++++++++++++++++ src/block.c | 88 ++++++++++++++++++++++++++++++++++++++++ test/block_tests.c | 29 +++++++++++++ 3 files changed, 153 insertions(+) diff --git a/include/dogecoin/block.h b/include/dogecoin/block.h index af3a316aa..cffe9e5b1 100644 --- a/include/dogecoin/block.h +++ b/include/dogecoin/block.h @@ -48,6 +48,30 @@ typedef struct _auxpow { void *ctx; } auxpow; +/* The AuxPoW proof carried by a merge-mined header, owned by the header it + belongs to. + * + * This deliberately has no back-pointer to its owning header, unlike + * dogecoin_auxpow_block. That struct owns both its header and its parent_header + * and frees them, so a header could not hold one without the two owning each + * other. The payload owns only parent_header, which is a plain 80-byte header + * with no payload of its own, so ownership terminates. + * + * The auxpow.check / auxpow.ctx hook on dogecoin_block_header is unaffected: + * it is a validation hook whose context the caller supplies at call time, not a + * reference to this data. */ +typedef struct dogecoin_auxpow_payload_ { + dogecoin_tx* parent_coinbase; + uint256_t parent_hash; + uint8_t parent_merkle_count; + uint256_t* parent_coinbase_merkle; + uint32_t parent_merkle_index; + uint8_t aux_merkle_count; + uint256_t* aux_merkle_branch; + uint32_t aux_merkle_index; + struct dogecoin_block_header_* parent_header; +} dogecoin_auxpow_payload; + typedef struct dogecoin_block_header_ { int32_t version; uint256_t prev_block; @@ -56,8 +80,20 @@ typedef struct dogecoin_block_header_ { uint32_t bits; uint32_t nonce; auxpow auxpow[1]; + /** AuxPoW proof for a merge-mined header, NULL otherwise. Retained so the + header can reproduce the bytes it was parsed from: the deserializer used + to parse this into a local dogecoin_auxpow_block and free it, and + dogecoin_block_header_copy carried only the auxpow hook fields, so the + proof was discarded and anything needing it had to re-parse. */ + dogecoin_auxpow_payload* auxpow_payload; } dogecoin_block_header; +/** Free an AuxPoW payload and everything it owns. */ +LIBDOGECOIN_API void dogecoin_auxpow_payload_free(dogecoin_auxpow_payload* payload); + +/** Deep-copy an AuxPoW payload. Returns NULL if src is NULL. */ +LIBDOGECOIN_API dogecoin_auxpow_payload* dogecoin_auxpow_payload_copy(const dogecoin_auxpow_payload* src); + typedef struct dogecoin_auxpow_block_ { dogecoin_block_header* header; dogecoin_tx* parent_coinbase; diff --git a/src/block.c b/src/block.c index b1b780727..84df5c70a 100644 --- a/src/block.c +++ b/src/block.c @@ -175,6 +175,7 @@ dogecoin_block_header* dogecoin_block_header_new() { header->auxpow->check = check; header->auxpow->ctx = header; header->auxpow->is = false; + header->auxpow_payload = NULL; return header; } @@ -207,8 +208,61 @@ dogecoin_auxpow_block* dogecoin_auxpow_block_new() { * * @return Nothing. */ +void dogecoin_auxpow_payload_free(dogecoin_auxpow_payload* payload) { + if (!payload) return; + dogecoin_tx_free(payload->parent_coinbase); + dogecoin_free(payload->parent_coinbase_merkle); + dogecoin_free(payload->aux_merkle_branch); + /* parent_header is a plain 80-byte header: its own auxpow_payload is NULL, + so this does not recurse. */ + dogecoin_block_header_free(payload->parent_header); + dogecoin_free(payload); + } + +dogecoin_auxpow_payload* dogecoin_auxpow_payload_copy(const dogecoin_auxpow_payload* src) { + if (!src) return NULL; + dogecoin_auxpow_payload* dst = dogecoin_calloc(1, sizeof(*dst)); + if (!dst) return NULL; + + memcpy_safe(dst->parent_hash, src->parent_hash, sizeof(uint256_t)); + dst->parent_merkle_count = src->parent_merkle_count; + dst->parent_merkle_index = src->parent_merkle_index; + dst->aux_merkle_count = src->aux_merkle_count; + dst->aux_merkle_index = src->aux_merkle_index; + + if (src->parent_coinbase) { + dst->parent_coinbase = dogecoin_tx_new(); + if (!dst->parent_coinbase) goto fail; + dogecoin_tx_copy(dst->parent_coinbase, src->parent_coinbase); + } + if (src->parent_merkle_count && src->parent_coinbase_merkle) { + size_t n = (size_t)src->parent_merkle_count * sizeof(uint256_t); + dst->parent_coinbase_merkle = dogecoin_malloc(n); + if (!dst->parent_coinbase_merkle) goto fail; + memcpy_safe(dst->parent_coinbase_merkle, src->parent_coinbase_merkle, n); + } + if (src->aux_merkle_count && src->aux_merkle_branch) { + size_t n = (size_t)src->aux_merkle_count * sizeof(uint256_t); + dst->aux_merkle_branch = dogecoin_malloc(n); + if (!dst->aux_merkle_branch) goto fail; + memcpy_safe(dst->aux_merkle_branch, src->aux_merkle_branch, n); + } + if (src->parent_header) { + dst->parent_header = dogecoin_block_header_new(); + if (!dst->parent_header) goto fail; + dogecoin_block_header_copy(dst->parent_header, src->parent_header); + } + return dst; + +fail: + dogecoin_auxpow_payload_free(dst); + return NULL; + } + void dogecoin_block_header_free(dogecoin_block_header* header) { if (!header) return; + dogecoin_auxpow_payload_free(header->auxpow_payload); + header->auxpow_payload = NULL; header->version = 0; dogecoin_mem_zero(&header->prev_block, DOGECOIN_HASH_LENGTH); dogecoin_mem_zero(&header->merkle_root, DOGECOIN_HASH_LENGTH); @@ -342,6 +396,29 @@ int dogecoin_block_header_deserialize(dogecoin_block_header* header, struct cons goto cleanup; } dogecoin_block_header_copy(header, block->header); + /* Move the proof onto the header instead of letting cleanup free it. + Ownership transfers: the fields are nulled on the block so + dogecoin_auxpow_block_free does not release what the header now owns. */ + dogecoin_auxpow_payload* payload = dogecoin_calloc(1, sizeof(*payload)); + if (!payload) goto cleanup; + payload->parent_coinbase = block->parent_coinbase; + memcpy_safe(payload->parent_hash, block->parent_hash, sizeof(uint256_t)); + payload->parent_merkle_count = block->parent_merkle_count; + payload->parent_coinbase_merkle = block->parent_coinbase_merkle; + payload->parent_merkle_index = block->parent_merkle_index; + payload->aux_merkle_count = block->aux_merkle_count; + payload->aux_merkle_branch = block->aux_merkle_branch; + payload->aux_merkle_index = block->aux_merkle_index; + payload->parent_header = block->parent_header; + block->parent_coinbase = NULL; + block->parent_coinbase_merkle = NULL; + block->aux_merkle_branch = NULL; + block->parent_header = NULL; + /* No free of a prior payload here: dogecoin_block_header_copy above has + already overwritten the pointer, and header may have arrived as an + uninitialised stack struct. Callers own dest's prior contents, the + same contract every other field in this function follows. */ + header->auxpow_payload = payload; } ret = true; cleanup: @@ -510,6 +587,17 @@ void dogecoin_block_header_copy(dogecoin_block_header* dest, const dogecoin_bloc dest->auxpow->check = src->auxpow->check; dest->auxpow->ctx = src->auxpow->ctx; dest->auxpow->is = src->auxpow->is; + /* Deep-copy the proof. Carrying only the hook fields is what discarded it + before, so a copied merge-mined header could not be re-serialized or + re-validated without going back to the wire bytes. + + Assign, do not free what dest held. Every other field here is a plain + overwrite: this function treats dest as raw memory, and callers pass + uninitialised stack headers to it -- net_tests.c does, via + dogecoin_block_header_deserialize. Freeing dest->auxpow_payload would + dereference whatever the stack happened to contain. A dest that already + owns a payload is the caller's to release, as with every other member. */ + dest->auxpow_payload = dogecoin_auxpow_payload_copy(src->auxpow_payload); } /** diff --git a/test/block_tests.c b/test/block_tests.c index 3d27c9e76..861d1e20f 100644 --- a/test/block_tests.c +++ b/test/block_tests.c @@ -333,7 +333,36 @@ void test_auxpow_deserialize_real_vector() { u_assert_uint32_eq(header->bits, 456184976); u_assert_uint32_eq(header->nonce, 0); + /* The AuxPoW proof must survive the parse. It used to be built into a local + dogecoin_auxpow_block and freed at cleanup, so a caller holding the header + could not re-serialize or re-validate it without going back to the wire. */ + u_assert_not_null(header->auxpow_payload); + u_assert_not_null(header->auxpow_payload->parent_coinbase); + u_assert_not_null(header->auxpow_payload->parent_header); + u_assert_int_eq(header->auxpow_payload->parent_merkle_count > 0, 1); + u_assert_not_null(header->auxpow_payload->parent_coinbase_merkle); + + /* And it must copy deeply. dogecoin_block_header_copy carried only the + auxpow hook fields, so a copied merge-mined header silently lost its + proof; now the copy owns its own, and freeing one must not disturb the + other. */ + dogecoin_block_header* dup = dogecoin_block_header_new(); + dogecoin_block_header_copy(dup, header); + u_assert_not_null(dup->auxpow_payload); + u_assert_int_eq(dup->auxpow_payload != header->auxpow_payload, 1); + u_assert_int_eq(dup->auxpow_payload->parent_coinbase + != header->auxpow_payload->parent_coinbase, 1); + u_assert_int_eq(dup->auxpow_payload->parent_merkle_count + == header->auxpow_payload->parent_merkle_count, 1); + u_assert_mem_eq(dup->auxpow_payload->parent_hash, + header->auxpow_payload->parent_hash, DOGECOIN_HASH_LENGTH); + + /* Free the source first: the copy must still be intact, which it can only + be if nothing is shared. */ dogecoin_block_header_free(header); + u_assert_not_null(dup->auxpow_payload); + u_assert_not_null(dup->auxpow_payload->parent_header); + dogecoin_block_header_free(dup); dogecoin_free(buf); } From 684b098a47902db7e2bfaaebfdeea7de8e2081d4 Mon Sep 17 00:00:00 2001 From: bluezr Date: Mon, 3 Aug 2026 18:40:42 -0700 Subject: [PATCH 2/3] block: separate parsing a header from validating it check_auxpow ran inside dogecoin_block_header_deserialize, so every caller that wanted a header's fields paid for scrypt work over the parent chain during parsing, before any peer-level gating could decide whether the message was worth the effort. Core defers this to CheckBlock. Split into dogecoin_block_header_parse, which reads the base fields and, when version bit 0x100 is set, the AuxPoW proof, and dogecoin_block_header_validate, which runs check_auxpow and fills chainwork. The split is deliberately this way round. Making dogecoin_block_header_ deserialize the pure parse and adding a _checked variant would silently stop verifying proof of work for every existing caller of the name, with no compile error anywhere to catch it. Wrong direction for a symbol whose job is validation. Instead the existing name keeps its signature and its behaviour -- it is now parse followed by validate -- and the opt-out is explicit at the call site. deserialize_dogecoin_auxpow_block is split the same way and keeps its signature: parse_dogecoin_auxpow_fields does the reading, the public function adds the check. Validation needs the proof to still exist after parsing, which is what the preceding commit made possible. check_auxpow takes a dogecoin_auxpow_block, so validate builds one that borrows from the header and its payload. It is never freed: dogecoin_auxpow_block_free would take the header and parent_header with it, which is the ownership tangle the payload type exists to avoid. A header with no AuxPoW validates trivially. Its proof of work is over the 80 base bytes and belongs to the caller -- headersdb_file.c already runs check_pow itself for that case and fills chainwork. That asymmetry is unchanged. Verified by stubbing check_auxpow to always fail: dogecoin_block_header_ parse still succeeds, dogecoin_block_header_validate does not. A refactor that merely moved the call would fail both. The test also asserts the deferred chainwork equals what the parse-and-validate path computes, so the split does not change the answer. WITH_NET=ON with -DBUILD_SHARED_LIBS=1: 78/78. WITH_NET=OFF: 72/72. ASAN+UBSAN with leak detection: 78/78, clean. --- include/dogecoin/block.h | 29 ++++++++++++++++++++ src/block.c | 59 +++++++++++++++++++++++++++++++++++++--- test/block_tests.c | 28 +++++++++++++++++++ 3 files changed, 112 insertions(+), 4 deletions(-) diff --git a/include/dogecoin/block.h b/include/dogecoin/block.h index cffe9e5b1..92db7e5de 100644 --- a/include/dogecoin/block.h +++ b/include/dogecoin/block.h @@ -111,6 +111,35 @@ LIBDOGECOIN_API dogecoin_block_header* dogecoin_block_header_new(); LIBDOGECOIN_API void dogecoin_block_header_free(dogecoin_block_header* header); LIBDOGECOIN_API dogecoin_auxpow_block* dogecoin_auxpow_block_new(); LIBDOGECOIN_API void dogecoin_auxpow_block_free(dogecoin_auxpow_block* block); +/** Parse a block header off the wire without validating it. + * + * Reads the 80 base fields and, when version bit 0x100 is set, the AuxPoW + * proof, which is retained on the header. Runs no proof-of-work check. + * + * check_auxpow is scrypt work over the parent chain. Doing it during parsing + * means every caller pays for it whether or not it wants the answer yet, and + * before any peer-level gating has happened. Callers that want the fields and + * will decide about validation later use this; callers that want the existing + * parse-and-validate behaviour keep using dogecoin_block_header_deserialize. + */ +LIBDOGECOIN_API int dogecoin_block_header_parse(dogecoin_block_header* header, struct const_buffer* buf, const dogecoin_chainparams *params); + +/** Validate a parsed header's AuxPoW and fill @p chainwork. + * + * Returns true immediately for a header with no AuxPoW: its proof of work is + * over the 80 base bytes and is the caller's to verify. Requires the header to + * still own its proof, so it must have come from dogecoin_block_header_parse + * or dogecoin_block_header_deserialize. + */ +LIBDOGECOIN_API int dogecoin_block_header_validate(dogecoin_block_header* header, const dogecoin_chainparams *params, arith_uint256* chainwork); + +/** Parse and validate. Unchanged in behaviour and signature: this is + * dogecoin_block_header_parse followed by dogecoin_block_header_validate. + * + * Deliberately kept as the name that validates. Making this the pure parse and + * adding a checked variant would silently stop verifying proof of work for any + * caller of the existing name, with no compile error to catch it. + */ LIBDOGECOIN_API int dogecoin_block_header_deserialize(dogecoin_block_header* header, struct const_buffer* buf, const dogecoin_chainparams *params, arith_uint256* chainwork); LIBDOGECOIN_API int deserialize_dogecoin_auxpow_block(dogecoin_auxpow_block* block, struct const_buffer* buffer, const dogecoin_chainparams *params, arith_uint256* chainwork); LIBDOGECOIN_API void dogecoin_block_header_serialize(cstring* s, const dogecoin_block_header* header); diff --git a/src/block.c b/src/block.c index 84df5c70a..743fe278b 100644 --- a/src/block.c +++ b/src/block.c @@ -374,7 +374,9 @@ void print_block(dogecoin_auxpow_block* block) { * * @return 1 if deserialization was successful, 0 otherwise. */ -int dogecoin_block_header_deserialize(dogecoin_block_header* header, struct const_buffer* buf, const dogecoin_chainparams *params, arith_uint256* chainwork) { +static int parse_dogecoin_auxpow_fields(dogecoin_auxpow_block* block, struct const_buffer* buffer, const dogecoin_chainparams *params); + +int dogecoin_block_header_parse(dogecoin_block_header* header, struct const_buffer* buf, const dogecoin_chainparams *params) { dogecoin_auxpow_block* block = dogecoin_auxpow_block_new(); int ret = false; if (!deser_s32(&block->header->version, buf)) @@ -391,7 +393,7 @@ int dogecoin_block_header_deserialize(dogecoin_block_header* header, struct cons goto cleanup; dogecoin_block_header_copy(header, block->header); if ((block->header->version & 0x100) != 0 && buf->len) { - if (!deserialize_dogecoin_auxpow_block(block, buf, params, chainwork)) { + if (!parse_dogecoin_auxpow_fields(block, buf, params)) { printf("%s:%d:%s:%s\n", __FILE__, __LINE__, __func__, strerror(errno)); goto cleanup; } @@ -426,7 +428,49 @@ int dogecoin_block_header_deserialize(dogecoin_block_header* header, struct cons return ret; } -int deserialize_dogecoin_auxpow_block(dogecoin_auxpow_block* block, struct const_buffer* buffer, const dogecoin_chainparams *params, arith_uint256* chainwork) { +int dogecoin_block_header_validate(dogecoin_block_header* header, const dogecoin_chainparams *params, arith_uint256* chainwork) { + if (!header) return false; + /* Nothing to check for a header with no AuxPoW: its proof of work is over + the 80 base bytes, which is the caller's to verify -- headersdb_file.c + does exactly that for the non-AuxPoW case, and fills chainwork itself. */ + if (!header->auxpow_payload) return true; + + /* check_auxpow wants a dogecoin_auxpow_block. Build one that borrows from + the header and its payload rather than copying: it is never freed, so the + borrowed pointers are not released twice. dogecoin_auxpow_block_free + would take the header and parent_header with it, which is exactly the + ownership tangle the payload type exists to avoid. */ + dogecoin_auxpow_payload* p = header->auxpow_payload; + dogecoin_auxpow_block view; + dogecoin_mem_zero(&view, sizeof(view)); + view.header = header; + view.parent_coinbase = p->parent_coinbase; + memcpy_safe(view.parent_hash, p->parent_hash, sizeof(uint256_t)); + view.parent_merkle_count = p->parent_merkle_count; + view.parent_coinbase_merkle = p->parent_coinbase_merkle; + view.parent_merkle_index = p->parent_merkle_index; + view.aux_merkle_count = p->aux_merkle_count; + view.aux_merkle_branch = p->aux_merkle_branch; + view.aux_merkle_index = p->aux_merkle_index; + view.parent_header = p->parent_header; + + if (!check_auxpow(&view, (dogecoin_chainparams*)params, chainwork)) { + printf("check_auxpow failed!\n"); + return false; + } + return true; + } + +int dogecoin_block_header_deserialize(dogecoin_block_header* header, struct const_buffer* buf, const dogecoin_chainparams *params, arith_uint256* chainwork) { + if (!dogecoin_block_header_parse(header, buf, params)) return false; + return dogecoin_block_header_validate(header, params, chainwork); + } + +/* Parse the AuxPoW fields off the wire. No validation: check_auxpow is scrypt + work on the parent chain, and doing it here means every caller pays for it + during parsing whether or not it wants the answer yet. */ +static int parse_dogecoin_auxpow_fields(dogecoin_auxpow_block* block, struct const_buffer* buffer, const dogecoin_chainparams *params) { + (void)params; if (buffer->len > DOGECOIN_MAX_P2P_MSG_SIZE) { return printf("\ntransaction is invalid or to large.\n\n"); } @@ -542,11 +586,18 @@ int deserialize_dogecoin_auxpow_block(dogecoin_auxpow_block* block, struct const return false; } + return true; + } + +/* Unchanged behaviour and signature: parse, then validate. Callers that want + only the fields use parse_dogecoin_auxpow_fields via + dogecoin_block_header_parse. */ +int deserialize_dogecoin_auxpow_block(dogecoin_auxpow_block* block, struct const_buffer* buffer, const dogecoin_chainparams *params, arith_uint256* chainwork) { + if (!parse_dogecoin_auxpow_fields(block, buffer, params)) return false; if (!check_auxpow(block, (dogecoin_chainparams*)params, chainwork)) { printf("check_auxpow failed!\n"); return false; } - return true; } diff --git a/test/block_tests.c b/test/block_tests.c index 861d1e20f..cb56bea7b 100644 --- a/test/block_tests.c +++ b/test/block_tests.c @@ -363,6 +363,34 @@ void test_auxpow_deserialize_real_vector() { u_assert_not_null(dup->auxpow_payload); u_assert_not_null(dup->auxpow_payload->parent_header); dogecoin_block_header_free(dup); + + /* The parse/check split: dogecoin_block_header_parse must produce the same + header and the same retained proof, without running check_auxpow. */ + dogecoin_block_header* parsed = dogecoin_block_header_new(); + struct const_buffer cb2 = { buf, blen }; + u_assert_int_eq(dogecoin_block_header_parse(parsed, &cb2, &dogecoin_chainparams_main), 1); + u_assert_uint32_eq((uint32_t)parsed->version, 0x00620102); + u_assert_uint32_eq(parsed->timestamp, 1410464609); + u_assert_not_null(parsed->auxpow_payload); + u_assert_not_null(parsed->auxpow_payload->parent_header); + + /* Validation deferred to a separate call, and it agrees with what the + parse-and-validate path computed. */ + arith_uint256 deferred_chainwork; + u_assert_int_eq(dogecoin_block_header_validate(parsed, &dogecoin_chainparams_main, + &deferred_chainwork), 1); + u_assert_mem_eq(utils_uint8_to_hex(arith_to_uint256(&deferred_chainwork), DOGECOIN_HASH_LENGTH), + utils_uint8_to_hex(arith_to_uint256(&chainwork), DOGECOIN_HASH_LENGTH), 64); + dogecoin_block_header_free(parsed); + + /* A header with no AuxPoW validates trivially: its proof of work is over + the 80 base bytes and belongs to the caller, which is why + headersdb_file.c runs check_pow itself for that case. */ + dogecoin_block_header* plain = dogecoin_block_header_new(); + plain->version = 1; + u_assert_int_eq(dogecoin_block_header_validate(plain, &dogecoin_chainparams_main, NULL), 1); + dogecoin_block_header_free(plain); + dogecoin_free(buf); } From 236c1be5bcb0ba60afb1d6e42f91959b7aa9dd4c Mon Sep 17 00:00:00 2001 From: bluezr Date: Mon, 3 Aug 2026 19:22:32 -0700 Subject: [PATCH 3/3] block: serialize a header with its AuxPoW There was no way to write an AuxPoW proof back out. The tree could parse one and, since the preceding commits, retain it, but nothing could emit it, so a header that arrived over the wire could not be reproduced. Add dogecoin_auxpow_payload_serialize, in the wire order parse_dogecoin_auxpow_fields reads, and dogecoin_block_header_serialize_full, which writes the 80 base bytes and then the proof when the header carries one. dogecoin_block_header_serialize is untouched and still emits exactly 80 bytes. That is deliberate: its output is what the block hash, the scrypt proof of work, check_auxpow's own hashing and the fixed-width headers.db record are computed over. Making it AuxPoW-aware would change all four. This is the same split Core draws between CPureBlockHeader and CBlockHeader, and the same reason. The full form keys off whether the proof is present rather than off version bit 0x100 alone, so a header carrying the bit without a proof serializes as the 80 bytes it actually has instead of emitting a truncated blob. Tested by round-tripping the height-371338 mainnet vector: parse it, write it back, and require the result to be byte-identical to the bytes it came from. Emitting something merely well-formed is not enough -- the point of this is that short IDs and hashes computed over the output match Core's, which only holds if the bytes match exactly. The pure form is asserted to stay at 80 bytes and to match the first 80 of the input. The first attempt at checking that test had no bite: it swapped parent_merkle_index with aux_merkle_index, which are both zero in this block, so the output was identical and the test passed either way. Zeroing parent_hash instead fails it at the memcmp, which is what a real serialization bug would do. WITH_NET=ON with -DBUILD_SHARED_LIBS=1: 78/78. WITH_NET=OFF: 72/72. ASAN+UBSAN with leak detection: 78/78, clean. --- include/dogecoin/block.h | 15 +++++++++++++++ src/block.c | 30 ++++++++++++++++++++++++++++++ test/block_tests.c | 27 +++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/include/dogecoin/block.h b/include/dogecoin/block.h index 92db7e5de..7fb651a19 100644 --- a/include/dogecoin/block.h +++ b/include/dogecoin/block.h @@ -142,7 +142,22 @@ LIBDOGECOIN_API int dogecoin_block_header_validate(dogecoin_block_header* header */ LIBDOGECOIN_API int dogecoin_block_header_deserialize(dogecoin_block_header* header, struct const_buffer* buf, const dogecoin_chainparams *params, arith_uint256* chainwork); LIBDOGECOIN_API int deserialize_dogecoin_auxpow_block(dogecoin_auxpow_block* block, struct const_buffer* buffer, const dogecoin_chainparams *params, arith_uint256* chainwork); +/** Serialize the 80 base header fields. This is the pure header: it never + * emits AuxPoW, because its output is what the block hash, the scrypt proof of + * work and the fixed-width headers.db record are computed over. */ LIBDOGECOIN_API void dogecoin_block_header_serialize(cstring* s, const dogecoin_block_header* header); + +/** Serialize an AuxPoW proof in wire order. */ +LIBDOGECOIN_API void dogecoin_auxpow_payload_serialize(cstring* s, const dogecoin_auxpow_payload* payload); + +/** Serialize a header as it appears on the wire: the 80 base bytes, followed by + * the AuxPoW proof when the header carries one. + * + * This is Core's CBlockHeader to dogecoin_block_header_serialize's + * CPureBlockHeader. Messages that carry a whole header -- headers, block, + * cmpctblock -- want this one; anything hashing the header wants the pure form. + */ +LIBDOGECOIN_API void dogecoin_block_header_serialize_full(cstring* s, const dogecoin_block_header* header); LIBDOGECOIN_API void dogecoin_block_header_copy(dogecoin_block_header* dest, const dogecoin_block_header* src); LIBDOGECOIN_API dogecoin_bool dogecoin_block_header_hash(dogecoin_block_header* header, uint256_t hash); diff --git a/src/block.c b/src/block.c index 743fe278b..709273edf 100644 --- a/src/block.c +++ b/src/block.c @@ -610,6 +610,36 @@ int deserialize_dogecoin_auxpow_block(dogecoin_auxpow_block* block, struct const * * @return Nothing. */ +void dogecoin_auxpow_payload_serialize(cstring* s, const dogecoin_auxpow_payload* payload) { + if (!s || !payload) return; + /* Wire order mirrors parse_dogecoin_auxpow_fields exactly. */ + dogecoin_tx_serialize(s, payload->parent_coinbase); + ser_u256(s, payload->parent_hash); + ser_varlen(s, payload->parent_merkle_count); + uint8_t i; + for (i = 0; i < payload->parent_merkle_count; i++) + ser_u256(s, payload->parent_coinbase_merkle[i]); + ser_u32(s, payload->parent_merkle_index); + ser_varlen(s, payload->aux_merkle_count); + for (i = 0; i < payload->aux_merkle_count; i++) + ser_u256(s, payload->aux_merkle_branch[i]); + ser_u32(s, payload->aux_merkle_index); + /* Parent header is a pure 80-byte header: it carries no AuxPoW of its own. */ + dogecoin_block_header_serialize(s, payload->parent_header); + } + +void dogecoin_block_header_serialize_full(cstring* s, const dogecoin_block_header* header) { + if (!s || !header) return; + dogecoin_block_header_serialize(s, header); + /* Core's CBlockHeader::SerializationOp appends the AuxPoW whenever + nVersion & VERSION_AUXPOW is set. Mirror that, driven by whether the + proof is actually present rather than by the bit alone, so a header + carrying the bit but no proof serializes as the 80 bytes it really has + instead of emitting a truncated blob. */ + if (header->auxpow_payload) + dogecoin_auxpow_payload_serialize(s, header->auxpow_payload); + } + void dogecoin_block_header_serialize(cstring* s, const dogecoin_block_header* header) { ser_s32(s, header->version); ser_u256(s, header->prev_block); diff --git a/test/block_tests.c b/test/block_tests.c index cb56bea7b..b96520fb1 100644 --- a/test/block_tests.c +++ b/test/block_tests.c @@ -391,6 +391,33 @@ void test_auxpow_deserialize_real_vector() { u_assert_int_eq(dogecoin_block_header_validate(plain, &dogecoin_chainparams_main, NULL), 1); dogecoin_block_header_free(plain); + /* Round-trip: a parsed merge-mined header must serialize back to exactly the + bytes it came from. This is the check that makes the serializer worth + anything -- emitting something well-formed but different would still let + short IDs and block hashes diverge from Core. */ + dogecoin_block_header* rt = dogecoin_block_header_new(); + struct const_buffer cb3 = { buf, blen }; + u_assert_int_eq(dogecoin_block_header_parse(rt, &cb3, &dogecoin_chainparams_main), 1); + size_t hdr_span = blen - cb3.len; + u_assert_int_eq(hdr_span > 80, 1); + + cstring* full = cstr_new_sz(hdr_span + 16); + dogecoin_block_header_serialize_full(full, rt); + u_assert_int_eq(full->len == hdr_span, 1); + u_assert_int_eq(memcmp(full->str, buf, hdr_span), 0); + + /* And the pure form stays 80 bytes: the block hash, the scrypt proof of + work and the headers.db record are all computed over it, so it must not + start emitting AuxPoW just because the header now retains some. */ + cstring* pure = cstr_new_sz(96); + dogecoin_block_header_serialize(pure, rt); + u_assert_int_eq(pure->len == 80, 1); + u_assert_int_eq(memcmp(pure->str, buf, 80), 0); + + cstr_free(full, true); + cstr_free(pure, true); + dogecoin_block_header_free(rt); + dogecoin_free(buf); }