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!: updates the HashNode and HashLeaf methods to return error instead of panic and refactors the code #136

Merged
merged 76 commits into from
Mar 23, 2023

Conversation

staheri14
Copy link
Collaborator

@staheri14 staheri14 commented Mar 14, 2023

Overview

Closes #109 and #99.
This pull request ensures that the HashNode and HashLeaf functions return errors instead of panicking, and that caller functions properly handle any emitted errors. When generating proofs, any hashing errors are propagated with additional context. During verification, hashing errors are treated as unsuccessful proof verification.

Note that in this PR, the term Irrecoverable errors used to describe errors that any number of retries can correct the them.

Checklist

  • New and updated code has appropriate documentation
  • New and updated code has new and/or updated testing
  • Required CI checks are passing
  • Visual proof for any user facing features like CLI or documentation updates
  • Linked issues closed with keywords

@staheri14 staheri14 changed the title feat!: updates the HashNode and HashLeaf method to return error instead of panic feat!: updates the HashNode and HashLeaf methods to return error instead of panic Mar 14, 2023
@codecov
Copy link

codecov bot commented Mar 15, 2023

Codecov Report

Merging #136 (e5f5e42) into master (c63e970) will decrease coverage by 0.14%.
The diff coverage is 96.99%.

@@            Coverage Diff             @@
##           master     #136      +/-   ##
==========================================
- Coverage   96.33%   96.20%   -0.14%     
==========================================
  Files           6        6              
  Lines         464      527      +63     
==========================================
+ Hits          447      507      +60     
- Misses         11       12       +1     
- Partials        6        8       +2     
Impacted Files Coverage Δ
proof.go 93.80% <90.69%> (-3.04%) ⬇️
hasher.go 97.45% <100.00%> (+1.16%) ⬆️
nmt.go 99.02% <100.00%> (+0.20%) ⬆️

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

@staheri14 staheri14 marked this pull request as ready for review March 21, 2023 17:21
@staheri14
Copy link
Collaborator Author

staheri14 commented Mar 21, 2023

This PR is ready for review, with the exception of the code coverage percentage, which is currently 0.14% below the threshold. This 0.14% difference in the coverage is related to two lines (i.e., this and this) of the proof.go file, where the HashLeaf function emits an error. However, these situations should not occur in practice, as the input is already validated in earlier lines (e.g., via ValidateLeaf or by explicitly adding a namespace to the data prior to hashing it), making it impossible for any test scenario to hit those lines. Despite this, I have chosen not to silence the errors, as doing so could make the code vulnerable to unwanted bugs if someone updates the HashLeaf logic without updating the ValidateLeaf logic as well.
With the exception of these two lines, all other files are fully covered by an extensive set of tests.

Copy link
Collaborator

@rootulp rootulp left a comment

Choose a reason for hiding this comment

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

No blocking feedback, great work!

