Skip to content

Commit 3d6af79

Browse files
authored
proof: Avoid all reallocations (#23)
This change introduces pre-allocation into the Proof generation types, thereby reducing the total number of slice allocations to one. A new helper for computing the compact range size compact.RangeSize is introduced, and the signature of the previous compact.RangeNodes now requires providing a slice to append the result to.
1 parent 816afa4 commit 3d6af79

5 files changed

Lines changed: 82 additions & 50 deletions

File tree

compact/nodes.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,11 @@ func (id NodeID) Coverage() (uint64, uint64) {
5656
return id.Index << id.Level, (id.Index + 1) << id.Level
5757
}
5858

59-
// RangeNodes returns node IDs that comprise the [begin, end) compact range.
60-
func RangeNodes(begin, end uint64) []NodeID {
59+
// RangeNodes appends the IDs of the nodes that comprise the [begin, end)
60+
// compact range to the given slice, and returns the new slice. The caller may
61+
// pre-allocate space with the help of the RangeSize function.
62+
func RangeNodes(begin, end uint64, ids []NodeID) []NodeID {
6163
left, right := Decompose(begin, end)
62-
ids := make([]NodeID, 0, bits.OnesCount64(left)+bits.OnesCount64(right))
6364

6465
pos := begin
6566
// Iterate over perfect subtrees along the left border of the range, ordered
@@ -80,3 +81,9 @@ func RangeNodes(begin, end uint64) []NodeID {
8081

8182
return ids
8283
}
84+
85+
// RangeSize returns the number of nodes in the [begin, end) compact range.
86+
func RangeSize(begin, end uint64) int {
87+
left, right := Decompose(begin, end)
88+
return bits.OnesCount64(left) + bits.OnesCount64(right)
89+
}

compact/nodes_test.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
"github.com/google/go-cmp/cmp"
2222
)
2323

24-
func TestRangeNodes(t *testing.T) {
24+
func TestRangeNodesAndSize(t *testing.T) {
2525
n := func(level uint, index uint64) NodeID {
2626
return NewNodeID(level, index)
2727
}
@@ -31,9 +31,9 @@ func TestRangeNodes(t *testing.T) {
3131
want []NodeID
3232
}{
3333
// Empty ranges.
34-
{end: 0, want: []NodeID{}},
35-
{begin: 10, end: 10, want: []NodeID{}},
36-
{begin: 1024, end: 1024, want: []NodeID{}},
34+
{end: 0, want: nil},
35+
{begin: 10, end: 10, want: nil},
36+
{begin: 1024, end: 1024, want: nil},
3737
// One entry.
3838
{begin: 10, end: 11, want: []NodeID{n(0, 10)}},
3939
{begin: 1024, end: 1025, want: []NodeID{n(0, 1024)}},
@@ -67,19 +67,35 @@ func TestRangeNodes(t *testing.T) {
6767
{begin: 1, end: 17, want: []NodeID{n(0, 1), n(1, 1), n(2, 1), n(3, 1), n(0, 16)}},
6868
} {
6969
t.Run(fmt.Sprintf("range:%d:%d", tc.begin, tc.end), func(t *testing.T) {
70-
got := RangeNodes(tc.begin, tc.end)
70+
got := RangeNodes(tc.begin, tc.end, nil)
7171
if diff := cmp.Diff(got, tc.want); diff != "" {
7272
t.Fatalf("RangeNodes: diff(-want +got):\n%s", diff)
7373
}
74+
if got, want := RangeSize(tc.begin, tc.end), len(tc.want); got != want {
75+
t.Errorf("RangeSize: got %d, want %d", got, want)
76+
}
7477
})
7578
}
7679
}
7780

81+
func TestRangeNodesAppend(t *testing.T) {
82+
prefix := []NodeID{NewNodeID(0, 0), NewNodeID(10, 0), NewNodeID(11, 5)}
83+
nodes := RangeNodes(123, 456, prefix)
84+
85+
if got, min := len(nodes), len(prefix); got < min {
86+
t.Fatalf("RangeNodes returned %d IDs, want >= %d", got, min)
87+
}
88+
got := nodes[:len(prefix)]
89+
if diff := cmp.Diff(got, prefix); diff != "" {
90+
t.Fatalf("RangeNodes: diff(-prefix +got):\n%s", diff)
91+
}
92+
}
93+
7894
func TestGenRangeNodes(t *testing.T) {
7995
const size = uint64(512)
8096
for begin := uint64(0); begin <= size; begin++ {
8197
for end := begin; end <= size; end++ {
82-
got := RangeNodes(begin, end)
98+
got := RangeNodes(begin, end, nil)
8399
want := refRangeNodes(NewNodeID(63, 0), begin, end)
84100
if diff := cmp.Diff(got, want); diff != "" {
85101
t.Fatalf("RangeNodes(%d, %d): diff(-want +got):\n%s", begin, end, diff)
@@ -89,11 +105,11 @@ func TestGenRangeNodes(t *testing.T) {
89105
}
90106

91107
// refRangeNodes returns node IDs that comprise the [begin, end) compact range.
92-
// This is a reference implementation for cross-cthehecking.
108+
// This is a reference implementation for cross-checking.
93109
func refRangeNodes(root NodeID, begin, end uint64) []NodeID {
94110
b, e := root.Coverage()
95111
if end <= b || begin >= e {
96-
return []NodeID{}
112+
return nil
97113
}
98114
if b >= begin && e <= end {
99115
return []NodeID{root}

compact/range.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,8 @@ func (f *RangeFactory) NewRange(begin, end uint64, hashes [][]byte) (*Range, err
4141
if end < begin {
4242
return nil, fmt.Errorf("invalid range: end=%d, want >= %d", end, begin)
4343
}
44-
left, right := Decompose(begin, end)
45-
ones := bits.OnesCount64(left) + bits.OnesCount64(right)
46-
if ln := len(hashes); ln != ones {
47-
return nil, fmt.Errorf("invalid hashes: got %d values, want %d", ln, ones)
44+
if got, want := len(hashes), RangeSize(begin, end); got != want {
45+
return nil, fmt.Errorf("invalid hashes: got %d values, want %d", got, want)
4846
}
4947
return &Range{f: f, begin: begin, end: end, hashes: hashes}, nil
5048
}

compact/range_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ func TestNewRangeWithStorage(t *testing.T) {
368368
}); err != nil {
369369
t.Fatalf("%d: Append: %v", i, err)
370370
}
371-
hashes := getHashes(RangeNodes(0, i+1))
371+
hashes := getHashes(RangeNodes(0, i+1, nil))
372372
var err error
373373
if cr, err = factory.NewRange(0, i+1, hashes); err != nil {
374374
t.Fatalf("%d: NewRange: %v", i+1, err)

proof/proof.go

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func Inclusion(index, size uint64) (Nodes, error) {
4949
if index >= size {
5050
return Nodes{}, fmt.Errorf("index %d out of bounds for tree size %d", index, size)
5151
}
52-
return nodes(index, 0, size), nil
52+
return nodes(index, 0, size).skipFirst(), nil
5353
}
5454

5555
// Consistency returns the information on how to fetch and construct a
@@ -72,57 +72,59 @@ func Consistency(size1, size2 uint64) (Nodes, error) {
7272
// into this node in the tree of size2.
7373
p := nodes(index, level, size2)
7474

75-
// Handle the case when size1 is not a power of 2.
76-
if index != 0 {
77-
// Prepend the earlier computed node to the proof.
78-
// TODO(pavelkalinnikov): This code path is invoked almost always. Avoid
79-
// the extra allocation that append does.
80-
p.IDs = append(p.IDs, compact.NodeID{})
81-
copy(p.IDs[1:], p.IDs)
82-
p.IDs[0] = compact.NewNodeID(level, index)
83-
84-
// Fixup the indices into the IDs slice.
85-
if p.begin < p.end {
86-
p.begin++
87-
p.end++
88-
}
75+
// Handle the case when size1 is a power of 2.
76+
if index == 0 {
77+
return p.skipFirst(), nil
8978
}
90-
9179
return p, nil
9280
}
9381

9482
// nodes returns the node IDs necessary to prove that the (level, index) node
9583
// is included in the Merkle tree of the given size.
9684
func nodes(index uint64, level uint, size uint64) Nodes {
97-
node := compact.NewNodeID(level, index)
98-
begin, _ := node.Coverage()
85+
// Compute the `fork` node, where the path from root to (level, index) node
86+
// diverges from the path to (0, size).
87+
//
88+
// The sibling of this node is the ephemeral node which represents a subtree
89+
// that is not complete in the tree of the given size. To compute the hash
90+
// of the ephemeral node, we need all the non-ephemeral nodes that cover the
91+
// same range of leaves.
92+
//
93+
// The `inner` variable is how many layers up from (level, index) the `fork`
94+
// and the ephemeral nodes are.
95+
inner := bits.Len64(index^(size>>level)) - 1
96+
fork := compact.NewNodeID(level+uint(inner), index>>inner)
9997

100-
// Compute the level at which the path to leaf `begin` diverges from the path
101-
// to `size`. This is where the ephemeral node is located. The ephemeral node
102-
// represents a subtree that is not complete in the tree of the given size,
103-
// so we instead provide the minimal list of non-ephemeral nodes which cover
104-
// the same range of leaves.
105-
ephemLevel := uint(bits.Len64(begin^size) - 1)
98+
begin, end := fork.Coverage()
99+
left := compact.RangeSize(0, begin)
100+
right := compact.RangeSize(end, size)
101+
102+
node := compact.NewNodeID(level, index)
103+
// Pre-allocate the exact number of nodes for the proof, in order:
104+
// - The seed node for which we are building the proof.
105+
// - The `inner` nodes at each level up to the fork node.
106+
// - The `right` nodes, comprising the ephemeral node.
107+
// - The `left` nodes, completing the coverage of the whole [0, size) range.
108+
nodes := append(make([]compact.NodeID, 0, 1+inner+right+left), node)
106109

107110
// The first portion of the proof consists of the siblings for nodes of the
108111
// path going up to the level at which the ephemeral node appears.
109-
// TODO(pavelkalinnikov): Pre-allocate the full capacity.
110-
nodes := make([]compact.NodeID, 0, ephemLevel-level)
111-
for ; node.Level < ephemLevel; node = node.Parent() {
112+
for ; node.Level < fork.Level; node = node.Parent() {
112113
nodes = append(nodes, node.Sibling())
113114
}
114-
// This portion of the proof covers the range under the reached node. The
115+
// This portion of the proof covers the range [begin, end) under it. The
115116
// ranges to the left and to the right from it remain to be covered.
116-
begin, end := node.Coverage()
117117

118118
// Add all the nodes (potentially none) that cover the right range, and
119119
// represent the ephemeral node. Reverse them so that the Rehash method can
120120
// process hashes in the convenient order, from lower to upper levels.
121121
len1 := len(nodes)
122-
nodes = append(nodes, reverse(compact.RangeNodes(end, size))...)
122+
nodes = compact.RangeNodes(end, size, nodes)
123+
reverse(nodes[len(nodes)-right:])
123124
len2 := len(nodes)
124125
// Add the nodes that cover the left range, ordered increasingly by level.
125-
nodes = append(nodes, reverse(compact.RangeNodes(0, begin))...)
126+
nodes = compact.RangeNodes(0, begin, nodes)
127+
reverse(nodes[len(nodes)-left:])
126128

127129
// nodes[len1:len2] contains the nodes representing the ephemeral node. If
128130
// it's empty or only has one node, make it zero.
@@ -133,7 +135,7 @@ func nodes(index uint64, level uint, size uint64) Nodes {
133135
len1, len2 = 0, 0
134136
}
135137

136-
return Nodes{IDs: nodes, begin: len1, end: len2, ephem: node.Sibling()}
138+
return Nodes{IDs: nodes, begin: len1, end: len2, ephem: fork.Sibling()}
137139
}
138140

139141
// Ephem returns the ephemeral node, and indices begin and end, such that
@@ -173,9 +175,18 @@ func (n Nodes) Rehash(h [][]byte, hc func(left, right []byte) []byte) ([][]byte,
173175
return h[:cursor], nil
174176
}
175177

176-
func reverse(ids []compact.NodeID) []compact.NodeID {
178+
func (n Nodes) skipFirst() Nodes {
179+
n.IDs = n.IDs[1:]
180+
// Fixup the indices into the IDs slice.
181+
if n.begin < n.end {
182+
n.begin--
183+
n.end--
184+
}
185+
return n
186+
}
187+
188+
func reverse(ids []compact.NodeID) {
177189
for i, j := 0, len(ids)-1; i < j; i, j = i+1, j-1 {
178190
ids[i], ids[j] = ids[j], ids[i]
179191
}
180-
return ids
181192
}

0 commit comments

Comments
 (0)