Skip to content

Commit 3693c9a

Browse files
authored
test: incorporates tests for short absence proof verification (#217)
## 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
1 parent 48b8f96 commit 3693c9a

File tree

2 files changed

+199
-14
lines changed

2 files changed

+199
-14
lines changed

proof.go

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -146,23 +146,22 @@ func (proof Proof) IsEmptyProof() bool {
146146
}
147147

148148
// VerifyNamespace verifies a whole namespace, i.e. 1) it verifies inclusion of
149-
// the provided `data` in the tree (or the proof.leafHash in case of absence
150-
// proof) 2) it verifies that the namespace is complete i.e., the data items
151-
// matching the namespace ID `nID` are within the range [`proof.start`,
152-
// `proof.end`) and no data of that namespace was left out. VerifyNamespace
153-
// deems an empty `proof` valid if the queried `nID` falls outside the namespace
154-
// range of the supplied `root` or if the `root` is empty
149+
// the provided `leaves` in the tree (or the proof.leafHash in case of
150+
// full/short absence proof) 2) it verifies that the namespace is complete
151+
// i.e., the data items matching the namespace `nID` are within the range
152+
// [`proof.start`, `proof.end`) and no data of that namespace was left out.
153+
// VerifyNamespace deems an empty `proof` valid if the queried `nID` falls
154+
// outside the namespace range of the supplied `root` or if the `root` is empty
155155
//
156156
// `h` MUST be the same as the underlying hash function used to generate the
157157
// proof. Otherwise, the verification will fail. `nID` is the namespace ID for
158-
// which the namespace `proof` is generated. `data` contains the namespaced data
159-
// items (but not namespace hash) underlying the leaves of the tree in the
160-
// range of [`proof.start`, `proof.end`). For an absence `proof`, the `data` is
161-
// empty. `data` items MUST be ordered according to their index in the tree,
162-
// with `data[0]` corresponding to the namespaced data at index `start`,
163-
//
164-
// and the last element in `data` corresponding to the data item at index
165-
// `end-1` of the tree.
158+
// which the namespace `proof` is generated. `leaves` contains the namespaced
159+
// leaves of the tree in the range of [`proof.start`, `proof.end`).
160+
// For an absence `proof`, the `leaves` is empty.
161+
// `leaves` items MUST be ordered according to their index in the tree,
162+
// with `leaves[0]` corresponding to the namespaced leaf at index `start`,
163+
// and the last element in `leaves` corresponding to the leaf at index `end-1`
164+
// of the tree.
166165
//
167166
// `root` is the root of the NMT against which the `proof` is verified.
168167
func (proof Proof) VerifyNamespace(h hash.Hash, nID namespace.ID, leaves [][]byte, root []byte) bool {

proof_test.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,3 +707,189 @@ func TestIsEmptyProofOverlapAbsenceProof(t *testing.T) {
707707
})
708708
}
709709
}
710+
711+
// TestVerifyNamespace_ShortAbsenceProof_Valid checks whether VerifyNamespace
712+
// can correctly verify short namespace absence proofs
713+
func TestVerifyNamespace_ShortAbsenceProof_Valid(t *testing.T) {
714+
// create a Merkle tree with 8 leaves
715+
tree := exampleNMT(1, true, 1, 2, 3, 4, 6, 7, 8, 9)
716+
qNS := []byte{5} // does not belong to the tree
717+
root, err := tree.Root()
718+
assert.NoError(t, err)
719+
// In the following illustration, nodes are suffixed with the range
720+
// of leaves they cover, with the upper bound being non-inclusive.
721+
// For example, Node3_4 denotes a node that covers the 3rd leaf (excluding the 4th leaf),
722+
// while Node4_6 represents the node that covers the 4th and 5th leaves.
723+
//
724+
// Node0_8 Tree Root
725+
// / \
726+
// / \
727+
// Node0_4 Node4_8 Non-Leaf Node
728+
// / \ / \
729+
// / \ / \
730+
// Node0_2 Node2_4 Node4_6 Node6_8 Non-Leaf Node
731+
// / \ / \ / \ / \
732+
// Node0_1 Node1_2 Node2_3 Node3_4 Node4_5 Node5_6 Node6_7 Node7_8 Leaf Hash
733+
// 1 2 3 4 6 7 8 9 Leaf namespace
734+
// 0 1 2 3 4 5 6 7 Leaf index
735+
736+
// nodes needed for the full absence proof of qNS
737+
Node4_5 := tree.leafHashes[4]
738+
Node5_6 := tree.leafHashes[5]
739+
Node6_8, err := tree.computeRoot(6, 8)
740+
assert.NoError(t, err)
741+
Node0_4, err := tree.computeRoot(0, 4)
742+
assert.NoError(t, err)
743+
744+
// nodes needed for the short absence proof of qNS; the proof of inclusion
745+
// of the parent of Node4_5
746+
747+
Node4_6, err := tree.computeRoot(4, 6)
748+
assert.NoError(t, err)
749+
750+
// nodes needed for another short absence parent of qNS; the proof of
751+
// inclusion of the grandparent of Node4_5
752+
Node4_8, err := tree.computeRoot(4, 8)
753+
assert.NoError(t, err)
754+
755+
tests := []struct {
756+
name string
757+
qNID []byte
758+
leafHash []byte
759+
nodes [][]byte
760+
start int
761+
end int
762+
}{
763+
{
764+
name: "valid full absence proof",
765+
qNID: qNS,
766+
leafHash: Node4_5,
767+
nodes: [][]byte{Node0_4, Node5_6, Node6_8},
768+
start: 4, // the index position of leafHash at its respective level
769+
end: 5,
770+
},
771+
{
772+
name: "valid short absence proof: one level higher",
773+
qNID: qNS,
774+
leafHash: Node4_6,
775+
nodes: [][]byte{Node0_4, Node6_8},
776+
start: 2, // the index position of leafHash at its respective level
777+
end: 3,
778+
},
779+
{
780+
name: "valid short absence proof: two levels higher",
781+
qNID: qNS,
782+
leafHash: Node4_8,
783+
nodes: [][]byte{Node0_4},
784+
start: 1, // the index position of leafHash at its respective level
785+
end: 2,
786+
},
787+
}
788+
for _, tt := range tests {
789+
t.Run(tt.name, func(t *testing.T) {
790+
proof := Proof{
791+
leafHash: tt.leafHash,
792+
nodes: tt.nodes,
793+
start: tt.start,
794+
end: tt.end,
795+
}
796+
797+
res := proof.VerifyNamespace(sha256.New(), qNS, nil, root)
798+
assert.True(t, res)
799+
})
800+
}
801+
}
802+
803+
// TestVerifyNamespace_ShortAbsenceProof_Invalid checks whether VerifyNamespace rejects invalid short absence proofs.
804+
func TestVerifyNamespace_ShortAbsenceProof_Invalid(t *testing.T) {
805+
// create a Merkle tree with 8 leaves
806+
tree := exampleNMT(1, true, 1, 2, 3, 4, 6, 8, 8, 8)
807+
qNS := []byte{7} // does not belong to the tree
808+
root, err := tree.Root()
809+
assert.NoError(t, err)
810+
// In the following illustration, nodes are suffixed with the range
811+
// of leaves they cover, with the upper bound being non-inclusive.
812+
// For example, Node3_4 denotes a node that covers the 3rd leaf (excluding the 4th leaf),
813+
// while Node4_6 represents the node that covers the 4th and 5th leaves.
814+
//
815+
// Node0_8 Tree Root
816+
// / \
817+
// / \
818+
// Node0_4 Node4_8 Non-Leaf Node
819+
// / \ / \
820+
// / \ / \
821+
// Node0_2 Node2_4 Node4_6 Node6_8 Non-Leaf Node
822+
// / \ / \ / \ / \
823+
// Node0_1 Node1_2 Node2_3 Node3_4 Node4_5 Node5_6 Node6_7 Node7_8 Leaf Hash
824+
// 1 2 3 4 6 8 8 8 Leaf namespace
825+
// 0 1 2 3 4 5 6 7 Leaf index
826+
827+
// nodes needed for the full absence proof of qNS
828+
Node5_6 := tree.leafHashes[5]
829+
Node4_5 := tree.leafHashes[4]
830+
Node6_8, err := tree.computeRoot(6, 8)
831+
assert.NoError(t, err)
832+
Node0_4, err := tree.computeRoot(0, 4)
833+
assert.NoError(t, err)
834+
835+
// nodes needed for the short absence proof of qNS; the proof of inclusion of the parent of Node5_6;
836+
// the verification should fail since the namespace range o Node4_6, the parent, has overlap with the qNS i.e., 7
837+
Node4_6, err := tree.computeRoot(4, 6)
838+
assert.NoError(t, err)
839+
840+
// nodes needed for another short absence parent of qNS; the proof of inclusion of the grandparent of Node5_6
841+
// the verification should fail since the namespace range of Node4_8, the grandparent, has overlap with the qNS i.e., 7
842+
Node4_8, err := tree.computeRoot(4, 8)
843+
assert.NoError(t, err)
844+
845+
tests := []struct {
846+
name string
847+
qNID []byte
848+
leafHash []byte
849+
nodes [][]byte
850+
start int
851+
end int
852+
want bool
853+
}{
854+
{
855+
name: "valid full absence proof",
856+
qNID: qNS,
857+
leafHash: Node5_6,
858+
nodes: [][]byte{Node0_4, Node4_5, Node6_8},
859+
start: 5, // the index position of leafHash at its respective level
860+
end: 6,
861+
want: true,
862+
},
863+
{
864+
name: "invalid short absence proof: one level higher",
865+
qNID: qNS,
866+
leafHash: Node4_6,
867+
nodes: [][]byte{Node0_4, Node6_8},
868+
start: 2, // the index position of leafHash at its respective level
869+
end: 3,
870+
want: false,
871+
},
872+
{
873+
name: "invalid short absence proof: two levels higher",
874+
qNID: qNS,
875+
leafHash: Node4_8,
876+
nodes: [][]byte{Node0_4},
877+
start: 1, // the index position of leafHash at its respective level
878+
end: 2,
879+
want: false,
880+
},
881+
}
882+
for _, tt := range tests {
883+
t.Run(tt.name, func(t *testing.T) {
884+
proof := Proof{
885+
leafHash: tt.leafHash,
886+
nodes: tt.nodes,
887+
start: tt.start,
888+
end: tt.end,
889+
}
890+
891+
res := proof.VerifyNamespace(sha256.New(), qNS, nil, root)
892+
assert.Equal(t, tt.want, res)
893+
})
894+
}
895+
}

0 commit comments

Comments
 (0)