From 0c64fecc458e5df3e534269085dd1b3069956d18 Mon Sep 17 00:00:00 2001 From: Dylan Date: Tue, 25 Nov 2025 08:12:55 -0500 Subject: [PATCH 1/2] Fix GetMerkleProof for odd numbered nodes --- go.mod | 4 +-- go.sum | 4 +-- subtree.go | 6 ++++ subtree_test.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 48a9dd5..05c76ad 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module github.com/bsv-blockchain/go-subtree -go 1.24.3 +go 1.24.6 require ( github.com/bsv-blockchain/go-bt/v2 v2.5.1 github.com/bsv-blockchain/go-safe-conversion v1.1.0 github.com/bsv-blockchain/go-tx-map v1.2.1 github.com/stretchr/testify v1.11.1 - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b + golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b ) require ( diff --git a/go.sum b/go.sum index 802e3ac..5331901 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA= +golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/subtree.go b/subtree.go index 90e3c46..91a1919 100644 --- a/subtree.go +++ b/subtree.go @@ -529,6 +529,12 @@ func (st *Subtree) GetMerkleProof(index int) ([]*chainhash.Hash, error) { // getLeafSiblingHash returns the hash of the sibling node at the leaf level func getLeafSiblingHash(nodes []Node, index int) *chainhash.Hash { if index%2 == 0 { + // For even index, sibling is at index+1 + // But if index+1 is out of bounds (odd number of leaves), + // duplicate the last node (Bitcoin convention) + if index+1 >= len(nodes) { + return &nodes[index].Hash + } return &nodes[index+1].Hash } diff --git a/subtree_test.go b/subtree_test.go index 7cac524..e952fcc 100644 --- a/subtree_test.go +++ b/subtree_test.go @@ -1147,3 +1147,97 @@ func Benchmark_NodeIndex(b *testing.B) { require.GreaterOrEqual(b, index, 0) } } + +func TestGetMerkleProofOddLeaves(t *testing.T) { + t.Run("odd number of leaves in power-of-two tree", func(t *testing.T) { + // Create a subtree with capacity for 4 nodes but only add 3 (odd number) + tree, err := NewTree(2) // height 2 = 2^2 = 4 leaves + require.NoError(t, err) + + // Add 3 nodes (odd number) + hash1, _ := chainhash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111") + hash2, _ := chainhash.NewHashFromStr("2222222222222222222222222222222222222222222222222222222222222222") + hash3, _ := chainhash.NewHashFromStr("3333333333333333333333333333333333333333333333333333333333333333") + + err = tree.AddNode(*hash1, 100, 250) + require.NoError(t, err) + err = tree.AddNode(*hash2, 200, 300) + require.NoError(t, err) + err = tree.AddNode(*hash3, 150, 275) + require.NoError(t, err) + + // Test GetMerkleProof for the last node (index 2) + // This should duplicate the last node as its sibling + proof, err := tree.GetMerkleProof(2) + require.NoError(t, err) + require.NotNil(t, proof) + + // The first hash in the proof should be the duplicate of the last node + require.Equal(t, hash3.String(), proof[0].String()) + }) + + t.Run("even number of leaves", func(t *testing.T) { + // Create a subtree with 4 nodes (even number) + tree, err := NewTreeByLeafCount(4) + require.NoError(t, err) + + // Add 4 nodes + hash1, _ := chainhash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111") + hash2, _ := chainhash.NewHashFromStr("2222222222222222222222222222222222222222222222222222222222222222") + hash3, _ := chainhash.NewHashFromStr("3333333333333333333333333333333333333333333333333333333333333333") + hash4, _ := chainhash.NewHashFromStr("4444444444444444444444444444444444444444444444444444444444444444") + + err = tree.AddNode(*hash1, 100, 250) + require.NoError(t, err) + err = tree.AddNode(*hash2, 200, 300) + require.NoError(t, err) + err = tree.AddNode(*hash3, 150, 275) + require.NoError(t, err) + err = tree.AddNode(*hash4, 175, 325) + require.NoError(t, err) + + // Test GetMerkleProof for the second-to-last node (index 2) + proof, err := tree.GetMerkleProof(2) + require.NoError(t, err) + require.NotNil(t, proof) + + // The first hash in the proof should be hash4 (its sibling) + require.Equal(t, hash4.String(), proof[0].String()) + + // Test GetMerkleProof for the last node (index 3) + proof, err = tree.GetMerkleProof(3) + require.NoError(t, err) + require.NotNil(t, proof) + + // The first hash in the proof should be hash3 (its sibling) + require.Equal(t, hash3.String(), proof[0].String()) + }) + + t.Run("large odd number of leaves", func(t *testing.T) { + // Test with 541 nodes (the example that was failing) + // Tree needs power of 2 capacity: 2^10 = 1024 > 541 + tree, err := NewTree(10) + require.NoError(t, err) + + // Add 541 nodes (odd number) + for i := 0; i < 541; i++ { + hash, _ := chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000001") + err = tree.AddNode(*hash, uint64(i), uint64(i*100)) + require.NoError(t, err) + } + + // Test GetMerkleProof for the last node (index 540) + // This was the one causing the panic + proof, err := tree.GetMerkleProof(540) + require.NoError(t, err) + require.NotNil(t, proof) + + // Should not panic and should return a valid proof + require.Greater(t, len(proof), 0) + + // The first hash in the proof should be the duplicate of the last node + // since 540 is even and has no sibling at index 541 + lastNodeHash := tree.Nodes[540].Hash + require.Equal(t, lastNodeHash.String(), proof[0].String()) + }) +} From 2fe96aea2bb43bb492210e33d445badaa9b1c054 Mon Sep 17 00:00:00 2001 From: Dylan Date: Tue, 25 Nov 2025 08:21:54 -0500 Subject: [PATCH 2/2] Fix lint --- subtree_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/subtree_test.go b/subtree_test.go index e952fcc..81e0771 100644 --- a/subtree_test.go +++ b/subtree_test.go @@ -1220,9 +1220,9 @@ func TestGetMerkleProofOddLeaves(t *testing.T) { require.NoError(t, err) // Add 541 nodes (odd number) - for i := 0; i < 541; i++ { + for i := uint64(0); i < uint64(541); i++ { hash, _ := chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000001") - err = tree.AddNode(*hash, uint64(i), uint64(i*100)) + err = tree.AddNode(*hash, i, i*100) require.NoError(t, err) } @@ -1233,7 +1233,7 @@ func TestGetMerkleProofOddLeaves(t *testing.T) { require.NotNil(t, proof) // Should not panic and should return a valid proof - require.Greater(t, len(proof), 0) + require.NotEmptyf(t, proof, "Merkle proof should not be empty") // The first hash in the proof should be the duplicate of the last node // since 540 is even and has no sibling at index 541