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: ensures consistent use of the assumption that NMT nodes are ordered ascendingly #188

Merged
merged 49 commits into from
May 12, 2023
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
e49d7b1
updates criteria of an empty proof
staheri14 Apr 21, 2023
5e9091c
replaces slice literal with nID1
staheri14 Apr 21, 2023
b6e3cd3
refactors the code to reject invalid empty proof, adds tests
staheri14 Apr 24, 2023
5c32cbc
corrects IsOfEmptyProof description
staheri14 Apr 24, 2023
84865d7
prefixes min and max with root
staheri14 Apr 24, 2023
c8a8059
fixes linter issues
staheri14 Apr 24, 2023
8a27636
revises some typos
staheri14 Apr 25, 2023
9e3d8de
renames IsOfEmptyProof to IsEmptyProof
staheri14 Apr 25, 2023
20b2205
returns immediately on invalid range
staheri14 Apr 25, 2023
6222d5f
adds unit tests
staheri14 Apr 25, 2023
30cbde4
Merge remote-tracking branch 'origin/master' into verifies-proof-range
staheri14 Apr 25, 2023
9fd31f2
considers start=end as invalid
staheri14 Apr 25, 2023
167629c
defines a utility function for proof range verification
staheri14 Apr 25, 2023
fcd3cd0
returns the error returned by validateRange
staheri14 Apr 26, 2023
d11e9c2
adds proof range check
staheri14 Apr 26, 2023
bb07d07
incorporates further tests covering new range check
staheri14 Apr 26, 2023
d491b52
returns err produced by validateRange
staheri14 Apr 26, 2023
e6cb4dc
Merge branch 'verifies-proof-range' into panics-in-verifyleafhashes
staheri14 Apr 26, 2023
a449d68
implements the necessary logic to handle empty proofs
staheri14 Apr 26, 2023
29007f4
develops tests
staheri14 Apr 26, 2023
8fdaab2
revises the err message
staheri14 Apr 26, 2023
409f0f8
extracts the NID from the leaf
staheri14 Apr 26, 2023
2d504d5
removes an excess line
staheri14 Apr 26, 2023
48010b1
Merge branch 'master' into panics-in-verifyleafhashes
staheri14 Apr 27, 2023
712ed08
revises tests descriptions
staheri14 Apr 27, 2023
e01ce18
declares a variable for nonEmptyProof for readability
staheri14 Apr 27, 2023
fc595aa
removes an excess line
staheri14 Apr 27, 2023
021adc0
replaces manual extraction of min and max NID with proper func calls
staheri14 Apr 27, 2023
951ad75
adds node format validation to the validateSiblingsNamespaceOrder
staheri14 Apr 27, 2023
81e86bf
implements test for the invalid siblings format
staheri14 Apr 28, 2023
f70d2d7
resolves linter issues
staheri14 Apr 28, 2023
53597b9
Merge branch 'master' into resolves-panic-validateSiblings
staheri14 May 1, 2023
f3076fe
revises the error message
staheri14 May 1, 2023
e46a1b2
compares min and max
staheri14 May 1, 2023
fb8ee89
deletes stale comments
staheri14 May 1, 2023
05c2ca0
Merge branch 'resolves-panic-validateSiblings' into check-namespace-r…
staheri14 May 1, 2023
224d8cf
adds a comment
staheri14 May 1, 2023
01ecb89
adds tests
staheri14 May 1, 2023
e0905fe
removes an invalid test case
staheri14 May 1, 2023
d64c018
updates error messages and removes some duplicate helper functions
staheri14 May 1, 2023
c398e19
simplifies the namespace computation under the IgnoreMaxNamespace flag
staheri14 May 1, 2023
c0af83b
deletes unused min and max functions
staheri14 May 1, 2023
b0eef2f
revises the IgnoreMaxNamespace description to match the implementation
staheri14 May 1, 2023
21fccff
Merge branch 'master' into use-namespace-ordering-assumption-consiste…
staheri14 May 8, 2023
e73b843
adds computeRange and its unittests
staheri14 May 8, 2023
a3fcfe0
deletes the old compute range function
staheri14 May 8, 2023
4373c00
adds function descriptions
staheri14 May 8, 2023
9c070ad
edits function name
staheri14 May 8, 2023
6768a68
Merge branch 'master' into use-namespace-ordering-assumption-consiste…
staheri14 May 12, 2023
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
39 changes: 22 additions & 17 deletions hasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ const (
var _ hash.Hash = (*Hasher)(nil)

var (
ErrUnorderedSiblings = errors.New("NMT sibling nodes should be ordered lexicographically by namespace IDs")
ErrInvalidNodeLen = errors.New("invalid NMT node size")
ErrInvalidLeafLen = errors.New("invalid NMT leaf size")
ErrUnorderedSiblings = errors.New("NMT sibling nodes should be ordered lexicographically by namespace IDs")
ErrInvalidNodeLen = errors.New("invalid NMT node size")
ErrInvalidLeafLen = errors.New("invalid NMT leaf size")
ErrInvalidNodeNamespaceOrder = errors.New("invalid NMT node namespace order")
)

type Hasher struct {
Expand Down Expand Up @@ -201,20 +202,28 @@ func (n *Hasher) ValidateNodeFormat(node []byte) (err error) {
if nodeLen != expectedNodeLen {
return fmt.Errorf("%w: got: %v, want %v", ErrInvalidNodeLen, nodeLen, expectedNodeLen)
}
// check the namespace order
minNID := namespace.ID(MinNamespace(node, n.NamespaceSize()))
maxNID := namespace.ID(MaxNamespace(node, n.NamespaceSize()))
if maxNID.Less(minNID) {
return fmt.Errorf("%w: max namespace ID %d is less than min namespace ID %d ", ErrInvalidNodeNamespaceOrder, maxNID, minNID)
}
return nil
}

// validateSiblingsNamespaceOrder checks whether left and right as two sibling
// nodes in an NMT have correct namespace IDs relative to each other, more
// specifically, the maximum namespace ID of the left sibling should not exceed
// the minimum namespace ID of the right sibling. It returns ErrUnorderedSiblings error if the check fails. Note that the function assumes
// that the left and right nodes are in correct format, i.e., they are
// namespaced hash values. Otherwise, it panics.
// the minimum namespace ID of the right sibling. It returns ErrUnorderedSiblings error if the check fails.
func (n *Hasher) validateSiblingsNamespaceOrder(left, right []byte) (err error) {
// each NMT node has two namespace IDs for the min and max
totalNamespaceLen := 2 * n.NamespaceLen
leftMaxNs := namespace.ID(left[n.NamespaceLen:totalNamespaceLen])
rightMinNs := namespace.ID(right[:n.NamespaceLen])
if err := n.ValidateNodeFormat(left); err != nil {
return fmt.Errorf("%w: left node does not match the namesapce hash format", err)
}
if err := n.ValidateNodeFormat(right); err != nil {
return fmt.Errorf("%w: right node does not match the namesapce hash format", err)
}
leftMaxNs := namespace.ID(MaxNamespace(left, n.NamespaceSize()))
rightMinNs := namespace.ID(MinNamespace(right, n.NamespaceSize()))

// check the namespace range of the left and right children
if rightMinNs.Less(leftMaxNs) {
Expand Down Expand Up @@ -274,14 +283,10 @@ func (n *Hasher) HashNode(left, right []byte) ([]byte, error) {
leftMinNs, leftMaxNs := left[:n.NamespaceLen], left[n.NamespaceLen:flagLen]
rightMinNs, rightMaxNs := right[:n.NamespaceLen], right[n.NamespaceLen:flagLen]

minNs := min(leftMinNs, rightMinNs)
var maxNs []byte
if n.ignoreMaxNs && n.precomputedMaxNs.Equal(leftMinNs) {
maxNs = n.precomputedMaxNs
Copy link
Contributor Author

@staheri14 staheri14 May 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If leftMinNs represents the maximum possible namespace ID, then by definition, rightMax must also be the maximum possible namespace ID (left and right nodes are already checked to be ordered based on their namespaces). Therefore, this check can be removed.

} else if n.ignoreMaxNs && n.precomputedMaxNs.Equal(rightMinNs) {
minNs := leftMinNs
maxNs := rightMaxNs
if n.ignoreMaxNs && n.precomputedMaxNs.Equal(rightMinNs) {
maxNs = leftMaxNs
} else {
maxNs = max(leftMaxNs, rightMaxNs)
}

res := make([]byte, 0)
Expand Down
87 changes: 67 additions & 20 deletions hasher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,6 @@ func Test_namespacedTreeHasher_HashNode(t *testing.T) {
concat([]byte{0, 0, 0, 0}, randHash),
concat([]byte{0, 0, 1, 1}, randHash))),
},
// XXX: can this happen in practice? or is this an invalid state?
{
"leftmin>rightmin && leftmax<rightmax", 2,
children{
concat([]byte{1, 1, 0, 0}, randHash),
concat([]byte{0, 0, 0, 1}, randHash),
},
concat([]byte{0, 0, 0, 1}, // minNID||maxNID
sum(crypto.SHA256, []byte{NodePrefix}, // Hash(NodePrefix||left||right)
concat([]byte{1, 1, 0, 0}, randHash),
concat([]byte{0, 0, 0, 1}, randHash))),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -221,7 +209,8 @@ func TestNamespaceHasherSum(t *testing.T) {
}
}

func TestHashNode_ChildrenNamespaceRange(t *testing.T) {
// TestHashNode tests the HashNode function for cases that it should and should not produce errors.
func TestHashNode_Error(t *testing.T) {
// create a dummy hash to use as the digest of the left and right child
randHash := createByteSlice(sha256.Size, 0x01)
type children struct {
Expand All @@ -237,7 +226,7 @@ func TestHashNode_ChildrenNamespaceRange(t *testing.T) {
errType error
}{
{
"left.maxNs>right.minNs", 2,
"unordered siblings: left.maxNs>right.minNs", 2,
children{
concat([]byte{0, 0, 1, 1}, randHash),
concat([]byte{0, 0, 1, 1}, randHash),
Expand All @@ -246,7 +235,7 @@ func TestHashNode_ChildrenNamespaceRange(t *testing.T) {
ErrUnorderedSiblings,
},
{
"left.maxNs=right.minNs", 2,
"ordered siblings: left.maxNs=right.minNs", 2,
children{
concat([]byte{0, 0, 1, 1}, randHash),
concat([]byte{1, 1, 2, 2}, randHash),
Expand All @@ -255,14 +244,32 @@ func TestHashNode_ChildrenNamespaceRange(t *testing.T) {
nil,
},
{
"left.maxNs<right.minNs", 2,
"ordered siblings: left.maxNs<right.minNs", 2,
children{
concat([]byte{0, 0, 1, 1}, randHash),
concat([]byte{2, 2, 3, 3}, randHash),
},
false,
nil,
},
{
"invalid left sibling format: left.minNs>left.maxNs", 2,
children{
concat([]byte{2, 2, 0, 0}, randHash),
concat([]byte{1, 1, 4, 4}, randHash),
},
true,
ErrInvalidNodeNamespaceOrder,
},
{
"invalid right sibling format: right.minNs>right.maxNs", 2,
children{
concat([]byte{0, 0, 1, 1}, randHash),
concat([]byte{4, 4, 1, 1}, randHash),
},
true,
ErrInvalidNodeNamespaceOrder,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -276,7 +283,10 @@ func TestHashNode_ChildrenNamespaceRange(t *testing.T) {
}
}

func TestValidateSiblingsNamespaceOrder(t *testing.T) {
func TestValidateSiblings(t *testing.T) {
// create a dummy hash to use as the digest of the left and right child
randHash := createByteSlice(sha256.Size, 0x01)

type children struct {
l []byte // namespace hash of the left child with the format of MinNs||MaxNs||h
r []byte // namespace hash of the right child with the format of MinNs||MaxNs||h
Expand All @@ -288,19 +298,29 @@ func TestValidateSiblingsNamespaceOrder(t *testing.T) {
children children
wantErr bool
}{
{
"wrong left node format", 2,
children{concat([]byte{0, 0, 1, 1}, randHash[:len(randHash)-1]), concat([]byte{0, 0, 1, 1}, randHash)},
true,
},
{
"wrong right node format", 2,
children{concat([]byte{0, 0, 1, 1}, randHash), concat([]byte{0, 0, 1, 1}, randHash[:len(randHash)-1])},
true,
},
{
"left.maxNs>right.minNs", 2,
children{[]byte{0, 0, 1, 1}, []byte{0, 0, 1, 1}},
children{concat([]byte{0, 0, 1, 1}, randHash), concat([]byte{0, 0, 1, 1}, randHash)},
true,
},
{
"left.maxNs=right.minNs", 2,
children{[]byte{0, 0, 1, 1}, []byte{1, 1, 2, 2}},
children{concat([]byte{0, 0, 1, 1}, randHash), concat([]byte{1, 1, 2, 2}, randHash)},
false,
},
{
"left.maxNs<right.minNs", 2,
children{[]byte{0, 0, 1, 1}, []byte{2, 2, 3, 3}},
children{concat([]byte{0, 0, 1, 1}, randHash), concat([]byte{2, 2, 3, 3}, randHash)},
false,
},
}
Expand Down Expand Up @@ -362,6 +382,33 @@ func TestValidateNodeFormat(t *testing.T) {
true,
ErrInvalidNodeLen,
},
{
"invalid node: minNS > maxNs",
2,
[]byte{3, 3},
[]byte{1, 1},
concat(hashValue),
true,
ErrInvalidNodeNamespaceOrder,
},
{
"valid node: minNs = maxNs",
2,
minNID,
minNID,
concat(hashValue),
false,
nil,
},
{
"valid node: minNs < maxNs",
2,
minNID,
maxNID,
concat(hashValue),
false,
nil,
},
}

for _, tt := range tests {
Expand Down
12 changes: 8 additions & 4 deletions proof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,13 +286,16 @@ func TestVerifyLeafHashes_Err(t *testing.T) {
root, err := nmt.Root()
require.NoError(t, err)

corruptRoot := root[:nmt.NamespaceSize()]

// create an NMT proof
nID5 := namespace.ID{5, 5}
proof5, err := nmt.ProveNamespace(nID5)
require.NoError(t, err)
// corrupt the leafHash so that the proof verification fails during the root computation.
// note that the leaf at index 4 has the namespace ID of 5.
leafHash5 := nmt.leafHashes[4][:nmt.NamespaceSize()]
leafHash5 := nmt.leafHashes[4]
corruptLeafHash5 := leafHash5[:nmt.NamespaceSize()]

// corrupt the leafHash: replace its namespace ID with a different one.
nID3 := createByteSlice(nameIDSize, 3)
Expand Down Expand Up @@ -339,7 +342,8 @@ func TestVerifyLeafHashes_Err(t *testing.T) {
root []byte
wantErr bool
}{
{"wrong leafHash: not namespaced", proof5, hasher, true, nID5, [][]byte{leafHash5}, root, true},
{"corrupt root", proof5, hasher, true, nID5, [][]byte{leafHash5}, corruptRoot, true},
{"wrong leafHash: not namespaced", proof5, hasher, true, nID5, [][]byte{corruptLeafHash5}, root, true},
{"wrong leafHash: smaller namespace", proof5, hasher, true, nID5, [][]byte{leafHash5SmallerNID}, root, true},
{"wong leafHash: bigger namespace", proof5, hasher, true, nID5, [][]byte{leafHash5BiggerNID}, root, true},
{"wrong proof.nodes: the last node has an incorrect format", proof4InvalidNodes, hasher, false, nID4, [][]byte{leafHash4}, root, true},
Expand Down Expand Up @@ -507,8 +511,8 @@ func TestVerifyNamespace_False(t *testing.T) {
args args
result bool
}{
{"nID size of proof < nID size of VerifyNamespace's nmt hasher", proof4_1, args{hasher, nid4_2, [][]byte{leaf}, root2}, false},
{"nID size of proof > nID size of VerifyNamespace's nmt hasher", proof4_2, args{hasher, nid4_1, [][]byte{leaf}, root1}, false},
{"nID size of proof.nodes < nID size of VerifyNamespace's nmt hasher", proof4_1, args{hasher, nid4_2, [][]byte{leaf}, root2}, false},
{"nID size of proof.nodes > nID size of VerifyNamespace's nmt hasher", proof4_2, args{hasher, nid4_1, [][]byte{leaf}, root1}, false},
{"nID size of root < nID size of VerifyNamespace's nmt hasher", proof4_2, args{hasher, nid4_2, [][]byte{leaf}, root1}, false},
{"nID size of root > nID size of VerifyNamespace's nmt hasher", proof4_1, args{hasher, nid4_1, [][]byte{leaf}, root2}, false},
{"nID size of proof.leafHash < nID size of VerifyNamespace's nmt hasher", absenceProof9_2, args{hasher, nid9_2, [][]byte{}, root2}, false},
Expand Down