Skip to content

Commit

Permalink
feat!: add an additional constraint of empty leafHash field to the em…
Browse files Browse the repository at this point in the history
…pty proof definition (#192)

## Overview
Closes #181
The updates made by this PR will result in a breaking change, as empty
proofs with a non-empty `leafHash` field will no longer be considered
valid. This means that their verification through `VerifyNamespace` will
fail.
Example:
```go
nIDSize := 1
tree := exampleNMT(nIDSize, 1, 2, 3, 4)
root, err := tree.Root()
require.NoError(t, err)
hasher := 
proof := Proof{
		start:    0,
		end:      0,
		nodes:    nil,
		leafHash: tree.leafHashes[0],
	}

res := proof.VerifyNamespace(tree.treeHasher.baseHasher, []byte{0}, [][]byte{}, root)
require.True(res) // this is false with the changes in this PR, however, previously, it was true
```

## 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 authored May 8, 2023
1 parent 4b97a09 commit fd00c52
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 29 deletions.
2 changes: 1 addition & 1 deletion proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func NewAbsenceProof(proofStart, proofEnd int, proofNodes [][]byte, leafHash []b

// 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
return proof.start == proof.end && len(proof.nodes) == 0 && len(proof.leafHash) == 0
}

// VerifyNamespace verifies a whole namespace, i.e. 1) it verifies inclusion of
Expand Down
145 changes: 117 additions & 28 deletions proof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,24 @@ func TestVerifyNamespace_EmptyProof(t *testing.T) {
require.NoError(t, err)

// build a proof for an NID that is outside the namespace range of the tree
// start = end = 0, nodes = empty
// start = end = 0, nodes = empty, leafHash = empty
nID0 := []byte{0}
validEmptyProofZeroRange, err := tree.ProveNamespace(nID0)
validEmptyProof, 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
// build a proof for an NID that is within the namespace range of the tree, then corrupt it to have a zero range
// start = end = 0, nodes = non-empty, leafHash = empty
nID1 := []byte{1}
zeroRangeOnlyProof, err := tree.ProveNamespace(nID1)
invalidEmptyProof, err := tree.ProveNamespace(nID1)
require.NoError(t, err)
// modify the proof to contain a zero range
zeroRangeOnlyProof.start = 0
zeroRangeOnlyProof.end = 0
invalidEmptyProof.start = 0
invalidEmptyProof.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{}
// root of an empty tree
emptyRoot := tree.treeHasher.EmptyRoot()
hasher := tree.treeHasher.baseHasher

hasher := sha256.New()
type args struct {
proof Proof
hasher hash.Hash
Expand All @@ -66,12 +54,18 @@ func TestVerifyNamespace_EmptyProof(t *testing.T) {
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},
// in the following tests, proof should always contain an empty range

// test cases for a non-empty tree hence non-empty root
{"valid empty proof & empty leaves & nID not in range", args{validEmptyProof, hasher, nID0, [][]byte{}, root}, true, true},
{"invalid empty proof & empty leaves & nID in range", args{invalidEmptyProof, hasher, nID1, [][]byte{}, root}, false, false},
{"valid empty proof & non-empty leaves & nID not in range", args{validEmptyProof, hasher, nID0, [][]byte{{1}}, root}, false, true},
{"valid empty proof & empty leaves & nID in range", args{validEmptyProof, hasher, nID1, [][]byte{}, root}, false, true},

// test cases for an empty tree hence empty root
{"valid empty proof & empty leaves & nID not in range ", args{validEmptyProof, hasher, nID0, [][]byte{}, emptyRoot}, true, true},
{"invalid empty proof & empty leaves & nID in range", args{invalidEmptyProof, hasher, nID1, [][]byte{}, emptyRoot}, false, false},
{"valid empty proof & non-empty leaves & nID not in range", args{validEmptyProof, hasher, nID0, [][]byte{{1}}, emptyRoot}, false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -578,3 +572,98 @@ func TestVerifyLeafHashes_False(t *testing.T) {
})
}
}

func TestIsEmptyProof(t *testing.T) {
tests := []struct {
name string
proof Proof
expected bool
}{
{
name: "valid empty proof",
proof: Proof{
leafHash: nil,
nodes: nil,
start: 1,
end: 1,
},
expected: true,
},
{
name: "invalid empty proof - start != end",
proof: Proof{
leafHash: nil,
nodes: nil,
start: 0,
end: 1,
},
expected: false,
},
{
name: "invalid empty proof - non-empty nodes",
proof: Proof{
leafHash: nil,
nodes: [][]byte{{0x01}},
start: 1,
end: 1,
},
expected: false,
},
{
name: "invalid absence proof - non-empty leafHash",
proof: Proof{
leafHash: []byte{0x01},
nodes: nil,
start: 1,
end: 1,
},
expected: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := test.proof.IsEmptyProof()
assert.Equal(t, test.expected, result)
})
}
}

// TestIsEmptyProofOverlapAbsenceProof ensures there is no overlap between empty proofs and absence proofs.
func TestIsEmptyProofOverlapAbsenceProof(t *testing.T) {
tests := []struct {
name string
proof Proof
}{
{
name: "valid empty proof",
proof: Proof{
leafHash: nil,
nodes: nil,
start: 1,
end: 1,
},
},
{
name: "valid absence proof",
proof: Proof{
leafHash: []byte{0x01, 0x02, 0x03},
nodes: nil,
start: 1,
end: 1,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := test.proof.IsEmptyProof()
absenceResult := test.proof.IsOfAbsence()
if result {
assert.False(t, absenceResult)
}
if absenceResult {
assert.False(t, result)
}
})
}
}

0 comments on commit fd00c52

Please sign in to comment.