From c41093da50276e7cbd219d54a4ab6edb29a9c48c Mon Sep 17 00:00:00 2001 From: Sanaz Taheri <35961250+staheri14@users.noreply.github.com> Date: Fri, 12 May 2023 10:55:10 -0700 Subject: [PATCH] feat!: accounts for IgnoreMaxNamespace flag in the ProveNamespace method (#194) ## Overview Closes https://github.com/celestiaorg/nmt/issues/159 --- nmt.go | 19 +++++++++++++-- nmt_test.go | 66 ++++++++++++++++++++++++++++++++++++--------------- proof_test.go | 18 +++++++------- 3 files changed, 73 insertions(+), 30 deletions(-) diff --git a/nmt.go b/nmt.go index 63a2e13..40ffcec 100644 --- a/nmt.go +++ b/nmt.go @@ -212,9 +212,24 @@ func (n *NamespacedMerkleTree) ProveRange(start, end int) (Proof, error) { // Any error returned by this method is irrecoverable and indicates an illegal state of the tree (n). func (n *NamespacedMerkleTree) ProveNamespace(nID namespace.ID) (Proof, error) { isMaxNsIgnored := n.treeHasher.IsMaxNamespaceIDIgnored() - // case 1) In the cases (n.nID < minNID) or (n.maxNID < nID), return empty + + // check if the tree is empty + if n.Size() == 0 { + return NewEmptyRangeProof(isMaxNsIgnored), nil + } + + // compute the root of the tree + root, err := n.Root() + if err != nil { + return Proof{}, fmt.Errorf("failed to get root: %w", err) + } + // extract the min and max namespace of the tree from the root + treeMinNs := namespace.ID(MinNamespace(root, n.NamespaceSize())) + treeMaxNs := namespace.ID(MaxNamespace(root, n.NamespaceSize())) + + // case 1) In the cases (n.nID < treeMinNs) or (treeMaxNs < nID), return empty // range proof - if nID.Less(n.minNID) || n.maxNID.Less(nID) { + if nID.Less(treeMinNs) || treeMaxNs.Less(nID) { return NewEmptyRangeProof(isMaxNsIgnored), nil } diff --git a/nmt_test.go b/nmt_test.go index 1acb0b2..a184a00 100644 --- a/nmt_test.go +++ b/nmt_test.go @@ -513,7 +513,10 @@ func TestIgnoreMaxNamespace(t *testing.T) { if gotIgnored := proof.IsMaxNamespaceIDIgnored(); gotIgnored != tc.ignoreMaxNamespace { t.Fatalf("Proof.IsMaxNamespaceIDIgnored() got: %v, want: %v", gotIgnored, tc.ignoreMaxNamespace) } - leaves := tree.Get(d.NamespaceID()) + var leaves [][]byte + if !proof.IsEmptyProof() { + leaves = tree.Get(d.NamespaceID()) + } r, err := tree.Root() require.NoError(t, err) if !proof.VerifyNamespace(hash, d.NamespaceID(), leaves, r) { @@ -818,13 +821,13 @@ func TestMinMaxNamespace(t *testing.T) { testCases := []testCase{ { name: "example tree with four leaves", - tree: exampleNMT(1, 0, 0, 1, 3), + tree: exampleNMT(1, true, 0, 0, 1, 3), wantMin: namespace.ID{0}, wantMax: namespace.ID{3}, }, { name: "example tree with eight leaves", - tree: exampleNMT(2, 1, 2, 3, 4, 5, 6, 7, 8), + tree: exampleNMT(2, true, 1, 2, 3, 4, 5, 6, 7, 8), wantMin: namespace.ID{1, 1}, wantMax: namespace.ID{8, 8}, }, @@ -843,8 +846,8 @@ func TestMinMaxNamespace(t *testing.T) { } // exampleNMT creates a new NamespacedMerkleTree with the given namespace ID size and leaf namespace IDs. Each byte in the leavesNIDs parameter corresponds to one leaf's namespace ID. If nidSize is greater than 1, the function repeats each NID in leavesNIDs nidSize times before prepending it to the leaf data. -func exampleNMT(nidSize int, leavesNIDs ...byte) *NamespacedMerkleTree { - tree := New(sha256.New(), NamespaceIDSize(nidSize)) +func exampleNMT(nidSize int, ignoreMaxNamespace bool, leavesNIDs ...byte) *NamespacedMerkleTree { + tree := New(sha256.New(), NamespaceIDSize(nidSize), IgnoreMaxNamespace(ignoreMaxNamespace)) for i, nid := range leavesNIDs { namespace := bytes.Repeat([]byte{nid}, nidSize) d := append(namespace, []byte(fmt.Sprintf("leaf_%d", i))...) @@ -867,17 +870,17 @@ func Test_buildRangeProof_Err(t *testing.T) { nIDSize := 2 // create a nmt, 8 leaves namespaced sequentially from 1-8 - treeWithCorruptLeafHash := exampleNMT(nIDSize, nIDList...) + treeWithCorruptLeafHash := exampleNMT(nIDSize, true, nIDList...) // corrupt a leaf hash treeWithCorruptLeafHash.leafHashes[4] = treeWithCorruptLeafHash.leafHashes[4][:treeWithCorruptLeafHash.NamespaceSize()] // create an NMT with 8 sequentially namespaced leaves, numbered from 1 to 8. - treeWithUnorderedLeafHashes := exampleNMT(nIDSize, nIDList...) + treeWithUnorderedLeafHashes := exampleNMT(nIDSize, true, nIDList...) // swap the positions of the 4th and 5th leaves swap(treeWithUnorderedLeafHashes.leaves, 4, 5) swap(treeWithUnorderedLeafHashes.leafHashes, 4, 5) - validTree := exampleNMT(nIDSize, nIDList...) + validTree := exampleNMT(nIDSize, true, nIDList...) tests := []struct { name string @@ -911,12 +914,12 @@ func Test_buildRangeProof_Err(t *testing.T) { // Test_ProveRange_Err tests that ProveRange returns an error when the underlying tree has an invalid state e.g., leaves are not ordered by namespace ID or a leaf hash is corrupted. func Test_ProveRange_Err(t *testing.T) { // create an NMT with 8 sequentially namespaced leaves, numbered from 1 to 8. - treeWithCorruptLeafHash := exampleNMT(2, 1, 2, 3, 4, 5, 6, 7, 8) + treeWithCorruptLeafHash := exampleNMT(2, true, 1, 2, 3, 4, 5, 6, 7, 8) // corrupt a leaf hash treeWithCorruptLeafHash.leafHashes[4] = treeWithCorruptLeafHash.leafHashes[4][:treeWithCorruptLeafHash.NamespaceSize()] // create an NMT with 8 sequentially namespaced leaves, numbered from 1 to 8. - treeWithUnorderedLeafHashes := exampleNMT(2, 1, 2, 3, 4, 5, 6, 7, 8) + treeWithUnorderedLeafHashes := exampleNMT(2, true, 1, 2, 3, 4, 5, 6, 7, 8) // swap the positions of the 4th and 5th leaves swap(treeWithUnorderedLeafHashes.leaves, 4, 5) swap(treeWithUnorderedLeafHashes.leafHashes, 4, 5) @@ -949,12 +952,12 @@ func Test_ProveRange_Err(t *testing.T) { // The Test_ProveNamespace_Err function tests that ProveNamespace returns an error when the underlying tree is in an invalid state, such as when the leaves are not ordered by namespace ID or when a leaf hash is corrupt. func Test_ProveNamespace_Err(t *testing.T) { // create an NMT with 8 sequentially namespaced leaves, numbered from 1 to 8. - treeWithCorruptLeafHash := exampleNMT(2, 1, 2, 3, 4, 5, 6, 7, 8) + treeWithCorruptLeafHash := exampleNMT(2, true, 1, 2, 3, 4, 5, 6, 7, 8) // corrupt a leaf hash treeWithCorruptLeafHash.leafHashes[4] = treeWithCorruptLeafHash.leafHashes[4][:treeWithCorruptLeafHash.NamespaceSize()] // create an NMT with 8 sequentially namespaced leaves, numbered from 1 to 8. - treeWithUnorderedLeafHashes := exampleNMT(2, 1, 2, 3, 4, 5, 6, 7, 8) + treeWithUnorderedLeafHashes := exampleNMT(2, true, 1, 2, 3, 4, 5, 6, 7, 8) // swap the positions of the 4th and 5th leaves swap(treeWithUnorderedLeafHashes.leaves, 4, 5) swap(treeWithUnorderedLeafHashes.leafHashes, 4, 5) @@ -987,12 +990,12 @@ func Test_ProveNamespace_Err(t *testing.T) { // Test_Root_Error tests that the Root method returns an error when the underlying tree is in an invalid state, such as when the leaves are not ordered by namespace ID or when a leaf is corrupt. func Test_Root_Error(t *testing.T) { // create an NMT with 8 sequentially namespaced leaves, numbered from 1 to 8. - treeWithCorruptLeafHash := exampleNMT(2, 1, 2, 3, 4, 5, 6, 7, 8) + treeWithCorruptLeafHash := exampleNMT(2, true, 1, 2, 3, 4, 5, 6, 7, 8) // corrupt a leaf hash treeWithCorruptLeafHash.leafHashes[4] = treeWithCorruptLeafHash.leafHashes[4][:treeWithCorruptLeafHash.NamespaceSize()-1] // create an NMT with 8 sequentially namespaced leaves, numbered from 1 to 8. - treeWithUnorderedLeaves := exampleNMT(2, 1, 2, 3, 4, 5, 6, 7, 8) + treeWithUnorderedLeaves := exampleNMT(2, true, 1, 2, 3, 4, 5, 6, 7, 8) // swap the positions of the 4th and 5th leaves swap(treeWithUnorderedLeaves.leaves, 4, 5) swap(treeWithUnorderedLeaves.leafHashes, 4, 5) @@ -1023,18 +1026,18 @@ func Test_computeRoot_Error(t *testing.T) { nIDList := []byte{1, 2, 3, 4, 5, 6, 7, 8} // create an NMT with 8 sequentially namespaced leaves, numbered from 1 to 8. - treeWithCorruptLeafHash := exampleNMT(nIDSize, nIDList...) + treeWithCorruptLeafHash := exampleNMT(nIDSize, true, nIDList...) // corrupt a leaf hash treeWithCorruptLeafHash.leafHashes[4] = treeWithCorruptLeafHash.leafHashes[4][:treeWithCorruptLeafHash.NamespaceSize()-1] // create an NMT with 8 sequentially namespaced leaves, numbered from 1 to 8. - treeWithUnorderedLeaves := exampleNMT(nIDSize, nIDList...) + treeWithUnorderedLeaves := exampleNMT(nIDSize, true, nIDList...) // swap the positions of the 4th and 5th leaves swap(treeWithUnorderedLeaves.leaves, 4, 5) swap(treeWithUnorderedLeaves.leafHashes, 4, 5) // create an NMT with 8 sequentially namespaced leaves, numbered from 1 to 8. - validTree := exampleNMT(nIDSize, nIDList...) + validTree := exampleNMT(nIDSize, true, nIDList...) tests := []struct { name string @@ -1067,12 +1070,12 @@ func Test_computeRoot_Error(t *testing.T) { // Test_MinMaxNamespace_Err tests that the MinNamespace and MaxNamespace methods return an error when the underlying tree is in an invalid state, such as when the leaves are not ordered by namespace ID or when a leaf is corrupt. func Test_MinMaxNamespace_Err(t *testing.T) { // create an NMT with 8 sequentially namespaced leaves, numbered from 1 to 8. - treeWithCorruptLeafHash := exampleNMT(2, 1, 2, 3, 4, 5, 6, 7, 8) + treeWithCorruptLeafHash := exampleNMT(2, true, 1, 2, 3, 4, 5, 6, 7, 8) // corrupt a leaf hash treeWithCorruptLeafHash.leafHashes[4] = treeWithCorruptLeafHash.leafHashes[4][:treeWithCorruptLeafHash.NamespaceSize()-1] // create an NMT with 8 sequentially namespaced leaves, numbered from 1 to 8. - treeWithUnorderedLeaves := exampleNMT(2, 1, 2, 3, 4, 5, 6, 7, 8) + treeWithUnorderedLeaves := exampleNMT(2, true, 1, 2, 3, 4, 5, 6, 7, 8) // swap the positions of the 4th and 5th leaves swap(treeWithUnorderedLeaves.leaves, 4, 5) swap(treeWithUnorderedLeaves.leafHashes, 4, 5) @@ -1102,3 +1105,28 @@ func Test_MinMaxNamespace_Err(t *testing.T) { }) } } + +// TestProveNamespace_MaxNamespace checks the output of the ProveNamespace method when queried for the maximum namespace ID. +func TestProveNamespace_MaxNamespace(t *testing.T) { + nidSize := 1 + MaxNS := byte(math.MaxUint8) + tests := []struct { + name string + nIDList []byte + isEmptyProof bool + }{ + {"tree with no leaf", []byte{}, true}, + {"tree with one leaf with MaxNS", []byte{MaxNS}, false}, + {"tree with two leaves, the right leaf has MaxNS", []byte{1, MaxNS}, true}, + {"tree with four leaves, the right half has MaxNS", []byte{1, 2, MaxNS, MaxNS}, true}, + {"tree with 8 leaves, the right half has MaxNS", []byte{1, 2, 3, 4, MaxNS, MaxNS, MaxNS, MaxNS}, true}, + } + for _, tt := range tests { + tree := exampleNMT(nidSize, true, tt.nIDList...) + t.Run(tt.name, func(t *testing.T) { + proof, err := tree.ProveNamespace(namespace.ID{MaxNS}) + assert.NoError(t, err) + assert.Equal(t, tt.isEmptyProof, proof.IsEmptyProof()) + }) + } +} diff --git a/proof_test.go b/proof_test.go index 820df5b..7cf6113 100644 --- a/proof_test.go +++ b/proof_test.go @@ -17,7 +17,7 @@ import ( func TestVerifyNamespace_EmptyProof(t *testing.T) { // create a tree with 4 leaves nIDSize := 1 - tree := exampleNMT(nIDSize, 1, 2, 3, 4) + tree := exampleNMT(nIDSize, true, 1, 2, 3, 4) root, err := tree.Root() require.NoError(t, err) @@ -275,7 +275,7 @@ func safeAppend(id, data []byte) []byte { func TestVerifyLeafHashes_Err(t *testing.T) { // create a sample tree nameIDSize := 2 - nmt := exampleNMT(nameIDSize, 1, 2, 3, 4, 5, 6, 7, 8) + nmt := exampleNMT(nameIDSize, true, 1, 2, 3, 4, 5, 6, 7, 8) hasher := nmt.treeHasher root, err := nmt.Root() require.NoError(t, err) @@ -359,7 +359,7 @@ func TestVerifyInclusion_False(t *testing.T) { hasher := sha256.New() // create a sample tree with namespace ID size of 1 - nmt1 := exampleNMT(1, 1, 2, 3, 4, 5, 6, 7, 8) + nmt1 := exampleNMT(1, true, 1, 2, 3, 4, 5, 6, 7, 8) root1, err := nmt1.Root() require.NoError(t, err) nid4_1 := namespace.ID{4} @@ -368,7 +368,7 @@ func TestVerifyInclusion_False(t *testing.T) { leaf4_1 := nmt1.leaves[3][nmt1.NamespaceSize():] // create a sample tree with namespace ID size of 2 - nmt2 := exampleNMT(2, 1, 2, 3, 4, 5, 6, 7, 8) + nmt2 := exampleNMT(2, true, 1, 2, 3, 4, 5, 6, 7, 8) root2, err := nmt2.Root() require.NoError(t, err) nid4_2 := namespace.ID{4, 4} @@ -412,7 +412,7 @@ func TestVerifyInclusion_EmptyProofs(t *testing.T) { // create a tree nIDSize := 1 - tree := exampleNMT(nIDSize, 1, 2, 3, 4, 5, 6, 7, 8) + tree := exampleNMT(nIDSize, true, 1, 2, 3, 4, 5, 6, 7, 8) root, err := tree.Root() require.NoError(t, err) @@ -458,7 +458,7 @@ func TestVerifyNamespace_False(t *testing.T) { nIDs := []byte{1, 2, 3, 4, 5, 6, 7, 8, 11} // create a sample tree with namespace ID size of 1 - nmt1 := exampleNMT(1, nIDs...) + nmt1 := exampleNMT(1, true, nIDs...) root1, err := nmt1.Root() require.NoError(t, err) nid4_1 := namespace.ID{4} @@ -466,7 +466,7 @@ func TestVerifyNamespace_False(t *testing.T) { require.NoError(t, err) // create a sample tree with namespace ID size of 2 - nmt2 := exampleNMT(2, nIDs...) + nmt2 := exampleNMT(2, true, nIDs...) root2, err := nmt2.Root() require.NoError(t, err) nid4_2 := namespace.ID{4, 4} @@ -525,7 +525,7 @@ func TestVerifyLeafHashes_False(t *testing.T) { nIDs := []byte{1, 2, 3, 4, 5, 6, 7, 8} // create a sample tree with namespace ID size of 1 - nmt1 := exampleNMT(1, nIDs...) + nmt1 := exampleNMT(1, true, nIDs...) root1, err := nmt1.Root() require.NoError(t, err) nid4_1 := namespace.ID{4} @@ -533,7 +533,7 @@ func TestVerifyLeafHashes_False(t *testing.T) { require.NoError(t, err) // create a sample tree with namespace ID size of 2 - nmt2 := exampleNMT(2, nIDs...) + nmt2 := exampleNMT(2, true, nIDs...) root2, err := nmt2.Root() require.NoError(t, err) nid4_2 := namespace.ID{4, 4}