Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
6 changes: 6 additions & 0 deletions subtree.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
94 changes: 94 additions & 0 deletions subtree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 := uint64(0); i < uint64(541); i++ {
hash, _ := chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000001")
err = tree.AddNode(*hash, i, 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.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
lastNodeHash := tree.Nodes[540].Hash
require.Equal(t, lastNodeHash.String(), proof[0].String())
})
}
Loading