Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: adds input range check to optimize VerifyLeafHashes and VerifyInclusion methods #253

Merged
merged 15 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions docs/spec/nmt.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,21 +198,21 @@ More formally, the short namespace absence proof consists of the following compo
1) Find the index of a leaf in the tree that meets two conditions:
1) Its namespace is the smallest namespace greater than `NS`.
1) The namespace of the leaf to its left is smaller than `NS`.
1) Traverse up the branch connecting that leaf to the root and locate one of the parents/grandparents of that leaf whose namespace range does not overlap with the queried namespace.
1) Traverse up the branch connecting that leaf to the root and locate one of the parents/grandparents of that leaf whose namespace range does not overlap with the queried namespace.
The `SubtreeHash` is the hash of that node.
1) `start` and `end` range: These represent the indices of the `SubtreeHash` within its respective level.
1) `start` and `end` range: These represent the indices of the `SubtreeHash` within its respective level.
Nodes at each level are indexed from left to right starting at index `0`.
1) `nodes`: This set comprises the index-based Merkle inclusion proof of the `SubtreeHash` to the tree root `T`.

Below, we illustrate the short namespace absence proof for namespace `NS = 02` in an 8-leaf tree:
The namespace `03` is the smallest namespace larger than `02`.
By traversing the branch from the leaf with namespace `03` to the root, we find a node with hash `03 04 52c7c03` whose namespace range doesn't overlap with `02`.
The namespace `03` is the smallest namespace larger than `02`.
By traversing the branch from the leaf with namespace `03` to the root, we find a node with hash `03 04 52c7c03` whose namespace range doesn't overlap with `02`.
This node is the highest such node along the branch.
The `SubtreeHash` is the hash of that node, which is `03 04 52c7c03`.
The `start` and `end` indices indicate its position in the respective level.
In this case, `start = 1` and `end = 2`.
The `start` and `end` indices indicate its position in the respective level.
In this case, `start = 1` and `end = 2`.
Note that node indices start at `0` from left to right at each level.
The `nodes` form the index-based Merkle inclusion proof of the `SubtreeHash` to the tree root `T`.
The `nodes` form the index-based Merkle inclusion proof of the `SubtreeHash` to the tree root `T`.
The `nodes` set includes `00 00 ead8d25`, the left sibling of `03 04 52c7c03`.

In summary, the short namespace absence proof for `NS = 02` in this tree consists of `SubtreeHash = 03 04 52c7c03`, `start = 1`, `end = 2`, and the `nodes` set containing `00 00 ead8d25`.
Expand Down
25 changes: 23 additions & 2 deletions proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ import (
pb "github.com/celestiaorg/nmt/pb"
)

// ErrFailedCompletenessCheck indicates that the verification of a namespace proof failed due to the lack of completeness property.
var ErrFailedCompletenessCheck = errors.New("failed completeness check")
var (
// ErrFailedCompletenessCheck indicates that the verification of a namespace proof failed due to the lack of completeness property.
ErrFailedCompletenessCheck = errors.New("failed completeness check")
ErrWrongLeafHashesSize = errors.New("wrong leafHashes size")
)

// Proof represents a namespace proof of a namespace.ID in an NMT. In case this
// proof proves the absence of a namespace.ID in a tree it also contains the
Expand Down Expand Up @@ -250,6 +253,7 @@ func (proof Proof) VerifyNamespace(h hash.Hash, nID namespace.ID, leaves [][]byt
// 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
Expand All @@ -260,6 +264,15 @@ func (proof Proof) VerifyLeafHashes(nth *NmtHasher, verifyCompleteness bool, nID
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)
}

