Skip to content

Commit 4bd35d9

Browse files
authored
Fix GetMerkleProof for odd numbered nodes (#66)
* Fix GetMerkleProof for odd numbered nodes * Fix lint
1 parent fe364c6 commit 4bd35d9

File tree

4 files changed

+104
-4
lines changed

4 files changed

+104
-4
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
module github.com/bsv-blockchain/go-subtree
22

3-
go 1.24.3
3+
go 1.24.6
44

55
require (
66
github.com/bsv-blockchain/go-bt/v2 v2.5.1
77
github.com/bsv-blockchain/go-safe-conversion v1.1.0
88
github.com/bsv-blockchain/go-tx-map v1.2.1
99
github.com/stretchr/testify v1.11.1
10-
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
10+
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b
1111
)
1212

1313
require (

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
2828
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
2929
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
3030
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
31-
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
32-
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
31+
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA=
32+
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
3333
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
3434
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
3535
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

subtree.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,12 @@ func (st *Subtree) GetMerkleProof(index int) ([]*chainhash.Hash, error) {
529529
// getLeafSiblingHash returns the hash of the sibling node at the leaf level
530530
func getLeafSiblingHash(nodes []Node, index int) *chainhash.Hash {
531531
if index%2 == 0 {
532+
// For even index, sibling is at index+1
533+
// But if index+1 is out of bounds (odd number of leaves),
534+
// duplicate the last node (Bitcoin convention)
535+
if index+1 >= len(nodes) {
536+
return &nodes[index].Hash
537+
}
532538
return &nodes[index+1].Hash
533539
}
534540

subtree_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,3 +1147,97 @@ func Benchmark_NodeIndex(b *testing.B) {
11471147
require.GreaterOrEqual(b, index, 0)
11481148
}
11491149
}
1150+
1151+
func TestGetMerkleProofOddLeaves(t *testing.T) {
1152+
t.Run("odd number of leaves in power-of-two tree", func(t *testing.T) {
1153+
// Create a subtree with capacity for 4 nodes but only add 3 (odd number)
1154+
tree, err := NewTree(2) // height 2 = 2^2 = 4 leaves
1155+
require.NoError(t, err)
1156+
1157+
// Add 3 nodes (odd number)
1158+
hash1, _ := chainhash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111")
1159+
hash2, _ := chainhash.NewHashFromStr("2222222222222222222222222222222222222222222222222222222222222222")
1160+
hash3, _ := chainhash.NewHashFromStr("3333333333333333333333333333333333333333333333333333333333333333")
1161+
1162+
err = tree.AddNode(*hash1, 100, 250)
1163+
require.NoError(t, err)
1164+
err = tree.AddNode(*hash2, 200, 300)
1165+
require.NoError(t, err)
1166+
err = tree.AddNode(*hash3, 150, 275)
1167+
require.NoError(t, err)
1168+
1169+
// Test GetMerkleProof for the last node (index 2)
1170+
// This should duplicate the last node as its sibling
1171+
proof, err := tree.GetMerkleProof(2)
1172+
require.NoError(t, err)
1173+
require.NotNil(t, proof)
1174+
1175+
// The first hash in the proof should be the duplicate of the last node
1176+
require.Equal(t, hash3.String(), proof[0].String())
1177+
})
1178+
1179+
t.Run("even number of leaves", func(t *testing.T) {
1180+
// Create a subtree with 4 nodes (even number)
1181+
tree, err := NewTreeByLeafCount(4)
1182+
require.NoError(t, err)
1183+
1184+
// Add 4 nodes
1185+
hash1, _ := chainhash.NewHashFromStr("1111111111111111111111111111111111111111111111111111111111111111")
1186+
hash2, _ := chainhash.NewHashFromStr("2222222222222222222222222222222222222222222222222222222222222222")
1187+
hash3, _ := chainhash.NewHashFromStr("3333333333333333333333333333333333333333333333333333333333333333")
1188+
hash4, _ := chainhash.NewHashFromStr("4444444444444444444444444444444444444444444444444444444444444444")
1189+
1190+
err = tree.AddNode(*hash1, 100, 250)
1191+
require.NoError(t, err)
1192+
err = tree.AddNode(*hash2, 200, 300)
1193+
require.NoError(t, err)
1194+
err = tree.AddNode(*hash3, 150, 275)
1195+
require.NoError(t, err)
1196+
err = tree.AddNode(*hash4, 175, 325)
1197+
require.NoError(t, err)
1198+
1199+
// Test GetMerkleProof for the second-to-last node (index 2)
1200+
proof, err := tree.GetMerkleProof(2)
1201+
require.NoError(t, err)
1202+
require.NotNil(t, proof)
1203+
1204+
// The first hash in the proof should be hash4 (its sibling)
1205+
require.Equal(t, hash4.String(), proof[0].String())
1206+
1207+
// Test GetMerkleProof for the last node (index 3)
1208+
proof, err = tree.GetMerkleProof(3)
1209+
require.NoError(t, err)
1210+
require.NotNil(t, proof)
1211+
1212+
// The first hash in the proof should be hash3 (its sibling)
1213+
require.Equal(t, hash3.String(), proof[0].String())
1214+
})
1215+
1216+
t.Run("large odd number of leaves", func(t *testing.T) {
1217+
// Test with 541 nodes (the example that was failing)
1218+
// Tree needs power of 2 capacity: 2^10 = 1024 > 541
1219+
tree, err := NewTree(10)
1220+
require.NoError(t, err)
1221+
1222+
// Add 541 nodes (odd number)
1223+
for i := uint64(0); i < uint64(541); i++ {
1224+
hash, _ := chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000001")
1225+
err = tree.AddNode(*hash, i, i*100)
1226+
require.NoError(t, err)
1227+
}
1228+
1229+
// Test GetMerkleProof for the last node (index 540)
1230+
// This was the one causing the panic
1231+
proof, err := tree.GetMerkleProof(540)
1232+
require.NoError(t, err)
1233+
require.NotNil(t, proof)
1234+
1235+
// Should not panic and should return a valid proof
1236+
require.NotEmptyf(t, proof, "Merkle proof should not be empty")
1237+
1238+
// The first hash in the proof should be the duplicate of the last node
1239+
// since 540 is even and has no sibling at index 541
1240+
lastNodeHash := tree.Nodes[540].Hash
1241+
require.Equal(t, lastNodeHash.String(), proof[0].String())
1242+
})
1243+
}

0 commit comments

Comments
 (0)