[bump] Derive corrected subtree-0 root from coinbase BUMP, not over-climbed STUMP#184
Conversation
…er-climbed STUMP merkle-service sometimes delivers subtree 0's STUMP at full block height rather than the subtree-internal height. The old correction climbed every level of that STUMP, overshooting the subtree-0 root and landing on the block root, which then poisoned subtreeHashes[0]; the compound placement also keyed internal height off the raw STUMP height. The resulting compound BUMP root diverged from the block-header merkle root, ValidateCompoundRoot rejected it, and the tx was never marked MINED (mainnet block 951360, 2026-05-30). Derive the corrected subtree-0 root and the placement internal height solely from the coinbase BUMP (authoritatively full block height): internalHeight = coinbaseBUMPheight - ceil(log2(numSubtrees)). Tolerate over-tall STUMPs by reading only their first internalHeight levels. Adds reproduction tests at the block-951360 shape (over-tall subtree-0 STUMP, 3 subtrees). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
👋 Thanks, @rafa-js!This pull request comes from a fork. For security, our CI runs in a restricted mode.
Thanks for contributing to bsv-blockchain/arcade! 🚀 |
oskarszoon
left a comment
There was a problem hiding this comment.
Approve. Verified against live mainnet block 951360 (on-chain root 5ed5a7a9…, proof depth 17 → internalHeight 17−2=15): the bounded coinbase-BUMP fold yields the true subtree-0 root cbaaf149… instead of over-climbing to the block root. ceil(log2) has no float drift in range (nice touch pinning it with TestLog2Ceil_NoFloatDrift), bounds are guarded, 89 tests pass.
On the !=→< relaxation: it's safe. It loosens an input-shape check, not the acceptance gate — ValidateCompoundRoot still recomputes against the node's own chain-header merkle root, so a malformed or over-tall STUMP can only cause a loud rejection, never a forged accepted proof. Net it strengthens vs the old unbounded climb that caused this incident.
One thing to confirm before merge, not a blocker: the coinbase-BUMP-absent path for multi-subtree blocks isn't fixed (it falls back to the STUMP height and skips the subtree-0 correction). It still fails loud — the original stuck-tx symptom — so it's no worse than today, but it's not fixed and it's untested. Does ingestion guarantee a coinbase BUMP accompanies every multi-subtree block's STUMP set? If not, worth a follow-up for that residual + a test documenting the fallback.
Optional: the safety argument rests on ValidateCompoundRoot staying mandatory after BuildCompoundBUMP — a durable invariant/test guarding that would be cheap insurance.
A multi-subtree block can reach BuildCompoundBUMP with no coinbase BUMP (datahub treats a zero-length coinbase-BUMP field as absent). Subtree 0's root then cannot be corrected, so the compound root cannot match the header root and ValidateCompoundRoot rejects it — fail-safe, never forging an accepted proof. Lock that contract for the compound hot path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Thanks for the careful review. Coinbase-BUMP-absent multi-subtree path: confirmed — ingestion does not guarantee a coinbase BUMP. The datahub parser treats a zero-length coinbase-BUMP field as "absent" ( In that case the subtree-0 root can't be corrected: the datahub computes it against the coinbase placeholder, and without the coinbase BUMP there's no real coinbase txid to fold and no full-block path to bound the climb. The compound root then can't match the header root, so the mandatory I've added I'll open a separate follow-up issue to track "multi-subtree blocks without a coinbase BUMP can't be MINED" (an ingestion/datahub-guarantee question rather than a bump bug).
|
There was a problem hiding this comment.
Pull request overview
This PR fixes compound BUMP construction when merkle-service provides an over-tall (full-block-height) subtree-0 STUMP by deriving subtree-0 correction and placement height from the coinbase BUMP (authoritative full-block-height path) instead of trusting STUMP heights.
Changes:
- Remove the unbounded STUMP-based corrected-subtree-root computation and derive the corrected subtree-0 root from a bounded fold of the coinbase BUMP.
- In
BuildCompoundBUMP, deriveinternalHeightfrom the coinbase BUMP when available and tolerate over-tall STUMPs by truncating to the required height (reject only under-tall STUMPs). - Update/add tests to reproduce the block-951360 incident shape and to validate the bounded coinbase-based correction behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| bump/bump.go | Reworks subtree-0 correction and compound placement height derivation to use coinbase BUMP; relaxes STUMP height guard to tolerate over-tall STUMPs. |
| bump/bump_test.go | Enhances coinbase BUMP test helper to model full-block paths and adds/updates reproducer tests for the incident scenario. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func buildCoinbaseBUMP(subtree0Leaves []chainhash.Hash, coinbaseTxID chainhash.Hash, blockHeight uint32, subtreeRoots []chainhash.Hash) []byte { | ||
| trueLeaves := make([]chainhash.Hash, len(subtree0Leaves)) | ||
| copy(trueLeaves, subtree0Leaves) | ||
| trueLeaves[0] = coinbaseTxID | ||
| return buildSTUMP(trueLeaves, 0, blockHeight) | ||
| base := buildSTUMP(trueLeaves, 0, blockHeight) | ||
| return appendBlockClimb(base, subtreeRoots) | ||
| } |
What Changed
computeCorrectedSubtreeRoot, whosestumpPath.ComputeRoot(coinbaseTxID)climbed every level of the subtree-0 STUMP with no height bound.correctedSubtree0Root(coinbaseBUMP []byte, numSubtrees int)to derive the corrected subtree-0 root solely from the coinbase BUMP:internalHeight = len(cbPath.Path) − ceil(log2(numSubtrees)), then fold the coinbase exactlyinternalHeightlevels via the existing bounded primitivesubtree0RootFromCoinbaseBUMP.BuildCompoundBUMP, derive the placementinternalHeightfrom the coinbase BUMP when present (same formula), falling back to the first STUMP's height only when no coinbase BUMP is available.!=to<so an over-tall STUMP is tolerated (only its firstinternalHeightlevels are read); a STUMP shorter thaninternalHeightis still rejected.subtree0RootFromCoinbaseBUMP.Why It Was Necessary
merkle-service sometimes delivers subtree 0's STUMP at full block height instead of the subtree-internal height. The old unbounded climb then overshoots the subtree-0 root and lands on the block merkle root, poisoning
subtreeHashes[0]; the compound's top tree is built over[block_root, s1, s2, …]andValidateCompoundRootrejects it. The transaction is never markedMINED, so it remains stuck and gets rebroadcast. Observed on mainnet block 951360 (tx85598b7b2d994153487198f9b69b482471d496c50e694f17bb92dd611edadc97,subtreeCount = 3): block root5ed5a7a9…ecbc, wrong computed root49c8fd3a…4f23, STUMPtreeHeight = 17vs. trueinternalHeight = 15. The fix derivesinternalHeightfrom the coinbase BUMP (authoritatively full block height), making the correction immune to over-tall STUMPs.Closes #183
Testing Performed
go test ./bump/...— 89 tests pass, including two new reproductions:TestBuildCompoundBUMP_FullBlockSubtree0STUMP_Block951360(over-tall subtree-0 STUMP + normal siblings; validates against the block root) andTestCorrectedSubtree0Root_Block951360_FromCoinbaseBUMP(corrected root equals the real subtree root, not the block root).go vet ./bump/...andgofmt -l bump/are clean.Impact / Risk
BuildCompoundBUMP/AssembleBUMPare unchanged.internalHeightequals the previous STUMP-derived value, so existing behavior is preserved; the change only affects over-tall STUMPs that previously failed.coinbaseBUMP == nilpath with an over-tall STUMP would still hit the guard.Notifications