// perform some consistency checks:
if nID.Size() != nth.NamespaceSize() {
return false, fmt.Errorf("namespace ID size (%d) does not match the namespace size of the NMT hasher (%d)", nID.Size(), nth.NamespaceSize())
Expand Down Expand Up @@ -395,6 +408,7 @@ func (proof Proof) VerifyLeafHashes(nth *NmtHasher, verifyCompleteness bool, nID
// 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
Expand All @@ -411,6 +425,13 @@ func (proof Proof) VerifyInclusion(h hash.Hash, nid namespace.ID, leavesWithoutN
return false
}

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

nth := NewNmtHasher(h, nid.Size(), proof.isMaxNamespaceIDIgnored)

// perform some consistency checks:
Expand Down
169 changes: 168 additions & 1 deletion proof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,168 @@ func TestVerifyNamespace_False(t *testing.T) {
}
}

func TestVerifyInclusion_MismatchingRange(t *testing.T) {
nIDs := []byte{1, 2, 3, 4, 6, 6, 6, 9}
nmt := exampleNMT(1, true, nIDs...)
root, err := nmt.Root()
require.NoError(t, err)

nid6 := namespace.ID{6}
// node at index 5 has namespace ID 6
incProof6, err := nmt.ProveNamespace(nid6)
require.NoError(t, err)
// leaves with namespace ID 6
leaf4 := nmt.leaves[4][nmt.NamespaceSize():]
leaf5 := nmt.leaves[5][nmt.NamespaceSize():]
leaf6 := nmt.leaves[6][nmt.NamespaceSize():]

type args struct {
nIDSize namespace.IDSize
nID namespace.ID
leavesWithoutNamespace [][]byte
root []byte
}
tests := []struct {
name string
proof Proof
args args
result bool
}{
{
"inclusion proof: size of proof's range = size of leavesWithoutNamespace",
incProof6,
args{1, nid6, [][]byte{leaf4, leaf5, leaf6}, root},
true,
},
{
"inclusion proof: size of proof's range > size of" +
" a non-empty leavesWithoutNamespace",
incProof6,
args{1, nid6, [][]byte{leaf4, leaf5}, root},
false,
},
{
"inclusion proof: size of proof's range > size of" +
" an empty leavesWithoutNamespace",
incProof6,
args{1, nid6, [][]byte{}, root},
false,
},
{
"inclusion proof: size of proof's range < size of" +
" leavesWithoutNamespace",
incProof6,
args{1, nid6, [][]byte{leaf4, leaf5, leaf6, leaf6}, root},
false,
},
{
// in this testcase the nameID does not really matter since the
// leaves are empty
"empty proof: size of proof's range = size of leavesWithoutNamespace",
Proof{start: 1, end: 1},
args{1, nid6, [][]byte{}, root},
true,
},
{
"empty proof: size of proof's range < size of" +
" leavesWithoutNamespace",
Proof{start: 1, end: 1},
args{1, nid6, [][]byte{leaf4, leaf5, leaf6}, root},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hasher := sha256.New()
got := tt.proof.VerifyInclusion(hasher, tt.args.nID,
tt.args.leavesWithoutNamespace, tt.args.root)
assert.Equal(t, tt.result, got)
})
}
}

func TestVerifyLeafHashes_MismatchingRange(t *testing.T) {
nIDs := []byte{1, 2, 3, 4, 6, 6, 6, 9}
nmt := exampleNMT(1, true, nIDs...)
root, err := nmt.Root()
require.NoError(t, err)

nid5 := namespace.ID{5}
// namespace 5 does not exist in the tree, hence the proof is an absence proof
absenceProof5, err := nmt.ProveNamespace(nid5)
require.NoError(t, err)
leafHash5 := nmt.leafHashes[4]

nid6 := namespace.ID{6}
// node at index 5 has namespace ID 6
incProof6, err := nmt.Prove(5)
require.NoError(t, err)
leafHash6 := nmt.leafHashes[5]

type args struct {
nIDSize namespace.IDSize
nID namespace.ID
leafHashes [][]byte
root []byte
}
tests := []struct {
name string
proof Proof
args args
result bool
err error
}{
{
"absence proof: size of proof's range = size of leafHashes",
absenceProof5,
args{1, namespace.ID{5}, [][]byte{leafHash5}, root},
true, nil,
},
{
"absence proof: size of proof's range > size of leafHashes",
absenceProof5,
args{1, nid5, [][]byte{}, root},
false, ErrWrongLeafHashesSize,
},
{
"absence proof: size of proof's range < size of leafHashes",
absenceProof5,
args{1, nid5, [][]byte{leafHash5, leafHash5}, root},
false, ErrWrongLeafHashesSize,
},
{
"inclusion proof: size of proof's range = size of leafHashes",
incProof6,
args{1, nid6, [][]byte{leafHash6}, root},
true, nil,
},
{
"inclusion proof: size of proof's range > size of leafHashes",
incProof6,
args{1, nid6, [][]byte{}, root},
false, ErrWrongLeafHashesSize,
},
{
"inclusion proof: size of proof's range < size of leafHashes",
incProof6,
args{1, nid6, [][]byte{leafHash6, leafHash6}, root},
false,
ErrWrongLeafHashesSize,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hasher := NewNmtHasher(sha256.New(), tt.args.nIDSize, true)
got, err := tt.proof.VerifyLeafHashes(hasher, false, tt.args.nID,
tt.args.leafHashes, tt.args.root)
assert.Equal(t, tt.result, got)
if tt.err != nil {
assert.ErrorAs(t, err, &tt.err)
}
})
}
}

func TestVerifyLeafHashes_False(t *testing.T) {
nIDs := []byte{1, 2, 3, 4, 6, 7, 8, 9}

Expand Down Expand Up @@ -594,7 +756,12 @@ func TestVerifyLeafHashes_False(t *testing.T) {
args args
result bool
}{
{"nID size of proof < nID size of VerifyLeafHashes' nmt hasher", proof4_1, args{2, nid4_2, [][]byte{leafHash2}, root2}, false},
{
"nID size of proof < nID size of VerifyLeafHashes' nmt hasher",
proof4_1,
args{2, nid4_2, [][]byte{leafHash2}, root2},
false,
},
{"nID size of proof > nID size of VerifyLeafHashes' nmt hasher", proof4_2, args{1, nid4_1, [][]byte{leafHash1}, root1}, false},
{"nID size of root < nID size of VerifyLeafHashes' nmt hasher", proof4_2, args{2, nid4_2, [][]byte{leafHash2}, root1}, false},
{"nID size of root > nID size of VerifyLeafHashes' nmt hasher", proof4_1, args{1, nid4_1, [][]byte{leafHash1}, root2}, false},
Expand Down
Loading