github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/ledger/complete/mtrie/node/node.go (about)

     1  package node
     2  
     3  import (
     4  	"encoding/hex"
     5  	"fmt"
     6  
     7  	"github.com/onflow/flow-go/ledger"
     8  	"github.com/onflow/flow-go/ledger/common/hash"
     9  )
    10  
    11  // Node defines an Mtrie node
    12  //
    13  // DEFINITIONS:
    14  //   - HEIGHT of a node v in a tree is the number of edges on the longest
    15  //     downward path between v and a tree leaf.
    16  //
    17  // Conceptually, an MTrie is a sparse Merkle Trie, which has two node types:
    18  //   - INTERIM node: has at least one child (i.e. lChild or rChild is not
    19  //     nil). Interim nodes do not store a path and have no payload.
    20  //   - LEAF node: has _no_ children.
    21  //
    22  // Per convention, we also consider nil as a leaf. Formally, nil is the generic
    23  // representative for any empty (sub)-trie (i.e. a trie without allocated
    24  // registers).
    25  //
    26  // Nodes are supposed to be treated as _immutable_ data structures.
    27  // TODO: optimized data structures might be able to reduce memory consumption
    28  type Node struct {
    29  	// Implementation Comments:
    30  	// Formally, a tree can hold up to 2^maxDepth number of registers. However,
    31  	// the current implementation is designed to operate on a sparsely populated
    32  	// tree, holding much less than 2^64 registers.
    33  
    34  	lChild    *Node           // Left Child
    35  	rChild    *Node           // Right Child
    36  	height    int             // height where the Node is at
    37  	path      ledger.Path     // the storage path (dummy value for interim nodes)
    38  	payload   *ledger.Payload // the payload this node is storing (leaf nodes only)
    39  	hashValue hash.Hash       // hash value of node (cached)
    40  }
    41  
    42  // NewNode creates a new Node.
    43  // UNCHECKED requirement: combination of values must conform to
    44  // a valid node type (see documentation of `Node` for details)
    45  func NewNode(height int,
    46  	lchild,
    47  	rchild *Node,
    48  	path ledger.Path,
    49  	payload *ledger.Payload,
    50  	hashValue hash.Hash,
    51  ) *Node {
    52  	n := &Node{
    53  		lChild:    lchild,
    54  		rChild:    rchild,
    55  		height:    height,
    56  		path:      path,
    57  		hashValue: hashValue,
    58  		payload:   payload,
    59  	}
    60  	return n
    61  }
    62  
    63  // NewLeaf creates a compact leaf Node.
    64  // UNCHECKED requirement: height must be non-negative
    65  // UNCHECKED requirement: payload is non nil
    66  // UNCHECKED requirement: payload should be deep copied if received from external sources
    67  func NewLeaf(path ledger.Path,
    68  	payload *ledger.Payload,
    69  	height int,
    70  ) *Node {
    71  	n := &Node{
    72  		lChild:  nil,
    73  		rChild:  nil,
    74  		height:  height,
    75  		path:    path,
    76  		payload: payload,
    77  	}
    78  	n.hashValue = n.computeHash()
    79  	return n
    80  }
    81  
    82  // NewInterimNode creates a new interim Node.
    83  // UNCHECKED requirement:
    84  //   - for any child `c` that is non-nil, its height must satisfy: height = c.height + 1
    85  func NewInterimNode(height int, lchild, rchild *Node) *Node {
    86  	n := &Node{
    87  		lChild:  lchild,
    88  		rChild:  rchild,
    89  		height:  height,
    90  		payload: nil,
    91  	}
    92  	n.hashValue = n.computeHash()
    93  	return n
    94  }
    95  
    96  // NewInterimCompactifiedNode creates a new compactified interim Node. For compactification,
    97  // we only consider the immediate children. When starting with a maximally pruned trie and
    98  // creating only InterimCompactifiedNodes during an update, the resulting trie remains maximally
    99  // pruned. Details on compactification:
   100  //   - If _both_ immediate children represent completely unallocated sub-tries, then the sub-trie
   101  //     with the new interim node is also completely empty. We return nil.
   102  //   - If either child is a leaf (i.e. representing a single allocated register) _and_ the other
   103  //     child represents a completely unallocated sub-trie, the new interim node also only holds
   104  //     a single allocated register. In this case, we return a compactified leaf.
   105  //
   106  // UNCHECKED requirement:
   107  //   - for any child `c` that is non-nil, its height must satisfy: height = c.height + 1
   108  func NewInterimCompactifiedNode(height int, lChild, rChild *Node) *Node {
   109  	if lChild.IsDefaultNode() {
   110  		lChild = nil
   111  	}
   112  	if rChild.IsDefaultNode() {
   113  		rChild = nil
   114  	}
   115  
   116  	// CASE (a): _both_ children do _not_ contain any allocated registers:
   117  	if lChild == nil && rChild == nil {
   118  		return nil // return nil representing as completely empty sub-trie
   119  	}
   120  
   121  	// CASE (b): one child is a compactified leaf (single allocated register) _and_ the other child represents
   122  	// an empty subtrie => in total we have one allocated register, which we represent as single leaf node
   123  	if rChild == nil && lChild.IsLeaf() {
   124  		h := hash.HashInterNode(lChild.hashValue, ledger.GetDefaultHashForHeight(lChild.height))
   125  		return &Node{height: height, path: lChild.path, payload: lChild.payload, hashValue: h}
   126  	}
   127  	if lChild == nil && rChild.IsLeaf() {
   128  		h := hash.HashInterNode(ledger.GetDefaultHashForHeight(rChild.height), rChild.hashValue)
   129  		return &Node{height: height, path: rChild.path, payload: rChild.payload, hashValue: h}
   130  	}
   131  
   132  	// CASE (b): both children contain some allocated registers => we can't compactify; return a full interim leaf
   133  	return NewInterimNode(height, lChild, rChild)
   134  }
   135  
   136  // IsDefaultNode returns true iff the sub-trie represented by this root node contains
   137  // only unallocated registers. This is the case, if the node is nil or the node's hash
   138  // is equal to the default hash value at the respective height.
   139  func (n *Node) IsDefaultNode() bool {
   140  	if n == nil {
   141  		return true
   142  	}
   143  	return n.hashValue == ledger.GetDefaultHashForHeight(n.height)
   144  }
   145  
   146  // computeHash returns the hashValue of the node
   147  func (n *Node) computeHash() hash.Hash {
   148  	// check for leaf node
   149  	if n.lChild == nil && n.rChild == nil {
   150  		// if payload is non-nil, compute the hash based on the payload content
   151  		if n.payload != nil {
   152  			return ledger.ComputeCompactValue(hash.Hash(n.path), n.payload.Value(), n.height)
   153  		}
   154  		// if payload is nil, return the default hash
   155  		return ledger.GetDefaultHashForHeight(n.height)
   156  	}
   157  
   158  	// this is an interim node at least one of lChild or rChild is not nil.
   159  	var h1, h2 hash.Hash
   160  	if n.lChild != nil {
   161  		h1 = n.lChild.Hash()
   162  	} else {
   163  		h1 = ledger.GetDefaultHashForHeight(n.height - 1)
   164  	}
   165  
   166  	if n.rChild != nil {
   167  		h2 = n.rChild.Hash()
   168  	} else {
   169  		h2 = ledger.GetDefaultHashForHeight(n.height - 1)
   170  	}
   171  	return hash.HashInterNode(h1, h2)
   172  }
   173  
   174  // VerifyCachedHash verifies the hash of a node is valid
   175  func verifyCachedHashRecursive(n *Node) bool {
   176  	if n == nil {
   177  		return true
   178  	}
   179  	if !verifyCachedHashRecursive(n.lChild) || !verifyCachedHashRecursive(n.rChild) {
   180  		return false
   181  	}
   182  
   183  	computedHash := n.computeHash()
   184  	return n.hashValue == computedHash
   185  }
   186  
   187  // VerifyCachedHash verifies the hash of a node is valid
   188  func (n *Node) VerifyCachedHash() bool {
   189  	return verifyCachedHashRecursive(n)
   190  }
   191  
   192  // Hash returns the Node's hash value.
   193  // Do NOT MODIFY returned slice!
   194  func (n *Node) Hash() hash.Hash {
   195  	return n.hashValue
   196  }
   197  
   198  // Height returns the Node's height.
   199  // Per definition, the height of a node v in a tree is the number
   200  // of edges on the longest downward path between v and a tree leaf.
   201  func (n *Node) Height() int {
   202  	return n.height
   203  }
   204  
   205  // Path returns a pointer to the Node's register storage path.
   206  // If the node is not a leaf, the function returns `nil`.
   207  func (n *Node) Path() *ledger.Path {
   208  	if n.IsLeaf() {
   209  		return &n.path
   210  	}
   211  	return nil
   212  }
   213  
   214  // Payload returns the the Node's payload.
   215  // Do NOT MODIFY returned slices!
   216  func (n *Node) Payload() *ledger.Payload {
   217  	return n.payload
   218  }
   219  
   220  // LeftChild returns the the Node's left child.
   221  // Only INTERIM nodes have children.
   222  // Do NOT MODIFY returned Node!
   223  func (n *Node) LeftChild() *Node { return n.lChild }
   224  
   225  // RightChild returns the the Node's right child.
   226  // Only INTERIM nodes have children.
   227  // Do NOT MODIFY returned Node!
   228  func (n *Node) RightChild() *Node { return n.rChild }
   229  
   230  // IsLeaf returns true if and only if Node is a LEAF.
   231  func (n *Node) IsLeaf() bool {
   232  	// Per definition, a node is a leaf if and only it has no children
   233  	return n == nil || (n.lChild == nil && n.rChild == nil)
   234  }
   235  
   236  // FmtStr provides formatted string representation of the Node and sub tree
   237  func (n *Node) FmtStr(prefix string, subpath string) string {
   238  	right := ""
   239  	if n.rChild != nil {
   240  		right = fmt.Sprintf("\n%v", n.rChild.FmtStr(prefix+"\t", subpath+"1"))
   241  	}
   242  	left := ""
   243  	if n.lChild != nil {
   244  		left = fmt.Sprintf("\n%v", n.lChild.FmtStr(prefix+"\t", subpath+"0"))
   245  	}
   246  	payloadSize := 0
   247  	if n.payload != nil {
   248  		payloadSize = n.payload.Size()
   249  	}
   250  	hashStr := hex.EncodeToString(n.hashValue[:])
   251  	hashStr = hashStr[:3] + "..." + hashStr[len(hashStr)-3:]
   252  	return fmt.Sprintf("%v%v: (path:%v, payloadSize:%d hash:%v)[%s] (obj %p) %v %v ", prefix, n.height, n.path, payloadSize, hashStr, subpath, n, left, right)
   253  }
   254  
   255  // AllPayloads returns the payload of this node and all payloads of the subtrie
   256  func (n *Node) AllPayloads() []*ledger.Payload {
   257  	return n.appendSubtreePayloads([]*ledger.Payload{})
   258  }
   259  
   260  // appendSubtreePayloads appends the payloads of the subtree with this node as root
   261  // to the provided Payload slice. Follows same pattern as Go's native append method.
   262  func (n *Node) appendSubtreePayloads(result []*ledger.Payload) []*ledger.Payload {
   263  	if n == nil {
   264  		return result
   265  	}
   266  	if n.IsLeaf() {
   267  		return append(result, n.Payload())
   268  	}
   269  	result = n.lChild.appendSubtreePayloads(result)
   270  	result = n.rChild.appendSubtreePayloads(result)
   271  	return result
   272  }