headerssync: retain Elements identity and proof fields
What changed, and why it matters
This commit fixes a bug in how Elements nodes temporarily store block headers during initial sync. The node was stripping out Elements-specific fields (block height, proof, dynamic federation parameters, and signblock witness) when compressing headers for memory efficiency. When it later rebuilt the full header, those fields were missing, so the rebuilt header's hash no longer matched the original header that was received. This could cause sync failures or validation problems for Elements nodes, but it does not change any consensus rules.
Treat as a bug-fix commit with availability/reliability impact. Review whether the larger CompressedHeader size materially affects memory limits under REDOWNLOAD_BUFFER_SIZE, and consider backporting to maintained release branches because the sync path is affected. No emergency consensus deployment is indicated.
Security signals we found
Header integrity loss in sync path: reconstructed header hash would not match received header
Loss of proof and signblock witness data needed for signed/dynafed validation
Potential denial-of-service via sync failure or validation rejection of otherwise valid headers
No consensus rule changes; fix is data-fidelity only
Evidence from the diff
CompressedHeader in src/headerssync.h previously retained only Bitcoin PoW fields (hashMerkleRoot, nTime, nBits, nNonce). For Elements, CBlockHeader additionally carries block_height, proof, m_dynafed_params, and m_signblock_witness. Because GetFullHeader() reconstructed only the Bitcoin fields, headers passing through the low-work presync/redownload path lost their Elements identity and proof data. The commit adds those four fields to CompressedHeader and restores them in GetFullHeader(), ensuring the reconstructed header matches the received header. The static_assert is relaxed from exactly 48 bytes to <= 512 bytes, and the memory-bounding comment is updated.
Changed components
src/headerssync.hsrc/headerssync.cppHeadersSyncState presync/redownload pathCompressedHeader structElements signed/dynafed header validationInspect captured patch +26 / −3
### src/headerssync.cpp
@@ -19,9 +19,15 @@ constexpr size_t HEADER_COMMITMENT_PERIOD{624};
//! received and validated against commitments.
constexpr size_t REDOWNLOAD_BUFFER_SIZE{14827}; // 14827/624 = ~23.8 commitments
-// Our memory analysis assumes 48 bytes for a CompressedHeader (so we should
-// re-calculate parameters if we compress further)
-static_assert(sizeof(CompressedHeader) == 48);
+// NOTE (ELEMENTS): The upstream Bitcoin memory analysis assumed 48 bytes for
+// a CompressedHeader, which holds only the PoW fields. Elements must retain
+// the identity/proof fields (block_height, proof, dynafed params, signblock
+// witness) so that signed/dynafed headers can be reconstructed faithfully, so
+// CompressedHeader is now larger than 48 bytes. The redownload buffer is
+// bounded by REDOWNLOAD_BUFFER_SIZE headers per peer, so the per-peer memory
+// cost is REDOWNLOAD_BUFFER_SIZE * sizeof(CompressedHeader); this remains
+// small but should be reconsidered if REDOWNLOAD_BUFFER_SIZE is ever raised.
+static_assert(sizeof(CompressedHeader) <= 512);
HeadersSyncState::HeadersSyncState(NodeId id, const Consensus::Params& consensus_params,
const CBlockIndex* chain_start, const arith_uint256& minimum_required_work) :
### src/headerssync.h
@@ -25,6 +25,15 @@ struct CompressedHeader {
uint32_t nTime{0};
uint32_t nBits{0};
uint32_t nNonce{0};
+ // ELEMENTS: fields needed to faithfully reconstruct signed/dynafed
+ // headers. These participate in the header's identity (block_height,
+ // dynafed params, proof challenge) or are required for downstream
+ // validation (proof solution, signblock witness), so they must be retained
+ // across the headers-sync presync/redownload path.
+ uint32_t block_height{0};
+ CProof proof;
+ DynaFedParams m_dynafed_params;
+ CScriptWitness m_signblock_witness;
CompressedHeader()
{
@@ -38,6 +47,10 @@ struct CompressedHeader {
nTime = header.nTime;
nBits = header.nBits;
nNonce = header.nNonce;
+ block_height = header.block_height;
+ proof = header.proof;
+ m_dynafed_params = header.m_dynafed_params;
+ m_signblock_witness = header.m_signblock_witness;
}
CBlockHeader GetFullHeader(const uint256& hash_prev_block) {
@@ -46,8 +59,12 @@ struct CompressedHeader {
ret.hashPrevBlock = hash_prev_block;
ret.hashMerkleRoot = hashMerkleRoot;
ret.nTime = nTime;
+ ret.block_height = block_height;
ret.nBits = nBits;
ret.nNonce = nNonce;
+ ret.proof = proof;
+ ret.m_dynafed_params = m_dynafed_params;
+ ret.m_signblock_witness = m_signblock_witness;
return ret;
};
};Why this scored 63/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.