Skip to content

block: let a header own its AuxPoW proof - #392

Open
xanimo wants to merge 1 commit into
dogecoinfoundation:0.1.5-devfrom
xanimo:0.1.5-dev-auxpow-payload
Open

block: let a header own its AuxPoW proof#392
xanimo wants to merge 1 commit into
dogecoinfoundation:0.1.5-devfrom
xanimo:0.1.5-dev-auxpow-payload

Conversation

@xanimo

@xanimo xanimo commented Aug 4, 2026

Copy link
Copy Markdown
Member

Prerequisite for the AuxPoW follow-up sequence (parse/check split, then an
AuxPoW serializer). Independent of the BIP157/BIP152 stacks — this branches
straight off 0.1.5-dev.

dogecoin_block_header carried an auxpow member holding only the validation
hook — check, ctx, is — never the proof. The proof was built into a local
dogecoin_auxpow_block inside dogecoin_block_header_deserialize and freed at
cleanup:, so by the time a caller had its header, the parent coinbase, merkle
branches and parent header were gone.

dogecoin_block_header_copy made it 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.

What this adds

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:111 passes the block
directly — not a reference to this data.

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.

Checked for bite, not just for passing: disabling only the deep copy in
_copy and leaving retention intact fails at line 351. A test that passed
either way would have proved nothing.

WITH_NET=ON with -DBUILD_SHARED_LIBS=1: 78/78. WITH_NET=OFF: 72/72.

What it unblocks

  • parse/check split — deferring check_auxpow needs the proof to still be
    around after parsing, which it now is
  • AuxPoW serializer — needs a header that can answer for its own proof; this
    is what makes one writable, and the encode side of
    dogecoin_compact_block_serialize needs it before libdogecoin can originate a
    cmpctblock for a block it assembled rather than received
  • header_raw becomes an optimisation rather than a necessity

@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, compact blocks give a reason to keep auxpow proofs around after validation, which is good for other uses too.

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.
@xanimo
xanimo force-pushed the 0.1.5-dev-auxpow-payload branch from 7cbf5ea to 75f6348 Compare August 5, 2026 21:04
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