certspotter/merkletree/collapsed_tree.go

183 lines
5.1 KiB
Go
Raw Normal View History

2023-02-03 19:58:22 +01:00
// Copyright (C) 2022 Opsmate, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License, v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
// See the Mozilla Public License for details.
package merkletree
import (
"encoding/json"
"fmt"
"math/bits"
2023-09-01 16:56:05 +02:00
"slices"
2023-02-03 19:58:22 +01:00
)
// CollapsedTree is an efficient representation of a Merkle (sub)tree that permits appending
// nodes and calculating the root hash.
2023-02-03 19:58:22 +01:00
type CollapsedTree struct {
offset uint64
nodes []Hash
size uint64
2023-02-03 19:58:22 +01:00
}
func calculateNumNodes(size uint64) int {
return bits.OnesCount64(size)
2023-02-03 19:58:22 +01:00
}
2024-06-12 17:21:58 +02:00
// TODO: phase out this function
2023-02-03 19:58:22 +01:00
func EmptyCollapsedTree() *CollapsedTree {
return &CollapsedTree{nodes: []Hash{}, size: 0}
}
2024-06-12 17:21:58 +02:00
// TODO: phase out this function
2023-02-03 19:58:22 +01:00
func NewCollapsedTree(nodes []Hash, size uint64) (*CollapsedTree, error) {
tree := new(CollapsedTree)
if err := tree.Init(nodes, size); err != nil {
return nil, err
2023-02-03 19:58:22 +01:00
}
return tree, nil
2023-02-03 19:58:22 +01:00
}
2023-08-30 17:53:31 +02:00
func (tree CollapsedTree) Equal(other CollapsedTree) bool {
return tree.offset == other.offset && tree.size == other.size && slices.Equal(tree.nodes, other.nodes)
2023-08-30 17:53:31 +02:00
}
2024-06-12 17:21:58 +02:00
func (tree CollapsedTree) Clone() CollapsedTree {
return CollapsedTree{
offset: tree.offset,
nodes: slices.Clone(tree.nodes),
size: tree.size,
2024-06-12 17:21:58 +02:00
}
}
// Add a new leaf hash to the end of the tree.
// Returns an error if and only if the new tree would be too large for the subtree offset.
// Always returns a nil error if tree.Offset() == 0.
func (tree *CollapsedTree) Add(hash Hash) error {
if tree.offset > 0 {
maxSize := uint64(1) << bits.TrailingZeros64(tree.offset)
if tree.size+1 > maxSize {
return fmt.Errorf("subtree at offset %d is already at maximum size %d", tree.offset, maxSize)
}
}
2023-02-03 19:58:22 +01:00
tree.nodes = append(tree.nodes, hash)
tree.size++
tree.collapse()
return nil
}
2024-06-12 17:21:58 +02:00
func (tree *CollapsedTree) Append(other CollapsedTree) error {
if tree.offset+tree.size != other.offset {
return fmt.Errorf("subtree at offset %d cannot be appended to subtree ending at offset %d", other.offset, tree.offset+tree.size)
}
if tree.offset > 0 {
newSize := tree.size + other.size
maxSize := uint64(1) << bits.TrailingZeros64(tree.offset)
if newSize > maxSize {
return fmt.Errorf("size of new subtree (%d) would exceed maximum size %d for a subtree at offset %d", newSize, maxSize, tree.offset)
}
}
if tree.size > 0 {
maxSize := uint64(1) << bits.TrailingZeros64(tree.size)
if other.size > maxSize {
return fmt.Errorf("tree of size %d is too large to append to a tree of size %d (maximum size is %d)", other.size, tree.size, maxSize)
}
2023-02-19 03:14:36 +01:00
}
tree.nodes = append(tree.nodes, other.nodes...)
tree.size += other.size
tree.collapse()
return nil
}
func (tree *CollapsedTree) collapse() {
numNodes := calculateNumNodes(tree.size)
for len(tree.nodes) > numNodes {
2023-02-03 19:58:22 +01:00
left, right := tree.nodes[len(tree.nodes)-2], tree.nodes[len(tree.nodes)-1]
tree.nodes = tree.nodes[:len(tree.nodes)-2]
tree.nodes = append(tree.nodes, HashChildren(left, right))
}
}
func (tree CollapsedTree) CalculateRoot() Hash {
2023-02-03 19:58:22 +01:00
if len(tree.nodes) == 0 {
return HashNothing()
}
i := len(tree.nodes) - 1
hash := tree.nodes[i]
for i > 0 {
i -= 1
hash = HashChildren(tree.nodes[i], hash)
}
return hash
}
// Return the subtree offset (0 if this represents an entire tree)
func (tree CollapsedTree) Offset() uint64 {
return tree.offset
}
// Return a non-nil slice containing the nodes. The slice
// must not be modified.
func (tree CollapsedTree) Nodes() []Hash {
if tree.nodes == nil {
return []Hash{}
} else {
return tree.nodes
}
}
// Return the number of leaf nodes in the tree.
func (tree CollapsedTree) Size() uint64 {
2023-02-03 19:58:22 +01:00
return tree.size
}
type collapsedTreeMessage struct {
Offset uint64 `json:"offset,omitempty"`
Nodes []Hash `json:"nodes"` // never nil
Size uint64 `json:"size"`
}
func (tree CollapsedTree) MarshalJSON() ([]byte, error) {
return json.Marshal(collapsedTreeMessage{
Offset: tree.offset,
Nodes: tree.Nodes(),
Size: tree.size,
2023-02-03 19:58:22 +01:00
})
}
func (tree *CollapsedTree) UnmarshalJSON(b []byte) error {
var rawTree collapsedTreeMessage
2023-02-03 19:58:22 +01:00
if err := json.Unmarshal(b, &rawTree); err != nil {
return fmt.Errorf("error unmarshalling Collapsed Merkle Tree: %w", err)
}
if err := tree.InitSubtree(rawTree.Offset, rawTree.Nodes, rawTree.Size); err != nil {
return fmt.Errorf("error unmarshalling Collapsed Merkle Tree: %w", err)
}
return nil
}
func (tree *CollapsedTree) Init(nodes []Hash, size uint64) error {
if len(nodes) != calculateNumNodes(size) {
return fmt.Errorf("nodes has wrong length (should be %d, not %d)", calculateNumNodes(size), len(nodes))
2023-02-03 19:58:22 +01:00
}
tree.size = size
tree.nodes = nodes
2023-02-03 19:58:22 +01:00
return nil
}
func (tree *CollapsedTree) InitSubtree(offset uint64, nodes []Hash, size uint64) error {
if offset > 0 {
maxSize := uint64(1) << bits.TrailingZeros64(offset)
if size > maxSize {
return fmt.Errorf("subtree size (%d) is too large for offset %d (maximum size is %d)", size, offset, maxSize)
}
}
tree.offset = offset
return tree.Init(nodes, size)
}