From bbecc5315e1c0e0078e4d0668f7f6bba9fea112d Mon Sep 17 00:00:00 2001 From: Evan Forbes <42654277+evan-forbes@users.noreply.github.com> Date: Tue, 27 Jun 2023 07:44:00 -0500 Subject: [PATCH 1/7] feat: prepare for testing fraudulant blocks by allowing for custom hashers and the ability to add leaves without validation (#211) ## Overview Currently, the hasher is not swappable, this is only problematic when trying to create fraudulent block test cases, since the standard hasher prevents such behavior. ## 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 - [ ] Linked issues closed with keywords --- hasher.go | 55 +++++++++++++++++++++++++++++++++----------------- hasher_test.go | 4 ++-- nmt.go | 44 +++++++++++++++++++++++++++++++++++++--- nmt_test.go | 29 ++++++++++++++++++++++++++ proof.go | 2 +- proof_test.go | 5 +++-- 6 files changed, 112 insertions(+), 27 deletions(-) diff --git a/hasher.go b/hasher.go index 5dd6470..cb5b3fb 100644 --- a/hasher.go +++ b/hasher.go @@ -14,7 +14,7 @@ const ( NodePrefix = 1 ) -var _ hash.Hash = (*Hasher)(nil) +var _ hash.Hash = (*NmtHasher)(nil) var ( ErrUnorderedSiblings = errors.New("NMT sibling nodes should be ordered lexicographically by namespace IDs") @@ -23,7 +23,24 @@ var ( ErrInvalidNodeNamespaceOrder = errors.New("invalid NMT node namespace order") ) -type Hasher struct { +// Hasher describes the interface nmts use to hash leafs and nodes. +// +// Note: it is not advised to create alternative hashers if following the +// specification is desired. The main reason this exists is to not follow the +// specification for testing purposes. +type Hasher interface { + IsMaxNamespaceIDIgnored() bool + NamespaceSize() namespace.IDSize + HashLeaf(data []byte) ([]byte, error) + HashNode(leftChild, rightChild []byte) ([]byte, error) + EmptyRoot() []byte +} + +var _ Hasher = &NmtHasher{} + +// NmtHasher is the default hasher. It follows the description of the original +// hashing function described in the LazyLedger white paper. +type NmtHasher struct { //nolint:revive baseHasher hash.Hash NamespaceLen namespace.IDSize @@ -41,16 +58,16 @@ type Hasher struct { data []byte // written data of the NMT node } -func (n *Hasher) IsMaxNamespaceIDIgnored() bool { +func (n *NmtHasher) IsMaxNamespaceIDIgnored() bool { return n.ignoreMaxNs } -func (n *Hasher) NamespaceSize() namespace.IDSize { +func (n *NmtHasher) NamespaceSize() namespace.IDSize { return n.NamespaceLen } -func NewNmtHasher(baseHasher hash.Hash, nidLen namespace.IDSize, ignoreMaxNamespace bool) *Hasher { - return &Hasher{ +func NewNmtHasher(baseHasher hash.Hash, nidLen namespace.IDSize, ignoreMaxNamespace bool) *NmtHasher { + return &NmtHasher{ baseHasher: baseHasher, NamespaceLen: nidLen, ignoreMaxNs: ignoreMaxNamespace, @@ -59,7 +76,7 @@ func NewNmtHasher(baseHasher hash.Hash, nidLen namespace.IDSize, ignoreMaxNamesp } // Size returns the number of bytes Sum will return. -func (n *Hasher) Size() int { +func (n *NmtHasher) Size() int { return n.baseHasher.Size() + int(n.NamespaceLen)*2 } @@ -69,7 +86,7 @@ func (n *Hasher) Size() int { // write is allowed. // It panics if more than one single write is attempted. // If the data does not match the format of an NMT non-leaf node or leaf node, an error will be returned. -func (n *Hasher) Write(data []byte) (int, error) { +func (n *NmtHasher) Write(data []byte) (int, error) { if n.data != nil { panic("only a single Write is allowed") } @@ -101,7 +118,7 @@ func (n *Hasher) Write(data []byte) (int, error) { // Sum computes the hash. Does not append the given suffix, violating the // interface. // It may panic if the data being hashed is invalid. This should never happen since the Write method refuses an invalid data and errors out. -func (n *Hasher) Sum([]byte) []byte { +func (n *NmtHasher) Sum([]byte) []byte { switch n.tp { case LeafPrefix: res, err := n.HashLeaf(n.data) @@ -125,17 +142,17 @@ func (n *Hasher) Sum([]byte) []byte { } // Reset resets the Hash to its initial state. -func (n *Hasher) Reset() { +func (n *NmtHasher) Reset() { n.tp, n.data = 255, nil // reset with an invalid node type, as zero value is a valid Leaf n.baseHasher.Reset() } // BlockSize returns the hash's underlying block size. -func (n *Hasher) BlockSize() int { +func (n *NmtHasher) BlockSize() int { return n.baseHasher.BlockSize() } -func (n *Hasher) EmptyRoot() []byte { +func (n *NmtHasher) EmptyRoot() []byte { n.baseHasher.Reset() emptyNs := bytes.Repeat([]byte{0}, int(n.NamespaceLen)) h := n.baseHasher.Sum(nil) @@ -145,7 +162,7 @@ func (n *Hasher) EmptyRoot() []byte { } // ValidateLeaf verifies if data is namespaced and returns an error if not. -func (n *Hasher) ValidateLeaf(data []byte) (err error) { +func (n *NmtHasher) ValidateLeaf(data []byte) (err error) { nidSize := int(n.NamespaceSize()) lenData := len(data) if lenData < nidSize { @@ -160,7 +177,7 @@ func (n *Hasher) ValidateLeaf(data []byte) (err error) { // leaves minNs = maxNs = ns(leaf) = leaf[:NamespaceLen]. HashLeaf can return the ErrInvalidNodeLen error if the input is not namespaced. // //nolint:errcheck -func (n *Hasher) HashLeaf(ndata []byte) ([]byte, error) { +func (n *NmtHasher) HashLeaf(ndata []byte) ([]byte, error) { h := n.baseHasher h.Reset() @@ -187,7 +204,7 @@ func (n *Hasher) HashLeaf(ndata []byte) ([]byte, error) { // MustHashLeaf is a wrapper around HashLeaf that panics if an error is // encountered. The ndata must be a valid leaf node. -func (n *Hasher) MustHashLeaf(ndata []byte) []byte { +func (n *NmtHasher) MustHashLeaf(ndata []byte) []byte { res, err := n.HashLeaf(ndata) if err != nil { panic(err) @@ -197,7 +214,7 @@ func (n *Hasher) MustHashLeaf(ndata []byte) []byte { // ValidateNodeFormat checks whether the supplied node conforms to the // namespaced hash format and returns ErrInvalidNodeLen if not. -func (n *Hasher) ValidateNodeFormat(node []byte) (err error) { +func (n *NmtHasher) ValidateNodeFormat(node []byte) (err error) { expectedNodeLen := n.Size() nodeLen := len(node) if nodeLen != expectedNodeLen { @@ -216,7 +233,7 @@ func (n *Hasher) ValidateNodeFormat(node []byte) (err error) { // 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. -func (n *Hasher) validateSiblingsNamespaceOrder(left, right []byte) (err error) { +func (n *NmtHasher) validateSiblingsNamespaceOrder(left, right []byte) (err error) { if err := n.ValidateNodeFormat(left); err != nil { return fmt.Errorf("%w: left node does not match the namesapce hash format", err) } @@ -237,7 +254,7 @@ func (n *Hasher) validateSiblingsNamespaceOrder(left, right []byte) (err error) // validity of the inputs of HashNode. It verifies whether left // and right comply by the namespace hash format, and are correctly ordered // according to their namespace IDs. -func (n *Hasher) ValidateNodes(left, right []byte) error { +func (n *NmtHasher) ValidateNodes(left, right []byte) error { if err := n.ValidateNodeFormat(left); err != nil { return err } @@ -260,7 +277,7 @@ func (n *Hasher) ValidateNodes(left, right []byte) error { // slightly changes. Let MAXNID be the maximum possible namespace ID value i.e., 2^NamespaceIDSize-1. // If the namespace range of the right child is start=end=MAXNID, indicating that it represents the root of a subtree whose leaves all have the namespace ID of `MAXNID`, then exclude the right child from the namespace range calculation. Instead, // assign the namespace range of the left child as the parent's namespace range. -func (n *Hasher) HashNode(left, right []byte) ([]byte, error) { +func (n *NmtHasher) HashNode(left, right []byte) ([]byte, error) { // validate the inputs if err := n.ValidateNodes(left, right); err != nil { return nil, err diff --git a/hasher_test.go b/hasher_test.go index 8227ed3..0bf1178 100644 --- a/hasher_test.go +++ b/hasher_test.go @@ -600,7 +600,7 @@ func TestWrite_Err(t *testing.T) { tests := []struct { name string - hasher *Hasher + hasher *NmtHasher data []byte wantErr bool errType error @@ -639,7 +639,7 @@ func TestSum_Err(t *testing.T) { tests := []struct { name string - hasher *Hasher + hasher *NmtHasher data []byte nodeType byte wantWriteErr bool diff --git a/nmt.go b/nmt.go index 415e724..5b897f0 100644 --- a/nmt.go +++ b/nmt.go @@ -39,6 +39,7 @@ type Options struct { // in the "Hasher. IgnoreMaxNamespace bool NodeVisitor NodeVisitorFn + Hasher Hasher } type Option func(*Options) @@ -82,8 +83,15 @@ func NodeVisitor(nodeVisitorFn NodeVisitorFn) Option { } } +// CustomHasher replaces the default hasher. +func CustomHasher(h Hasher) Option { + return func(o *Options) { + o.Hasher = h + } +} + type NamespacedMerkleTree struct { - treeHasher *Hasher + treeHasher Hasher visit NodeVisitorFn // just cache stuff until we pass in a store and keep all nodes in there @@ -128,9 +136,18 @@ func New(h hash.Hash, setters ...Option) *NamespacedMerkleTree { for _, setter := range setters { setter(opts) } - treeHasher := NewNmtHasher(h, opts.NamespaceIDSize, opts.IgnoreMaxNamespace) + + // first create the default hasher using the updated options + hasher := NewNmtHasher(h, opts.NamespaceIDSize, opts.IgnoreMaxNamespace) + opts.Hasher = hasher + + // set the options a second time to replace the hasher if needed + for _, setter := range setters { + setter(opts) + } + return &NamespacedMerkleTree{ - treeHasher: treeHasher, + treeHasher: opts.Hasher, visit: opts.NodeVisitor, leaves: make([][]byte, 0, opts.InitialCapacity), leafHashes: make([][]byte, 0, opts.InitialCapacity), @@ -491,6 +508,27 @@ func (n *NamespacedMerkleTree) MaxNamespace() (namespace.ID, error) { return MaxNamespace(r, n.NamespaceSize()), nil } +// ForceAddLeaf adds a namespaced data to the tree without validating its +// namespace ID. This method should only be used by tests that are attempting to +// create out of order trees. The default hasher will fail for trees that are +// out of order. +func (n *NamespacedMerkleTree) ForceAddLeaf(leaf namespace.PrefixedData) error { + nID := namespace.ID(leaf[:n.NamespaceSize()]) + // compute the leaf hash + res, err := n.treeHasher.HashLeaf(leaf) + if err != nil { + return err + } + + // update relevant "caches": + n.leaves = append(n.leaves, leaf) + n.leafHashes = append(n.leafHashes, res) + n.updateNamespaceRanges() + n.updateMinMaxID(nID) + n.rawRoot = nil + return nil +} + // computeRoot calculates the namespace Merkle root for a tree/sub-tree that // encompasses the leaves within the range of [start, end). // Any errors returned by this method are irrecoverable and indicate an illegal state of the tree (n). diff --git a/nmt_test.go b/nmt_test.go index b1acac2..c4907b5 100644 --- a/nmt_test.go +++ b/nmt_test.go @@ -553,6 +553,19 @@ func TestNodeVisitor(t *testing.T) { } } +func TestCustomHasher(t *testing.T) { + type customHasher struct { + *NmtHasher + } + + h := customHasher{NewNmtHasher(sha256.New(), namespace.IDSize(8), true)} + + tree := New(sha256.New(), NamespaceIDSize(8), IgnoreMaxNamespace(true), CustomHasher(h)) + + _, ok := tree.treeHasher.(customHasher) + require.True(t, ok) +} + func TestNamespacedMerkleTree_ProveErrors(t *testing.T) { tests := []struct { name string @@ -1138,3 +1151,19 @@ func TestEmptyRoot_NMT(t *testing.T) { assert.True(t, bytes.Equal(gotEmptyRoot, expectedEmptyRoot)) } + +func TestForcedOutOfOrderNamespacedMerkleTree(t *testing.T) { + data := [][]byte{ + append(namespace.ID{0}, []byte("leaf_0")...), + append(namespace.ID{2}, []byte("leaf_1")...), + append(namespace.ID{1}, []byte("leaf_2")...), + append(namespace.ID{1}, []byte("leaf_3")...), + } + nidSize := 1 + tree := New(sha256.New(), NamespaceIDSize(nidSize)) + + for _, d := range data { + err := tree.ForceAddLeaf(d) + assert.NoError(t, err) + } +} diff --git a/proof.go b/proof.go index 2db2589..2a1037c 100644 --- a/proof.go +++ b/proof.go @@ -228,7 +228,7 @@ func (proof Proof) VerifyNamespace(h hash.Hash, nID namespace.ID, leaves [][]byt // 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 // outside the leafHashes list. -func (proof Proof) VerifyLeafHashes(nth *Hasher, verifyCompleteness bool, nID namespace.ID, leafHashes [][]byte, root []byte) (bool, error) { +func (proof Proof) VerifyLeafHashes(nth *NmtHasher, 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() { return false, fmt.Errorf("proof range [proof.start=%d, proof.end=%d) is not valid: %w", proof.Start(), proof.End(), ErrInvalidRange) diff --git a/proof_test.go b/proof_test.go index 291de09..df0f90f 100644 --- a/proof_test.go +++ b/proof_test.go @@ -276,7 +276,8 @@ func TestVerifyLeafHashes_Err(t *testing.T) { // create a sample tree nameIDSize := 2 nmt := exampleNMT(nameIDSize, true, 1, 2, 3, 4, 5, 6, 7, 8) - hasher := nmt.treeHasher + nmthasher := nmt.treeHasher + hasher := nmthasher.(*NmtHasher) root, err := nmt.Root() require.NoError(t, err) @@ -330,7 +331,7 @@ func TestVerifyLeafHashes_Err(t *testing.T) { tests := []struct { name string proof Proof - Hasher *Hasher + Hasher *NmtHasher verifyCompleteness bool nID namespace.ID leafHashes [][]byte From 564300aaa2125d5881ecf62f024a22db190db17c Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 27 Jun 2023 14:44:55 +0200 Subject: [PATCH 2/7] feat(proof.go): adding JSON marshal/unmarshal (#214) Adds json marshaling/unmarshaling to `nmt.Proof`. As found elsewhere in the go stdlib, `UnmarshalJSON` must be defined on the pointer receiver although `MarshalJSON` is a value receiver. This is needed because the fields are private. Does this need to be the case? If we make them public, JSON marshaling will work by default. --- proof.go | 34 ++++++++++++++++++++++++++++++++++ proof_test.go | 23 +++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/proof.go b/proof.go index 2a1037c..de307db 100644 --- a/proof.go +++ b/proof.go @@ -2,6 +2,7 @@ package nmt import ( "bytes" + "encoding/json" "errors" "fmt" "hash" @@ -47,6 +48,39 @@ type Proof struct { isMaxNamespaceIDIgnored bool } +type jsonProof struct { + Start int `json:"start"` + End int `json:"end"` + Nodes [][]byte `json:"nodes"` + LeafHash []byte `json:"leaf_hash"` + IsMaxNamespaceIDIgnored bool `json:"is_max_namespace_id_ignored"` +} + +func (proof Proof) MarshalJSON() ([]byte, error) { + jsonProofObj := jsonProof{ + Start: proof.start, + End: proof.end, + Nodes: proof.nodes, + LeafHash: proof.leafHash, + IsMaxNamespaceIDIgnored: proof.isMaxNamespaceIDIgnored, + } + return json.Marshal(jsonProofObj) +} + +func (proof *Proof) UnmarshalJSON(data []byte) error { + var jsonProofObj jsonProof + err := json.Unmarshal(data, &jsonProofObj) + if err != nil { + return err + } + proof.start = jsonProofObj.Start + proof.end = jsonProofObj.End + proof.nodes = jsonProofObj.Nodes + proof.leafHash = jsonProofObj.LeafHash + proof.isMaxNamespaceIDIgnored = jsonProofObj.IsMaxNamespaceIDIgnored + return nil +} + // Start index of this proof. func (proof Proof) Start() int { return proof.start diff --git a/proof_test.go b/proof_test.go index df0f90f..cb2cccc 100644 --- a/proof_test.go +++ b/proof_test.go @@ -13,6 +13,29 @@ import ( "github.com/celestiaorg/nmt/namespace" ) +func TestJsonMarshal_Proof(t *testing.T) { + // create a tree with 4 leaves + nIDSize := 1 + tree := exampleNMT(nIDSize, true, 1, 2, 3, 4) + + // build a proof for an NID that is within the namespace range of the tree + nID := []byte{1} + proof, err := tree.ProveNamespace(nID) + require.NoError(t, err) + + // marshal the proof to JSON + jsonProof, err := proof.MarshalJSON() + require.NoError(t, err) + + // unmarshal the proof from JSON + var unmarshalledProof Proof + err = unmarshalledProof.UnmarshalJSON(jsonProof) + require.NoError(t, err) + + // verify that the unmarshalled proof is equal to the original proof + assert.Equal(t, proof, unmarshalledProof) +} + // 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 From 9f0f36e87af75efce55cb95e38fb99bd152894d8 Mon Sep 17 00:00:00 2001 From: Sanaz Taheri <35961250+staheri14@users.noreply.github.com> Date: Wed, 28 Jun 2023 10:52:16 -0700 Subject: [PATCH 3/7] doc: introduces short namespace absence proof in the NMT specs (#209) ## Overview Closes #205 Additionally, in line with [this](https://github.com/celestiaorg/nmt/issues/206), to ensure consistency with how **namespace ID** and **namespace** (i.e., a combination of namespace ID and version) are utilized in the core-app, I have replaced all instances of words that refer to or imply **namespace ID** with the more accurate term "**namespace**". ## 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 --- docs/spec/nmt.md | 140 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 107 insertions(+), 33 deletions(-) diff --git a/docs/spec/nmt.md b/docs/spec/nmt.md index 95a3115..40bf3ff 100644 --- a/docs/spec/nmt.md +++ b/docs/spec/nmt.md @@ -3,9 +3,9 @@ ## Abstract Namespaced Merkle Tree (NMT) is one of the core components of the Celestia blockchain. -Transactions in Celestia are associated with a namespace ID which signifies the application they belong to. -Nodes interested in a specific application only need to download transactions of a certain namespace ID. -The Namespaced Merkle Tree (NMT) was introduced in the [LazyLedger article](https://arxiv.org/abs/1905.09274) to organize transactions in Celestia blocks based on their namespace IDs. +Transactions in Celestia are associated with a namespace which signifies the application they belong to. +Nodes interested in a specific application only need to download transactions of a certain namespace. +The Namespaced Merkle Tree (NMT) was introduced in the [LazyLedger article](https://arxiv.org/abs/1905.09274) to organize transactions in Celestia blocks based on their namespaces. The NMT allows for efficient and verifiable queries of application-specific transactions by accessing based on the block header which contains the NMT root. This specification explains the NMT data structure and provides an overview of its current implementation in this repository. The NMT library, which is the implementaion of NMT data structure, is explained in the [NMT Library](./../nmt-lib.md) document. @@ -13,12 +13,12 @@ The NMT library, which is the implementaion of NMT data structure, is explained ## NMT Data Structure Namespaced Merkle Tree, at its core, is a normal Merkle tree that employs a modified hash function, namely a [namespaced hash](#namespaced-hash) to ensure each node in the tree encompasses the range of namespaces of its descendants' messages. -Messages stored in the NMT leaves are all namespace-prefixed with the format `||` and arranged in ascending order based on their namespace IDs. +Messages stored in the NMT leaves are all namespace-prefixed with the format `||` and arranged in ascending order based on their namespaces. All namespace identifiers have a fixed and known size. ### Namespaced Hash -NMT utilizes a namespaced hash function i.e., `NsH()`, which in addition to the normal digest calculation, it returns the range of namespace IDs covered by a node's children. The hash output is formatted as `minNs||maxNs||h(.)`, where `minNs` is the lowest namespace identifier among all the node's descendants, `maxNs` is the highest, and `h(.)` represents the hash digest of `.` (e.g., `SHA256(.)`). +NMT utilizes a namespaced hash function i.e., `NsH()`, which in addition to the normal digest calculation, it returns the range of namespaces covered by a node's children. The hash output is formatted as `minNs||maxNs||h(.)`, where `minNs` is the lowest namespace among all the node's descendants, `maxNs` is the highest, and `h(.)` represents the hash digest of `.` (e.g., `SHA256(.)`). **Leaf Nodes**: Each leaf in the tree represents the namespaced hash of a namespaced message `d = ||`. The hash is computed as follows: @@ -43,9 +43,9 @@ NsH(n) = min(l.minNs, r.minNs) || max(l.maxNs, r.maxNs) || h(0x01, l, r) The inclusion of the `0x01` value in the hash calculation serves as a non-leaf prefix and is done to conform to [RFC 6962](https://www.rfc-editor.org/rfc/rfc6962#section-2.1). -In an NMT data structure, the `minNs` and `maxNs` values of the root node denote the minimum and maximum namespace IDs, respectively, of all messages within the tree. +In an NMT data structure, the `minNs` and `maxNs` values of the root node denote the minimum and maximum namespaces, respectively, of all messages within the tree. -An example of an NMT is shown in the figure below which utilizes SHA256 as the underlying hash function and namespace ID size of `1` byte. +An example of an NMT is shown in the figure below which utilizes SHA256 as the underlying hash function and namespace size of `1` byte. The code snippets necessary to create this tree are provided in [NMT Library](./../nmt-lib.md) documentation, and the data items and tree nodes are represented as hex strings. For the sake of brevity, we have only included the first 7 hex digits of SHA256 for each namespace hash. @@ -60,11 +60,11 @@ For the sake of brevity, we have only included the first 7 hex digits of SHA256 / \ / \ NsH() NsH() NsH() NsH() / \ / \ - 00 00 5fa0c9c 00 00 52385a0 01 01 71ca46a 03 03 b4a2792 Leaf Nodes + 00 00 5fa0c9c 00 00 52385a0 01 01 71ca46a 03 03 b4a2792 Leaf Hashes | | | | NsH() NsH() NsH() NsH() | | | | -00 6c6561665f30 00 6c6561665f31 01 6c6561665f32 03 6c6561665f33 Namespaced Data Items +00 6c6561665f30 00 6c6561665f31 01 6c6561665f32 03 6c6561665f33 Leaves with namespaces 0 1 2 3 Leaf Indices ``` @@ -73,45 +73,46 @@ Figure 1. ### Namespace Proof -NMT supports standard Merkle tree functionalities, including inclusion and range proofs, and offers namespace ID querying and proof generation. -The following enumerated cases explain potential outcomes when querying an NMT for a namespace ID. +NMT supports standard Merkle tree functionalities, including inclusion and range proofs, and offers namespace querying and proof generation. +The following enumerated cases explain potential outcomes when querying an NMT for a namespace. #### Namespace Inclusion Proof -When the queried namespace ID `nID` has corresponding items in the tree with root `T`, the query is resolved by a +When the queried namespace `NS` has corresponding items in the tree with root `T`, the query is resolved by a namespace inclusion proof which consists of: -1) The starting index `start` and the ending index `end` of the leaves that match `nID`. +1) The starting index `start` and the ending index `end` of the leaves that match `NS`. 2) Nodes of the tree that are necessary for the regular Merkle range proof of `[start, end)` to `T`. In specific, the nodes include 1) the [namespaced hash](#namespaced-hash) of the left siblings for the Merkle inclusion proof of the `start` leaf and 2) the [namespaced hash](#namespaced-hash) of the right siblings of the Merkle inclusion proof of the `end-1` leaf. Nodes are sorted according to the in-order traversal of the tree. -For example, the NMT proof of `nID = 0` in Figure 1 would be `[start = 0, end = 2)` and the Merkle inclusion proof embodies one single tree node i.e., `01 03 52c7c03`. +For example, the NMT proof of `NS = 0` in Figure 1 would be `[start = 0, end = 2)` and the Merkle inclusion proof embodies one single tree node i.e., `01 03 52c7c03`. #### Namespace Absence Proof -If the namespace ID being queried falls within the range of namespace IDs in the tree root, but there is no corresponding message, an absence proof will be generated. -An absence proof asserts that no message in the tree matches the queried namespace ID `nID`. +If the namespace being queried falls within the range of namespaces in the tree root, but there is no corresponding message, an absence proof will be generated. +An absence proof asserts that no message in the tree matches the queried namespace `NS`. The absence proof consists of: 1) The index of a leaf of the tree that - 1) its namespace ID is the smallest namespace ID larger than `nID` and - 2) the namespace ID of the leaf to the left of it is smaller than `nID`. + 1) its namespace is the smallest namespace larger than `NS` and + 2) the namespace of the leaf to the left of it is smaller than `NS`. 2) A regular Merkle inclusion proof for the said leaf to the tree root `T`. +3) The hash of the said leaf denoted by `LeafHash`. Note that the proof only requires the hash of the leaf, not its underlying message. -This is because the aim of the proof is to demonstrate the absence of `nID`. +This is because the aim of the proof is to demonstrate the absence of `NS`. -In Figure 1, if we query for `nID = 2`, we will receive an absence proof since there is no matching item for it. +In Figure 1, if we query for `NS = 2`, we will receive an absence proof since there is no matching item for it. The index of the leaf included in the proof will be `3`, which corresponds to the node `03 03 b4a2792`. The Merkle inclusion proof for this leaf consists of the following nodes, in the order they appear in an in-order traversal of the tree: `00 00 ead8d25` and `01 01 71ca46a`. #### Namespace Empty Proof -If the requested namespace ID falls outside the namespace range represented by the tree root `T`, the query will be resolved with an empty namespace proof. -In Figure 1, a query for `nID=6` would be responded by an empty proof as it falls outside the root's namsespace range. +If the requested namespace falls outside the namespace range represented by the tree root `T`, the query will be resolved with an empty namespace proof. +In Figure 1, a query for `NS=6` would be responded by an empty proof as it falls outside the root's namsespace range. ### Namespace Proof verification @@ -119,29 +120,29 @@ In Figure 1, a query for `nID=6` would be responded by an empty proof as it fall An NMT inclusion proof is deemed valid if it meets the following: -- The namespace ID of the leaves in the returned range `[start, end)` match the queried `nID`. +- The namespace of the leaves in the returned range `[start, end)` match the queried `NS`. - **Inclusion**: The supplied Merkle proof for the range `[start, end)` is valid for the given tree root `T`. -- **Completeness**: There are no other leaves matching `nID` that do not belong to the returned range `[start, end)`. +- **Completeness**: There are no other leaves matching `NS` that do not belong to the returned range `[start, end)`. Proof _inclusion_ can be verified via a regular Merkle range proof verification. However, _completeness_ of the proof requires additional checks. -Specifically, 1) the maximum namespace ID of the nodes in the proof that are on the left side of the branch connecting the `start` leaf to the root must be less than the provided namespace ID (`nID`), and 2) the minimum namespace ID of the nodes in the proof that are on the right side of the branch connecting the - `end-1` leaf to the root must be greater than the provided namespace ID (`nID`). +Specifically, 1) the maximum namespace of the nodes in the proof that are on the left side of the branch connecting the `start` leaf to the root must be less than the provided namespace (`NS`), and 2) the minimum namespace of the nodes in the proof that are on the right side of the branch connecting the + `end-1` leaf to the root must be greater than the provided namespace (`NS`). -As an example, the namespace proof for `nID = 0` for the NMT of Figure 1 (which consists of one single node i.e., `01 03 52c7c03`) is complete. This is because the node `01 03 52c7c03` is located on the right side of the branch connecting the leaf at index `end = 1` to the root and its `minNs` value is `01`, which is greater than `nID = 0`. +As an example, the namespace proof for `NS = 0` for the NMT of Figure 1 (which consists of one single node i.e., `01 03 52c7c03`) is complete. This is because the node `01 03 52c7c03` is located on the right side of the branch connecting the leaf at index `end = 1` to the root and its `minNs` value is `01`, which is greater than `NS = 0`. #### Verification of NMT Absence Proof An NMT absence proof is deemed valid if it meets the following: -1) The minimum namespace ID of the leaf in the proof is greater than the queried `nID`. +1) The minimum namespace of the leaf in the proof is greater than the queried `NS`. 2) The verification of Merkle inclusion proof of the returned leaf is valid. 3) It satisfies the proof completeness as explained in the [Verification of NMT Inclusion Proof](#verification-of-nmt-inclusion-proof). Note that hash of the leaf does not have to be verified against the underlying message. #### Verification of Empty NMT proof -If the queried `nID` falls outside the namespace range of the tree root, or the namespace tree is empty, then an empty NMT proof is valid by definition. +If the queried `NS` falls outside the namespace range of the tree root, or the namespace tree is empty, then an empty NMT proof is valid by definition. ### Index-based Merkle Range Proof @@ -152,10 +153,10 @@ As such, we only focus on the Merkle range proof in this section. The `start` and `end` are zero-based indices of the leaves in the tree, where `start` is inclusive and `end` is exclusive. The `start` ranges from `0` to `n-1`, where `n` is the number of leaves in the tree, while end ranges from `1` to `n`. -#### Index-based Merkle Range Proof Generation (for leaves with identical namespace IDs) +#### Index-based Merkle Range Proof Generation (for leaves with identical namespaces) A range query for a range of `[start, end)` is answered by a `proof` consisting of two pieces of data: `nodes` and `start`/`end`. -The proof generation and verification logic explained here is based on the assumption that all the leaves within the queried range have the same namespace ID. +The proof generation and verification logic explained here is based on the assumption that all the leaves within the queried range have the same namespace. The assumption is to conform to the current implementation of the [NMT library](https://github.com/celestiaorg/nmt), however, it is not a limitation imposed by the NMT data structure. - The `nodes` data is a set of tree nodes that constitutes the Merkle range proof of `[start, end)` to the tree with root T. @@ -167,16 +168,89 @@ This is the same as the queried range. - In the event that the queried range is outside the range of the NMT leaves indices, an empty `proof` is returned. This empty `proof` consists of an empty set of `nodes` and an empty range, where `start` is `0` and `end` is also `0`. -#### Index-based Merkle Range Proof Verification (for leaves with identical namespace IDs) +#### Index-based Merkle Range Proof Verification (for leaves with identical namespaces) Let `leaves` refer to the namespaced messages of the leaves in the queried range `[start, end)`, while `T` represents the root of the tree against which the `proof` is being verified. The `proof` can be verified by taking the following steps: -- Verify that all the leaves are namespaced messages with the same namespace ID. +- Verify that all the leaves are namespaced messages with the same namespace. - Compute the tree root `T'` using the leaves and the `proof.nodes`. If the computed root `T'` is equal to the expected root `T` of the tree, then the `proof` is valid. To compute the tree root `T'`, the [namespaced hash function](#namespaced-hash) should be utilized. +### Short Namespace Absence Proof + +The short namespace absence proof is a more efficient variant of the regular namespace absence proof. +It differs from the original namespace absence proof definition where instead of providing the inclusion proof of the `LeafHash` to the root (as `proof.nodes`), +a short absence proof supplies the Merkle inclusion proof of one of the predecessors of the `LeafHash`. +This predecessor is located along the branch connecting the `LeafHash` to the root. +Importantly, the namespace range of this predecessor does not overlap with the queried namespace i.e., the absent namespace. +As this predecessor is located closer to the root compared to the `LeafHash`, it will have shorter Merkle inclusion proof i.e., lower number of elements in the `proof.nodes`. + +At present, the NMT library does not support the generation of short namespace absence proofs. +However, it is capable of correctly verifying such proofs. + +#### Short Namespace Absence Proof Generation + +More formally, the short namespace absence proof consists of the following components: + +1) `SubtreeHash`: To compute the `SubtreeHash`, the following steps should be followed: + 1) Find the index of a leaf in the tree that meets two conditions: + 1) Its namespace is the smallest namespace greater than `NS`. + 1) The namespace of the leaf to its left is smaller than `NS`. + 1) Traverse up the branch connecting that leaf to the root and locate one of the parents/grandparents of that leaf whose namespace range does not overlap with the queried namespace. + The `SubtreeHash` is the hash of that node. +1) `start` and `end` range: These represent the indices of the `SubtreeHash` within its respective level. +Nodes at each level are indexed from left to right starting at index `0`. +1) `nodes`: This set comprises the index-based Merkle inclusion proof of the `SubtreeHash` to the tree root `T`. + +Below, we illustrate the short namespace absence proof for namespace `NS = 02` in an 8-leaf tree: +The namespace `03` is the smallest namespace larger than `02`. +By traversing the branch from the leaf with namespace `03` to the root, we find a node with hash `03 04 52c7c03` whose namespace range doesn't overlap with `02`. +This node is the highest such node along the branch. +The `SubtreeHash` is the hash of that node, which is `03 04 52c7c03`. +The `start` and `end` indices indicate its position in the respective level. +In this case, `start = 1` and `end = 2`. +Note that node indices start at `0` from left to right at each level. +The `nodes` form the index-based Merkle inclusion proof of the `SubtreeHash` to the tree root `T`. +The `nodes` set includes `00 00 ead8d25`, the left sibling of `03 04 52c7c03`. + +In summary, the short namespace absence proof for `NS = 02` in this tree consists of `SubtreeHash = 03 04 52c7c03`, `start = 1`, `end = 2`, and the `nodes` set containing `00 00 ead8d25`. + +```markdown + 00 04 b1c2cc5 Tree Root + / \ + / \ + NsH() NsH() + / \ + / \ + / ------------- + 00 00 ead8d25 |03 04 52c7c03| Non-Leaf Nodes + / \ ------------- + / \ / \ + NsH() NsH() NsH() NsH() + / \ / \ + 00 00 5fa0c9c 00 00 52385a0 03 03 71ca46a 04 04 b4a2792 Leaf Hashes + | | | | + NsH() NsH() NsH() NsH() + | | | | +00 6c6561665f30 00 6c6561665f31 03 6c6561665f32 04 6c6561665f33 Leaves with namespaces + + 0 1 2 3 Leaf Indices +``` + +#### Short Namespace Absence Proof Verification + +A short namespace absence proof is deemed valid if it meets the following: + +1) The minimum namespace of the `SubtreeHash` in the `proof` is greater than the queried `NS`. +It is important to note that there can be multiple candidates, each belonging to a different level of the tree, that satisfy this requirement for the `SubtreeHash`. +All such candidates are deemed valid from the verification perspective. +2) The verification of Merkle inclusion proof of the returned `SubtreeHash` is valid. +3) The `proof` satisfies the proof completeness as explained in the [Verification of NMT Inclusion Proof](#verification-of-nmt-inclusion-proof). +That is, all nodes on the left side of the branch connecting `SubtreeHash` to the root `T` have a maximum namespace less than the queried `NS`. +Conversely, all nodes on the right side are expected to have a minimum namespace larger than `NS`. + ## Resources 1. Al-Bassam, Mustafa. "Lazyledger: A distributed data availability ledger with client-side smart contracts." _arXiv preprint arXiv:1905.09274_ From 48b8f96a8764c19ea89ca076986f844cb29e8e9d Mon Sep 17 00:00:00 2001 From: Sanaz Taheri <35961250+staheri14@users.noreply.github.com> Date: Wed, 28 Jun 2023 14:36:00 -0700 Subject: [PATCH 4/7] doc: aligns the nmt API doc with the current implementation (#215) --- README.md | 5 ++++- docs/nmt-lib.md | 21 +++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7c2ab5a..20e937d 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,10 @@ func main() { } } // compute the root - root := tree.Root() + root, err := tree.Root() + if err != nil { + panic(fmt.Sprintf("unexpected error: %v", err)) + } // the root's min/max namespace is the min and max namespace of all leaves: minNS := nmt.MinNamespace(root, tree.NamespaceSize()) maxNS := nmt.MaxNamespace(root, tree.NamespaceSize()) diff --git a/docs/nmt-lib.md b/docs/nmt-lib.md index fbc7afc..557ef6f 100644 --- a/docs/nmt-lib.md +++ b/docs/nmt-lib.md @@ -67,22 +67,22 @@ func (n *NamespacedMerkleTree) Push(namespacedData namespace.PrefixedData) error E.g., ```go -d := append(namespace.ID{0}, []byte("leaf_0")...) // the first `tree.NamespaceSize()` bytes of each data item is treated as its namespace ID. +d := namespace.PrefixedData(append(namespace.ID{0}, []byte("leaf_0")...)) // the first `tree.NamespaceSize()` bytes of each data item is treated as its namespace. if err := tree.Push(d); err != nil { - // something went wrong +// something went wrong } // add a few more data items -d1 := append(namespace.ID{0}, []byte("leaf_1")...) +d1 := namespace.PrefixedData(append(namespace.ID{0}, []byte("leaf_1")...)) if err := tree.Push(d1); err != nil { - // something went wrong +// something went wrong } -d2 := append(namespace.ID{1}, []byte("leaf_2")...) +d2 := namespace.PrefixedData(append(namespace.ID{1}, []byte("leaf_2")...)) if err := tree.Push(d2); err != nil { - // something went wrong +// something went wrong } -d3 := append(namespace.ID{3}, []byte("leaf_3")...) +d3 := namespace.PrefixedData(append(namespace.ID{3}, []byte("leaf_3")...)) if err := tree.Push(d3); err != nil { - // something went wrong +// something went wrong } ``` @@ -116,16 +116,17 @@ Figure 1. ## Get Root The `Root()` method calculates the NMT root based on the data that has been added through the use of the `Push` method. +The root value is valid if the method does not return an error. ```go -func (n *NamespacedMerkleTree) Root() []byte +func (n *NamespacedMerkleTree) Root() ([]byte, error) ``` For example: ```go // compute the root -root := tree.Root() +root, err := tree.Root() ``` In the provided code example, the root would be `00 03 b1c2cc5` (as also illustrated in Figure 1). From 3693c9a626b7ddaae996b3c75602f5accfa45739 Mon Sep 17 00:00:00 2001 From: Sanaz Taheri <35961250+staheri14@users.noreply.github.com> Date: Tue, 4 Jul 2023 13:00:08 -0700 Subject: [PATCH 5/7] test: incorporates tests for short absence proof verification (#217) ## Overview After investigating the current implementation, verified that the nmt library is capable of verifying short absence proofs. This PR contains the necessary tests to demonstrate and test this capability. In line with #210 ## 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 --- proof.go | 27 ++++---- proof_test.go | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+), 14 deletions(-) diff --git a/proof.go b/proof.go index de307db..eea0730 100644 --- a/proof.go +++ b/proof.go @@ -146,23 +146,22 @@ func (proof Proof) IsEmptyProof() bool { } // 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 -// matching the namespace ID `nID` are within the range [`proof.start`, -// `proof.end`) and no data of that namespace was left out. VerifyNamespace -// deems an empty `proof` valid if the queried `nID` falls outside the namespace -// range of the supplied `root` or if the `root` is empty +// the provided `leaves` in the tree (or the proof.leafHash in case of +// full/short absence proof) 2) it verifies that the namespace is complete +// i.e., the data items matching the namespace `nID` are within the range +// [`proof.start`, `proof.end`) and no data of that namespace was left out. +// VerifyNamespace deems an empty `proof` valid if the queried `nID` falls +// outside the namespace range of the supplied `root` or if the `root` is empty // // `h` MUST be the same as the underlying hash function used to generate the // proof. Otherwise, the verification will fail. `nID` is the namespace ID for -// which the namespace `proof` is generated. `data` contains the namespaced data -// items (but not namespace hash) underlying the leaves of the tree in the -// range of [`proof.start`, `proof.end`). For an absence `proof`, the `data` is -// empty. `data` items MUST be ordered according to their index in the tree, -// with `data[0]` corresponding to the namespaced data at index `start`, -// -// and the last element in `data` corresponding to the data item at index -// `end-1` of the tree. +// which the namespace `proof` is generated. `leaves` contains the namespaced +// leaves of the tree in the range of [`proof.start`, `proof.end`). +// For an absence `proof`, the `leaves` is empty. +// `leaves` items MUST be ordered according to their index in the tree, +// with `leaves[0]` corresponding to the namespaced leaf at index `start`, +// and the last element in `leaves` corresponding to the leaf at index `end-1` +// of the tree. // // `root` is the root of the NMT against which the `proof` is verified. func (proof Proof) VerifyNamespace(h hash.Hash, nID namespace.ID, leaves [][]byte, root []byte) bool { diff --git a/proof_test.go b/proof_test.go index cb2cccc..9d308c3 100644 --- a/proof_test.go +++ b/proof_test.go @@ -707,3 +707,189 @@ func TestIsEmptyProofOverlapAbsenceProof(t *testing.T) { }) } } + +// TestVerifyNamespace_ShortAbsenceProof_Valid checks whether VerifyNamespace +// can correctly verify short namespace absence proofs +func TestVerifyNamespace_ShortAbsenceProof_Valid(t *testing.T) { + // create a Merkle tree with 8 leaves + tree := exampleNMT(1, true, 1, 2, 3, 4, 6, 7, 8, 9) + qNS := []byte{5} // does not belong to the tree + root, err := tree.Root() + assert.NoError(t, err) + // In the following illustration, nodes are suffixed with the range + // of leaves they cover, with the upper bound being non-inclusive. + // For example, Node3_4 denotes a node that covers the 3rd leaf (excluding the 4th leaf), + // while Node4_6 represents the node that covers the 4th and 5th leaves. + // + // Node0_8 Tree Root + // / \ + // / \ + // Node0_4 Node4_8 Non-Leaf Node + // / \ / \ + // / \ / \ + // Node0_2 Node2_4 Node4_6 Node6_8 Non-Leaf Node + // / \ / \ / \ / \ + // Node0_1 Node1_2 Node2_3 Node3_4 Node4_5 Node5_6 Node6_7 Node7_8 Leaf Hash + // 1 2 3 4 6 7 8 9 Leaf namespace + // 0 1 2 3 4 5 6 7 Leaf index + + // nodes needed for the full absence proof of qNS + Node4_5 := tree.leafHashes[4] + Node5_6 := tree.leafHashes[5] + Node6_8, err := tree.computeRoot(6, 8) + assert.NoError(t, err) + Node0_4, err := tree.computeRoot(0, 4) + assert.NoError(t, err) + + // nodes needed for the short absence proof of qNS; the proof of inclusion + // of the parent of Node4_5 + + Node4_6, err := tree.computeRoot(4, 6) + assert.NoError(t, err) + + // nodes needed for another short absence parent of qNS; the proof of + // inclusion of the grandparent of Node4_5 + Node4_8, err := tree.computeRoot(4, 8) + assert.NoError(t, err) + + tests := []struct { + name string + qNID []byte + leafHash []byte + nodes [][]byte + start int + end int + }{ + { + name: "valid full absence proof", + qNID: qNS, + leafHash: Node4_5, + nodes: [][]byte{Node0_4, Node5_6, Node6_8}, + start: 4, // the index position of leafHash at its respective level + end: 5, + }, + { + name: "valid short absence proof: one level higher", + qNID: qNS, + leafHash: Node4_6, + nodes: [][]byte{Node0_4, Node6_8}, + start: 2, // the index position of leafHash at its respective level + end: 3, + }, + { + name: "valid short absence proof: two levels higher", + qNID: qNS, + leafHash: Node4_8, + nodes: [][]byte{Node0_4}, + start: 1, // the index position of leafHash at its respective level + end: 2, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + proof := Proof{ + leafHash: tt.leafHash, + nodes: tt.nodes, + start: tt.start, + end: tt.end, + } + + res := proof.VerifyNamespace(sha256.New(), qNS, nil, root) + assert.True(t, res) + }) + } +} + +// TestVerifyNamespace_ShortAbsenceProof_Invalid checks whether VerifyNamespace rejects invalid short absence proofs. +func TestVerifyNamespace_ShortAbsenceProof_Invalid(t *testing.T) { + // create a Merkle tree with 8 leaves + tree := exampleNMT(1, true, 1, 2, 3, 4, 6, 8, 8, 8) + qNS := []byte{7} // does not belong to the tree + root, err := tree.Root() + assert.NoError(t, err) + // In the following illustration, nodes are suffixed with the range + // of leaves they cover, with the upper bound being non-inclusive. + // For example, Node3_4 denotes a node that covers the 3rd leaf (excluding the 4th leaf), + // while Node4_6 represents the node that covers the 4th and 5th leaves. + // + // Node0_8 Tree Root + // / \ + // / \ + // Node0_4 Node4_8 Non-Leaf Node + // / \ / \ + // / \ / \ + // Node0_2 Node2_4 Node4_6 Node6_8 Non-Leaf Node + // / \ / \ / \ / \ + // Node0_1 Node1_2 Node2_3 Node3_4 Node4_5 Node5_6 Node6_7 Node7_8 Leaf Hash + // 1 2 3 4 6 8 8 8 Leaf namespace + // 0 1 2 3 4 5 6 7 Leaf index + + // nodes needed for the full absence proof of qNS + Node5_6 := tree.leafHashes[5] + Node4_5 := tree.leafHashes[4] + Node6_8, err := tree.computeRoot(6, 8) + assert.NoError(t, err) + Node0_4, err := tree.computeRoot(0, 4) + assert.NoError(t, err) + + // nodes needed for the short absence proof of qNS; the proof of inclusion of the parent of Node5_6; + // the verification should fail since the namespace range o Node4_6, the parent, has overlap with the qNS i.e., 7 + Node4_6, err := tree.computeRoot(4, 6) + assert.NoError(t, err) + + // nodes needed for another short absence parent of qNS; the proof of inclusion of the grandparent of Node5_6 + // the verification should fail since the namespace range of Node4_8, the grandparent, has overlap with the qNS i.e., 7 + Node4_8, err := tree.computeRoot(4, 8) + assert.NoError(t, err) + + tests := []struct { + name string + qNID []byte + leafHash []byte + nodes [][]byte + start int + end int + want bool + }{ + { + name: "valid full absence proof", + qNID: qNS, + leafHash: Node5_6, + nodes: [][]byte{Node0_4, Node4_5, Node6_8}, + start: 5, // the index position of leafHash at its respective level + end: 6, + want: true, + }, + { + name: "invalid short absence proof: one level higher", + qNID: qNS, + leafHash: Node4_6, + nodes: [][]byte{Node0_4, Node6_8}, + start: 2, // the index position of leafHash at its respective level + end: 3, + want: false, + }, + { + name: "invalid short absence proof: two levels higher", + qNID: qNS, + leafHash: Node4_8, + nodes: [][]byte{Node0_4}, + start: 1, // the index position of leafHash at its respective level + end: 2, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + proof := Proof{ + leafHash: tt.leafHash, + nodes: tt.nodes, + start: tt.start, + end: tt.end, + } + + res := proof.VerifyNamespace(sha256.New(), qNS, nil, root) + assert.Equal(t, tt.want, res) + }) + } +} From 9d22de935d750c5af847be633a32d576867d36ca Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Wed, 19 Jul 2023 17:07:15 +0300 Subject: [PATCH 6/7] feat(proof): create a proto representation of the proof (#220) ## Overview Added and generated a proto representation of the Proof ## 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 --------- Co-authored-by: Rootul P --- go.mod | 3 +- go.sum | 31 ++++ pb/proof.pb.go | 493 +++++++++++++++++++++++++++++++++++++++++++++++++ pb/proof.proto | 20 ++ proof.go | 25 +++ proof_test.go | 64 +++++++ 6 files changed, 635 insertions(+), 1 deletion(-) create mode 100644 pb/proof.pb.go create mode 100644 pb/proof.proto diff --git a/go.mod b/go.mod index 54d7bab..b3b469b 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,15 @@ module github.com/celestiaorg/nmt go 1.19 require ( + github.com/gogo/protobuf v1.3.2 github.com/google/gofuzz v1.2.0 github.com/stretchr/testify v1.8.1 + github.com/tidwall/gjson v1.14.4 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index f8a5bba..5bf4373 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,12 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -19,6 +23,33 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pb/proof.pb.go b/pb/proof.pb.go new file mode 100644 index 0000000..ce0ab54 --- /dev/null +++ b/pb/proof.pb.go @@ -0,0 +1,493 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: pb/proof.proto + +package proof_pb + +import ( + fmt "fmt" + proto "github.com/gogo/protobuf/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type Proof struct { + // Start index of the leaves that match the queried namespace.ID. + Start int64 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"` + // End index (non-inclusive) of the leaves that match the queried + // namespace.ID. + End int64 `protobuf:"varint,2,opt,name=end,proto3" json:"end,omitempty"` + // Nodes hold the tree nodes necessary for the Merkle range proof. + Nodes [][]byte `protobuf:"bytes,3,rep,name=nodes,proto3" json:"nodes,omitempty"` + // LeafHash contains the namespace.ID if NMT does not have it and + // it should be proven. leafHash is necessary to prove the Absence Proof. + // This field will be empty in case of Inclusion Proof. + LeafHash []byte `protobuf:"bytes,4,opt,name=leafHash,proto3" json:"leafHash,omitempty"` + // The isMaxNamespaceIgnored flag influences the calculation of the + // namespace ID range for intermediate nodes in the tree + IsMaxNamespaceIgnored bool `protobuf:"varint,5,opt,name=isMaxNamespaceIgnored,proto3" json:"isMaxNamespaceIgnored,omitempty"` +} + +func (m *Proof) Reset() { *m = Proof{} } +func (m *Proof) String() string { return proto.CompactTextString(m) } +func (*Proof) ProtoMessage() {} +func (*Proof) Descriptor() ([]byte, []int) { + return fileDescriptor_2e2daa763cd7daf3, []int{0} +} +func (m *Proof) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Proof) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Proof.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Proof) XXX_Merge(src proto.Message) { + xxx_messageInfo_Proof.Merge(m, src) +} +func (m *Proof) XXX_Size() int { + return m.Size() +} +func (m *Proof) XXX_DiscardUnknown() { + xxx_messageInfo_Proof.DiscardUnknown(m) +} + +var xxx_messageInfo_Proof proto.InternalMessageInfo + +func (m *Proof) GetStart() int64 { + if m != nil { + return m.Start + } + return 0 +} + +func (m *Proof) GetEnd() int64 { + if m != nil { + return m.End + } + return 0 +} + +func (m *Proof) GetNodes() [][]byte { + if m != nil { + return m.Nodes + } + return nil +} + +func (m *Proof) GetLeafHash() []byte { + if m != nil { + return m.LeafHash + } + return nil +} + +func (m *Proof) GetIsMaxNamespaceIgnored() bool { + if m != nil { + return m.IsMaxNamespaceIgnored + } + return false +} + +func init() { + proto.RegisterType((*Proof)(nil), "proof.pb.Proof") +} + +func init() { proto.RegisterFile("pb/proof.proto", fileDescriptor_2e2daa763cd7daf3) } + +var fileDescriptor_2e2daa763cd7daf3 = []byte{ + // 186 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2b, 0x48, 0xd2, 0x2f, + 0x28, 0xca, 0xcf, 0x4f, 0xd3, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x80, 0x72, 0x92, 0x94, + 0xa6, 0x33, 0x72, 0xb1, 0x06, 0x80, 0x38, 0x42, 0x22, 0x5c, 0xac, 0xc5, 0x25, 0x89, 0x45, 0x25, + 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0xcc, 0x41, 0x10, 0x8e, 0x90, 0x00, 0x17, 0x73, 0x6a, 0x5e, 0x8a, + 0x04, 0x13, 0x58, 0x0c, 0xc4, 0x04, 0xa9, 0xcb, 0xcb, 0x4f, 0x49, 0x2d, 0x96, 0x60, 0x56, 0x60, + 0xd6, 0xe0, 0x09, 0x82, 0x70, 0x84, 0xa4, 0xb8, 0x38, 0x72, 0x52, 0x13, 0xd3, 0x3c, 0x12, 0x8b, + 0x33, 0x24, 0x58, 0x14, 0x18, 0x35, 0x78, 0x82, 0xe0, 0x7c, 0x21, 0x13, 0x2e, 0xd1, 0xcc, 0x62, + 0xdf, 0xc4, 0x0a, 0xbf, 0xc4, 0xdc, 0xd4, 0xe2, 0x82, 0xc4, 0xe4, 0x54, 0xcf, 0xf4, 0xbc, 0xfc, + 0xa2, 0xd4, 0x14, 0x09, 0x56, 0x05, 0x46, 0x0d, 0x8e, 0x20, 0xec, 0x92, 0x4e, 0x12, 0x27, 0x1e, + 0xc9, 0x31, 0x5e, 0x78, 0x24, 0xc7, 0xf8, 0xe0, 0x91, 0x1c, 0xe3, 0x84, 0xc7, 0x72, 0x0c, 0x17, + 0x1e, 0xcb, 0x31, 0xdc, 0x78, 0x2c, 0xc7, 0x90, 0xc4, 0x06, 0xf6, 0x84, 0x31, 0x20, 0x00, 0x00, + 0xff, 0xff, 0x1b, 0x93, 0x09, 0xff, 0xd6, 0x00, 0x00, 0x00, +} + +func (m *Proof) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Proof) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Proof) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.IsMaxNamespaceIgnored { + i-- + if m.IsMaxNamespaceIgnored { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x28 + } + if len(m.LeafHash) > 0 { + i -= len(m.LeafHash) + copy(dAtA[i:], m.LeafHash) + i = encodeVarintProof(dAtA, i, uint64(len(m.LeafHash))) + i-- + dAtA[i] = 0x22 + } + if len(m.Nodes) > 0 { + for iNdEx := len(m.Nodes) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.Nodes[iNdEx]) + copy(dAtA[i:], m.Nodes[iNdEx]) + i = encodeVarintProof(dAtA, i, uint64(len(m.Nodes[iNdEx]))) + i-- + dAtA[i] = 0x1a + } + } + if m.End != 0 { + i = encodeVarintProof(dAtA, i, uint64(m.End)) + i-- + dAtA[i] = 0x10 + } + if m.Start != 0 { + i = encodeVarintProof(dAtA, i, uint64(m.Start)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func encodeVarintProof(dAtA []byte, offset int, v uint64) int { + offset -= sovProof(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *Proof) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Start != 0 { + n += 1 + sovProof(uint64(m.Start)) + } + if m.End != 0 { + n += 1 + sovProof(uint64(m.End)) + } + if len(m.Nodes) > 0 { + for _, b := range m.Nodes { + l = len(b) + n += 1 + l + sovProof(uint64(l)) + } + } + l = len(m.LeafHash) + if l > 0 { + n += 1 + l + sovProof(uint64(l)) + } + if m.IsMaxNamespaceIgnored { + n += 2 + } + return n +} + +func sovProof(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozProof(x uint64) (n int) { + return sovProof(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *Proof) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProof + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Proof: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Proof: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Start", wireType) + } + m.Start = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProof + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Start |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field End", wireType) + } + m.End = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProof + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.End |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Nodes", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProof + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthProof + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthProof + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Nodes = append(m.Nodes, make([]byte, postIndex-iNdEx)) + copy(m.Nodes[len(m.Nodes)-1], dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LeafHash", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProof + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthProof + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthProof + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.LeafHash = append(m.LeafHash[:0], dAtA[iNdEx:postIndex]...) + if m.LeafHash == nil { + m.LeafHash = []byte{} + } + iNdEx = postIndex + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field IsMaxNamespaceIgnored", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProof + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.IsMaxNamespaceIgnored = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := skipProof(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthProof + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipProof(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowProof + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowProof + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowProof + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthProof + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupProof + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthProof + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthProof = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowProof = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupProof = fmt.Errorf("proto: unexpected end of group") +) diff --git a/pb/proof.proto b/pb/proof.proto new file mode 100644 index 0000000..e1db55d --- /dev/null +++ b/pb/proof.proto @@ -0,0 +1,20 @@ +syntax="proto3"; + +package proof.pb; + +message Proof { + // Start index of the leaves that match the queried namespace.ID. + int64 start = 1; + // End index (non-inclusive) of the leaves that match the queried + // namespace.ID. + int64 end = 2; + // Nodes hold the tree nodes necessary for the Merkle range proof. + repeated bytes nodes = 3; + // LeafHash contains the namespace.ID if NMT does not have it and + // it should be proven. LeafHash is necessary to prove the Absence Proof. + // This field will be empty in case of Inclusion Proof. + bytes leafHash = 4; + // The isMaxNamespaceIgnored flag influences the calculation of the + // namespace ID range for intermediate nodes in the tree. + bool isMaxNamespaceIgnored=5; +} \ No newline at end of file diff --git a/proof.go b/proof.go index eea0730..a6a6085 100644 --- a/proof.go +++ b/proof.go @@ -9,6 +9,7 @@ import ( "math/bits" "github.com/celestiaorg/nmt/namespace" + pb "github.com/celestiaorg/nmt/pb" ) // ErrFailedCompletenessCheck indicates that the verification of a namespace proof failed due to the lack of completeness property. @@ -453,6 +454,30 @@ func (proof Proof) VerifyInclusion(h hash.Hash, nid namespace.ID, leavesWithoutN return res } +// ProtoToProof creates a proof from its proto representation. +func ProtoToProof(protoProof pb.Proof) Proof { + if protoProof.Start == 0 && protoProof.End == 0 { + return NewEmptyRangeProof(protoProof.IsMaxNamespaceIgnored) + } + + if len(protoProof.LeafHash) > 0 { + return NewAbsenceProof( + int(protoProof.Start), + int(protoProof.End), + protoProof.Nodes, + protoProof.LeafHash, + protoProof.IsMaxNamespaceIgnored, + ) + } + + return NewInclusionProof( + int(protoProof.Start), + int(protoProof.End), + protoProof.Nodes, + protoProof.IsMaxNamespaceIgnored, + ) +} + // nextSubtreeSize returns the number of leaves of the subtree adjacent to start // that does not overlap end. func nextSubtreeSize(start, end uint64) int { diff --git a/proof_test.go b/proof_test.go index 9d308c3..2dba96f 100644 --- a/proof_test.go +++ b/proof_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/celestiaorg/nmt/namespace" + pb "github.com/celestiaorg/nmt/pb" ) func TestJsonMarshal_Proof(t *testing.T) { @@ -893,3 +894,66 @@ func TestVerifyNamespace_ShortAbsenceProof_Invalid(t *testing.T) { }) } } + +func Test_ProtoToProof(t *testing.T) { + verifier := func(t *testing.T, proof Proof, protoProof pb.Proof) { + require.Equal(t, int64(proof.Start()), protoProof.Start) + require.Equal(t, int64(proof.End()), protoProof.End) + require.Equal(t, proof.Nodes(), protoProof.Nodes) + require.Equal(t, proof.LeafHash(), protoProof.LeafHash) + require.Equal(t, proof.IsMaxNamespaceIDIgnored(), protoProof.IsMaxNamespaceIgnored) + } + + tests := []struct { + name string + protoProof pb.Proof + verifyFn func(t *testing.T, proof Proof, protoProof pb.Proof) + }{ + { + name: "Inclusion proof", + protoProof: pb.Proof{ + Start: 0, + End: 1, + Nodes: [][]byte{bytes.Repeat([]byte{1}, 10)}, + LeafHash: nil, + IsMaxNamespaceIgnored: true, + }, + verifyFn: verifier, + }, + { + name: "Absence Proof", + protoProof: pb.Proof{ + Start: 0, + End: 1, + Nodes: [][]byte{bytes.Repeat([]byte{1}, 10)}, + LeafHash: bytes.Repeat([]byte{1}, 10), + IsMaxNamespaceIgnored: true, + }, + verifyFn: verifier, + }, + { + name: "Empty Proof", + protoProof: pb.Proof{ + Start: 0, + End: 0, + Nodes: [][]byte{bytes.Repeat([]byte{1}, 10)}, + LeafHash: nil, + IsMaxNamespaceIgnored: true, + }, + verifyFn: func(t *testing.T, proof Proof, protoProof pb.Proof) { + require.Equal(t, proof.Start(), 0) + require.Equal(t, proof.End(), 0) + require.Nil(t, proof.Nodes()) + require.Nil(t, proof.LeafHash()) + require.Equal(t, proof.IsMaxNamespaceIDIgnored(), protoProof.IsMaxNamespaceIgnored) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + proof := ProtoToProof(tt.protoProof) + tt.verifyFn(t, proof, tt.protoProof) + }) + } +} From 33110363c1d9bc966c1a6f22fd6c35ff41f00b31 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Mon, 24 Jul 2023 17:25:45 +0300 Subject: [PATCH 7/7] fix: add go option in proto to be able to import pb.Proof (#226) --- pb/proof.pb.go | 16 +++++++++------- pb/proof.proto | 2 ++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pb/proof.pb.go b/pb/proof.pb.go index ce0ab54..ad2e097 100644 --- a/pb/proof.pb.go +++ b/pb/proof.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gogo. DO NOT EDIT. // source: pb/proof.proto -package proof_pb +package pb import ( fmt "fmt" @@ -31,11 +31,11 @@ type Proof struct { // Nodes hold the tree nodes necessary for the Merkle range proof. Nodes [][]byte `protobuf:"bytes,3,rep,name=nodes,proto3" json:"nodes,omitempty"` // LeafHash contains the namespace.ID if NMT does not have it and - // it should be proven. leafHash is necessary to prove the Absence Proof. + // it should be proven. LeafHash is necessary to prove the Absence Proof. // This field will be empty in case of Inclusion Proof. LeafHash []byte `protobuf:"bytes,4,opt,name=leafHash,proto3" json:"leafHash,omitempty"` // The isMaxNamespaceIgnored flag influences the calculation of the - // namespace ID range for intermediate nodes in the tree + // namespace ID range for intermediate nodes in the tree. IsMaxNamespaceIgnored bool `protobuf:"varint,5,opt,name=isMaxNamespaceIgnored,proto3" json:"isMaxNamespaceIgnored,omitempty"` } @@ -114,7 +114,7 @@ func init() { func init() { proto.RegisterFile("pb/proof.proto", fileDescriptor_2e2daa763cd7daf3) } var fileDescriptor_2e2daa763cd7daf3 = []byte{ - // 186 bytes of a gzipped FileDescriptorProto + // 217 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2b, 0x48, 0xd2, 0x2f, 0x28, 0xca, 0xcf, 0x4f, 0xd3, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x80, 0x72, 0x92, 0x94, 0xa6, 0x33, 0x72, 0xb1, 0x06, 0x80, 0x38, 0x42, 0x22, 0x5c, 0xac, 0xc5, 0x25, 0x89, 0x45, 0x25, @@ -123,10 +123,12 @@ var fileDescriptor_2e2daa763cd7daf3 = []byte{ 0xd6, 0xe0, 0x09, 0x82, 0x70, 0x84, 0xa4, 0xb8, 0x38, 0x72, 0x52, 0x13, 0xd3, 0x3c, 0x12, 0x8b, 0x33, 0x24, 0x58, 0x14, 0x18, 0x35, 0x78, 0x82, 0xe0, 0x7c, 0x21, 0x13, 0x2e, 0xd1, 0xcc, 0x62, 0xdf, 0xc4, 0x0a, 0xbf, 0xc4, 0xdc, 0xd4, 0xe2, 0x82, 0xc4, 0xe4, 0x54, 0xcf, 0xf4, 0xbc, 0xfc, - 0xa2, 0xd4, 0x14, 0x09, 0x56, 0x05, 0x46, 0x0d, 0x8e, 0x20, 0xec, 0x92, 0x4e, 0x12, 0x27, 0x1e, + 0xa2, 0xd4, 0x14, 0x09, 0x56, 0x05, 0x46, 0x0d, 0x8e, 0x20, 0xec, 0x92, 0x4e, 0xe6, 0x27, 0x1e, 0xc9, 0x31, 0x5e, 0x78, 0x24, 0xc7, 0xf8, 0xe0, 0x91, 0x1c, 0xe3, 0x84, 0xc7, 0x72, 0x0c, 0x17, - 0x1e, 0xcb, 0x31, 0xdc, 0x78, 0x2c, 0xc7, 0x90, 0xc4, 0x06, 0xf6, 0x84, 0x31, 0x20, 0x00, 0x00, - 0xff, 0xff, 0x1b, 0x93, 0x09, 0xff, 0xd6, 0x00, 0x00, 0x00, + 0x1e, 0xcb, 0x31, 0xdc, 0x78, 0x2c, 0xc7, 0x10, 0x25, 0x9b, 0x9e, 0x59, 0x92, 0x51, 0x9a, 0xa4, + 0x97, 0x9c, 0x9f, 0xab, 0x9f, 0x9c, 0x9a, 0x93, 0x5a, 0x5c, 0x92, 0x99, 0x98, 0x5f, 0x94, 0xae, + 0x9f, 0x97, 0x5b, 0xa2, 0x5f, 0x90, 0x94, 0xc4, 0x06, 0xf6, 0xa3, 0x31, 0x20, 0x00, 0x00, 0xff, + 0xff, 0x64, 0x62, 0x02, 0x2f, 0xf5, 0x00, 0x00, 0x00, } func (m *Proof) Marshal() (dAtA []byte, err error) { diff --git a/pb/proof.proto b/pb/proof.proto index e1db55d..45bd337 100644 --- a/pb/proof.proto +++ b/pb/proof.proto @@ -2,6 +2,8 @@ syntax="proto3"; package proof.pb; +option go_package = "github.com/celestiaorg/nmt/pb"; + message Proof { // Start index of the leaves that match the queried namespace.ID. int64 start = 1;