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!: rejects invalid empty range proofs #180

Merged
merged 8 commits into from
Apr 25, 2023
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
31 changes: 22 additions & 9 deletions proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ func NewAbsenceProof(proofStart, proofEnd int, proofNodes [][]byte, leafHash []b
return Proof{proofStart, proofEnd, proofNodes, leafHash, ignoreMaxNamespace}
}

// IsEmptyProof checks whether the proof corresponds to an empty proof as defined in NMT specifications https://github.com/celestiaorg/nmt/blob/master/docs/spec/nmt.md.
func (proof Proof) IsEmptyProof() bool {
return proof.start == proof.end && len(proof.nodes) == 0
}

// 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
Expand Down Expand Up @@ -150,18 +155,26 @@ func (proof Proof) VerifyNamespace(h hash.Hash, nID namespace.ID, leaves [][]byt
}

isEmptyRange := proof.start == proof.end
if len(leaves) == 0 && isEmptyRange && len(proof.nodes) == 0 {
// empty proofs are always rejected unless nID is outside the range of
// namespaces covered by the root we special case the empty root, since
// it purports to cover the zero namespace but does not actually include
// any such nodes
min := namespace.ID(MinNamespace(root, nIDLen))
max := namespace.ID(MaxNamespace(root, nIDLen))
if nID.Less(min) || max.Less(nID) || bytes.Equal(root, nth.EmptyRoot()) {
return true
if isEmptyRange {
if proof.IsEmptyProof() && len(leaves) == 0 {
rootMin := namespace.ID(MinNamespace(root, nIDLen))
rootMax := namespace.ID(MaxNamespace(root, nIDLen))
// empty proofs are always rejected unless 1) nID is outside the range of
// namespaces covered by the root 2) the root represents an empty tree, since
// it purports to cover the zero namespace but does not actually include
// any such nodes
if nID.Less(rootMin) || rootMax.Less(nID) {
return true
}
if bytes.Equal(root, nth.EmptyRoot()) {
return true
}
return false
}
// the proof range is empty, and invalid
return false
}

gotLeafHashes := make([][]byte, 0, len(leaves))
if proof.IsOfAbsence() {
gotLeafHashes = append(gotLeafHashes, proof.leafHash)
Expand Down
70 changes: 70 additions & 0 deletions proof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,76 @@ import (
"github.com/celestiaorg/nmt/namespace"
)

// TestVerifyNamespace_EmptyProof tests the correct behaviour of VerifyNamespace for valid and invalid empty proofs.
func TestVerifyNamespace_EmptyProof(t *testing.T) {
// create a tree with 4 leaves
nIDSize := 1
tree := exampleNMT(nIDSize, 1, 2, 3, 4)
root, err := tree.Root()
require.NoError(t, err)

// build a proof for an NID that is outside the namespace range of the tree
// start = end = 0, nodes = empty
nID0 := []byte{0}
validEmptyProofZeroRange, err := tree.ProveNamespace(nID0)
require.NoError(t, err)

// build a proof for an NID that is outside the namespace range of the tree
// start = end = 1, nodes = nil
validEmptyProofNonZeroRange, err := tree.ProveNamespace(nID0)
require.NoError(t, err)
// modify the proof range to be non-zero, it should still be valid
validEmptyProofNonZeroRange.start = 1
validEmptyProofNonZeroRange.end = 1

// build a proof for an NID that is within the namespace range of the tree
// start = end = 0, nodes = non-empty
nID1 := []byte{1}
zeroRangeOnlyProof, err := tree.ProveNamespace(nID1)
require.NoError(t, err)
// modify the proof to contain a zero range
zeroRangeOnlyProof.start = 0
zeroRangeOnlyProof.end = 0

// build a proof for an NID that is within the namespace range of the tree
// start = 0, end = 1, nodes = empty
emptyNodesOnlyProof, err := tree.ProveNamespace(nID1)
require.NoError(t, err)
// modify the proof nodes to be empty
emptyNodesOnlyProof.nodes = [][]byte{}

hasher := sha256.New()
type args struct {
proof Proof
hasher hash.Hash
nID namespace.ID
leaves [][]byte
root []byte
}

tests := []struct {
name string
args args
want bool
isValidEmptyProof bool
}{
{"valid empty proof with (start == end) == 0 and empty leaves", args{validEmptyProofZeroRange, hasher, nID0, [][]byte{}, root}, true, true},
{"valid empty proof with (start == end) != 0 and empty leaves", args{validEmptyProofNonZeroRange, hasher, nID0, [][]byte{}, root}, true, true},
{"valid empty proof with (start == end) == 0 and non-empty leaves", args{validEmptyProofZeroRange, hasher, nID0, [][]byte{{1}}, root}, false, true},
{"valid empty proof with (start == end) != 0 and non-empty leaves", args{validEmptyProofNonZeroRange, hasher, nID0, [][]byte{{1}}, root}, false, true},
{"invalid empty proof: start == end == 0, nodes == non-empty", args{zeroRangeOnlyProof, hasher, nID1, [][]byte{}, root}, false, false},
{"invalid empty proof: start == 0, end == 1, nodes == empty", args{emptyNodesOnlyProof, hasher, nID1, [][]byte{}, root}, false, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.True(t, tt.args.proof.IsEmptyProof() == tt.isValidEmptyProof)
if got := tt.args.proof.VerifyNamespace(tt.args.hasher, tt.args.nID, tt.args.leaves, tt.args.root); got != tt.want {
t.Errorf("VerifyNamespace() = %v, want %v", got, tt.want)
}
})
}
}

func TestProof_VerifyNamespace_False(t *testing.T) {
const testNidLen = 3

Expand Down