From 51df389ac1dfc7a73a7305a312e3edbb855b19b0 Mon Sep 17 00:00:00 2001 From: sweexordious Date: Mon, 27 May 2024 12:34:26 +0400 Subject: [PATCH 01/16] feat: draft verify power 2 --- proof.go | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/proof.go b/proof.go index 6e824e5..e21c958 100644 --- a/proof.go +++ b/proof.go @@ -402,6 +402,130 @@ func (proof Proof) VerifyLeafHashes(nth *NmtHasher, verifyCompleteness bool, nID return bytes.Equal(rootHash, root), nil } +// The VerifyPowerOfTwoInnerInclusion function checks whether the given proof is a valid Merkle +// range proof for the leaves in the leafHashes input. It returns true or false accordingly. +// If there is an issue during the proof verification e.g., a node does not conform to the namespace hash format, then a proper error is returned to indicate the root cause of the issue. +// The leafHashes parameter is a list of leaf hashes, where each leaf hash is represented +// by a byte slice. +// The size of leafHashes should match the proof range i.e., end-start. +// If the verifyCompleteness parameter is set to true, the function also checks +// the completeness of the proof by verifying that there is no leaf in the +// tree represented by the root parameter that matches the namespace ID nID +// outside the leafHashes list. +func (proof Proof) VerifyPowerOfTwoInnerInclusion(nth *NmtHasher, innerNodes [][]byte, root []byte) (bool, error) { + // check that the proof range is valid + if proof.Start() < 0 || proof.Start() >= proof.End() { + return false, fmt.Errorf("proof range [proof.start=%d, proof.end=%d) is not valid: %w", proof.Start(), proof.End(), ErrInvalidRange) + } + + // check whether the number of leaves match the proof range i.e., end-start. + // If not, make an early return. + expectedLeafHashesCount := proof.End() - proof.Start() + if len(leafHashes) != expectedLeafHashesCount { + return false, fmt.Errorf( + "supplied leafHashes size %d, expected size %d: %w", + len(leafHashes), expectedLeafHashesCount, ErrWrongLeafHashesSize) + } + + // check that the root is valid w.r.t the NMT hasher + if err := nth.ValidateNodeFormat(root); err != nil { + return false, fmt.Errorf("root does not match the NMT hasher's hash format: %w", err) + } + // check that all the proof.nodes are valid w.r.t the NMT hasher + for _, node := range proof.nodes { + if err := nth.ValidateNodeFormat(node); err != nil { + return false, fmt.Errorf("proof nodes do not match the NMT hasher's hash format: %w", err) + } + } + // check that all the leafHashes are valid w.r.t the NMT hasher + for _, leafHash := range innerNodes { + if err := nth.ValidateNodeFormat(leafHash); err != nil { + return false, fmt.Errorf("inner nodes does not match the NMT hasher's hash format: %w", err) + } + } + + // TODO the proof contains a leaf hash for an absence proof. + // Can we have proof of absence using subtree roots? + + type innerNodeRange struct { + start int + end int + } + + var ranges []innerNodeRange + + var computeRoot func(start, end int) ([]byte, error) + // computeRoot can return error iff the HashNode function fails while calculating the root + computeRoot = func(start, end int) ([]byte, error) { + + // if current range does not overlap with the proof range, pop and + // return a proof node if present, else return nil because subtree + // doesn't exist + if end <= proof.Start() || start >= proof.End() { + return popIfNonEmpty(&proof.nodes), nil + } + + // Recursively get left and right subtree + k := getSplitPoint(end - start) + left, err := computeRoot(start, start+k) + if err != nil { + return nil, fmt.Errorf("failed to compute subtree root [%d, %d): %w", start, start+k, err) + } + right, err := computeRoot(start+k, end) + if err != nil { + return nil, fmt.Errorf("failed to compute subtree root [%d, %d): %w", start+k, end, err) + } + + // only right leaf/subtree can be non-existent + if right == nil { + return left, nil + } + hash, err := nth.HashNode(left, right) + if err != nil { + return nil, fmt.Errorf("failed to hash node: %w", err) + } + return hash, nil + } + + // estimate the leaf size of the subtree containing the proof range + proofRangeSubtreeEstimate := getSplitPoint(proof.end) * 2 + if proofRangeSubtreeEstimate < 1 { + proofRangeSubtreeEstimate = 1 + } + + proofRange := proof.end - proof.start + + for proofRange != 0 { + subtreeRootRange := largestPowerOfTwo(uint(proofRange)) + subtreeRootEndIndex := proof.start + subtreeRootRange + ranges = append(ranges, innerNodeRange{proof.start, subtreeRootEndIndex}) + proofRange -= subtreeRootRange + } + + if end == subtreeRootEndIndex { + return popIfNonEmpty(&innerNodes), nil // Sou + } + + rootHash, err := computeRoot(0, proofRangeSubtreeEstimate) + if err != nil { + return false, fmt.Errorf("failed to compute root [%d, %d): %w", 0, proofRangeSubtreeEstimate, err) + } + for i := 0; i < len(proof.nodes); i++ { + rootHash, err = nth.HashNode(rootHash, proof.nodes[i]) + if err != nil { + return false, fmt.Errorf("failed to hash node: %w", err) + } + } + + return bytes.Equal(rootHash, root), nil +} + +// largestPowerOfTwo calculates the largest power of two +// that is smaller than 'bound' +func largestPowerOfTwo(bound uint) int { + return 1 << (bits.Len(bound) - 1) +} + // VerifyInclusion checks that the inclusion proof is valid by using leaf data // and the provided proof to regenerate and compare the root. Note that the leavesWithoutNamespace data should not contain the prefixed namespace, unlike the tree.Push method, // which takes prefixed data. All leaves implicitly have the same namespace ID: From 4daae42cc06ed890cb035c7b776928c281fc2400 Mon Sep 17 00:00:00 2001 From: sweexordious Date: Thu, 30 May 2024 01:38:17 +0400 Subject: [PATCH 02/16] feat: add support for subtree roots proof verification --- nmt.go | 35 +++ nmt_test.go | 172 ++++++++++++ proof.go | 310 ++++++++++++++-------- proof_test.go | 715 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1125 insertions(+), 107 deletions(-) diff --git a/nmt.go b/nmt.go index e9c318a..5d2aa7f 100644 --- a/nmt.go +++ b/nmt.go @@ -644,6 +644,41 @@ func (n *NamespacedMerkleTree) updateMinMaxID(id namespace.ID) { } } +// ComputeSubtreeRoot takes a leaf range and returns the corresponding subtree root. +// This method requires the merkle tree size to be a power of two. +// Also, it requires the start and end range to correctly reference an inner node. +func (n *NamespacedMerkleTree) ComputeSubtreeRoot(start, end int) ([]byte, error) { + // check if the tree's number of leaves is a power of two. + if !isPowerOfTwo(n.Size()) { + return nil, fmt.Errorf("the tree size %d needs to be a power of two", n.Size()) + } + if start < 0 { + return nil, fmt.Errorf("start %d shouldn't be strictly negative", start) + } + if end <= start { + return nil, fmt.Errorf("end %d should be stricly bigger than start %d", end, start) + } + uStart, err := safeIntToUint(start) + if err != nil { + return nil, err + } + uEnd, err := safeIntToUint(end) + if err != nil { + return nil, err + } + // check if the provided range correctly references an inner node. + // calculates the ideal tree from the provided range, and verifies if it is the same as the range + if idealTreeRange := nextSubtreeSize(uint64(uStart), uint64(uEnd)); end-start != idealTreeRange { + return nil, fmt.Errorf("the provided range [%d, %d) does not construct a valid subtree root range", start, end) + } + return n.computeRoot(start, end) +} + +// isPowerOfTwo checks if a number is a power of two +func isPowerOfTwo(n int) bool { + return n > 0 && (n&(n-1)) == 0 +} + type leafRange struct { // start and end denote the indices of a leaf in the tree. start ranges from // 0 up to the total number of leaves minus 1 end ranges from 1 up to the diff --git a/nmt_test.go b/nmt_test.go index 6e0565e..b10e0e4 100644 --- a/nmt_test.go +++ b/nmt_test.go @@ -862,6 +862,20 @@ func exampleNMT(nidSize int, ignoreMaxNamespace bool, leavesNIDs ...byte) *Names return tree } +// exampleNMT2 Replica of exampleNMT except that it uses the namespace IDs in the +// leaves instead of the index. +func exampleNMT2(nidSize int, ignoreMaxNamespace bool, leavesNIDs ...byte) *NamespacedMerkleTree { + tree := New(sha256.New(), NamespaceIDSize(nidSize), IgnoreMaxNamespace(ignoreMaxNamespace)) + for _, nid := range leavesNIDs { + namespace := bytes.Repeat([]byte{nid}, nidSize) + d := append(namespace, []byte(fmt.Sprintf("leaf_%d", nid))...) + if err := tree.Push(d); err != nil { + panic(fmt.Sprintf("unexpected error: %v", err)) + } + } + return tree +} + func swap(slice [][]byte, i int, j int) { temp := slice[i] slice[i] = slice[j] @@ -1175,3 +1189,161 @@ func TestForcedOutOfOrderNamespacedMerkleTree(t *testing.T) { assert.NoError(t, err) } } + +func TestIsPowerOfTwo(t *testing.T) { + tests := []struct { + input int + expected bool + }{ + {input: 0, expected: false}, + {input: 1, expected: true}, + {input: 2, expected: true}, + {input: 3, expected: false}, + {input: 4, expected: true}, + {input: 5, expected: false}, + {input: 8, expected: true}, + {input: 16, expected: true}, + {input: -1, expected: false}, + {input: -2, expected: false}, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("input=%d", tt.input), func(t *testing.T) { + result := isPowerOfTwo(tt.input) + if result != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestComputeSubtreeRoot(t *testing.T) { + n := exampleNMT2(1, true, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) + tests := []struct { + start, end int + tree *NamespacedMerkleTree + expectedRoot []byte + expectError bool + }{ + { + start: 0, + end: 16, + tree: n, + expectedRoot: func() []byte { + root, err := n.Root() + require.NoError(t, err) + return root + }(), + }, + { + start: 0, + end: 8, + tree: n, + expectedRoot: func() []byte { + // because the root of the range [0,8) coincides with the root of this tree + root, err := exampleNMT2(1, true, 0, 1, 2, 3, 4, 5, 6, 7).Root() + require.NoError(t, err) + return root + }(), + }, + { + start: 8, + end: 16, + tree: n, + expectedRoot: func() []byte { + // because the root of the range [8,16) coincides with the root of this tree + root, err := exampleNMT2(1, true, 8, 9, 10, 11, 12, 13, 14, 15).Root() + require.NoError(t, err) + return root + }(), + }, + { + start: 8, + end: 12, + tree: n, + expectedRoot: func() []byte { + // because the root of the range [8,12) coincides with the root of this tree + root, err := exampleNMT2(1, true, 8, 9, 10, 11).Root() + require.NoError(t, err) + return root + }(), + }, + { + start: 4, + end: 8, + tree: n, + expectedRoot: func() []byte { + // because the root of the range [4,8) coincides with the root of this tree + root, err := exampleNMT2(1, true, 4, 5, 6, 7).Root() + require.NoError(t, err) + return root + }(), + }, + { + start: 4, + end: 6, + tree: n, + expectedRoot: func() []byte { + // because the root of the range [4,6) coincides with the root of this tree + root, err := exampleNMT2(1, true, 4, 5).Root() + require.NoError(t, err) + return root + }(), + }, + { + start: 4, + end: 5, + tree: n, + expectedRoot: func() []byte { + // because the root of the range [4,5) coincides with the root of this tree + root, err := exampleNMT2(1, true, 4).Root() + require.NoError(t, err) + return root + }(), + }, + { // doesn't correctly reference an inner node + start: 2, + end: 6, + tree: n, + expectError: true, + }, + { + start: -1, // invalid start + end: 4, + tree: n, + expectError: true, + }, + { + start: 4, + end: 4, // start == end + tree: n, + expectError: true, + }, + { + start: 5, // start >= end + end: 4, + tree: n, + expectError: true, + }, + { + start: 0, + end: 16, + tree: func() *NamespacedMerkleTree { + return exampleNMT2(1, true, 0, 1, 2, 3, 4) // tree leaves are not a power of 2 + }(), + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("treeSize=%d,start=%d,end=%d", tt.tree.Size(), tt.start, tt.end), func(t *testing.T) { + root, err := tt.tree.ComputeSubtreeRoot(tt.start, tt.end) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedRoot, root) + } + }) + } +} diff --git a/proof.go b/proof.go index e21c958..babf1c5 100644 --- a/proof.go +++ b/proof.go @@ -9,7 +9,7 @@ import ( "math/bits" "github.com/celestiaorg/nmt/namespace" - pb "github.com/celestiaorg/nmt/pb" + "github.com/celestiaorg/nmt/pb" ) var ( @@ -402,69 +402,147 @@ func (proof Proof) VerifyLeafHashes(nth *NmtHasher, verifyCompleteness bool, nID return bytes.Equal(rootHash, root), nil } -// The VerifyPowerOfTwoInnerInclusion function checks whether the given proof is a valid Merkle -// range proof for the leaves in the leafHashes input. It returns true or false accordingly. -// If there is an issue during the proof verification e.g., a node does not conform to the namespace hash format, then a proper error is returned to indicate the root cause of the issue. -// The leafHashes parameter is a list of leaf hashes, where each leaf hash is represented -// by a byte slice. -// The size of leafHashes should match the proof range i.e., end-start. -// If the verifyCompleteness parameter is set to true, the function also checks -// the completeness of the proof by verifying that there is no leaf in the -// tree represented by the root parameter that matches the namespace ID nID -// outside the leafHashes list. -func (proof Proof) VerifyPowerOfTwoInnerInclusion(nth *NmtHasher, innerNodes [][]byte, root []byte) (bool, error) { - // check that the proof range is valid - if proof.Start() < 0 || proof.Start() >= proof.End() { - return false, fmt.Errorf("proof range [proof.start=%d, proof.end=%d) is not valid: %w", proof.Start(), proof.End(), ErrInvalidRange) +// VerifyInclusion checks that the inclusion proof is valid by using leaf data +// and the provided proof to regenerate and compare the root. Note that the leavesWithoutNamespace data should not contain the prefixed namespace, unlike the tree.Push method, +// which takes prefixed data. All leaves implicitly have the same namespace ID: +// `nid`. +// The size of the leavesWithoutNamespace should be equal to the proof range i.e., end-start. +// VerifyInclusion does not verify the completeness of the proof, so it's possible for leavesWithoutNamespace to be a subset of the leaves in the tree that have the namespace ID nid. +func (proof Proof) VerifyInclusion(h hash.Hash, nid namespace.ID, leavesWithoutNamespace [][]byte, root []byte) bool { + // check the range of the proof + isEmptyRange := proof.start == proof.end + if isEmptyRange { + // the only case in which an empty proof is valid is when the supplied leavesWithoutNamespace is also empty. + // rationale: no proof (i.e., an empty proof) is needed to prove that an empty set of leaves belong to the tree with root `root`. + // unlike VerifyNamespace(), we do not care about the queried `nid` here, because VerifyInclusion does not verify the completeness of the proof + // i.e., whether the leavesWithoutNamespace is the full set of leaves matching the queried `nid`. + if proof.IsEmptyProof() && len(leavesWithoutNamespace) == 0 { + return true + } + // if the proof range is empty but !proof.IsEmptyProof() || len(leavesWithoutNamespace) != 0, then the verification should fail + return false } - // check whether the number of leaves match the proof range i.e., end-start. + // check whether the number of leavesWithoutNamespace match the proof range i.e., end-start. // If not, make an early return. - expectedLeafHashesCount := proof.End() - proof.Start() - if len(leafHashes) != expectedLeafHashesCount { - return false, fmt.Errorf( - "supplied leafHashes size %d, expected size %d: %w", - len(leafHashes), expectedLeafHashesCount, ErrWrongLeafHashesSize) + expectedLeavesCount := proof.End() - proof.Start() + if len(leavesWithoutNamespace) != expectedLeavesCount { + return false } + nth := NewNmtHasher(h, nid.Size(), proof.isMaxNamespaceIDIgnored) + + // perform some consistency checks: // check that the root is valid w.r.t the NMT hasher if err := nth.ValidateNodeFormat(root); err != nil { - return false, fmt.Errorf("root does not match the NMT hasher's hash format: %w", err) + return false } // check that all the proof.nodes are valid w.r.t the NMT hasher for _, node := range proof.nodes { + if err := nth.ValidateNodeFormat(node); err != nil { + return false + } + } + + // add namespace to all the leaves + hashes := make([][]byte, len(leavesWithoutNamespace)) + for i, d := range leavesWithoutNamespace { + // prepend the namespace to the leaf data + leafData := append( + append(make([]byte, 0, len(d)+len(nid)), nid...), d..., + ) + res, err := nth.HashLeaf(leafData) + if err != nil { + return false // this never can happen since the leafData is guaranteed to be namespaced + } + hashes[i] = res + } + + res, err := proof.VerifyLeafHashes(nth, false, nid, hashes, root) + if err != nil { + return false + } + return res +} + +// VerifySubtreeRootInclusion verifies that a set of subtree roots is included in +// an NMT. +// Note: This method is Celestia specific. +// It makes the following assumptions: +// - The subtree roots are created according to the ADR-013 +// https://github.com/celestiaorg/celestia-app/blob/main/docs/architecture/adr-013-non-interactive-default-rules-for-zero-padding.md +// - The tree's number of leaves is a power of two +// Using this method without making sure the above assumptions are respected +// can return invalid results. +// The subtreeRootThreshold is also defined in ADR-013. +// More information on the algorithm used can be found in the toLeafRanges() method docs. +func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]byte, subtreeRootThreshold int, root []byte) (bool, error) { + // check that the proof range is valid + if proof.Start() < 0 || proof.Start() >= proof.End() { + return false, fmt.Errorf("proof range [proof.start=%d, proof.end=%d) is not valid: %w", proof.Start(), proof.End(), ErrInvalidRange) + } + + // check that the root is valid w.r.t the NMT hasher + if err := nth.ValidateNodeFormat(root); err != nil { + return false, fmt.Errorf("root does not match the NMT hasher's hash format: %w", err) + } + // check that all the proof.Notes() are valid w.r.t the NMT hasher + for _, node := range proof.Nodes() { if err := nth.ValidateNodeFormat(node); err != nil { return false, fmt.Errorf("proof nodes do not match the NMT hasher's hash format: %w", err) } } - // check that all the leafHashes are valid w.r.t the NMT hasher - for _, leafHash := range innerNodes { - if err := nth.ValidateNodeFormat(leafHash); err != nil { + // check that all the subtree roots are valid w.r.t the NMT hasher + for _, subtreeRoot := range subtreeRoots { + if err := nth.ValidateNodeFormat(subtreeRoot); err != nil { return false, fmt.Errorf("inner nodes does not match the NMT hasher's hash format: %w", err) } } - // TODO the proof contains a leaf hash for an absence proof. - // Can we have proof of absence using subtree roots? - - type innerNodeRange struct { - start int - end int + // get the subtree roots leaf ranges + ranges, err := toLeafRanges(proof.Start(), proof.End(), subtreeRootThreshold) + if err != nil { + return false, err } - var ranges []innerNodeRange + // check whether the number of ranges matches the number of subtree roots. + // if not, make an early return. + if len(subtreeRoots) != len(ranges) { + return false, fmt.Errorf("number of subtree roots %d is different than the number of leaf ranges %d", len(subtreeRoots), len(ranges)) + } var computeRoot func(start, end int) ([]byte, error) // computeRoot can return error iff the HashNode function fails while calculating the root computeRoot = func(start, end int) ([]byte, error) { + // reached a leaf + if end-start == 1 { + // if the leaf index falls within the proof range, pop and return a + // leaf + if proof.Start() <= start && start < proof.End() { + // advance the list of ranges + ranges = ranges[1:] + // advance leafHashes + return popIfNonEmpty(&subtreeRoots), nil + } - // if current range does not overlap with the proof range, pop and + // if the leaf index is outside the proof range, pop and return a + // proof node (which in this case is a leaf) if present, else return + // nil because leaf doesn't exist + return popIfNonEmpty(&proof.nodes), nil + } + + // if the current range does not overlap with the proof range, pop and // return a proof node if present, else return nil because subtree // doesn't exist if end <= proof.Start() || start >= proof.End() { return popIfNonEmpty(&proof.nodes), nil } + if len(ranges) != 0 && ranges[0].start == start && ranges[0].end == end { + ranges = ranges[1:] + return popIfNonEmpty(&subtreeRoots), nil + } + // Recursively get left and right subtree k := getSplitPoint(end - start) left, err := computeRoot(start, start+k) @@ -488,30 +566,16 @@ func (proof Proof) VerifyPowerOfTwoInnerInclusion(nth *NmtHasher, innerNodes [][ } // estimate the leaf size of the subtree containing the proof range - proofRangeSubtreeEstimate := getSplitPoint(proof.end) * 2 + proofRangeSubtreeEstimate := getSplitPoint(proof.End()) * 2 if proofRangeSubtreeEstimate < 1 { proofRangeSubtreeEstimate = 1 } - - proofRange := proof.end - proof.start - - for proofRange != 0 { - subtreeRootRange := largestPowerOfTwo(uint(proofRange)) - subtreeRootEndIndex := proof.start + subtreeRootRange - ranges = append(ranges, innerNodeRange{proof.start, subtreeRootEndIndex}) - proofRange -= subtreeRootRange - } - - if end == subtreeRootEndIndex { - return popIfNonEmpty(&innerNodes), nil // Sou - } - rootHash, err := computeRoot(0, proofRangeSubtreeEstimate) if err != nil { return false, fmt.Errorf("failed to compute root [%d, %d): %w", 0, proofRangeSubtreeEstimate, err) } - for i := 0; i < len(proof.nodes); i++ { - rootHash, err = nth.HashNode(rootHash, proof.nodes[i]) + for i := 0; i < len(proof.Nodes()); i++ { + rootHash, err = nth.HashNode(rootHash, proof.Nodes()[i]) if err != nil { return false, fmt.Errorf("failed to hash node: %w", err) } @@ -520,73 +584,91 @@ func (proof Proof) VerifyPowerOfTwoInnerInclusion(nth *NmtHasher, innerNodes [][ return bytes.Equal(rootHash, root), nil } -// largestPowerOfTwo calculates the largest power of two -// that is smaller than 'bound' -func largestPowerOfTwo(bound uint) int { - return 1 << (bits.Len(bound) - 1) -} - -// VerifyInclusion checks that the inclusion proof is valid by using leaf data -// and the provided proof to regenerate and compare the root. Note that the leavesWithoutNamespace data should not contain the prefixed namespace, unlike the tree.Push method, -// which takes prefixed data. All leaves implicitly have the same namespace ID: -// `nid`. -// The size of the leavesWithoutNamespace should be equal to the proof range i.e., end-start. -// VerifyInclusion does not verify the completeness of the proof, so it's possible for leavesWithoutNamespace to be a subset of the leaves in the tree that have the namespace ID nid. -func (proof Proof) VerifyInclusion(h hash.Hash, nid namespace.ID, leavesWithoutNamespace [][]byte, root []byte) bool { - // check the range of the proof - isEmptyRange := proof.start == proof.end - if isEmptyRange { - // the only case in which an empty proof is valid is when the supplied leavesWithoutNamespace is also empty. - // rationale: no proof (i.e., an empty proof) is needed to prove that an empty set of leaves belong to the tree with root `root`. - // unlike VerifyNamespace(), we do not care about the queried `nid` here, because VerifyInclusion does not verify the completeness of the proof - // i.e., whether the leavesWithoutNamespace is the full set of leaves matching the queried `nid`. - if proof.IsEmptyProof() && len(leavesWithoutNamespace) == 0 { - return true - } - // if the proof range is empty but !proof.IsEmptyProof() || len(leavesWithoutNamespace) != 0, then the verification should fail - return false +// toLeafRanges returns the leaf ranges corresponding to the provided subtree roots. +// It uses the subtree root threshold to calculate the maximum number of leaves a subtree root can +// commit to. +// The subtree root threshold is defined as per ADR-013: +// https://github.com/celestiaorg/celestia-app/blob/main/docs/architecture/adr-013-non-interactive-default-rules-for-zero-padding.md +// This method assumes: +// - The subtree roots are created according to the ADR-013 non-interactive defaults rules +// - The tree's number of leaves is a power of two +// The algorithm is as follows: +// - Let `d` be `y - x` (the range of the proof). +// - `i` is the index of the next subtree root. +// - While `d != 0`: +// - Let `z` be the largest power of 2 that fits in `d`; here we are finding the range for the next subtree root. +// - The range for the next subtree root is `[x, x + z)`, i.e., `S_i` is the subtree root of leaves at indices `[x, x + z)`. +// - `d = d - z` (move past the first subtree root and its range). +// - `i = i + 1`. +// - Go back to the loop condition. +// +// Note: This method is Celestia specific. +func toLeafRanges(proofStart, proofEnd, subtreeRootThreshold int) ([]leafRange, error) { + if proofStart < 0 { + return nil, fmt.Errorf("proof start %d shouldn't be strictly negative", proofStart) } - - // check whether the number of leavesWithoutNamespace match the proof range i.e., end-start. - // If not, make an early return. - expectedLeavesCount := proof.End() - proof.Start() - if len(leavesWithoutNamespace) != expectedLeavesCount { - return false + if proofEnd <= proofStart { + return nil, fmt.Errorf("proof end %d should be stricly bigger than proof start %d", proofEnd, proofStart) } - - nth := NewNmtHasher(h, nid.Size(), proof.isMaxNamespaceIDIgnored) - - // perform some consistency checks: - // check that the root is valid w.r.t the NMT hasher - if err := nth.ValidateNodeFormat(root); err != nil { - return false + if subtreeRootThreshold < 0 { + return nil, fmt.Errorf("subtree root threshold cannot be negative %d", subtreeRootThreshold) } - // check that all the proof.nodes are valid w.r.t the NMT hasher - for _, node := range proof.nodes { - if err := nth.ValidateNodeFormat(node); err != nil { - return false - } + currentStart := proofStart + currentLeafRange := proofEnd - proofStart + var ranges []leafRange + maximumLeafRange, err := subtreeRootThresholdToLeafRange(subtreeRootThreshold) + if err != nil { + return nil, err } - - // add namespace to all the leaves - hashes := make([][]byte, len(leavesWithoutNamespace)) - for i, d := range leavesWithoutNamespace { - // prepend the namespace to the leaf data - leafData := append( - append(make([]byte, 0, len(d)+len(nid)), nid...), d..., - ) - res, err := nth.HashLeaf(leafData) + for currentLeafRange != 0 { + nextRange, err := nextLeafRange(currentStart, proofEnd, maximumLeafRange) if err != nil { - return false // this never can happen since the leafData is guaranteed to be namespaced + return nil, err } - hashes[i] = res + ranges = append(ranges, nextRange) + currentStart = nextRange.end + currentLeafRange = currentLeafRange - nextRange.end + nextRange.start } + return ranges, nil +} - res, err := proof.VerifyLeafHashes(nth, false, nid, hashes, root) +// subtreeRootThresholdToLeafRange calculates the maximum number of leaves a subtree root +// can commit to. +// The subtree root threshold is defined as per ADR-013: +// https://github.com/celestiaorg/celestia-app/blob/main/docs/architecture/adr-013-non-interactive-default-rules-for-zero-padding.md +func subtreeRootThresholdToLeafRange(subtreeRootThreshold int) (int, error) { + if subtreeRootThreshold < 0 { + return 0, fmt.Errorf("subtree root threshold cannot be negative %d", subtreeRootThreshold) + } + return 1 << subtreeRootThreshold, nil +} + +// nextLeafRange takes a proof start, proof end, and the maximum range a subtree +// root can cover, and returns the corresponding subtree root range. +// The subtreeRootMaximum LeafRange is calculated using subtreeRootThresholdToLeafRange() method. +// Check toLeafRanges() for more information on the algorithm used. +// Note: This method is Celestia specific. +func nextLeafRange(currentStart, currentEnd, subtreeRootMaximumLeafRange int) (leafRange, error) { + currentLeafRange := currentEnd - currentStart + minimum := minInt(currentLeafRange, subtreeRootMaximumLeafRange) + uMinimum, err := safeIntToUint(minimum) if err != nil { - return false + return leafRange{}, fmt.Errorf("failed to convert subtree root range to Uint %w", err) } - return res + currentRange, err := largestPowerOfTwo(uMinimum) + if err != nil { + return leafRange{}, err + } + return leafRange{start: currentStart, end: currentStart + currentRange}, nil +} + +// largestPowerOfTwo calculates the largest power of two +// that is smaller than 'bound' +func largestPowerOfTwo(bound uint) (int, error) { + if bound == 0 { + return 0, fmt.Errorf("bound cannot be equal to 0") + } + return 1 << (bits.Len(bound) - 1), nil } // ProtoToProof creates a proof from its proto representation. @@ -636,3 +718,17 @@ func popIfNonEmpty(s *[][]byte) []byte { } return nil } + +func safeIntToUint(val int) (uint, error) { + if val < 0 { + return 0, fmt.Errorf("cannot convert a negative int %d to uint", val) + } + return uint(val), nil +} + +func minInt(val1, val2 int) int { + if val1 > val2 { + return val2 + } + return val1 +} diff --git a/proof_test.go b/proof_test.go index 235a403..2c9d957 100644 --- a/proof_test.go +++ b/proof_test.go @@ -3,6 +3,7 @@ package nmt import ( "bytes" "crypto/sha256" + "fmt" "hash" "testing" @@ -1124,3 +1125,717 @@ func Test_ProtoToProof(t *testing.T) { }) } } +func TestLargestPowerOfTwo(t *testing.T) { + tests := []struct { + bound uint + expected int + expectError bool + }{ + {bound: 1, expected: 1}, + {bound: 2, expected: 2}, + {bound: 3, expected: 2}, + {bound: 4, expected: 4}, + {bound: 5, expected: 4}, + {bound: 6, expected: 4}, + {bound: 7, expected: 4}, + {bound: 8, expected: 8}, + {bound: 0, expectError: true}, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("bound=%d", tt.bound), func(t *testing.T) { + result, err := largestPowerOfTwo(tt.bound) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestSubtreeRootThresholdToLeafRange(t *testing.T) { + tests := []struct { + subtreeRootThreshold int + expected int + expectErr bool + }{ + {0, 1, false}, + {1, 2, false}, + {-1, 0, true}, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("subtreeRootThreshold=%d", tt.subtreeRootThreshold), func(t *testing.T) { + result, err := subtreeRootThresholdToLeafRange(tt.subtreeRootThreshold) + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestToLeafRanges(t *testing.T) { + tests := []struct { + proofStart, proofEnd, subtreeRootThreshold int + expectedRanges []leafRange + expectError bool + }{ + { + proofStart: 0, + proofEnd: 8, + subtreeRootThreshold: 3, + expectedRanges: []leafRange{ + {start: 0, end: 8}, + }, + }, + { + proofStart: 0, + proofEnd: 9, + subtreeRootThreshold: 3, + expectedRanges: []leafRange{ + {start: 0, end: 8}, + {start: 8, end: 9}, + }, + }, + { + proofStart: 0, + proofEnd: 16, + subtreeRootThreshold: 1, + expectedRanges: []leafRange{ + {start: 0, end: 2}, + {start: 2, end: 4}, + {start: 4, end: 6}, + {start: 6, end: 8}, + {start: 8, end: 10}, + {start: 10, end: 12}, + {start: 12, end: 14}, + {start: 14, end: 16}, + }, + }, + { + proofStart: 0, + proofEnd: 16, + subtreeRootThreshold: 2, + expectedRanges: []leafRange{ + {start: 0, end: 4}, + {start: 4, end: 8}, + {start: 8, end: 12}, + {start: 12, end: 16}, + }, + }, + { + proofStart: 0, + proofEnd: 16, + subtreeRootThreshold: 3, + expectedRanges: []leafRange{ + {start: 0, end: 8}, + {start: 8, end: 16}, + }, + }, + { + proofStart: 0, + proofEnd: 16, + subtreeRootThreshold: 4, + expectedRanges: []leafRange{ + {start: 0, end: 16}, + }, + }, + { + proofStart: 4, + proofEnd: 12, + subtreeRootThreshold: 0, + expectedRanges: []leafRange{ + {start: 4, end: 5}, + {start: 5, end: 6}, + {start: 6, end: 7}, + {start: 7, end: 8}, + {start: 8, end: 9}, + {start: 9, end: 10}, + {start: 10, end: 11}, + {start: 11, end: 12}, + }, + }, + { + proofStart: 4, + proofEnd: 12, + subtreeRootThreshold: 1, + expectedRanges: []leafRange{ + {start: 4, end: 6}, + {start: 6, end: 8}, + {start: 8, end: 10}, + {start: 10, end: 12}, + }, + }, + { + proofStart: 4, + proofEnd: 12, + subtreeRootThreshold: 2, + expectedRanges: []leafRange{ + {start: 4, end: 8}, + {start: 8, end: 12}, + }, + }, + { + proofStart: 8, + proofEnd: 10, + subtreeRootThreshold: 1, + expectedRanges: []leafRange{ + {start: 8, end: 10}, + }, + }, + { + proofStart: -1, + proofEnd: 0, + subtreeRootThreshold: -1, + expectedRanges: nil, + expectError: true, + }, + { + proofStart: 0, + proofEnd: -1, + subtreeRootThreshold: -1, + expectedRanges: nil, + expectError: true, + }, + { + proofStart: 0, + proofEnd: 0, + subtreeRootThreshold: 2, + expectedRanges: nil, + expectError: true, + }, + { + proofStart: 0, + proofEnd: 0, + subtreeRootThreshold: -1, + expectedRanges: nil, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("proofStart=%d, proofEnd=%d, subtreeRootThreshold=%d", tt.proofStart, tt.proofEnd, tt.subtreeRootThreshold), func(t *testing.T) { + result, err := toLeafRanges(tt.proofStart, tt.proofEnd, tt.subtreeRootThreshold) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.True(t, compareRanges(result, tt.expectedRanges)) + } + }) + } +} + +func compareRanges(a, b []leafRange) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestNextLeafRange(t *testing.T) { + tests := []struct { + currentStart, currentEnd, subtreeRootMaximumLeafRange int + expectedRange leafRange + expectError bool + }{ + { + currentStart: 0, + currentEnd: 8, + subtreeRootMaximumLeafRange: 4, + expectedRange: leafRange{start: 0, end: 4}, + }, + { + currentStart: 4, + currentEnd: 10, + 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, + subtreeRootMaximumLeafRange: 2, + expectedRange: leafRange{start: 4, end: 6}, + }, + { + currentStart: 4, + currentEnd: 20, + subtreeRootMaximumLeafRange: 4, + expectedRange: leafRange{start: 4, end: 8}, + }, + { + currentStart: 4, + currentEnd: 20, + subtreeRootMaximumLeafRange: 8, + expectedRange: leafRange{start: 4, end: 12}, + }, + { + currentStart: 0, + currentEnd: 1, + subtreeRootMaximumLeafRange: 1, + expectedRange: leafRange{start: 0, end: 1}, + }, + { + currentStart: 0, + currentEnd: 16, + subtreeRootMaximumLeafRange: 16, + expectedRange: leafRange{start: 0, end: 16}, + }, + { + currentStart: 0, + currentEnd: 0, + subtreeRootMaximumLeafRange: 4, + expectError: true, + }, + { + currentStart: 5, + currentEnd: 2, + subtreeRootMaximumLeafRange: 4, + expectError: true, + }, + { + currentStart: 5, + currentEnd: 2, + subtreeRootMaximumLeafRange: 0, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("currentStart=%d, currentEnd=%d, subtreeRootMaximumLeafRange=%d", tt.currentStart, tt.currentEnd, tt.subtreeRootMaximumLeafRange), func(t *testing.T) { + result, err := nextLeafRange(tt.currentStart, tt.currentEnd, tt.subtreeRootMaximumLeafRange) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedRange, result) + } + }) + } +} + +func TestSafeIntToUint(t *testing.T) { + tests := []struct { + input int + expectedUint uint + expectedError error + }{ + { + input: 10, + expectedUint: 10, + expectedError: nil, + }, + { + input: 0, + expectedUint: 0, + expectedError: nil, + }, + { + input: -5, + expectedUint: 0, + expectedError: fmt.Errorf("cannot convert a negative int %d to uint", -5), + }, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("input=%d", tt.input), func(t *testing.T) { + result, err := safeIntToUint(tt.input) + if (err != nil) != (tt.expectedError != nil) || (err != nil && err.Error() != tt.expectedError.Error()) { + t.Errorf("expected error %v, got %v", tt.expectedError, err) + } + if result != tt.expectedUint { + t.Errorf("expected uint %v, got %v", tt.expectedUint, result) + } + }) + } +} + +func TestMinInt(t *testing.T) { + tests := []struct { + val1, val2 int + expected int + }{ + { + val1: 10, + val2: 20, + expected: 10, + }, + { + val1: -5, + val2: 6, + expected: -5, + }, + { + val1: 5, + val2: -6, + expected: -6, + }, + { + val1: -5, + val2: -6, + expected: -6, + }, + { + val1: 0, + val2: 0, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("val1=%d, val2=%d", tt.val1, tt.val2), func(t *testing.T) { + result := minInt(tt.val1, tt.val2) + if result != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, result) + } + }) + } +} + +func TestVerifySubtreeRootInclusion(t *testing.T) { + tree := exampleNMT(1, true, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) + root, err := tree.Root() + require.NoError(t, err) + + nmthasher := tree.treeHasher + hasher := nmthasher.(*NmtHasher) + + tests := []struct { + proof Proof + subtreeRoots [][]byte + subtreeRootThreshold int + root []byte + validProof bool + expectError bool + }{ + { + proof: func() Proof { + p, err := tree.ProveRange(0, 8) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot, err := tree.ComputeSubtreeRoot(0, 8) + require.NoError(t, err) + return [][]byte{subtreeRoot} + }(), + subtreeRootThreshold: 3, + root: root, + validProof: true, + }, + { + proof: func() Proof { + p, err := tree.ProveRange(0, 1) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot, err := tree.ComputeSubtreeRoot(0, 1) + require.NoError(t, err) + return [][]byte{subtreeRoot} + }(), + subtreeRootThreshold: 3, + root: root, + validProof: true, + }, + { + proof: func() Proof { + p, err := tree.ProveRange(0, 2) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot, err := tree.ComputeSubtreeRoot(0, 2) + require.NoError(t, err) + return [][]byte{subtreeRoot} + }(), + subtreeRootThreshold: 3, + root: root, + validProof: true, + }, + { + proof: func() Proof { + p, err := tree.ProveRange(2, 4) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot, err := tree.ComputeSubtreeRoot(2, 4) + require.NoError(t, err) + return [][]byte{subtreeRoot} + }(), + subtreeRootThreshold: 3, + root: root, + validProof: true, + }, + { + proof: func() Proof { + p, err := tree.ProveRange(0, 8) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot1, err := tree.ComputeSubtreeRoot(0, 4) + require.NoError(t, err) + subtreeRoot2, err := tree.ComputeSubtreeRoot(4, 8) + require.NoError(t, err) + return [][]byte{subtreeRoot1, subtreeRoot2} + }(), + subtreeRootThreshold: 2, + root: root, + validProof: true, + }, + { + proof: func() Proof { + p, err := tree.ProveRange(0, 8) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot1, err := tree.ComputeSubtreeRoot(0, 2) + require.NoError(t, err) + subtreeRoot2, err := tree.ComputeSubtreeRoot(2, 4) + require.NoError(t, err) + subtreeRoot3, err := tree.ComputeSubtreeRoot(4, 6) + require.NoError(t, err) + subtreeRoot4, err := tree.ComputeSubtreeRoot(6, 8) + require.NoError(t, err) + return [][]byte{subtreeRoot1, subtreeRoot2, subtreeRoot3, subtreeRoot4} + }(), + subtreeRootThreshold: 1, + root: root, + validProof: true, + }, + { + proof: func() Proof { + p, err := tree.ProveRange(0, 8) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot1, err := tree.ComputeSubtreeRoot(0, 1) + require.NoError(t, err) + subtreeRoot2, err := tree.ComputeSubtreeRoot(1, 2) + require.NoError(t, err) + subtreeRoot3, err := tree.ComputeSubtreeRoot(2, 3) + require.NoError(t, err) + subtreeRoot4, err := tree.ComputeSubtreeRoot(3, 4) + require.NoError(t, err) + subtreeRoot5, err := tree.ComputeSubtreeRoot(4, 5) + require.NoError(t, err) + subtreeRoot6, err := tree.ComputeSubtreeRoot(5, 6) + require.NoError(t, err) + subtreeRoot7, err := tree.ComputeSubtreeRoot(6, 7) + require.NoError(t, err) + subtreeRoot8, err := tree.ComputeSubtreeRoot(7, 8) + require.NoError(t, err) + return [][]byte{subtreeRoot1, subtreeRoot2, subtreeRoot3, subtreeRoot4, subtreeRoot5, subtreeRoot6, subtreeRoot7, subtreeRoot8} + }(), + subtreeRootThreshold: 0, + root: root, + validProof: true, + }, + { + proof: func() Proof { + p, err := tree.ProveRange(4, 8) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot, err := tree.ComputeSubtreeRoot(4, 8) + require.NoError(t, err) + return [][]byte{subtreeRoot} + }(), + subtreeRootThreshold: 3, + root: root, + validProof: true, + }, + { + proof: func() Proof { + p, err := tree.ProveRange(12, 14) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot, err := tree.ComputeSubtreeRoot(12, 14) + require.NoError(t, err) + return [][]byte{subtreeRoot} + }(), + subtreeRootThreshold: 3, + root: root, + validProof: true, + }, + { + proof: func() Proof { + p, err := tree.ProveRange(14, 16) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot, err := tree.ComputeSubtreeRoot(14, 16) + require.NoError(t, err) + return [][]byte{subtreeRoot} + }(), + subtreeRootThreshold: 3, + root: root, + validProof: true, + }, + { + proof: func() Proof { + p, err := tree.ProveRange(14, 15) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot, err := tree.ComputeSubtreeRoot(14, 15) + require.NoError(t, err) + return [][]byte{subtreeRoot} + }(), + subtreeRootThreshold: 3, + root: root, + validProof: true, + }, + { + proof: func() Proof { + p, err := tree.ProveRange(15, 16) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot, err := tree.ComputeSubtreeRoot(15, 16) + require.NoError(t, err) + return [][]byte{subtreeRoot} + }(), + subtreeRootThreshold: 3, + root: root, + validProof: true, + }, + { + proof: func() Proof { + p, err := tree.ProveRange(15, 16) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot, err := tree.ComputeSubtreeRoot(15, 16) + require.NoError(t, err) + return [][]byte{subtreeRoot} + }(), + subtreeRootThreshold: -3, // invalid subtree root threshold + root: root, + expectError: true, + }, + { + proof: func() Proof { + p, err := tree.ProveRange(15, 16) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot, err := tree.ComputeSubtreeRoot(15, 16) + require.NoError(t, err) + return [][]byte{subtreeRoot, subtreeRoot} // invalid number of subtree roots + }(), + subtreeRootThreshold: 3, + root: root, + expectError: true, + }, + { + proof: func() Proof { + p, err := tree.ProveRange(15, 16) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot, err := tree.ComputeSubtreeRoot(15, 16) + require.NoError(t, err) + return [][]byte{subtreeRoot} + }(), + subtreeRootThreshold: 3, + root: []byte("random root"), // invalid root format + expectError: true, + }, + { + proof: Proof{start: -1}, // invalid start + subtreeRoots: func() [][]byte { + subtreeRoot, err := tree.ComputeSubtreeRoot(15, 16) + require.NoError(t, err) + return [][]byte{subtreeRoot} + }(), + subtreeRootThreshold: 3, + root: root, + expectError: true, + }, + { + proof: Proof{end: 1, start: 2}, // invalid end + subtreeRoots: func() [][]byte { + subtreeRoot, err := tree.ComputeSubtreeRoot(15, 16) + require.NoError(t, err) + return [][]byte{subtreeRoot} + }(), + subtreeRootThreshold: 3, + root: root, + expectError: true, + }, + { + proof: Proof{ + start: 0, + end: 4, + nodes: [][]byte{[]byte("invalid proof node")}, // invalid proof node + }, + subtreeRoots: func() [][]byte { + subtreeRoot, err := tree.ComputeSubtreeRoot(15, 16) + require.NoError(t, err) + return [][]byte{subtreeRoot} + }(), + subtreeRootThreshold: 3, + root: root, + expectError: true, + }, + { + proof: func() Proof { + p, err := tree.ProveRange(15, 16) + require.NoError(t, err) + return p + }(), + subtreeRoots: [][]byte{[]byte("invalid subtree root")}, // invalid subtree root + subtreeRootThreshold: 3, + root: root, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("proofStart=%d, proofEnd=%d, subTreeRootThreshold=%d", tt.proof.Start(), tt.proof.End(), tt.subtreeRootThreshold), func(t *testing.T) { + result, err := tt.proof.VerifySubtreeRootInclusion(hasher, tt.subtreeRoots, tt.subtreeRootThreshold, tt.root) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.validProof, result) + } + }) + } +} From 4ba85fe08accb87c6eb5c524d47f9f24d71436d4 Mon Sep 17 00:00:00 2001 From: sweexordious Date: Thu, 30 May 2024 01:42:24 +0400 Subject: [PATCH 03/16] chore: gofumpt --- proof_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/proof_test.go b/proof_test.go index 2c9d957..e7b8491 100644 --- a/proof_test.go +++ b/proof_test.go @@ -1125,6 +1125,7 @@ func Test_ProtoToProof(t *testing.T) { }) } } + func TestLargestPowerOfTwo(t *testing.T) { tests := []struct { bound uint From 12b62f1dc0f31e8371eb4379fdbc59443ad38cd5 Mon Sep 17 00:00:00 2001 From: sweexordious Date: Thu, 30 May 2024 12:28:05 +0400 Subject: [PATCH 04/16] chore: export LeafRange --- nmt.go | 12 ++++++------ proof.go | 20 ++++++++++---------- proof_test.go | 46 +++++++++++++++++++++++----------------------- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/nmt.go b/nmt.go index 5d2aa7f..437d806 100644 --- a/nmt.go +++ b/nmt.go @@ -107,9 +107,9 @@ type NamespacedMerkleTree struct { // namespaceRanges can be used to efficiently look up the range for an // existing namespace without iterating through the leaves. The map key is - // the string representation of a namespace.ID and the leafRange indicates + // the string representation of a namespace.ID and the LeafRange indicates // the range of the leaves matching that namespace ID in the tree - namespaceRanges map[string]leafRange + namespaceRanges map[string]LeafRange // minNID is the minimum namespace ID of the leaves minNID namespace.ID // maxNID is the maximum namespace ID of the leaves @@ -151,7 +151,7 @@ func New(h hash.Hash, setters ...Option) *NamespacedMerkleTree { visit: opts.NodeVisitor, leaves: make([][]byte, 0, opts.InitialCapacity), leafHashes: make([][]byte, 0, opts.InitialCapacity), - namespaceRanges: make(map[string]leafRange), + namespaceRanges: make(map[string]LeafRange), minNID: bytes.Repeat([]byte{0xFF}, int(opts.NamespaceIDSize)), maxNID: bytes.Repeat([]byte{0x00}, int(opts.NamespaceIDSize)), } @@ -590,12 +590,12 @@ func (n *NamespacedMerkleTree) updateNamespaceRanges() { lastNsStr := string(lastPushed[:n.treeHasher.NamespaceSize()]) lastRange, found := n.namespaceRanges[lastNsStr] if !found { - n.namespaceRanges[lastNsStr] = leafRange{ + n.namespaceRanges[lastNsStr] = LeafRange{ start: lastIndex, end: lastIndex + 1, } } else { - n.namespaceRanges[lastNsStr] = leafRange{ + n.namespaceRanges[lastNsStr] = LeafRange{ start: lastRange.start, end: lastRange.end + 1, } @@ -679,7 +679,7 @@ func isPowerOfTwo(n int) bool { return n > 0 && (n&(n-1)) == 0 } -type leafRange struct { +type LeafRange struct { // start and end denote the indices of a leaf in the tree. start ranges from // 0 up to the total number of leaves minus 1 end ranges from 1 up to the // total number of leaves end is non-inclusive diff --git a/proof.go b/proof.go index babf1c5..61c471f 100644 --- a/proof.go +++ b/proof.go @@ -475,7 +475,7 @@ func (proof Proof) VerifyInclusion(h hash.Hash, nid namespace.ID, leavesWithoutN // Using this method without making sure the above assumptions are respected // can return invalid results. // The subtreeRootThreshold is also defined in ADR-013. -// More information on the algorithm used can be found in the toLeafRanges() method docs. +// More information on the algorithm used can be found in the ToLeafRanges() method docs. func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]byte, subtreeRootThreshold int, root []byte) (bool, error) { // check that the proof range is valid if proof.Start() < 0 || proof.Start() >= proof.End() { @@ -500,7 +500,7 @@ func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]b } // get the subtree roots leaf ranges - ranges, err := toLeafRanges(proof.Start(), proof.End(), subtreeRootThreshold) + ranges, err := ToLeafRanges(proof.Start(), proof.End(), subtreeRootThreshold) if err != nil { return false, err } @@ -584,7 +584,7 @@ func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]b return bytes.Equal(rootHash, root), nil } -// toLeafRanges returns the leaf ranges corresponding to the provided subtree roots. +// ToLeafRanges returns the leaf ranges corresponding to the provided subtree roots. // It uses the subtree root threshold to calculate the maximum number of leaves a subtree root can // commit to. // The subtree root threshold is defined as per ADR-013: @@ -603,7 +603,7 @@ func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]b // - Go back to the loop condition. // // Note: This method is Celestia specific. -func toLeafRanges(proofStart, proofEnd, subtreeRootThreshold int) ([]leafRange, error) { +func ToLeafRanges(proofStart, proofEnd, subtreeRootThreshold int) ([]LeafRange, error) { if proofStart < 0 { return nil, fmt.Errorf("proof start %d shouldn't be strictly negative", proofStart) } @@ -615,7 +615,7 @@ func toLeafRanges(proofStart, proofEnd, subtreeRootThreshold int) ([]leafRange, } currentStart := proofStart currentLeafRange := proofEnd - proofStart - var ranges []leafRange + var ranges []LeafRange maximumLeafRange, err := subtreeRootThresholdToLeafRange(subtreeRootThreshold) if err != nil { return nil, err @@ -646,20 +646,20 @@ func subtreeRootThresholdToLeafRange(subtreeRootThreshold int) (int, error) { // nextLeafRange takes a proof start, proof end, and the maximum range a subtree // root can cover, and returns the corresponding subtree root range. // The subtreeRootMaximum LeafRange is calculated using subtreeRootThresholdToLeafRange() method. -// Check toLeafRanges() for more information on the algorithm used. +// Check ToLeafRanges() for more information on the algorithm used. // Note: This method is Celestia specific. -func nextLeafRange(currentStart, currentEnd, subtreeRootMaximumLeafRange int) (leafRange, error) { +func nextLeafRange(currentStart, currentEnd, subtreeRootMaximumLeafRange int) (LeafRange, error) { currentLeafRange := currentEnd - currentStart minimum := minInt(currentLeafRange, subtreeRootMaximumLeafRange) uMinimum, err := safeIntToUint(minimum) if err != nil { - return leafRange{}, fmt.Errorf("failed to convert subtree root range to Uint %w", err) + return LeafRange{}, fmt.Errorf("failed to convert subtree root range to Uint %w", err) } currentRange, err := largestPowerOfTwo(uMinimum) if err != nil { - return leafRange{}, err + return LeafRange{}, err } - return leafRange{start: currentStart, end: currentStart + currentRange}, nil + return LeafRange{start: currentStart, end: currentStart + currentRange}, nil } // largestPowerOfTwo calculates the largest power of two diff --git a/proof_test.go b/proof_test.go index e7b8491..59ef849 100644 --- a/proof_test.go +++ b/proof_test.go @@ -1183,14 +1183,14 @@ func TestSubtreeRootThresholdToLeafRange(t *testing.T) { func TestToLeafRanges(t *testing.T) { tests := []struct { proofStart, proofEnd, subtreeRootThreshold int - expectedRanges []leafRange + expectedRanges []LeafRange expectError bool }{ { proofStart: 0, proofEnd: 8, subtreeRootThreshold: 3, - expectedRanges: []leafRange{ + expectedRanges: []LeafRange{ {start: 0, end: 8}, }, }, @@ -1198,7 +1198,7 @@ func TestToLeafRanges(t *testing.T) { proofStart: 0, proofEnd: 9, subtreeRootThreshold: 3, - expectedRanges: []leafRange{ + expectedRanges: []LeafRange{ {start: 0, end: 8}, {start: 8, end: 9}, }, @@ -1207,7 +1207,7 @@ func TestToLeafRanges(t *testing.T) { proofStart: 0, proofEnd: 16, subtreeRootThreshold: 1, - expectedRanges: []leafRange{ + expectedRanges: []LeafRange{ {start: 0, end: 2}, {start: 2, end: 4}, {start: 4, end: 6}, @@ -1222,7 +1222,7 @@ func TestToLeafRanges(t *testing.T) { proofStart: 0, proofEnd: 16, subtreeRootThreshold: 2, - expectedRanges: []leafRange{ + expectedRanges: []LeafRange{ {start: 0, end: 4}, {start: 4, end: 8}, {start: 8, end: 12}, @@ -1233,7 +1233,7 @@ func TestToLeafRanges(t *testing.T) { proofStart: 0, proofEnd: 16, subtreeRootThreshold: 3, - expectedRanges: []leafRange{ + expectedRanges: []LeafRange{ {start: 0, end: 8}, {start: 8, end: 16}, }, @@ -1242,7 +1242,7 @@ func TestToLeafRanges(t *testing.T) { proofStart: 0, proofEnd: 16, subtreeRootThreshold: 4, - expectedRanges: []leafRange{ + expectedRanges: []LeafRange{ {start: 0, end: 16}, }, }, @@ -1250,7 +1250,7 @@ func TestToLeafRanges(t *testing.T) { proofStart: 4, proofEnd: 12, subtreeRootThreshold: 0, - expectedRanges: []leafRange{ + expectedRanges: []LeafRange{ {start: 4, end: 5}, {start: 5, end: 6}, {start: 6, end: 7}, @@ -1265,7 +1265,7 @@ func TestToLeafRanges(t *testing.T) { proofStart: 4, proofEnd: 12, subtreeRootThreshold: 1, - expectedRanges: []leafRange{ + expectedRanges: []LeafRange{ {start: 4, end: 6}, {start: 6, end: 8}, {start: 8, end: 10}, @@ -1276,7 +1276,7 @@ func TestToLeafRanges(t *testing.T) { proofStart: 4, proofEnd: 12, subtreeRootThreshold: 2, - expectedRanges: []leafRange{ + expectedRanges: []LeafRange{ {start: 4, end: 8}, {start: 8, end: 12}, }, @@ -1285,7 +1285,7 @@ func TestToLeafRanges(t *testing.T) { proofStart: 8, proofEnd: 10, subtreeRootThreshold: 1, - expectedRanges: []leafRange{ + expectedRanges: []LeafRange{ {start: 8, end: 10}, }, }, @@ -1321,7 +1321,7 @@ func TestToLeafRanges(t *testing.T) { for _, tt := range tests { t.Run(fmt.Sprintf("proofStart=%d, proofEnd=%d, subtreeRootThreshold=%d", tt.proofStart, tt.proofEnd, tt.subtreeRootThreshold), func(t *testing.T) { - result, err := toLeafRanges(tt.proofStart, tt.proofEnd, tt.subtreeRootThreshold) + result, err := ToLeafRanges(tt.proofStart, tt.proofEnd, tt.subtreeRootThreshold) if tt.expectError { assert.Error(t, err) } else { @@ -1332,7 +1332,7 @@ func TestToLeafRanges(t *testing.T) { } } -func compareRanges(a, b []leafRange) bool { +func compareRanges(a, b []LeafRange) bool { if len(a) != len(b) { return false } @@ -1347,62 +1347,62 @@ func compareRanges(a, b []leafRange) bool { func TestNextLeafRange(t *testing.T) { tests := []struct { currentStart, currentEnd, subtreeRootMaximumLeafRange int - expectedRange leafRange + expectedRange LeafRange expectError bool }{ { currentStart: 0, currentEnd: 8, subtreeRootMaximumLeafRange: 4, - expectedRange: leafRange{start: 0, end: 4}, + expectedRange: LeafRange{start: 0, end: 4}, }, { currentStart: 4, currentEnd: 10, subtreeRootMaximumLeafRange: 8, - expectedRange: leafRange{start: 4, end: 8}, + expectedRange: LeafRange{start: 4, end: 8}, }, { currentStart: 4, currentEnd: 20, subtreeRootMaximumLeafRange: 16, - expectedRange: leafRange{start: 4, end: 20}, + expectedRange: LeafRange{start: 4, end: 20}, }, { currentStart: 4, currentEnd: 20, subtreeRootMaximumLeafRange: 1, - expectedRange: leafRange{start: 4, end: 5}, + expectedRange: LeafRange{start: 4, end: 5}, }, { currentStart: 4, currentEnd: 20, subtreeRootMaximumLeafRange: 2, - expectedRange: leafRange{start: 4, end: 6}, + expectedRange: LeafRange{start: 4, end: 6}, }, { currentStart: 4, currentEnd: 20, subtreeRootMaximumLeafRange: 4, - expectedRange: leafRange{start: 4, end: 8}, + expectedRange: LeafRange{start: 4, end: 8}, }, { currentStart: 4, currentEnd: 20, subtreeRootMaximumLeafRange: 8, - expectedRange: leafRange{start: 4, end: 12}, + expectedRange: LeafRange{start: 4, end: 12}, }, { currentStart: 0, currentEnd: 1, subtreeRootMaximumLeafRange: 1, - expectedRange: leafRange{start: 0, end: 1}, + expectedRange: LeafRange{start: 0, end: 1}, }, { currentStart: 0, currentEnd: 16, subtreeRootMaximumLeafRange: 16, - expectedRange: leafRange{start: 0, end: 16}, + expectedRange: LeafRange{start: 0, end: 16}, }, { currentStart: 0, From ad2f71d23af5cffc131ca758049ba77f1c4bf252 Mon Sep 17 00:00:00 2001 From: sweexordious Date: Thu, 30 May 2024 19:37:10 +0400 Subject: [PATCH 05/16] chore: export LeafRange attributes --- nmt.go | 19 ++++++------ proof.go | 8 ++--- proof_test.go | 84 +++++++++++++++++++++++++-------------------------- 3 files changed, 56 insertions(+), 55 deletions(-) diff --git a/nmt.go b/nmt.go index 437d806..912589e 100644 --- a/nmt.go +++ b/nmt.go @@ -436,7 +436,7 @@ func (n *NamespacedMerkleTree) foundInRange(nID namespace.ID) (found bool, start // This is a faster version of this code snippet: // https://github.com/celestiaorg/celestiaorg-prototype/blob/2aeca6f55ad389b9d68034a0a7038f80a8d2982e/simpleblock.go#L106-L117 foundRng, found := n.namespaceRanges[string(nID)] - return found, foundRng.start, foundRng.end + return found, foundRng.Start, foundRng.End } // NamespaceSize returns the underlying namespace size. Note that all namespaced @@ -591,13 +591,13 @@ func (n *NamespacedMerkleTree) updateNamespaceRanges() { lastRange, found := n.namespaceRanges[lastNsStr] if !found { n.namespaceRanges[lastNsStr] = LeafRange{ - start: lastIndex, - end: lastIndex + 1, + Start: lastIndex, + End: lastIndex + 1, } } else { n.namespaceRanges[lastNsStr] = LeafRange{ - start: lastRange.start, - end: lastRange.end + 1, + Start: lastRange.Start, + End: lastRange.End + 1, } } } @@ -680,10 +680,11 @@ func isPowerOfTwo(n int) bool { } type LeafRange struct { - // start and end denote the indices of a leaf in the tree. start ranges from - // 0 up to the total number of leaves minus 1 end ranges from 1 up to the - // total number of leaves end is non-inclusive - start, end int + // Start and End denote the indices of a leaf in the tree. + // Start ranges from 0 up to the total number of leaves minus 1. + // End ranges from 1 up to the total number of leaves. + // End is non-inclusive + Start, End int } // MinNamespace extracts the minimum namespace ID from a given namespace hash, diff --git a/proof.go b/proof.go index 61c471f..a2e79b3 100644 --- a/proof.go +++ b/proof.go @@ -538,7 +538,7 @@ func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]b return popIfNonEmpty(&proof.nodes), nil } - if len(ranges) != 0 && ranges[0].start == start && ranges[0].end == end { + if len(ranges) != 0 && ranges[0].Start == start && ranges[0].End == end { ranges = ranges[1:] return popIfNonEmpty(&subtreeRoots), nil } @@ -626,8 +626,8 @@ func ToLeafRanges(proofStart, proofEnd, subtreeRootThreshold int) ([]LeafRange, return nil, err } ranges = append(ranges, nextRange) - currentStart = nextRange.end - currentLeafRange = currentLeafRange - nextRange.end + nextRange.start + currentStart = nextRange.End + currentLeafRange = currentLeafRange - nextRange.End + nextRange.Start } return ranges, nil } @@ -659,7 +659,7 @@ func nextLeafRange(currentStart, currentEnd, subtreeRootMaximumLeafRange int) (L if err != nil { return LeafRange{}, err } - return LeafRange{start: currentStart, end: currentStart + currentRange}, nil + return LeafRange{Start: currentStart, End: currentStart + currentRange}, nil } // largestPowerOfTwo calculates the largest power of two diff --git a/proof_test.go b/proof_test.go index 59ef849..a8a14b4 100644 --- a/proof_test.go +++ b/proof_test.go @@ -1191,7 +1191,7 @@ func TestToLeafRanges(t *testing.T) { proofEnd: 8, subtreeRootThreshold: 3, expectedRanges: []LeafRange{ - {start: 0, end: 8}, + {Start: 0, End: 8}, }, }, { @@ -1199,8 +1199,8 @@ func TestToLeafRanges(t *testing.T) { proofEnd: 9, subtreeRootThreshold: 3, expectedRanges: []LeafRange{ - {start: 0, end: 8}, - {start: 8, end: 9}, + {Start: 0, End: 8}, + {Start: 8, End: 9}, }, }, { @@ -1208,14 +1208,14 @@ func TestToLeafRanges(t *testing.T) { proofEnd: 16, subtreeRootThreshold: 1, expectedRanges: []LeafRange{ - {start: 0, end: 2}, - {start: 2, end: 4}, - {start: 4, end: 6}, - {start: 6, end: 8}, - {start: 8, end: 10}, - {start: 10, end: 12}, - {start: 12, end: 14}, - {start: 14, end: 16}, + {Start: 0, End: 2}, + {Start: 2, End: 4}, + {Start: 4, End: 6}, + {Start: 6, End: 8}, + {Start: 8, End: 10}, + {Start: 10, End: 12}, + {Start: 12, End: 14}, + {Start: 14, End: 16}, }, }, { @@ -1223,10 +1223,10 @@ func TestToLeafRanges(t *testing.T) { proofEnd: 16, subtreeRootThreshold: 2, expectedRanges: []LeafRange{ - {start: 0, end: 4}, - {start: 4, end: 8}, - {start: 8, end: 12}, - {start: 12, end: 16}, + {Start: 0, End: 4}, + {Start: 4, End: 8}, + {Start: 8, End: 12}, + {Start: 12, End: 16}, }, }, { @@ -1234,8 +1234,8 @@ func TestToLeafRanges(t *testing.T) { proofEnd: 16, subtreeRootThreshold: 3, expectedRanges: []LeafRange{ - {start: 0, end: 8}, - {start: 8, end: 16}, + {Start: 0, End: 8}, + {Start: 8, End: 16}, }, }, { @@ -1243,7 +1243,7 @@ func TestToLeafRanges(t *testing.T) { proofEnd: 16, subtreeRootThreshold: 4, expectedRanges: []LeafRange{ - {start: 0, end: 16}, + {Start: 0, End: 16}, }, }, { @@ -1251,14 +1251,14 @@ func TestToLeafRanges(t *testing.T) { proofEnd: 12, subtreeRootThreshold: 0, expectedRanges: []LeafRange{ - {start: 4, end: 5}, - {start: 5, end: 6}, - {start: 6, end: 7}, - {start: 7, end: 8}, - {start: 8, end: 9}, - {start: 9, end: 10}, - {start: 10, end: 11}, - {start: 11, end: 12}, + {Start: 4, End: 5}, + {Start: 5, End: 6}, + {Start: 6, End: 7}, + {Start: 7, End: 8}, + {Start: 8, End: 9}, + {Start: 9, End: 10}, + {Start: 10, End: 11}, + {Start: 11, End: 12}, }, }, { @@ -1266,10 +1266,10 @@ func TestToLeafRanges(t *testing.T) { proofEnd: 12, subtreeRootThreshold: 1, expectedRanges: []LeafRange{ - {start: 4, end: 6}, - {start: 6, end: 8}, - {start: 8, end: 10}, - {start: 10, end: 12}, + {Start: 4, End: 6}, + {Start: 6, End: 8}, + {Start: 8, End: 10}, + {Start: 10, End: 12}, }, }, { @@ -1277,8 +1277,8 @@ func TestToLeafRanges(t *testing.T) { proofEnd: 12, subtreeRootThreshold: 2, expectedRanges: []LeafRange{ - {start: 4, end: 8}, - {start: 8, end: 12}, + {Start: 4, End: 8}, + {Start: 8, End: 12}, }, }, { @@ -1286,7 +1286,7 @@ func TestToLeafRanges(t *testing.T) { proofEnd: 10, subtreeRootThreshold: 1, expectedRanges: []LeafRange{ - {start: 8, end: 10}, + {Start: 8, End: 10}, }, }, { @@ -1354,55 +1354,55 @@ func TestNextLeafRange(t *testing.T) { currentStart: 0, currentEnd: 8, subtreeRootMaximumLeafRange: 4, - expectedRange: LeafRange{start: 0, end: 4}, + expectedRange: LeafRange{Start: 0, End: 4}, }, { currentStart: 4, currentEnd: 10, subtreeRootMaximumLeafRange: 8, - expectedRange: LeafRange{start: 4, end: 8}, + expectedRange: LeafRange{Start: 4, End: 8}, }, { currentStart: 4, currentEnd: 20, subtreeRootMaximumLeafRange: 16, - expectedRange: LeafRange{start: 4, end: 20}, + expectedRange: LeafRange{Start: 4, End: 20}, }, { currentStart: 4, currentEnd: 20, subtreeRootMaximumLeafRange: 1, - expectedRange: LeafRange{start: 4, end: 5}, + expectedRange: LeafRange{Start: 4, End: 5}, }, { currentStart: 4, currentEnd: 20, subtreeRootMaximumLeafRange: 2, - expectedRange: LeafRange{start: 4, end: 6}, + expectedRange: LeafRange{Start: 4, End: 6}, }, { currentStart: 4, currentEnd: 20, subtreeRootMaximumLeafRange: 4, - expectedRange: LeafRange{start: 4, end: 8}, + expectedRange: LeafRange{Start: 4, End: 8}, }, { currentStart: 4, currentEnd: 20, subtreeRootMaximumLeafRange: 8, - expectedRange: LeafRange{start: 4, end: 12}, + expectedRange: LeafRange{Start: 4, End: 12}, }, { currentStart: 0, currentEnd: 1, subtreeRootMaximumLeafRange: 1, - expectedRange: LeafRange{start: 0, end: 1}, + expectedRange: LeafRange{Start: 0, End: 1}, }, { currentStart: 0, currentEnd: 16, subtreeRootMaximumLeafRange: 16, - expectedRange: LeafRange{start: 0, end: 16}, + expectedRange: LeafRange{Start: 0, End: 16}, }, { currentStart: 0, From 82d18862d07ffc61d4ce88ca9b70ee1674394257 Mon Sep 17 00:00:00 2001 From: sweexordious Date: Fri, 31 May 2024 22:41:17 +0400 Subject: [PATCH 06/16] fix: remove unnecessary subtree root threshold conversion --- proof.go | 23 ++++------------------- proof_test.go | 24 ------------------------ 2 files changed, 4 insertions(+), 43 deletions(-) diff --git a/proof.go b/proof.go index a2e79b3..8ca8545 100644 --- a/proof.go +++ b/proof.go @@ -610,16 +610,13 @@ func ToLeafRanges(proofStart, proofEnd, subtreeRootThreshold int) ([]LeafRange, if proofEnd <= proofStart { return nil, fmt.Errorf("proof end %d should be stricly bigger than proof start %d", proofEnd, proofStart) } - if subtreeRootThreshold < 0 { + if subtreeRootThreshold <= 0 { return nil, fmt.Errorf("subtree root threshold cannot be negative %d", subtreeRootThreshold) } currentStart := proofStart currentLeafRange := proofEnd - proofStart var ranges []LeafRange - maximumLeafRange, err := subtreeRootThresholdToLeafRange(subtreeRootThreshold) - if err != nil { - return nil, err - } + maximumLeafRange := subtreeRootThreshold for currentLeafRange != 0 { nextRange, err := nextLeafRange(currentStart, proofEnd, maximumLeafRange) if err != nil { @@ -632,25 +629,13 @@ func ToLeafRanges(proofStart, proofEnd, subtreeRootThreshold int) ([]LeafRange, return ranges, nil } -// subtreeRootThresholdToLeafRange calculates the maximum number of leaves a subtree root -// can commit to. -// The subtree root threshold is defined as per ADR-013: -// https://github.com/celestiaorg/celestia-app/blob/main/docs/architecture/adr-013-non-interactive-default-rules-for-zero-padding.md -func subtreeRootThresholdToLeafRange(subtreeRootThreshold int) (int, error) { - if subtreeRootThreshold < 0 { - return 0, fmt.Errorf("subtree root threshold cannot be negative %d", subtreeRootThreshold) - } - return 1 << subtreeRootThreshold, nil -} - // nextLeafRange takes a proof start, proof end, and the maximum range a subtree // root can cover, and returns the corresponding subtree root range. -// The subtreeRootMaximum LeafRange is calculated using subtreeRootThresholdToLeafRange() method. // Check ToLeafRanges() for more information on the algorithm used. // Note: This method is Celestia specific. -func nextLeafRange(currentStart, currentEnd, subtreeRootMaximumLeafRange int) (LeafRange, error) { +func nextLeafRange(currentStart, currentEnd, subtreeRootThreshold int) (LeafRange, error) { currentLeafRange := currentEnd - currentStart - minimum := minInt(currentLeafRange, subtreeRootMaximumLeafRange) + minimum := minInt(currentLeafRange, subtreeRootThreshold) uMinimum, err := safeIntToUint(minimum) if err != nil { return LeafRange{}, fmt.Errorf("failed to convert subtree root range to Uint %w", err) diff --git a/proof_test.go b/proof_test.go index a8a14b4..db3bf6e 100644 --- a/proof_test.go +++ b/proof_test.go @@ -1156,30 +1156,6 @@ func TestLargestPowerOfTwo(t *testing.T) { } } -func TestSubtreeRootThresholdToLeafRange(t *testing.T) { - tests := []struct { - subtreeRootThreshold int - expected int - expectErr bool - }{ - {0, 1, false}, - {1, 2, false}, - {-1, 0, true}, - } - - for _, tt := range tests { - t.Run(fmt.Sprintf("subtreeRootThreshold=%d", tt.subtreeRootThreshold), func(t *testing.T) { - result, err := subtreeRootThresholdToLeafRange(tt.subtreeRootThreshold) - if tt.expectErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expected, result) - } - }) - } -} - func TestToLeafRanges(t *testing.T) { tests := []struct { proofStart, proofEnd, subtreeRootThreshold int From cff80b2395f141a82d7fe556c8a6ab6919df831b Mon Sep 17 00:00:00 2001 From: sweexordious Date: Fri, 31 May 2024 22:43:34 +0400 Subject: [PATCH 07/16] fix: tests --- proof_test.go | 56 +++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/proof_test.go b/proof_test.go index db3bf6e..b098ed3 100644 --- a/proof_test.go +++ b/proof_test.go @@ -1165,7 +1165,7 @@ func TestToLeafRanges(t *testing.T) { { proofStart: 0, proofEnd: 8, - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, expectedRanges: []LeafRange{ {Start: 0, End: 8}, }, @@ -1173,7 +1173,7 @@ func TestToLeafRanges(t *testing.T) { { proofStart: 0, proofEnd: 9, - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, expectedRanges: []LeafRange{ {Start: 0, End: 8}, {Start: 8, End: 9}, @@ -1182,7 +1182,7 @@ func TestToLeafRanges(t *testing.T) { { proofStart: 0, proofEnd: 16, - subtreeRootThreshold: 1, + subtreeRootThreshold: 2, expectedRanges: []LeafRange{ {Start: 0, End: 2}, {Start: 2, End: 4}, @@ -1197,7 +1197,7 @@ func TestToLeafRanges(t *testing.T) { { proofStart: 0, proofEnd: 16, - subtreeRootThreshold: 2, + subtreeRootThreshold: 4, expectedRanges: []LeafRange{ {Start: 0, End: 4}, {Start: 4, End: 8}, @@ -1208,7 +1208,7 @@ func TestToLeafRanges(t *testing.T) { { proofStart: 0, proofEnd: 16, - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, expectedRanges: []LeafRange{ {Start: 0, End: 8}, {Start: 8, End: 16}, @@ -1217,7 +1217,7 @@ func TestToLeafRanges(t *testing.T) { { proofStart: 0, proofEnd: 16, - subtreeRootThreshold: 4, + subtreeRootThreshold: 16, expectedRanges: []LeafRange{ {Start: 0, End: 16}, }, @@ -1225,7 +1225,7 @@ func TestToLeafRanges(t *testing.T) { { proofStart: 4, proofEnd: 12, - subtreeRootThreshold: 0, + subtreeRootThreshold: 1, expectedRanges: []LeafRange{ {Start: 4, End: 5}, {Start: 5, End: 6}, @@ -1240,7 +1240,7 @@ func TestToLeafRanges(t *testing.T) { { proofStart: 4, proofEnd: 12, - subtreeRootThreshold: 1, + subtreeRootThreshold: 2, expectedRanges: []LeafRange{ {Start: 4, End: 6}, {Start: 6, End: 8}, @@ -1251,7 +1251,7 @@ func TestToLeafRanges(t *testing.T) { { proofStart: 4, proofEnd: 12, - subtreeRootThreshold: 2, + subtreeRootThreshold: 4, expectedRanges: []LeafRange{ {Start: 4, End: 8}, {Start: 8, End: 12}, @@ -1260,7 +1260,7 @@ func TestToLeafRanges(t *testing.T) { { proofStart: 8, proofEnd: 10, - subtreeRootThreshold: 1, + subtreeRootThreshold: 2, expectedRanges: []LeafRange{ {Start: 8, End: 10}, }, @@ -1518,7 +1518,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, root: root, validProof: true, }, @@ -1533,7 +1533,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, root: root, validProof: true, }, @@ -1548,7 +1548,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, root: root, validProof: true, }, @@ -1563,7 +1563,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, root: root, validProof: true, }, @@ -1580,7 +1580,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot1, subtreeRoot2} }(), - subtreeRootThreshold: 2, + subtreeRootThreshold: 4, root: root, validProof: true, }, @@ -1601,7 +1601,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot1, subtreeRoot2, subtreeRoot3, subtreeRoot4} }(), - subtreeRootThreshold: 1, + subtreeRootThreshold: 2, root: root, validProof: true, }, @@ -1630,7 +1630,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot1, subtreeRoot2, subtreeRoot3, subtreeRoot4, subtreeRoot5, subtreeRoot6, subtreeRoot7, subtreeRoot8} }(), - subtreeRootThreshold: 0, + subtreeRootThreshold: 1, root: root, validProof: true, }, @@ -1645,7 +1645,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, root: root, validProof: true, }, @@ -1660,7 +1660,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, root: root, validProof: true, }, @@ -1675,7 +1675,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, root: root, validProof: true, }, @@ -1690,7 +1690,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, root: root, validProof: true, }, @@ -1705,7 +1705,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, root: root, validProof: true, }, @@ -1735,7 +1735,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot, subtreeRoot} // invalid number of subtree roots }(), - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, root: root, expectError: true, }, @@ -1750,7 +1750,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, root: []byte("random root"), // invalid root format expectError: true, }, @@ -1761,7 +1761,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, root: root, expectError: true, }, @@ -1772,7 +1772,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, root: root, expectError: true, }, @@ -1787,7 +1787,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, root: root, expectError: true, }, @@ -1798,7 +1798,7 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { return p }(), subtreeRoots: [][]byte{[]byte("invalid subtree root")}, // invalid subtree root - subtreeRootThreshold: 3, + subtreeRootThreshold: 8, root: root, expectError: true, }, From 47d211351a823a7765c8003fbadbb98d8dc6ca79 Mon Sep 17 00:00:00 2001 From: sweexordious Date: Sat, 1 Jun 2024 19:50:01 +0400 Subject: [PATCH 08/16] chore: remove unnecessary condition of a power of 2 tree --- nmt.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nmt.go b/nmt.go index 912589e..84bc817 100644 --- a/nmt.go +++ b/nmt.go @@ -645,13 +645,8 @@ func (n *NamespacedMerkleTree) updateMinMaxID(id namespace.ID) { } // ComputeSubtreeRoot takes a leaf range and returns the corresponding subtree root. -// This method requires the merkle tree size to be a power of two. // Also, it requires the start and end range to correctly reference an inner node. func (n *NamespacedMerkleTree) ComputeSubtreeRoot(start, end int) ([]byte, error) { - // check if the tree's number of leaves is a power of two. - if !isPowerOfTwo(n.Size()) { - return nil, fmt.Errorf("the tree size %d needs to be a power of two", n.Size()) - } if start < 0 { return nil, fmt.Errorf("start %d shouldn't be strictly negative", start) } From 661d05999e3864e3999950b477e2206f4f187edf Mon Sep 17 00:00:00 2001 From: sweexordious Date: Mon, 3 Jun 2024 02:08:22 +0400 Subject: [PATCH 09/16] chore: renaming --- proof.go | 32 +++-- proof_test.go | 336 +++++++++++++++++++++++++------------------------- 2 files changed, 185 insertions(+), 183 deletions(-) diff --git a/proof.go b/proof.go index 8ca8545..5d5e33b 100644 --- a/proof.go +++ b/proof.go @@ -474,9 +474,9 @@ func (proof Proof) VerifyInclusion(h hash.Hash, nid namespace.ID, leavesWithoutN // - The tree's number of leaves is a power of two // Using this method without making sure the above assumptions are respected // can return invalid results. -// The subtreeRootThreshold is also defined in ADR-013. +// The subtreeWidth is also defined in ADR-013. // More information on the algorithm used can be found in the ToLeafRanges() method docs. -func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]byte, subtreeRootThreshold int, root []byte) (bool, error) { +func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]byte, subtreeWidth int, root []byte) (bool, error) { // check that the proof range is valid if proof.Start() < 0 || proof.Start() >= proof.End() { return false, fmt.Errorf("proof range [proof.start=%d, proof.end=%d) is not valid: %w", proof.Start(), proof.End(), ErrInvalidRange) @@ -500,7 +500,7 @@ func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]b } // get the subtree roots leaf ranges - ranges, err := ToLeafRanges(proof.Start(), proof.End(), subtreeRootThreshold) + ranges, err := ToLeafRanges(proof.Start(), proof.End(), subtreeWidth) if err != nil { return false, err } @@ -519,8 +519,12 @@ func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]b // if the leaf index falls within the proof range, pop and return a // leaf if proof.Start() <= start && start < proof.End() { - // advance the list of ranges - ranges = ranges[1:] + // this check should always be the case. + // however, it is added to avoid nil pointer exceptions + if len(ranges) != 0 { + // advance the list of ranges + ranges = ranges[1:] + } // advance leafHashes return popIfNonEmpty(&subtreeRoots), nil } @@ -585,9 +589,9 @@ func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]b } // ToLeafRanges returns the leaf ranges corresponding to the provided subtree roots. -// It uses the subtree root threshold to calculate the maximum number of leaves a subtree root can +// It uses the subtree root width to calculate the maximum number of leaves a subtree root can // commit to. -// The subtree root threshold is defined as per ADR-013: +// The subtree root width is defined as per ADR-013: // https://github.com/celestiaorg/celestia-app/blob/main/docs/architecture/adr-013-non-interactive-default-rules-for-zero-padding.md // This method assumes: // - The subtree roots are created according to the ADR-013 non-interactive defaults rules @@ -603,20 +607,20 @@ func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]b // - Go back to the loop condition. // // Note: This method is Celestia specific. -func ToLeafRanges(proofStart, proofEnd, subtreeRootThreshold int) ([]LeafRange, error) { +func ToLeafRanges(proofStart, proofEnd, subtreeWidth int) ([]LeafRange, error) { if proofStart < 0 { return nil, fmt.Errorf("proof start %d shouldn't be strictly negative", proofStart) } if proofEnd <= proofStart { return nil, fmt.Errorf("proof end %d should be stricly bigger than proof start %d", proofEnd, proofStart) } - if subtreeRootThreshold <= 0 { - return nil, fmt.Errorf("subtree root threshold cannot be negative %d", subtreeRootThreshold) + if subtreeWidth <= 0 { + return nil, fmt.Errorf("subtree root width cannot be negative %d", subtreeWidth) } currentStart := proofStart currentLeafRange := proofEnd - proofStart var ranges []LeafRange - maximumLeafRange := subtreeRootThreshold + maximumLeafRange := subtreeWidth for currentLeafRange != 0 { nextRange, err := nextLeafRange(currentStart, proofEnd, maximumLeafRange) if err != nil { @@ -632,10 +636,12 @@ func ToLeafRanges(proofStart, proofEnd, subtreeRootThreshold int) ([]LeafRange, // nextLeafRange takes a proof start, proof end, and the maximum range a subtree // root can cover, and returns the corresponding subtree root range. // Check ToLeafRanges() for more information on the algorithm used. +// The subtreeWidth is calculated using SubTreeWidth() method +// in celestiaorg/go-square/inclusion package. // Note: This method is Celestia specific. -func nextLeafRange(currentStart, currentEnd, subtreeRootThreshold int) (LeafRange, error) { +func nextLeafRange(currentStart, currentEnd, subtreeWidth int) (LeafRange, error) { currentLeafRange := currentEnd - currentStart - minimum := minInt(currentLeafRange, subtreeRootThreshold) + minimum := minInt(currentLeafRange, subtreeWidth) uMinimum, err := safeIntToUint(minimum) if err != nil { return LeafRange{}, fmt.Errorf("failed to convert subtree root range to Uint %w", err) diff --git a/proof_test.go b/proof_test.go index b098ed3..f0d22c7 100644 --- a/proof_test.go +++ b/proof_test.go @@ -1158,75 +1158,50 @@ func TestLargestPowerOfTwo(t *testing.T) { func TestToLeafRanges(t *testing.T) { tests := []struct { - proofStart, proofEnd, subtreeRootThreshold int - expectedRanges []LeafRange - expectError bool + proofStart, proofEnd, subtreeWidth int + expectedRanges []LeafRange + expectError bool }{ { - proofStart: 0, - proofEnd: 8, - subtreeRootThreshold: 8, + proofStart: 0, + proofEnd: 8, + subtreeWidth: 1, expectedRanges: []LeafRange{ - {Start: 0, End: 8}, + {Start: 0, End: 1}, + {Start: 1, End: 2}, + {Start: 2, End: 3}, + {Start: 3, End: 4}, + {Start: 4, End: 5}, + {Start: 5, End: 6}, + {Start: 6, End: 7}, + {Start: 7, End: 8}, }, }, { - proofStart: 0, - proofEnd: 9, - subtreeRootThreshold: 8, + proofStart: 0, + proofEnd: 9, + subtreeWidth: 1, expectedRanges: []LeafRange{ - {Start: 0, End: 8}, + {Start: 0, End: 1}, + {Start: 1, End: 2}, + {Start: 2, End: 3}, + {Start: 3, End: 4}, + {Start: 4, End: 5}, + {Start: 5, End: 6}, + {Start: 6, End: 7}, + {Start: 7, End: 8}, {Start: 8, End: 9}, }, }, { - proofStart: 0, - proofEnd: 16, - subtreeRootThreshold: 2, - expectedRanges: []LeafRange{ - {Start: 0, End: 2}, - {Start: 2, End: 4}, - {Start: 4, End: 6}, - {Start: 6, End: 8}, - {Start: 8, End: 10}, - {Start: 10, End: 12}, - {Start: 12, End: 14}, - {Start: 14, End: 16}, - }, - }, - { - proofStart: 0, - proofEnd: 16, - subtreeRootThreshold: 4, - expectedRanges: []LeafRange{ - {Start: 0, End: 4}, - {Start: 4, End: 8}, - {Start: 8, End: 12}, - {Start: 12, End: 16}, - }, - }, - { - proofStart: 0, - proofEnd: 16, - subtreeRootThreshold: 8, - expectedRanges: []LeafRange{ - {Start: 0, End: 8}, - {Start: 8, End: 16}, - }, - }, - { - proofStart: 0, - proofEnd: 16, - subtreeRootThreshold: 16, - expectedRanges: []LeafRange{ - {Start: 0, End: 16}, - }, - }, - { - proofStart: 4, - proofEnd: 12, - subtreeRootThreshold: 1, + proofStart: 0, + proofEnd: 16, + subtreeWidth: 1, expectedRanges: []LeafRange{ + {Start: 0, End: 1}, + {Start: 1, End: 2}, + {Start: 2, End: 3}, + {Start: 3, End: 4}, {Start: 4, End: 5}, {Start: 5, End: 6}, {Start: 6, End: 7}, @@ -1235,69 +1210,85 @@ func TestToLeafRanges(t *testing.T) { {Start: 9, End: 10}, {Start: 10, End: 11}, {Start: 11, End: 12}, + {Start: 12, End: 13}, + {Start: 13, End: 14}, + {Start: 14, End: 15}, + {Start: 15, End: 16}, }, }, { - proofStart: 4, - proofEnd: 12, - subtreeRootThreshold: 2, - expectedRanges: []LeafRange{ - {Start: 4, End: 6}, - {Start: 6, End: 8}, - {Start: 8, End: 10}, - {Start: 10, End: 12}, - }, + proofStart: 0, + proofEnd: 100, + subtreeWidth: 2, + expectedRanges: func() []LeafRange { + var ranges []LeafRange + for i := 0; i < 100; i = i + 2 { + ranges = append(ranges, LeafRange{i, i + 2}) + } + return ranges + }(), }, { - proofStart: 4, - proofEnd: 12, - subtreeRootThreshold: 4, - expectedRanges: []LeafRange{ - {Start: 4, End: 8}, - {Start: 8, End: 12}, - }, + proofStart: 0, + proofEnd: 150, + subtreeWidth: 4, + expectedRanges: func() []LeafRange { + var ranges []LeafRange + for i := 0; i < 148; i = i + 4 { + ranges = append(ranges, LeafRange{i, i + 4}) + } + ranges = append(ranges, LeafRange{ + Start: 148, + End: 150, + }) + return ranges + }(), }, { - proofStart: 8, - proofEnd: 10, - subtreeRootThreshold: 2, - expectedRanges: []LeafRange{ - {Start: 8, End: 10}, - }, + proofStart: 0, + proofEnd: 400, + subtreeWidth: 8, + expectedRanges: func() []LeafRange { + var ranges []LeafRange + for i := 0; i < 400; i = i + 8 { + ranges = append(ranges, LeafRange{i, i + 8}) + } + return ranges + }(), }, { - proofStart: -1, - proofEnd: 0, - subtreeRootThreshold: -1, - expectedRanges: nil, - expectError: true, + proofStart: -1, + proofEnd: 0, + subtreeWidth: -1, + expectedRanges: nil, + expectError: true, }, { - proofStart: 0, - proofEnd: -1, - subtreeRootThreshold: -1, - expectedRanges: nil, - expectError: true, + proofStart: 0, + proofEnd: -1, + subtreeWidth: -1, + expectedRanges: nil, + expectError: true, }, { - proofStart: 0, - proofEnd: 0, - subtreeRootThreshold: 2, - expectedRanges: nil, - expectError: true, + proofStart: 0, + proofEnd: 0, + subtreeWidth: 2, + expectedRanges: nil, + expectError: true, }, { - proofStart: 0, - proofEnd: 0, - subtreeRootThreshold: -1, - expectedRanges: nil, - expectError: true, + proofStart: 0, + proofEnd: 0, + subtreeWidth: -1, + expectedRanges: nil, + expectError: true, }, } for _, tt := range tests { - t.Run(fmt.Sprintf("proofStart=%d, proofEnd=%d, subtreeRootThreshold=%d", tt.proofStart, tt.proofEnd, tt.subtreeRootThreshold), func(t *testing.T) { - result, err := ToLeafRanges(tt.proofStart, tt.proofEnd, tt.subtreeRootThreshold) + t.Run(fmt.Sprintf("proofStart=%d, proofEnd=%d, subtreeWidth=%d", tt.proofStart, tt.proofEnd, tt.subtreeWidth), func(t *testing.T) { + result, err := ToLeafRanges(tt.proofStart, tt.proofEnd, tt.subtreeWidth) if tt.expectError { assert.Error(t, err) } else { @@ -1322,9 +1313,12 @@ func compareRanges(a, b []LeafRange) bool { func TestNextLeafRange(t *testing.T) { tests := []struct { - currentStart, currentEnd, subtreeRootMaximumLeafRange int - expectedRange LeafRange - expectError bool + currentStart, currentEnd int + // the maximum leaf range == subtree width used in these tests do not follow ADR-013 + // they're just used to try different test cases + subtreeRootMaximumLeafRange int + expectedRange LeafRange + expectError bool }{ { currentStart: 0, @@ -1500,12 +1494,14 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { hasher := nmthasher.(*NmtHasher) tests := []struct { - proof Proof - subtreeRoots [][]byte - subtreeRootThreshold int - root []byte - validProof bool - expectError bool + proof Proof + subtreeRoots [][]byte + // the subtree widths used in these tests do not follow ADR-013 + // they're just used to try different test cases + subtreeWidth int + root []byte + validProof bool + expectError bool }{ { proof: func() Proof { @@ -1518,9 +1514,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 8, - root: root, - validProof: true, + subtreeWidth: 8, + root: root, + validProof: true, }, { proof: func() Proof { @@ -1533,9 +1529,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 8, - root: root, - validProof: true, + subtreeWidth: 8, + root: root, + validProof: true, }, { proof: func() Proof { @@ -1548,9 +1544,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 8, - root: root, - validProof: true, + subtreeWidth: 8, + root: root, + validProof: true, }, { proof: func() Proof { @@ -1563,9 +1559,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 8, - root: root, - validProof: true, + subtreeWidth: 8, + root: root, + validProof: true, }, { proof: func() Proof { @@ -1580,9 +1576,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot1, subtreeRoot2} }(), - subtreeRootThreshold: 4, - root: root, - validProof: true, + subtreeWidth: 4, + root: root, + validProof: true, }, { proof: func() Proof { @@ -1601,9 +1597,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot1, subtreeRoot2, subtreeRoot3, subtreeRoot4} }(), - subtreeRootThreshold: 2, - root: root, - validProof: true, + subtreeWidth: 2, + root: root, + validProof: true, }, { proof: func() Proof { @@ -1630,9 +1626,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot1, subtreeRoot2, subtreeRoot3, subtreeRoot4, subtreeRoot5, subtreeRoot6, subtreeRoot7, subtreeRoot8} }(), - subtreeRootThreshold: 1, - root: root, - validProof: true, + subtreeWidth: 1, + root: root, + validProof: true, }, { proof: func() Proof { @@ -1645,9 +1641,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 8, - root: root, - validProof: true, + subtreeWidth: 8, + root: root, + validProof: true, }, { proof: func() Proof { @@ -1660,9 +1656,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 8, - root: root, - validProof: true, + subtreeWidth: 8, + root: root, + validProof: true, }, { proof: func() Proof { @@ -1675,9 +1671,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 8, - root: root, - validProof: true, + subtreeWidth: 8, + root: root, + validProof: true, }, { proof: func() Proof { @@ -1690,9 +1686,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 8, - root: root, - validProof: true, + subtreeWidth: 8, + root: root, + validProof: true, }, { proof: func() Proof { @@ -1705,9 +1701,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 8, - root: root, - validProof: true, + subtreeWidth: 8, + root: root, + validProof: true, }, { proof: func() Proof { @@ -1720,9 +1716,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: -3, // invalid subtree root threshold - root: root, - expectError: true, + subtreeWidth: -3, // invalid subtree root width + root: root, + expectError: true, }, { proof: func() Proof { @@ -1735,9 +1731,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot, subtreeRoot} // invalid number of subtree roots }(), - subtreeRootThreshold: 8, - root: root, - expectError: true, + subtreeWidth: 8, + root: root, + expectError: true, }, { proof: func() Proof { @@ -1750,9 +1746,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 8, - root: []byte("random root"), // invalid root format - expectError: true, + subtreeWidth: 8, + root: []byte("random root"), // invalid root format + expectError: true, }, { proof: Proof{start: -1}, // invalid start @@ -1761,9 +1757,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 8, - root: root, - expectError: true, + subtreeWidth: 8, + root: root, + expectError: true, }, { proof: Proof{end: 1, start: 2}, // invalid end @@ -1772,9 +1768,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 8, - root: root, - expectError: true, + subtreeWidth: 8, + root: root, + expectError: true, }, { proof: Proof{ @@ -1787,9 +1783,9 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return [][]byte{subtreeRoot} }(), - subtreeRootThreshold: 8, - root: root, - expectError: true, + subtreeWidth: 8, + root: root, + expectError: true, }, { proof: func() Proof { @@ -1797,16 +1793,16 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { require.NoError(t, err) return p }(), - subtreeRoots: [][]byte{[]byte("invalid subtree root")}, // invalid subtree root - subtreeRootThreshold: 8, - root: root, - expectError: true, + subtreeRoots: [][]byte{[]byte("invalid subtree root")}, // invalid subtree root + subtreeWidth: 8, + root: root, + expectError: true, }, } for _, tt := range tests { - t.Run(fmt.Sprintf("proofStart=%d, proofEnd=%d, subTreeRootThreshold=%d", tt.proof.Start(), tt.proof.End(), tt.subtreeRootThreshold), func(t *testing.T) { - result, err := tt.proof.VerifySubtreeRootInclusion(hasher, tt.subtreeRoots, tt.subtreeRootThreshold, tt.root) + t.Run(fmt.Sprintf("proofStart=%d, proofEnd=%d, subTreeWidth=%d", tt.proof.Start(), tt.proof.End(), tt.subtreeWidth), func(t *testing.T) { + result, err := tt.proof.VerifySubtreeRootInclusion(hasher, tt.subtreeRoots, tt.subtreeWidth, tt.root) if tt.expectError { assert.Error(t, err) } else { From a81b748b6f51f3f256fca8e4388520fb32dc2a7b Mon Sep 17 00:00:00 2001 From: sweexordious Date: Mon, 3 Jun 2024 02:10:58 +0400 Subject: [PATCH 10/16] chore: remove unnecessary core --- nmt.go | 5 ----- nmt_test.go | 27 --------------------------- 2 files changed, 32 deletions(-) diff --git a/nmt.go b/nmt.go index 84bc817..76591c5 100644 --- a/nmt.go +++ b/nmt.go @@ -669,11 +669,6 @@ func (n *NamespacedMerkleTree) ComputeSubtreeRoot(start, end int) ([]byte, error return n.computeRoot(start, end) } -// isPowerOfTwo checks if a number is a power of two -func isPowerOfTwo(n int) bool { - return n > 0 && (n&(n-1)) == 0 -} - type LeafRange struct { // Start and End denote the indices of a leaf in the tree. // Start ranges from 0 up to the total number of leaves minus 1. diff --git a/nmt_test.go b/nmt_test.go index b10e0e4..10d3d2b 100644 --- a/nmt_test.go +++ b/nmt_test.go @@ -1190,33 +1190,6 @@ func TestForcedOutOfOrderNamespacedMerkleTree(t *testing.T) { } } -func TestIsPowerOfTwo(t *testing.T) { - tests := []struct { - input int - expected bool - }{ - {input: 0, expected: false}, - {input: 1, expected: true}, - {input: 2, expected: true}, - {input: 3, expected: false}, - {input: 4, expected: true}, - {input: 5, expected: false}, - {input: 8, expected: true}, - {input: 16, expected: true}, - {input: -1, expected: false}, - {input: -2, expected: false}, - } - - for _, tt := range tests { - t.Run(fmt.Sprintf("input=%d", tt.input), func(t *testing.T) { - result := isPowerOfTwo(tt.input) - if result != tt.expected { - t.Errorf("expected %v, got %v", tt.expected, result) - } - }) - } -} - func TestComputeSubtreeRoot(t *testing.T) { n := exampleNMT2(1, true, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) tests := []struct { From a68a69ffb1108e22d8c1c2b9bb17c994806c7d0c Mon Sep 17 00:00:00 2001 From: sweexordious Date: Mon, 24 Jun 2024 17:39:51 +0100 Subject: [PATCH 11/16] docs: document the assumptions --- proof.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/proof.go b/proof.go index 5d5e33b..82c1224 100644 --- a/proof.go +++ b/proof.go @@ -467,13 +467,11 @@ func (proof Proof) VerifyInclusion(h hash.Hash, nid namespace.ID, leavesWithoutN // VerifySubtreeRootInclusion verifies that a set of subtree roots is included in // an NMT. -// Note: This method is Celestia specific. -// It makes the following assumptions: +// Warning: This method is Celestia specific! Using it without verifying +// the following assumptions, can return unexpected errors, false positive/negatives: // - The subtree roots are created according to the ADR-013 // https://github.com/celestiaorg/celestia-app/blob/main/docs/architecture/adr-013-non-interactive-default-rules-for-zero-padding.md // - The tree's number of leaves is a power of two -// Using this method without making sure the above assumptions are respected -// can return invalid results. // The subtreeWidth is also defined in ADR-013. // More information on the algorithm used can be found in the ToLeafRanges() method docs. func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]byte, subtreeWidth int, root []byte) (bool, error) { From 9d92a94c3ffd522cbb73fd3104781cc3df9f52d3 Mon Sep 17 00:00:00 2001 From: sweexordious Date: Wed, 26 Jun 2024 15:17:54 +0100 Subject: [PATCH 12/16] docs: document end exclusive to leaf range --- proof.go | 1 + 1 file changed, 1 insertion(+) diff --git a/proof.go b/proof.go index 82c1224..5c5b5c2 100644 --- a/proof.go +++ b/proof.go @@ -587,6 +587,7 @@ func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]b } // ToLeafRanges returns the leaf ranges corresponding to the provided subtree roots. +// The proof range defined by proofStart and proofEnd is end exclusive. // It uses the subtree root width to calculate the maximum number of leaves a subtree root can // commit to. // The subtree root width is defined as per ADR-013: From 12ec5633fc3d1f746b9c11d71e149694d72d5c3d Mon Sep 17 00:00:00 2001 From: sweexordious Date: Mon, 1 Jul 2024 11:50:35 +0100 Subject: [PATCH 13/16] docs: document end exclusive to compute subtree root --- nmt.go | 1 + proof.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/nmt.go b/nmt.go index 76591c5..f8f82f5 100644 --- a/nmt.go +++ b/nmt.go @@ -646,6 +646,7 @@ func (n *NamespacedMerkleTree) updateMinMaxID(id namespace.ID) { // ComputeSubtreeRoot takes a leaf range and returns the corresponding subtree root. // Also, it requires the start and end range to correctly reference an inner node. +// The provided range, defined by start and end, is end-exclusive. func (n *NamespacedMerkleTree) ComputeSubtreeRoot(start, end int) ([]byte, error) { if start < 0 { return nil, fmt.Errorf("start %d shouldn't be strictly negative", start) diff --git a/proof.go b/proof.go index 5c5b5c2..289fc41 100644 --- a/proof.go +++ b/proof.go @@ -506,7 +506,7 @@ func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]b // check whether the number of ranges matches the number of subtree roots. // if not, make an early return. if len(subtreeRoots) != len(ranges) { - return false, fmt.Errorf("number of subtree roots %d is different than the number of leaf ranges %d", len(subtreeRoots), len(ranges)) + return false, fmt.Errorf("number of subtree roots %d is different than the number of the expected leaf ranges %d", len(subtreeRoots), len(ranges)) } var computeRoot func(start, end int) ([]byte, error) From 3ad98af20ba715d54aab08441e0129406161f49b Mon Sep 17 00:00:00 2001 From: sweexordious Date: Mon, 8 Jul 2024 21:29:37 +0100 Subject: [PATCH 14/16] docs: nextLeafRange docs --- proof.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proof.go b/proof.go index 289fc41..f8fae0e 100644 --- a/proof.go +++ b/proof.go @@ -637,6 +637,8 @@ func ToLeafRanges(proofStart, proofEnd, subtreeWidth int) ([]LeafRange, error) { // Check ToLeafRanges() for more information on the algorithm used. // The subtreeWidth is calculated using SubTreeWidth() method // in celestiaorg/go-square/inclusion package. +// The subtreeWidth is a power of two. +// Also, the LeafRange values, i.e., the range size, are all powers of two. // Note: This method is Celestia specific. func nextLeafRange(currentStart, currentEnd, subtreeWidth int) (LeafRange, error) { currentLeafRange := currentEnd - currentStart From 70bc2b03158aeb4d82c6de5b30810fae52316a09 Mon Sep 17 00:00:00 2001 From: sweexordious Date: Tue, 16 Jul 2024 15:35:27 +0200 Subject: [PATCH 15/16] fix: return error when the leaf ranges number is not valid --- nmt_test.go | 2 +- proof.go | 27 +++++---------------------- proof_test.go | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/nmt_test.go b/nmt_test.go index 10d3d2b..8c157f2 100644 --- a/nmt_test.go +++ b/nmt_test.go @@ -1300,7 +1300,7 @@ func TestComputeSubtreeRoot(t *testing.T) { }, { start: 0, - end: 16, + end: 2, tree: func() *NamespacedMerkleTree { return exampleNMT2(1, true, 0, 1, 2, 3, 4) // tree leaves are not a power of 2 }(), diff --git a/proof.go b/proof.go index f8fae0e..bde6723 100644 --- a/proof.go +++ b/proof.go @@ -512,27 +512,6 @@ func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]b var computeRoot func(start, end int) ([]byte, error) // computeRoot can return error iff the HashNode function fails while calculating the root computeRoot = func(start, end int) ([]byte, error) { - // reached a leaf - if end-start == 1 { - // if the leaf index falls within the proof range, pop and return a - // leaf - if proof.Start() <= start && start < proof.End() { - // this check should always be the case. - // however, it is added to avoid nil pointer exceptions - if len(ranges) != 0 { - // advance the list of ranges - ranges = ranges[1:] - } - // advance leafHashes - return popIfNonEmpty(&subtreeRoots), nil - } - - // if the leaf index is outside the proof range, pop and return a - // proof node (which in this case is a leaf) if present, else return - // nil because leaf doesn't exist - return popIfNonEmpty(&proof.nodes), nil - } - // if the current range does not overlap with the proof range, pop and // return a proof node if present, else return nil because subtree // doesn't exist @@ -540,7 +519,11 @@ func (proof Proof) VerifySubtreeRootInclusion(nth *NmtHasher, subtreeRoots [][]b return popIfNonEmpty(&proof.nodes), nil } - if len(ranges) != 0 && ranges[0].Start == start && ranges[0].End == end { + if len(ranges) == 0 { + return nil, fmt.Errorf(fmt.Sprintf("expected to have a subtree root for range [%d, %d)", start, end)) + } + + if ranges[0].Start == start && ranges[0].End == end { ranges = ranges[1:] return popIfNonEmpty(&subtreeRoots), nil } diff --git a/proof_test.go b/proof_test.go index f0d22c7..b2ea98f 100644 --- a/proof_test.go +++ b/proof_test.go @@ -1798,6 +1798,22 @@ func TestVerifySubtreeRootInclusion(t *testing.T) { root: root, expectError: true, }, + + { + proof: func() Proof { + p, err := tree.ProveRange(0, 8) + require.NoError(t, err) + return p + }(), + subtreeRoots: func() [][]byte { + subtreeRoot1, err := tree.ComputeSubtreeRoot(0, 4) + require.NoError(t, err) + return [][]byte{subtreeRoot1} // will error because it requires the subtree root of [4,8) too + }(), + subtreeWidth: 4, + root: root, + expectError: true, + }, } for _, tt := range tests { From a580bac133a08746c8f3421c6ab613f59ec5fff3 Mon Sep 17 00:00:00 2001 From: sweexordious Date: Tue, 16 Jul 2024 15:45:11 +0200 Subject: [PATCH 16/16] chore: remove unnecessary test --- nmt_test.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/nmt_test.go b/nmt_test.go index 8c157f2..8307681 100644 --- a/nmt_test.go +++ b/nmt_test.go @@ -1298,14 +1298,6 @@ func TestComputeSubtreeRoot(t *testing.T) { tree: n, expectError: true, }, - { - start: 0, - end: 2, - tree: func() *NamespacedMerkleTree { - return exampleNMT2(1, true, 0, 1, 2, 3, 4) // tree leaves are not a power of 2 - }(), - expectError: true, - }, } for _, tt := range tests {