diff --git a/proof.go b/proof.go index bc9c1799..2db25890 100644 --- a/proof.go +++ b/proof.go @@ -227,7 +227,7 @@ func (proof Proof) VerifyNamespace(h hash.Hash, nID namespace.ID, leaves [][]byt // 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 -// but is not present in the leafHashes list. +// outside the leafHashes list. func (proof Proof) VerifyLeafHashes(nth *Hasher, verifyCompleteness bool, nID namespace.ID, leafHashes [][]byte, root []byte) (bool, error) { // check that the proof range is valid if proof.Start() < 0 || proof.Start() >= proof.End() { @@ -248,13 +248,25 @@ func (proof Proof) VerifyLeafHashes(nth *Hasher, verifyCompleteness bool, nID na return false, fmt.Errorf("proof nodes do 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 _, leaf := range leafHashes { - if err := nth.ValidateNodeFormat(leaf); err != nil { + // check that all the leafHashes are valid w.r.t the NMT hasher + for _, leafHash := range leafHashes { + if err := nth.ValidateNodeFormat(leafHash); err != nil { return false, fmt.Errorf("leaf hash does not match the NMT hasher's hash format: %w", err) } } + // check that the namespace of leafHashes is the same as the queried namespace, except for the case of absence proof + if !proof.IsOfAbsence() { // in case of absence proof, the leafHash is the hash of a leaf next to the queried namespace, hence its namespace ID is not the same as the queried namespace ID + // check the namespace of all the leaf hashes to be the same as the queried namespace + for _, leafHash := range leafHashes { + minNsID := MinNamespace(leafHash, nth.NamespaceSize()) + maxNsID := MaxNamespace(leafHash, nth.NamespaceSize()) + if !nID.Equal(minNsID) || !nID.Equal(maxNsID) { + return false, fmt.Errorf("leaf hash %x does not belong to namespace %x", leafHash, nID) + } + } + } + var leafIndex uint64 // leftSubtrees is to be populated by the subtree roots upto [0, r.Start) leftSubtrees := make([][]byte, 0, len(proof.nodes)) diff --git a/proof_test.go b/proof_test.go index 62d5a797..291de098 100644 --- a/proof_test.go +++ b/proof_test.go @@ -522,7 +522,7 @@ func TestVerifyNamespace_False(t *testing.T) { } func TestVerifyLeafHashes_False(t *testing.T) { - nIDs := []byte{1, 2, 3, 4, 5, 6, 7, 8} + nIDs := []byte{1, 2, 3, 4, 6, 7, 8, 9} // create a sample tree with namespace ID size of 1 nmt1 := exampleNMT(1, true, nIDs...) @@ -532,6 +532,21 @@ func TestVerifyLeafHashes_False(t *testing.T) { proof4_1, err := nmt1.ProveNamespace(nid4_1) // leaf at index 3 has namespace ID 4 require.NoError(t, err) + leafHash1 := nmt1.leafHashes[3] + + // corrupt the namespace of the leafHash + leafHash1Corrupted := make([]byte, len(leafHash1)) + copy(leafHash1Corrupted, leafHash1) + leafHash1Corrupted[0] = 0 // change the min namespace + leafHash1Corrupted[1] = 0 // change the max namespace + + // create an absence proof with namespace ID size of 1 + nid5_1 := namespace.ID{5} + absenceProof5_1, err := nmt1.ProveNamespace(nid5_1) + require.NoError(t, err) + leafHash6_1 := nmt1.leafHashes[4] + assert.Equal(t, leafHash6_1, absenceProof5_1.leafHash) + // create a sample tree with namespace ID size of 2 nmt2 := exampleNMT(2, true, nIDs...) root2, err := nmt2.Root() @@ -540,7 +555,6 @@ func TestVerifyLeafHashes_False(t *testing.T) { proof4_2, err := nmt2.ProveNamespace(nid4_2) // leaf at index 3 has namespace ID 4 require.NoError(t, err) - leafHash1 := nmt1.leafHashes[3] leafHash2 := nmt2.leafHashes[3] type args struct { @@ -563,6 +577,8 @@ func TestVerifyLeafHashes_False(t *testing.T) { {"size of queried nID < nID size of VerifyLeafHashes' nmt hasher", proof4_2, args{2, nid4_1, [][]byte{leafHash2}, root2}, false}, {"nID size of leafHash < nID size of VerifyLeafHashes' nmt hasher", proof4_2, args{2, nid4_2, [][]byte{leafHash1}, root2}, false}, {"nID size of leafHash > nID size of VerifyLeafHashes' nmt hasher", proof4_1, args{1, nid4_1, [][]byte{leafHash2}, root1}, false}, + {"nID of leafHashes do not match the queried nID", proof4_1, args{1, nid4_1, [][]byte{leafHash1Corrupted}, root1}, false}, + {"absence proof: nID of leafHashes do not match the queried nID, which is a valid case", absenceProof5_1, args{1, nid5_1, [][]byte{leafHash6_1}, root1}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {