Skip to content

Commit

Permalink
refactor: Use slices instead of maps
Browse files Browse the repository at this point in the history
  • Loading branch information
dergoegge committed Aug 24, 2020
1 parent ee3e056 commit 7f63247
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 435 deletions.
206 changes: 109 additions & 97 deletions accumulator/batchproof.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,122 +112,134 @@ func FromBytesBatchProof(b []byte) (BatchProof, error) {
// TODO OH WAIT -- this is not how to to it! Don't hash all the way up to the
// roots to verify -- just hash up to any populated node! Saves a ton of CPU!

// verifyBatchProof takes a block proof and reconstructs / verifies it.
// takes a blockproof to verify, and the known correct roots to check against.
// also takes the number of leaves and forest rows (those are redundant
// if we don't do weird stuff with overly-high forests, which we might)
// it returns a bool of whether the proof worked, and a map of the sparse
// forest in the blockproof
func verifyBatchProof(
bp BatchProof, roots []Hash,
numLeaves uint64, rows uint8) (bool, map[uint64]Hash) {

// if nothing to prove, it worked
// verifyBatchProof verifies a batchproof by checking against the set of known correct roots.
// Takes a BatchProof, the accumulator roots, and the number of leaves in the forest.
// Returns wether or not the proof verified correctly, the partial proof tree,
// and the subset of roots that was computed.
func verifyBatchProof(bp BatchProof, roots []Hash, numLeaves uint64,
// cached should be a function that fetches nodes from the pollard and indicates whether they
// exist or not, this is only useful for the pollard and nil should be passed for the forest.
cached func(pos uint64) (bool, Hash)) (bool, [][3]node, []node) {
if len(bp.Targets) == 0 {
return true, nil
return true, nil, nil
}

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

rootPositions, rootRows := getRootsReverse(numLeaves, rows)

// partial forest is built, go through and hash everything to make sure
// you get the right roots
rows := treeRows(numLeaves)
proofPositions, computablePositions := ProofPositions(bp.Targets, numLeaves, rows)
// targetNodes holds nodes that are known, on the bottom row those are the targets,
// on the upper rows it holds computed nodes.
// rootCandidates holds the roots that where computed, and have to be compared to the actual roots
// at the end.
targetNodes := make([]node, 0, len(bp.Targets)*int(rows))
rootCandidates := make([]node, 0, len(roots))
// trees is a slice of 3-Tuples, each tuple represents a parent and its children.
// tuple[0] is the parent, tuple[1] is the left child and tuple[2] is the right child.
// trees holds the entire proof tree of the batchproof in this way, sorted by the tuple[0].
trees := make([][3]node, 0, len(computablePositions))
// initialise the targetNodes for row 0.
// TODO: this would be more straight forward if bp.Proofs wouldn't contain the targets
proofHashes := make([]Hash, 0, len(proofPositions))
targets := bp.Targets
var targetsMatched uint64
for len(targets) > 0 {
if targets[0] == numLeaves-1 && numLeaves&1 == 1 {
// row 0 root.
rootCandidates = append(rootCandidates, node{Val: roots[0], Pos: targets[0]})
bp.Proof = bp.Proof[1:]
break
}

tagRow := bp.Targets
nextRow := []uint64{}
sortUint64s(tagRow) // probably don't need to sort
if uint64(len(proofPositions)) > targetsMatched &&
targets[0]^1 == proofPositions[targetsMatched] {
lr := targets[0] & 1
targetNodes = append(targetNodes, node{Pos: targets[0], Val: bp.Proof[lr]})
proofHashes = append(proofHashes, bp.Proof[lr^1])
targetsMatched++
bp.Proof = bp.Proof[2:]
targets = targets[1:]
continue
}

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

if verbose {
fmt.Printf("tagrow len %d\n", len(tagRow))
targetNodes = append(targetNodes,
node{Pos: targets[0], Val: bp.Proof[0]},
node{Pos: targets[1], Val: bp.Proof[1]})
bp.Proof = bp.Proof[2:]
targets = targets[2:]
}

var left, right uint64

// iterate through rows
for row := uint8(0); row <= rows; row++ {
// iterate through tagged positions in this row
for len(tagRow) > 0 {
// Efficiency gains here. If there are two or more things to verify,
// check if the next thing to verify is the sibling of the current leaf
// we're on. Siblingness can be checked with bitwise XOR but since targets are
// sorted, we can do bitwise OR instead.
if len(tagRow) > 1 && tagRow[0]|1 == tagRow[1] {
left = tagRow[0]
right = tagRow[1]
tagRow = tagRow[2:]
} else { // if not only use one tagged position
right = tagRow[0] | 1
left = right ^ 1
tagRow = tagRow[1:]
}
proofHashes = append(proofHashes, bp.Proof...)
bp.Proof = proofHashes

// hash every target node with its sibling (which either is contained in the proof or also a target)
for len(targetNodes) > 0 {
var target, proof node
target = targetNodes[0]
if len(proofPositions) > 0 && target.Pos^1 == proofPositions[0] {
// target has a sibling in the proof positions, fetch proof
proof = node{Pos: proofPositions[0], Val: bp.Proof[0]}
proofPositions = proofPositions[1:]
bp.Proof = bp.Proof[1:]
targetNodes = targetNodes[1:]
goto hash
}

if verbose {
fmt.Printf("left %d rootPoss %d\n", left, rootPositions[0])
}
// check for roots
if left == rootPositions[0] {
if verbose {
fmt.Printf("one left in tagrow; should be root\n")
}
// Grab the hash of this position from the map
computedRootHash, ok := proofmap[left]
if !ok {
fmt.Printf("ERR no proofmap for root at %d\n", left)
return false, nil
}
// Verify that this root hash matches the one we stored
if computedRootHash != roots[0] {
fmt.Printf("row %d root, pos %d expect %04x got %04x\n",
row, left, roots[0][:4], computedRootHash[:4])
return false, nil
}
// otherwise OK and pop of the root
roots = roots[1:]
rootPositions = rootPositions[1:]
rootRows = rootRows[1:]
break
}
// target should have its sibling in targetNodes
if len(targetNodes) == 1 {
// sibling not found
return false, nil, nil
}

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

// this will crash if either is 0000
// reconstruct the next row and add the parent to the map
parhash := parentHash(proofmap[left], proofmap[right])
nextRow = append(nextRow, parentPos)
proofmap[parentPos] = parhash
hash:
// figure out which node is left and which is right
left := target
right := proof
if target.Pos&1 == 1 {
right, left = left, right
}

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

// if done with row and there's a root left on this row, remove it
if len(rootRows) > 0 && rootRows[0] == row {
// bit ugly to do these all separately eh
roots = roots[1:]
rootPositions = rootPositions[1:]
rootRows = rootRows[1:]
row := detectRow(parentPos, rows)
if numLeaves&(1<<row) > 0 && parentPos == rootPosition(numLeaves, row, rows) {
// the parent is a root -> store as candidate, to check against actual roots later.
rootCandidates = append(rootCandidates, node{Val: hash, Pos: parentPos})
continue
}
targetNodes = append(targetNodes, node{Val: hash, Pos: parentPos})
}
if len(rootCandidates) == 0 {
// no roots to verify
return false, nil, nil
}
rootMatches := 0
for _, root := range roots {
if len(rootCandidates) > rootMatches && root == rootCandidates[rootMatches].Val {
rootMatches++
}
}
if len(rootCandidates) != rootMatches {
// some roots did not match
return false, nil, nil
}

return true, proofmap
return true, trees, rootCandidates
}

// Reconstruct takes a number of leaves and rows, and turns a block proof back
Expand Down
2 changes: 1 addition & 1 deletion accumulator/forest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func addDelFullBatchProof(nAdds, nDels int) error {
}
bp.SortTargets()
// check block proof. Note this doesn't delete anything, just proves inclusion
worked, _ := verifyBatchProof(bp, f.getRoots(), f.numLeaves, f.rows)
worked, _, _ := verifyBatchProof(bp, f.getRoots(), f.numLeaves, nil)
// worked := f.VerifyBatchProof(bp)

if !worked {
Expand Down
83 changes: 6 additions & 77 deletions accumulator/forestproofs.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,84 +177,13 @@ func (f *Forest) ProveBatch(hs []Hash) (BatchProof, error) {
copy(sortedTargets, bp.Targets)
sortUint64s(sortedTargets)

// TODO feels like you could do all this with just slices and no maps...
// that would be better
// proofTree is the partially populated tree of everything needed for the
// proofs
proofTree := make(map[uint64]Hash)

// go through each target and add a proof for it up to the intersection
for _, pos := range sortedTargets {
// add hash for the deletion itself and its sibling
// if they already exist, skip the whole thing
_, alreadyThere := proofTree[pos]
if alreadyThere {
// fmt.Printf("%d omit already there\n", pos)
continue
}
// TODO change this for the real thing; no need to prove 0-tree root.
// but we still need to verify it and tag it as a target.
if pos == f.numLeaves-1 && pos&1 == 0 {
proofTree[pos] = f.data.read(pos)
// fmt.Printf("%d add as root\n", pos)
continue
}

// always put in both siblings when on the bottom row
// this can be out of order but it will be sorted later
proofTree[pos] = f.data.read(pos)
proofTree[pos^1] = f.data.read(pos ^ 1)
// fmt.Printf("added leaves %d, %d\n", pos, pos^1)

treeTop := detectSubTreeRows(pos, f.numLeaves, f.rows)
pos = parent(pos, f.rows)
// go bottom to top and add siblings into the partial tree
// start at row 1 though; we always populate the bottom leaf and sibling
// This either gets to the top, or intersects before that and deletes
// something
for h := uint8(1); h < treeTop; h++ {
// check if the sibling is already there, in which case we're done
// also check if the parent itself is there, in which case we delete it!
// I think this with the early ignore at the bottom make it optimal
_, selfThere := proofTree[pos]
_, sibThere := proofTree[pos^1]
if sibThere {
// sibling position already exists in partial tree; done
// with this branch

// TODO seems that this never happens and can be removed
panic("this never happens...?")
}
if selfThere {
// self position already there; remove as children are known
// fmt.Printf("remove proof from pos %d\n", pos)

delete(proofTree, pos)
delete(proofTree, pos^1) // right? can delete both..?
break
}
// fmt.Printf("add proof from pos %d\n", pos^1)
proofTree[pos^1] = f.data.read(pos ^ 1)
pos = parent(pos, f.rows)
}
}

var nodeSlice []node

// run through partial tree to turn it into a slice
for pos, hash := range proofTree {
nodeSlice = append(nodeSlice, node{pos, hash})
proofPositions, _ := ProofPositions(sortedTargets, f.numLeaves, f.rows)
targetsAndProof := mergeSortedSlices(proofPositions, sortedTargets)
bp.Proof = make([]Hash, len(targetsAndProof))
for i, proofPos := range targetsAndProof {
bp.Proof[i] = f.data.read(proofPos)
}
// fmt.Printf("made nodeSlice %d nodes\n", len(nodeSlice))

// sort the slice of nodes (even though we only want the hashes)
sortNodeSlice(nodeSlice)
// copy the sorted / in-order hashes into a hash slice
bp.Proof = make([]Hash, len(nodeSlice))

for i, n := range nodeSlice {
bp.Proof[i] = n.Val
}
if verbose {
fmt.Printf("blockproof targets: %v\n", bp.Targets)
}
Expand All @@ -266,6 +195,6 @@ func (f *Forest) ProveBatch(hs []Hash) (BatchProof, error) {

// VerifyBatchProof :
func (f *Forest) VerifyBatchProof(bp BatchProof) bool {
ok, _ := verifyBatchProof(bp, f.getRoots(), f.numLeaves, f.rows)
ok, _, _ := verifyBatchProof(bp, f.getRoots(), f.numLeaves, nil)
return ok
}
Loading

0 comments on commit 7f63247

Please sign in to comment.