diff --git a/.github/workflows/buf-ci.yml b/.github/workflows/buf-ci.yml new file mode 100644 index 00000000..7d3a62d7 --- /dev/null +++ b/.github/workflows/buf-ci.yml @@ -0,0 +1,22 @@ +name: buf-ci +on: + push: + branches: + - main + pull_request: +permissions: + contents: read + pull-requests: write +jobs: + buf: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: bufbuild/buf-setup-action@v1 + - uses: bufbuild/buf-breaking-action@v1 + with: + input: pb + against: 'https://github.com/celestiaorg/nmt.git#branch=main,subdir=pb' + - uses: bufbuild/buf-lint-action@v1 + with: + input: pb diff --git a/.github/workflows/buf-release.yml b/.github/workflows/buf-release.yml new file mode 100644 index 00000000..98832465 --- /dev/null +++ b/.github/workflows/buf-release.yml @@ -0,0 +1,23 @@ +name: buf-release +on: + push: + tags: + - "v*" +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: bufbuild/buf-setup-action@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + version: "1.44.0" + # Push the protobuf definitions to the BSR + - uses: bufbuild/buf-push-action@v1 + with: + buf_token: ${{ secrets.BUF_TOKEN }} + - name: "push the tag label to BSR" + run: | + set -euo pipefail + echo ${{ secrets.BUF_TOKEN }} | buf registry login --token-stdin + buf push --label ${{ github.ref_name }} diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 00000000..da4e3b12 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,12 @@ +version: v2 +modules: + - path: pb + name: buf.build/celestia/nmt +lint: + ignore_only: + PACKAGE_DIRECTORY_MATCH: + # ignoring because fixing means we will do a breaking change + - pb/proof.proto + PACKAGE_VERSION_SUFFIX: + # ignoring because fixing means we will do a breaking change + - pb/proof.proto diff --git a/go.mod b/go.mod index de29c96b..749201e0 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/gogo/protobuf v1.3.2 github.com/google/gofuzz v1.2.0 github.com/stretchr/testify v1.9.0 - github.com/tidwall/gjson v1.17.1 + github.com/tidwall/gjson v1.18.0 ) require ( diff --git a/go.sum b/go.sum index 05a31510..06b10d63 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= -github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= diff --git a/pb/proof.proto b/pb/proof.proto index 431685a6..9b48d392 100644 --- a/pb/proof.proto +++ b/pb/proof.proto @@ -1,4 +1,4 @@ -syntax="proto3"; +syntax = "proto3"; package proof.pb; @@ -6,10 +6,10 @@ option go_package = "github.com/celestiaorg/nmt/pb"; message Proof { // Start index of the leaves that match the queried namespace.ID. - int64 start = 1; + int64 start = 1; // End index (non-inclusive) of the leaves that match the queried // namespace.ID. - int64 end = 2; + int64 end = 2; // Nodes hold the tree nodes necessary for the Merkle range proof. repeated bytes nodes = 3; // leaf_hash contains the namespace.ID if NMT does not have it and @@ -18,5 +18,5 @@ message Proof { bytes leaf_hash = 4; // The is_max_namespace_ignored flag influences the calculation of the // namespace ID range for intermediate nodes in the tree. - bool is_max_namespace_ignored=5; -} \ No newline at end of file + bool is_max_namespace_ignored = 5; +} diff --git a/proof.go b/proof.go index 608deb9c..57b38b9f 100644 --- a/proof.go +++ b/proof.go @@ -528,6 +528,13 @@ func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]b return popIfNonEmpty(&subtreeRoots), nil } + if end-start == 1 { + // At this level, we reached a leaf, but we couldn't find any range corresponding + // to needed leaf [start, end). + // This means that the initial provided [start, end) range was invalid. + return nil, fmt.Errorf("the provided range [%d, %d) does not reference a valid inner node", proof.start, proof.end) + } + // Recursively get left and right subtree k := getSplitPoint(end - start) left, err := computeRoot(start, start+k) @@ -634,7 +641,13 @@ func nextLeafRange(currentStart, currentEnd, subtreeWidth int) (LeafRange, error if err != nil { return LeafRange{}, err } - return LeafRange{Start: currentStart, End: currentStart + currentRange}, nil + rangeEnd := currentStart + currentRange + idealTreeSize := nextSubtreeSize(uint64(currentStart), uint64(rangeEnd)) + if currentStart+idealTreeSize != rangeEnd { + // this will happen if the calculated range does not correctly reference an inner node in the tree. + return LeafRange{}, fmt.Errorf("provided subtree width %d doesn't allow creating a valid leaf range [%d, %d)", subtreeWidth, currentStart, rangeEnd) + } + return LeafRange{Start: currentStart, End: rangeEnd}, nil } // largestPowerOfTwo calculates the largest power of two diff --git a/proof_test.go b/proof_test.go index b2ea98fe..cb208cdf 100644 --- a/proof_test.go +++ b/proof_test.go @@ -1332,18 +1332,6 @@ func TestNextLeafRange(t *testing.T) { subtreeRootMaximumLeafRange: 8, expectedRange: LeafRange{Start: 4, End: 8}, }, - { - currentStart: 4, - currentEnd: 20, - subtreeRootMaximumLeafRange: 16, - expectedRange: LeafRange{Start: 4, End: 20}, - }, - { - currentStart: 4, - currentEnd: 20, - subtreeRootMaximumLeafRange: 1, - expectedRange: LeafRange{Start: 4, End: 5}, - }, { currentStart: 4, currentEnd: 20, @@ -1356,12 +1344,6 @@ func TestNextLeafRange(t *testing.T) { subtreeRootMaximumLeafRange: 4, expectedRange: LeafRange{Start: 4, End: 8}, }, - { - currentStart: 4, - currentEnd: 20, - subtreeRootMaximumLeafRange: 8, - expectedRange: LeafRange{Start: 4, End: 12}, - }, { currentStart: 0, currentEnd: 1, @@ -1392,6 +1374,36 @@ func TestNextLeafRange(t *testing.T) { subtreeRootMaximumLeafRange: 0, expectError: true, }, + { // A range not referencing any inner node + currentStart: 1, + currentEnd: 3, + subtreeRootMaximumLeafRange: 4, + expectError: true, + }, + { // A range not referencing any inner node + currentStart: 1, + currentEnd: 5, + subtreeRootMaximumLeafRange: 4, + expectError: true, + }, + { // A range not referencing any inner node + currentStart: 1, + currentEnd: 6, + subtreeRootMaximumLeafRange: 4, + expectError: true, + }, + { // A range not referencing any inner node + currentStart: 1, + currentEnd: 7, + subtreeRootMaximumLeafRange: 4, + expectError: true, + }, + { // A range not referencing any inner node + currentStart: 2, + currentEnd: 8, + subtreeRootMaximumLeafRange: 4, + expectError: true, + }, } for _, tt := range tests { @@ -1798,7 +1810,6 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { root: root, expectError: true, }, - { proof: func() Proof { p, err := tree.ProveRange(0, 8) @@ -1828,3 +1839,31 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { }) } } + +// TestVerifySubtreeRootInclusion_infiniteRecursion is motivated by a failing test +// case in celestia-node +func TestVerifySubtreeRootInclusion_infiniteRecursion(t *testing.T) { + namespaceIDs := bytes.Repeat([]byte{1}, 64) + tree := exampleNMT(1, true, namespaceIDs...) + root, err := tree.Root() + require.NoError(t, err) + + nmthasher := tree.treeHasher + hasher := nmthasher.(*NmtHasher) + subtreeRoot, err := tree.ComputeSubtreeRoot(0, 4) + require.NoError(t, err) + subtreeRoots := [][]byte{subtreeRoot, subtreeRoot, subtreeRoot, subtreeRoot, subtreeRoot, subtreeRoot, subtreeRoot} + subtreeWidth := 8 + + proof, err := tree.ProveRange(19, 64) + require.NoError(t, err) + + require.NotPanics(t, func() { + // This previously hits: + // runtime: goroutine stack exceeds 1000000000-byte limit + // runtime: sp=0x14020160480 stack=[0x14020160000, 0x14040160000] + // fatal error: stack overflow + _, err = proof.VerifySubtreeRootInclusion(hasher, subtreeRoots, subtreeWidth, root) + require.Error(t, err) + }) +}