Skip to content

Commit b3afd5d

Browse files
authored
Merge pull request #188 from dergoegge/useslices
refactor: Use slices instead of maps
2 parents 589529f + 3c1e23e commit b3afd5d

File tree

6 files changed

+289
-434
lines changed

6 files changed

+289
-434
lines changed

accumulator/batchproof.go

+119-96
Original file line numberDiff line numberDiff line change
@@ -112,122 +112,145 @@ 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+
// check if the target is the row 0 root.
150+
// this is the case if its the last leaf (pos==numLeaves-1)
151+
// AND the tree has a root at row 0 (numLeaves&1==1)
152+
if targets[0] == numLeaves-1 && numLeaves&1 == 1 {
153+
// target is the row 0 root, append it to the root candidates.
154+
rootCandidates = append(rootCandidates, node{Val: roots[0], Pos: targets[0]})
155+
bp.Proof = bp.Proof[1:]
156+
break
157+
}
141158

142-
tagRow := bp.Targets
143-
nextRow := []uint64{}
144-
sortUint64s(tagRow) // probably don't need to sort
159+
// `targets` might contain a target and its sibling or just the target, if
160+
// only the target is present the sibling will be in `proofPositions`.
161+
if uint64(len(proofPositions)) > targetsMatched &&
162+
targets[0]^1 == proofPositions[targetsMatched] {
163+
// the sibling of the target is included in the proof positions.
164+
lr := targets[0] & 1
165+
targetNodes = append(targetNodes, node{Pos: targets[0], Val: bp.Proof[lr]})
166+
proofHashes = append(proofHashes, bp.Proof[lr^1])
167+
targetsMatched++
168+
bp.Proof = bp.Proof[2:]
169+
targets = targets[1:]
170+
continue
171+
}
145172

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.
173+
// the sibling is not included in the proof positions, therefore it has to be included in `targets.
174+
// if there are less than 2 proof hashes or less than 2 targets left the proof is invalid
175+
// because there is a target without matching proof.
176+
if len(bp.Proof) < 2 || len(targets) < 2 {
177+
return false, nil, nil
178+
}
150179

151-
if verbose {
152-
fmt.Printf("tagrow len %d\n", len(tagRow))
180+
targetNodes = append(targetNodes,
181+
node{Pos: targets[0], Val: bp.Proof[0]},
182+
node{Pos: targets[1], Val: bp.Proof[1]})
183+
bp.Proof = bp.Proof[2:]
184+
targets = targets[2:]
153185
}
154186

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:]
187+
proofHashes = append(proofHashes, bp.Proof...)
188+
bp.Proof = proofHashes
189+
190+
// hash every target node with its sibling (which either is contained in the proof or also a target)
191+
for len(targetNodes) > 0 {
192+
var target, proof node
193+
target = targetNodes[0]
194+
if len(proofPositions) > 0 && target.Pos^1 == proofPositions[0] {
195+
// target has a sibling in the proof positions, fetch proof
196+
proof = node{Pos: proofPositions[0], Val: bp.Proof[0]}
197+
proofPositions = proofPositions[1:]
198+
bp.Proof = bp.Proof[1:]
199+
targetNodes = targetNodes[1:]
200+
} else {
201+
// target should have its sibling in targetNodes
202+
if len(targetNodes) == 1 {
203+
// sibling not found
204+
return false, nil, nil
173205
}
174206

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-
}
207+
proof = targetNodes[1]
208+
targetNodes = targetNodes[2:]
209+
}
201210

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-
}
211+
// figure out which node is left and which is right
212+
left := target
213+
right := proof
214+
if target.Pos&1 == 1 {
215+
right, left = left, right
216+
}
208217

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
218+
// get the hash of the parent from the cache or compute it
219+
parentPos := parent(target.Pos, rows)
220+
isParentCached, hash := cached(parentPos)
221+
if !isParentCached {
222+
hash = parentHash(left.Val, right.Val)
214223
}
224+
trees = append(trees, [3]node{{Val: hash, Pos: parentPos}, left, right})
215225

216-
// Make the nextRow the tagRow so we'll be iterating over it
217-
// reset th nextRow
218-
tagRow = nextRow
219-
nextRow = []uint64{}
226+
row := detectRow(parentPos, rows)
227+
if numLeaves&(1<<row) > 0 && parentPos == rootPosition(numLeaves, row, rows) {
228+
// the parent is a root -> store as candidate, to check against actual roots later.
229+
rootCandidates = append(rootCandidates, node{Val: hash, Pos: parentPos})
230+
continue
231+
}
232+
targetNodes = append(targetNodes, node{Val: hash, Pos: parentPos})
233+
}
220234

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:]
235+
if len(rootCandidates) == 0 {
236+
// no roots to verify
237+
return false, nil, nil
238+
}
239+
240+
// `roots` is ordered, therefore to verify that `rootCandidates` holds a subset of the roots
241+
// we count the roots that match in order.
242+
rootMatches := 0
243+
for _, root := range roots {
244+
if len(rootCandidates) > rootMatches && root == rootCandidates[rootMatches].Val {
245+
rootMatches++
227246
}
228247
}
248+
if len(rootCandidates) != rootMatches {
249+
// the proof is invalid because some root candidates were not included in `roots`.
250+
return false, nil, nil
251+
}
229252

230-
return true, proofmap
253+
return true, trees, rootCandidates
231254
}
232255

233256
// 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)