Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@
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))

Check failure on line 1225 in subtree_test.go

View workflow job for this annotation

GitHub Actions / 🪝 Pre-commit Checks / 🪝 Pre-commit Checks

G115: integer overflow conversion int -> uint64 (gosec)

Check failure on line 1225 in subtree_test.go

View workflow job for this annotation

GitHub Actions / 🪝 Pre-commit Checks / 🪝 Pre-commit Checks

G115: integer overflow conversion int -> uint64 (gosec)

Check failure on line 1225 in subtree_test.go

View workflow job for this annotation

GitHub Actions / 🪝 Pre-commit Checks / 🪝 Pre-commit Checks

G115: integer overflow conversion int -> uint64 (gosec)

Check failure on line 1225 in subtree_test.go

View workflow job for this annotation

GitHub Actions / 📊 Code Quality / ✨ Lint Code

G115: integer overflow conversion int -> uint64 (gosec)
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)

Check failure on line 1236 in subtree_test.go

View workflow job for this annotation

GitHub Actions / 🪝 Pre-commit Checks / 🪝 Pre-commit Checks

empty: use require.NotEmpty (testifylint)

Check failure on line 1236 in subtree_test.go

View workflow job for this annotation

GitHub Actions / 🪝 Pre-commit Checks / 🪝 Pre-commit Checks

empty: use require.NotEmpty (testifylint)

Check failure on line 1236 in subtree_test.go

View workflow job for this annotation

GitHub Actions / 🪝 Pre-commit Checks / 🪝 Pre-commit Checks

empty: use require.NotEmpty (testifylint)

Check failure on line 1236 in subtree_test.go

View workflow job for this annotation

GitHub Actions / 📊 Code Quality / ✨ Lint Code

empty: use require.NotEmpty (testifylint)

// 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