hasher_test.go Outdated
ErrInvalidLeafLen,
},
{
"invalid node: left.max > right.imn",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
"invalid node: left.max > right.imn",
"invalid node: left.max > right.min",

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed 7dfb628

nmt_test.go Outdated
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := tt.tree.Root()
Copy link
Collaborator

Choose a reason for hiding this comment

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

[question] should this invoke computeRoot? It currently seems identical to the above test:

func Test_Root_Error(t *testing.T) {
    // ...

			_, err := tt.tree.Root()

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for finding this, please see the corrected version in this commit 3fd5dab

gotLeafHashes = append(gotLeafHashes, hashLeafFunc(leafData))
leafHash, err := hashLeafFunc(gotLeaf)
if err != nil { // this can never happen due to the initial validation of the leaf at the beginning of the loop
return false
Copy link
Collaborator

Choose a reason for hiding this comment

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

[optional] if this can never happen, should it panic?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The reason it cannot happen is that the leaf is validated earlier in the code. However, if that section of the code changes in the future, we may encounter an error at this line. If the leaf is found to be invalid, we should return false instead of panicking.

Copy link
Collaborator Author

@staheri14 staheri14 Mar 22, 2023

Choose a reason for hiding this comment

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

If you check the line where the leaf gets validated, you will see that we return false. So, the same behaviour is replicated here.

hashes[i] = nth.HashLeaf(leafData)
res, err := nth.HashLeaf(leafData)
if err != nil {
return false // this never can happen since the leafData is guaranteed to be namespaced
Copy link
Collaborator

Choose a reason for hiding this comment

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

[question] given this should never happen, should it panic?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The same argument as in this also applies in here.

proof_test.go Outdated
Comment on lines 270 to 271
// proof4 is the inclusion proof for the leaf at index 3
leaf4WONamespace := nmt.leaves[3][nmt.NamespaceSize():] // the VerifyInclusion function expects the leaf without the namespace ID
Copy link
Collaborator

Choose a reason for hiding this comment

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

[question] what does the variable name here mean? It appears to truncate some leading bytes from the leaf so maybe leaf 4 without namespace? If so, proposal to rename

Suggested change
// proof4 is the inclusion proof for the leaf at index 3
leaf4WONamespace := nmt.leaves[3][nmt.NamespaceSize():] // the VerifyInclusion function expects the leaf without the namespace ID
// proof4 is the inclusion proof for the leaf at index 3
leaf4WithoutNamespace := nmt.leaves[3][nmt.NamespaceSize():] // the VerifyInclusion function expects the leaf without the namespace ID

[question] if VerifyInclusion expects the leaf without the namespace, why does this tampered test data fail?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

[question] what does the variable name here mean? It appears to truncate some leading bytes from the leaf so maybe leaf 4 without namespace? If so, proposal to rename

In this variable name i.e., leaf4WONamespace, the WO part is the abbreviation of without, however, it looks like unclear (since you asked), so I'm going to change it to the full version.

Copy link
Collaborator Author

@staheri14 staheri14 Mar 22, 2023

Choose a reason for hiding this comment

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

[question] if VerifyInclusion expects the leaf without the namespace, why does this tampered test data fail?

The reason for the function's failure is not related to the structure of leaf4WithoutNamespace. The format of leaf4WithoutNamespace is correct, and in the line you referred to, I trimmed the namespace to ensure it is a valid input for the function. The error is caused by the corrupted proof.nodes section that follows that line. The node length is cut to nmt.NamespaceSize() bytes, resulting in the error.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

[question] what does the variable name here mean? It appears to truncate some leading bytes from the leaf so maybe leaf 4 without namespace? If so, proposal to rename

Please see the new variable name in 3f1b523

Copy link
Member

@evan-forbes evan-forbes left a comment

Choose a reason for hiding this comment

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

only one question atm, but will continue to review

Comment on lines +107 to +109
if err != nil {
panic(err) // this should never happen since the data is already validated in the Write method
}
Copy link
Member

Choose a reason for hiding this comment

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

[question]
could this still be hit when verifying a proof since we're not calling the write method ahead of time?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If the Write method is not called prior to the Sum, then the code panics due to the absence of the tp field of the Hasher. More details follow:
For the Sum to get through, the tp field of the hasher should be set to one of the LeafPrefix or NodePrefix based on which the hasher determines whether the input data belongs to a leaf or a non-leaf node. Without making call to the Write method, the Hasher.tp field remains unset, hence in the mentioned switch statement, none of the LeafPrefix and NodePrefix cases are true, and the code goes straight to the default part which panics due to unset tp field.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Another point is that, it is not possible to call Sum without making a call to the Write method, as it corresponds to doing nothing (the Hasher state is empty). Doing so always results in panic due to undefined state

nmt/hasher.go

Line 121 in 85ec3c9

default:
.

Copy link
Member

@evan-forbes evan-forbes left a comment

Choose a reason for hiding this comment

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

LGTM!

@staheri14
Copy link
Collaborator Author

staheri14 commented Mar 23, 2023

benchstat results (Update: this is no longer needed as the gobencher has been fixed now):

goos: darwin
goarch: arm64
pkg: github.com/celestiaorg/nmt
                          │ master.txt  │        nmt-err-handling.txt        │
                          │   sec/op    │   sec/op     vs base               │
ComputeRoot/64-leaves-10    38.30µ ± 2%   38.93µ ± 4%       ~ (p=0.089 n=10)
ComputeRoot/128-leaves-10   74.73µ ± 1%   75.29µ ± 1%  +0.75% (p=0.023 n=10)
ComputeRoot/256-leaves-10   152.1µ ± 3%   152.6µ ± 1%       ~ (p=0.579 n=10)
geomean                     75.79µ        76.48µ       +0.91%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat: Handling Panics in HashNode Method
3 participants