Skip to content

feat(subtree): RootHashPadded accepts non-power-of-two leaf counts#127

Merged
mrz1836 merged 2 commits into
masterfrom
relax-roothashpadded-nonpow2
May 20, 2026
Merged

feat(subtree): RootHashPadded accepts non-power-of-two leaf counts#127
mrz1836 merged 2 commits into
masterfrom
relax-roothashpadded-nonpow2

Conversation

@ordishs
Copy link
Copy Markdown
Collaborator

@ordishs ordishs commented May 19, 2026

Summary

The pow2 precondition on RootHashPadded was overly strict. The natural root produced by BuildMerkleTreeStoreFromBytes already lives at height ceil(log2(length)) for any length >= 1 because the duplicate-when-odd rule is applied at every internal level. Self-hashing from there up to targetHeight composes correctly with the canonical flat merkle tree.

Changes

  • Replace int(math.Log2(float64(length))) with bits.Len(uint(length-1)) so the height calculation is exact for non-power-of-two lengths.
  • Drop the IsPowerOfTwo guard and the now-unused ErrNotPowerOfTwoLeafCount sentinel from the public API.
  • Convert the prior error-path test (TestRootHashPadded_NotPowerOfTwoErrors) into an acceptance test that asserts the lifted root matches both the natural root (when targetHeight == ceil(log2(length))) and the expected self-hashed value one level above.
  • Add TestRootHashPadded_MatchesFlatTreeForNonPowerOfTwoFinal, a sweep across r ∈ {1,2,3,5,6,7,9,11,13,15,17,21,23,27,31} that proves the composed root (complete left + lifted right) equals the canonical flat merkle root for each remainder size.

Motivation

This enables teranode PR #909 to delete duplicate lift-helper code (~40 lines across two files) and call RootHashPadded directly for the legacy-block partitioner's final subtree of arbitrary length.

Breaking change?

ErrNotPowerOfTwoLeafCount is removed from the public API. A grep across this repo and teranode shows no other callers; the only test using it has been converted. External consumers (if any) catching this sentinel would need to drop that branch.

Test plan

  • go test -race ./... (all green, including the new sweep test)
  • go vet ./...
  • golangci-lint run ./... (no new issues introduced)

The pow2 precondition on RootHashPadded was overly strict. The natural
root produced by BuildMerkleTreeStoreFromBytes already lives at height
ceil(log2(length)) for any length >= 1 because the duplicate-when-odd
rule is applied at every internal level. Self-hashing from there up to
targetHeight composes correctly with the canonical flat merkle tree.

- replace int(math.Log2(float64(length))) with bits.Len(uint(length-1))
  so the height calculation is exact for non-power-of-two lengths
- drop the IsPowerOfTwo guard and the now-unused
  ErrNotPowerOfTwoLeafCount sentinel
- convert the prior error-path test into an acceptance test plus a
  sweep over r in {1,2,3,5,6,7,9,11,13,15,17,21,23,27,31} that proves
  equivalence with the canonical flat root
@ordishs ordishs requested a review from mrz1836 as a code owner May 19, 2026 23:19
@github-actions github-actions Bot added the size/M Medium change (51–200 lines) label May 19, 2026
@github-actions github-actions Bot added the feature Any new significant addition label May 19, 2026
CI's gosec doesn't flag the uint(length-1) conversion because length >= 1
is provable from the preceding early return. nolintlint then reports the
//nolint directive as unused. Drop the annotation — the conversion is
safe and CI confirms it doesn't need suppression.
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Collaborator

@mrz1836 mrz1836 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mrz1836 mrz1836 merged commit 930e158 into master May 20, 2026
45 checks passed
@github-actions github-actions Bot deleted the relax-roothashpadded-nonpow2 branch May 20, 2026 01:21
ordishs added a commit to ordishs/teranode that referenced this pull request May 20, 2026
go-subtree v1.4.2 relaxes RootHashPadded to accept non-power-of-two
leaf counts (bsv-blockchain/go-subtree#127), making the local
liftSubtreeRootToTargetHeight / liftRootForTest helpers redundant.

- bump github.com/bsv-blockchain/go-subtree from v1.4.1 to v1.4.2
- delete liftSubtreeRootToTargetHeight in model/Block.go; CheckMerkleRoot
  calls last.RootHashPadded(targetHeight) directly
- delete liftRootForTest in util/subtree_merkle_root_equivalence_test.go;
  the test calls right.RootHashPadded(left.Height) directly
- drop the now-unused crypto/sha256 and math/bits imports in both files
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Any new significant addition size/M Medium change (51–200 lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants