Skip to content

Commit

Permalink
test: incorporates tests for short absence proof verification (#217)
Browse files Browse the repository at this point in the history
## Overview
After investigating the current implementation, verified that the nmt
library is capable of verifying short absence proofs. This PR contains
the necessary tests to demonstrate and test this capability.

In line with #210 

## Checklist

- [x] New and updated code has appropriate documentation
- [x] New and updated code has new and/or updated testing
- [x] Required CI checks are passing
- [x] Visual proof for any user facing features like CLI or
documentation updates
- [x] Linked issues closed with keywords
  • Loading branch information
staheri14 committed Jul 4, 2023
1 parent 48b8f96 commit 3693c9a
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 14 deletions.
27 changes: 13 additions & 14 deletions proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,23 +146,22 @@ func (proof Proof) IsEmptyProof() bool {
}

// VerifyNamespace verifies a whole namespace, i.e. 1) it verifies inclusion of
// the provided `data` in the tree (or the proof.leafHash in case of absence
// proof) 2) it verifies that the namespace is complete i.e., the data items
// matching the namespace ID `nID` are within the range [`proof.start`,
// `proof.end`) and no data of that namespace was left out. VerifyNamespace
// deems an empty `proof` valid if the queried `nID` falls outside the namespace
// range of the supplied `root` or if the `root` is empty
// the provided `leaves` in the tree (or the proof.leafHash in case of
// full/short absence proof) 2) it verifies that the namespace is complete
// i.e., the data items matching the namespace `nID` are within the range
// [`proof.start`, `proof.end`) and no data of that namespace was left out.
// VerifyNamespace deems an empty `proof` valid if the queried `nID` falls
// outside the namespace range of the supplied `root` or if the `root` is empty
//
// `h` MUST be the same as the underlying hash function used to generate the
// proof. Otherwise, the verification will fail. `nID` is the namespace ID for
// which the namespace `proof` is generated. `data` contains the namespaced data
// items (but not namespace hash) underlying the leaves of the tree in the
// range of [`proof.start`, `proof.end`). For an absence `proof`, the `data` is
// empty. `data` items MUST be ordered according to their index in the tree,
// with `data[0]` corresponding to the namespaced data at index `start`,
//
// and the last element in `data` corresponding to the data item at index
// `end-1` of the tree.
// which the namespace `proof` is generated. `leaves` contains the namespaced
// leaves of the tree in the range of [`proof.start`, `proof.end`).
// For an absence `proof`, the `leaves` is empty.
// `leaves` items MUST be ordered according to their index in the tree,
// with `leaves[0]` corresponding to the namespaced leaf at index `start`,
// and the last element in `leaves` corresponding to the leaf at index `end-1`
// of the tree.
//
// `root` is the root of the NMT against which the `proof` is verified.
func (proof Proof) VerifyNamespace(h hash.Hash, nID namespace.ID, leaves [][]byte, root []byte) bool {
Expand Down
186 changes: 186 additions & 0 deletions proof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -707,3 +707,189 @@ func TestIsEmptyProofOverlapAbsenceProof(t *testing.T) {
})
}
}

// TestVerifyNamespace_ShortAbsenceProof_Valid checks whether VerifyNamespace
// can correctly verify short namespace absence proofs
func TestVerifyNamespace_ShortAbsenceProof_Valid(t *testing.T) {
// create a Merkle tree with 8 leaves
tree := exampleNMT(1, true, 1, 2, 3, 4, 6, 7, 8, 9)
qNS := []byte{5} // does not belong to the tree
root, err := tree.Root()
assert.NoError(t, err)
// In the following illustration, nodes are suffixed with the range
// of leaves they cover, with the upper bound being non-inclusive.
// For example, Node3_4 denotes a node that covers the 3rd leaf (excluding the 4th leaf),
// while Node4_6 represents the node that covers the 4th and 5th leaves.
//
// Node0_8 Tree Root
// / \
// / \
// Node0_4 Node4_8 Non-Leaf Node
// / \ / \
// / \ / \
// Node0_2 Node2_4 Node4_6 Node6_8 Non-Leaf Node
// / \ / \ / \ / \
// Node0_1 Node1_2 Node2_3 Node3_4 Node4_5 Node5_6 Node6_7 Node7_8 Leaf Hash
// 1 2 3 4 6 7 8 9 Leaf namespace
// 0 1 2 3 4 5 6 7 Leaf index

// nodes needed for the full absence proof of qNS
Node4_5 := tree.leafHashes[4]
Node5_6 := tree.leafHashes[5]
Node6_8, err := tree.computeRoot(6, 8)
assert.NoError(t, err)
Node0_4, err := tree.computeRoot(0, 4)
assert.NoError(t, err)

// nodes needed for the short absence proof of qNS; the proof of inclusion
// of the parent of Node4_5

Node4_6, err := tree.computeRoot(4, 6)
assert.NoError(t, err)

// nodes needed for another short absence parent of qNS; the proof of
// inclusion of the grandparent of Node4_5
Node4_8, err := tree.computeRoot(4, 8)
assert.NoError(t, err)

tests := []struct {
name string
qNID []byte
leafHash []byte
nodes [][]byte
start int
end int
}{
{
name: "valid full absence proof",
qNID: qNS,
leafHash: Node4_5,
nodes: [][]byte{Node0_4, Node5_6, Node6_8},
start: 4, // the index position of leafHash at its respective level
end: 5,
},
{
name: "valid short absence proof: one level higher",
qNID: qNS,
leafHash: Node4_6,
nodes: [][]byte{Node0_4, Node6_8},
start: 2, // the index position of leafHash at its respective level
end: 3,
},
{
name: "valid short absence proof: two levels higher",
qNID: qNS,
leafHash: Node4_8,
nodes: [][]byte{Node0_4},
start: 1, // the index position of leafHash at its respective level
end: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
proof := Proof{
leafHash: tt.leafHash,
nodes: tt.nodes,
start: tt.start,
end: tt.end,
}

res := proof.VerifyNamespace(sha256.New(), qNS, nil, root)
assert.True(t, res)
})
}
}

// TestVerifyNamespace_ShortAbsenceProof_Invalid checks whether VerifyNamespace rejects invalid short absence proofs.
func TestVerifyNamespace_ShortAbsenceProof_Invalid(t *testing.T) {
// create a Merkle tree with 8 leaves
tree := exampleNMT(1, true, 1, 2, 3, 4, 6, 8, 8, 8)
qNS := []byte{7} // does not belong to the tree
root, err := tree.Root()
assert.NoError(t, err)
// In the following illustration, nodes are suffixed with the range
// of leaves they cover, with the upper bound being non-inclusive.
// For example, Node3_4 denotes a node that covers the 3rd leaf (excluding the 4th leaf),
// while Node4_6 represents the node that covers the 4th and 5th leaves.
//
// Node0_8 Tree Root
// / \
// / \
// Node0_4 Node4_8 Non-Leaf Node
// / \ / \
// / \ / \
// Node0_2 Node2_4 Node4_6 Node6_8 Non-Leaf Node
// / \ / \ / \ / \
// Node0_1 Node1_2 Node2_3 Node3_4 Node4_5 Node5_6 Node6_7 Node7_8 Leaf Hash
// 1 2 3 4 6 8 8 8 Leaf namespace
// 0 1 2 3 4 5 6 7 Leaf index

// nodes needed for the full absence proof of qNS
Node5_6 := tree.leafHashes[5]
Node4_5 := tree.leafHashes[4]
Node6_8, err := tree.computeRoot(6, 8)
assert.NoError(t, err)
Node0_4, err := tree.computeRoot(0, 4)
assert.NoError(t, err)

// nodes needed for the short absence proof of qNS; the proof of inclusion of the parent of Node5_6;
// the verification should fail since the namespace range o Node4_6, the parent, has overlap with the qNS i.e., 7
Node4_6, err := tree.computeRoot(4, 6)
assert.NoError(t, err)

// nodes needed for another short absence parent of qNS; the proof of inclusion of the grandparent of Node5_6
// the verification should fail since the namespace range of Node4_8, the grandparent, has overlap with the qNS i.e., 7
Node4_8, err := tree.computeRoot(4, 8)
assert.NoError(t, err)

tests := []struct {
name string
qNID []byte
leafHash []byte
nodes [][]byte
start int
end int
want bool
}{
{
name: "valid full absence proof",
qNID: qNS,
leafHash: Node5_6,
nodes: [][]byte{Node0_4, Node4_5, Node6_8},
start: 5, // the index position of leafHash at its respective level
end: 6,
want: true,
},
{
name: "invalid short absence proof: one level higher",
qNID: qNS,
leafHash: Node4_6,
nodes: [][]byte{Node0_4, Node6_8},
start: 2, // the index position of leafHash at its respective level
end: 3,
want: false,
},
{
name: "invalid short absence proof: two levels higher",
qNID: qNS,
leafHash: Node4_8,
nodes: [][]byte{Node0_4},
start: 1, // the index position of leafHash at its respective level
end: 2,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
proof := Proof{
leafHash: tt.leafHash,
nodes: tt.nodes,
start: tt.start,
end: tt.end,
}

res := proof.VerifyNamespace(sha256.New(), qNS, nil, root)
assert.Equal(t, tt.want, res)
})
}
}

0 comments on commit 3693c9a

Please sign in to comment.