Skip to content

Commit 96cf573

Browse files
committed
refactor: Use slices instead of maps
1 parent 573edd2 commit 96cf573

File tree

6 files changed

+277
-435
lines changed

6 files changed

+277
-435
lines changed

accumulator/batchproof.go

+109-97
Original file line numberDiff line numberDiff line change
@@ -112,122 +112,134 @@ func FromBytesBatchProof(b []byte) (BatchProof, error) {
112112
// TODO OH WAIT -- this is not how to to it! Don't hash all the way up to the
113113
// roots to verify -- just hash up to any populated node! Saves a ton of CPU!
114114

115-
// verifyBatchProof takes a block proof and reconstructs / verifies it.
116-
// takes a blockproof to verify, and the known correct roots to check against.
117-
// also takes the number of leaves and forest rows (those are redundant
118-
// if we don't do weird stuff with overly-high forests, which we might)
119-
// it returns a bool of whether the proof worked, and a map of the sparse
120-
// forest in the blockproof
121-
func verifyBatchProof(
122-
bp BatchProof, roots []Hash,
123-
numLeaves uint64, rows uint8) (bool, map[uint64]Hash) {
124-
125-
// if nothing to prove, it worked
115+
// verifyBatchProof verifies a batchproof by checking against the set of known correct roots.
116+
// Takes a BatchProof, the accumulator roots, and the number of leaves in the forest.
117+
// Returns wether or not the proof verified correctly, the partial proof tree,
118+
// and the subset of roots that was computed.
119+
func verifyBatchProof(bp BatchProof, roots []Hash, numLeaves uint64,
120+
// cached should be a function that fetches nodes from the pollard and indicates whether they
121+
// exist or not, this is only useful for the pollard and nil should be passed for the forest.
122+
cached func(pos uint64) (bool, Hash)) (bool, [][3]node, []node) {
126123
if len(bp.Targets) == 0 {
127-
return true, nil
124+
return true, nil, nil
128125
}
129126

130-
// Construct a map with positions to hashes
131-
proofmap, err := bp.Reconstruct(numLeaves, rows)
132-
if err != nil {
133-
fmt.Printf("VerifyBlockProof Reconstruct ERROR %s\n", err.Error())
134-
return false, proofmap
127+
if cached == nil {
128+
cached = func(_ uint64) (bool, Hash) { return false, empty }
135129
}
136130

137-
rootPositions, rootRows := getRootsReverse(numLeaves, rows)
138-
139-
// partial forest is built, go through and hash everything to make sure
140-
// you get the right roots
131+
rows := treeRows(numLeaves)
132+
proofPositions, computablePositions := ProofPositions(bp.Targets, numLeaves, rows)
133+
// targetNodes holds nodes that are known, on the bottom row those are the targets,
134+
// on the upper rows it holds computed nodes.
135+
// rootCandidates holds the roots that where computed, and have to be compared to the actual roots
136+
// at the end.
137+
targetNodes := make([]node, 0, len(bp.Targets)*int(rows))
138+
rootCandidates := make([]node, 0, len(roots))
139+
// trees is a slice of 3-Tuples, each tuple represents a parent and its children.
140+
// tuple[0] is the parent, tuple[1] is the left child and tuple[2] is the right child.
141+
// trees holds the entire proof tree of the batchproof in this way, sorted by the tuple[0].
142+
trees := make([][3]node, 0, len(computablePositions))
143+
// initialise the targetNodes for row 0.
144+
// TODO: this would be more straight forward if bp.Proofs wouldn't contain the targets
145+
proofHashes := make([]Hash, 0, len(proofPositions))
146+
targets := bp.Targets
147+
var targetsMatched uint64
148+
for len(targets) > 0 {
149+
if targets[0] == numLeaves-1 && numLeaves&1 == 1 {
150+
// row 0 root.
151+
rootCandidates = append(rootCandidates, node{Val: roots[0], Pos: targets[0]})
152+
bp.Proof = bp.Proof[1:]
153+
break
154+
}
141155

142-
tagRow := bp.Targets
143-
nextRow := []uint64{}
144-
sortUint64s(tagRow) // probably don't need to sort
156+
if uint64(len(proofPositions)) > targetsMatched &&
157+
targets[0]^1 == proofPositions[targetsMatched] {
158+
lr := targets[0] & 1
159+
targetNodes = append(targetNodes, node{Pos: targets[0], Val: bp.Proof[lr]})
160+
proofHashes = append(proofHashes, bp.Proof[lr^1])
161+
targetsMatched++
162+
bp.Proof = bp.Proof[2:]
163+
targets = targets[1:]
164+
continue
165+
}
145166

146-
// TODO it's ugly that I keep treating the 0-row as a special case,
147-
// and has led to a number of bugs. It *is* special in a way, in that
148-
// the bottom row is the only thing you actually prove and add/delete,
149-
// but it'd be nice if it could all be treated uniformly.
167+
if len(bp.Proof) < 2 || len(targets) < 2 {
168+
return false, nil, nil
169+
}
150170

151-
if verbose {
152-
fmt.Printf("tagrow len %d\n", len(tagRow))
171+
targetNodes = append(targetNodes,
172+
node{Pos: targets[0], Val: bp.Proof[0]},
173+
node{Pos: targets[1], Val: bp.Proof[1]})
174+
bp.Proof = bp.Proof[2:]
175+
targets = targets[2:]
153176
}
154177

155-
var left, right uint64
156-
157-
// iterate through rows
158-
for row := uint8(0); row <= rows; row++ {
159-
// iterate through tagged positions in this row
160-
for len(tagRow) > 0 {
161-
// Efficiency gains here. If there are two or more things to verify,
162-
// check if the next thing to verify is the sibling of the current leaf
163-
// we're on. Siblingness can be checked with bitwise XOR but since targets are
164-
// sorted, we can do bitwise OR instead.
165-
if len(tagRow) > 1 && tagRow[0]|1 == tagRow[1] {
166-
left = tagRow[0]
167-
right = tagRow[1]
168-
tagRow = tagRow[2:]
169-
} else { // if not only use one tagged position
170-
right = tagRow[0] | 1
171-
left = right ^ 1
172-
tagRow = tagRow[1:]
173-
}
178+
proofHashes = append(proofHashes, bp.Proof...)
179+
bp.Proof = proofHashes
180+
181+
// hash every target node with its sibling (which either is contained in the proof or also a target)
182+
for len(targetNodes) > 0 {
183+
var target, proof node
184+
target = targetNodes[0]
185+
if len(proofPositions) > 0 && target.Pos^1 == proofPositions[0] {
186+
// target has a sibling in the proof positions, fetch proof
187+
proof = node{Pos: proofPositions[0], Val: bp.Proof[0]}
188+
proofPositions = proofPositions[1:]
189+
bp.Proof = bp.Proof[1:]
190+
targetNodes = targetNodes[1:]
191+
goto hash
192+
}
174193

175-
if verbose {
176-
fmt.Printf("left %d rootPoss %d\n", left, rootPositions[0])
177-
}
178-
// check for roots
179-
if left == rootPositions[0] {
180-
if verbose {
181-
fmt.Printf("one left in tagrow; should be root\n")
182-
}
183-
// Grab the hash of this position from the map
184-
computedRootHash, ok := proofmap[left]
185-
if !ok {
186-
fmt.Printf("ERR no proofmap for root at %d\n", left)
187-
return false, nil
188-
}
189-
// Verify that this root hash matches the one we stored
190-
if computedRootHash != roots[0] {
191-
fmt.Printf("row %d root, pos %d expect %04x got %04x\n",
192-
row, left, roots[0][:4], computedRootHash[:4])
193-
return false, nil
194-
}
195-
// otherwise OK and pop of the root
196-
roots = roots[1:]
197-
rootPositions = rootPositions[1:]
198-
rootRows = rootRows[1:]
199-
break
200-
}
194+
// target should have its sibling in targetNodes
195+
if len(targetNodes) == 1 {
196+
// sibling not found
197+
return false, nil, nil
198+
}
201199

202-
// Grab the parent position of the leaf we've verified
203-
parentPos := parent(left, rows)
204-
if verbose {
205-
fmt.Printf("%d %04x %d %04x -> %d\n",
206-
left, proofmap[left], right, proofmap[right], parentPos)
207-
}
200+
proof = targetNodes[1]
201+
targetNodes = targetNodes[2:]
208202

209-
// this will crash if either is 0000
210-
// reconstruct the next row and add the parent to the map
211-
parhash := parentHash(proofmap[left], proofmap[right])
212-
nextRow = append(nextRow, parentPos)
213-
proofmap[parentPos] = parhash
203+
hash:
204+
// figure out which node is left and which is right
205+
left := target
206+
right := proof
207+
if target.Pos&1 == 1 {
208+
right, left = left, right
214209
}
215210

216-
// Make the nextRow the tagRow so we'll be iterating over it
217-
// reset th nextRow
218-
tagRow = nextRow
219-
nextRow = []uint64{}
211+
// get the hash of the parent from the cache or compute it
212+
parentPos := parent(target.Pos, rows)
213+
isParentCached, hash := cached(parentPos)
214+
if !isParentCached {
215+
hash = parentHash(left.Val, right.Val)
216+
}
217+
trees = append(trees, [3]node{{Val: hash, Pos: parentPos}, left, right})
220218

221-
// if done with row and there's a root left on this row, remove it
222-
if len(rootRows) > 0 && rootRows[0] == row {
223-
// bit ugly to do these all separately eh
224-
roots = roots[1:]
225-
rootPositions = rootPositions[1:]
226-
rootRows = rootRows[1:]
219+
row := detectRow(parentPos, rows)
220+
if numLeaves&(1<<row) > 0 && parentPos == rootPosition(numLeaves, row, rows) {
221+
// the parent is a root -> store as candidate, to check against actual roots later.
222+
rootCandidates = append(rootCandidates, node{Val: hash, Pos: parentPos})
223+
continue
227224
}
225+
targetNodes = append(targetNodes, node{Val: hash, Pos: parentPos})
226+
}
227+
if len(rootCandidates) == 0 {
228+
// no roots to verify
229+
return false, nil, nil
230+
}
231+
rootMatches := 0
232+
for _, root := range roots {
233+
if len(rootCandidates) > rootMatches && root == rootCandidates[rootMatches].Val {
234+
rootMatches++
235+
}
236+
}
237+
if len(rootCandidates) != rootMatches {
238+
// some roots did not match
239+
return false, nil, nil
228240
}
229241

230-
return true, proofmap
242+
return true, trees, rootCandidates
231243
}
232244

233245
// Reconstruct takes a number of leaves and rows, and turns a block proof back

accumulator/forest_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ func addDelFullBatchProof(nAdds, nDels int) error {
188188
}
189189
bp.SortTargets()
190190
// check block proof. Note this doesn't delete anything, just proves inclusion
191-
worked, _ := verifyBatchProof(bp, f.getRoots(), f.numLeaves, f.rows)
191+
worked, _, _ := verifyBatchProof(bp, f.getRoots(), f.numLeaves, nil)
192192
// worked := f.VerifyBatchProof(bp)
193193

194194
if !worked {

accumulator/forestproofs.go

+6-77
Original file line numberDiff line numberDiff line change
@@ -177,84 +177,13 @@ func (f *Forest) ProveBatch(hs []Hash) (BatchProof, error) {
177177
copy(sortedTargets, bp.Targets)
178178
sortUint64s(sortedTargets)
179179

180-
// TODO feels like you could do all this with just slices and no maps...
181-
// that would be better
182-
// proofTree is the partially populated tree of everything needed for the
183-
// proofs
184-
proofTree := make(map[uint64]Hash)
185-
186-
// go through each target and add a proof for it up to the intersection
187-
for _, pos := range sortedTargets {
188-
// add hash for the deletion itself and its sibling
189-
// if they already exist, skip the whole thing
190-
_, alreadyThere := proofTree[pos]
191-
if alreadyThere {
192-
// fmt.Printf("%d omit already there\n", pos)
193-
continue
194-
}
195-
// TODO change this for the real thing; no need to prove 0-tree root.
196-
// but we still need to verify it and tag it as a target.
197-
if pos == f.numLeaves-1 && pos&1 == 0 {
198-
proofTree[pos] = f.data.read(pos)
199-
// fmt.Printf("%d add as root\n", pos)
200-
continue
201-
}
202-
203-
// always put in both siblings when on the bottom row
204-
// this can be out of order but it will be sorted later
205-
proofTree[pos] = f.data.read(pos)
206-
proofTree[pos^1] = f.data.read(pos ^ 1)
207-
// fmt.Printf("added leaves %d, %d\n", pos, pos^1)
208-
209-
treeTop := detectSubTreeRows(pos, f.numLeaves, f.rows)
210-
pos = parent(pos, f.rows)
211-
// go bottom to top and add siblings into the partial tree
212-
// start at row 1 though; we always populate the bottom leaf and sibling
213-
// This either gets to the top, or intersects before that and deletes
214-
// something
215-
for h := uint8(1); h < treeTop; h++ {
216-
// check if the sibling is already there, in which case we're done
217-
// also check if the parent itself is there, in which case we delete it!
218-
// I think this with the early ignore at the bottom make it optimal
219-
_, selfThere := proofTree[pos]
220-
_, sibThere := proofTree[pos^1]
221-
if sibThere {
222-
// sibling position already exists in partial tree; done
223-
// with this branch
224-
225-
// TODO seems that this never happens and can be removed
226-
panic("this never happens...?")
227-
}
228-
if selfThere {
229-
// self position already there; remove as children are known
230-
// fmt.Printf("remove proof from pos %d\n", pos)
231-
232-
delete(proofTree, pos)
233-
delete(proofTree, pos^1) // right? can delete both..?
234-
break
235-
}
236-
// fmt.Printf("add proof from pos %d\n", pos^1)
237-
proofTree[pos^1] = f.data.read(pos ^ 1)
238-
pos = parent(pos, f.rows)
239-
}
240-
}
241-
242-
var nodeSlice []node
243-
244-
// run through partial tree to turn it into a slice
245-
for pos, hash := range proofTree {
246-
nodeSlice = append(nodeSlice, node{pos, hash})
180+
proofPositions, _ := ProofPositions(sortedTargets, f.numLeaves, f.rows)
181+
targetsAndProof := mergeSortedSlices(proofPositions, sortedTargets)
182+
bp.Proof = make([]Hash, len(targetsAndProof))
183+
for i, proofPos := range targetsAndProof {
184+
bp.Proof[i] = f.data.read(proofPos)
247185
}
248-
// fmt.Printf("made nodeSlice %d nodes\n", len(nodeSlice))
249186

250-
// sort the slice of nodes (even though we only want the hashes)
251-
sortNodeSlice(nodeSlice)
252-
// copy the sorted / in-order hashes into a hash slice
253-
bp.Proof = make([]Hash, len(nodeSlice))
254-
255-
for i, n := range nodeSlice {
256-
bp.Proof[i] = n.Val
257-
}
258187
if verbose {
259188
fmt.Printf("blockproof targets: %v\n", bp.Targets)
260189
}
@@ -266,6 +195,6 @@ func (f *Forest) ProveBatch(hs []Hash) (BatchProof, error) {
266195

267196
// VerifyBatchProof :
268197
func (f *Forest) VerifyBatchProof(bp BatchProof) bool {
269-
ok, _ := verifyBatchProof(bp, f.getRoots(), f.numLeaves, f.rows)
198+
ok, _, _ := verifyBatchProof(bp, f.getRoots(), f.numLeaves, nil)
270199
return ok
271200
}

0 commit comments

Comments
 (0)