mirror of https://github.com/ethereum/go-ethereum
bmt: Binary Merkle Tree Hash (#14334)
bmt is a new package that provides hashers for binary merkle tree hashes on size-limited chunks. the main motivation is that using BMT hash as the chunk hash of the swarm hash offers logsize inclusion proofs for arbitrary files on a 32-byte resolution completely viable to use in challenges on the blockchain.pull/15092/head
parent
32d8d42274
commit
2bacf36d80
@ -0,0 +1,562 @@ |
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package bmt provides a binary merkle tree implementation
|
||||
package bmt |
||||
|
||||
import ( |
||||
"fmt" |
||||
"hash" |
||||
"io" |
||||
"strings" |
||||
"sync" |
||||
"sync/atomic" |
||||
) |
||||
|
||||
/* |
||||
Binary Merkle Tree Hash is a hash function over arbitrary datachunks of limited size |
||||
It is defined as the root hash of the binary merkle tree built over fixed size segments |
||||
of the underlying chunk using any base hash function (e.g keccak 256 SHA3) |
||||
|
||||
It is used as the chunk hash function in swarm which in turn is the basis for the |
||||
128 branching swarm hash http://swarm-guide.readthedocs.io/en/latest/architecture.html#swarm-hash
|
||||
|
||||
The BMT is optimal for providing compact inclusion proofs, i.e. prove that a |
||||
segment is a substring of a chunk starting at a particular offset |
||||
The size of the underlying segments is fixed at 32 bytes (called the resolution |
||||
of the BMT hash), the EVM word size to optimize for on-chain BMT verification |
||||
as well as the hash size optimal for inclusion proofs in the merkle tree of the swarm hash. |
||||
|
||||
Two implementations are provided: |
||||
|
||||
* RefHasher is optimized for code simplicity and meant as a reference implementation |
||||
* Hasher is optimized for speed taking advantage of concurrency with minimalistic |
||||
control structure to coordinate the concurrent routines |
||||
It implements the ChunkHash interface as well as the go standard hash.Hash interface |
||||
|
||||
*/ |
||||
|
||||
const ( |
||||
// DefaultSegmentCount is the maximum number of segments of the underlying chunk
|
||||
DefaultSegmentCount = 128 // Should be equal to storage.DefaultBranches
|
||||
// DefaultPoolSize is the maximum number of bmt trees used by the hashers, i.e,
|
||||
// the maximum number of concurrent BMT hashing operations performed by the same hasher
|
||||
DefaultPoolSize = 8 |
||||
) |
||||
|
||||
// BaseHasher is a hash.Hash constructor function used for the base hash of the BMT.
|
||||
type BaseHasher func() hash.Hash |
||||
|
||||
// Hasher a reusable hasher for fixed maximum size chunks representing a BMT
|
||||
// implements the hash.Hash interface
|
||||
// reuse pool of Tree-s for amortised memory allocation and resource control
|
||||
// supports order-agnostic concurrent segment writes
|
||||
// as well as sequential read and write
|
||||
// can not be called concurrently on more than one chunk
|
||||
// can be further appended after Sum
|
||||
// Reset gives back the Tree to the pool and guaranteed to leave
|
||||
// the tree and itself in a state reusable for hashing a new chunk
|
||||
type Hasher struct { |
||||
pool *TreePool // BMT resource pool
|
||||
bmt *Tree // prebuilt BMT resource for flowcontrol and proofs
|
||||
blocksize int // segment size (size of hash) also for hash.Hash
|
||||
count int // segment count
|
||||
size int // for hash.Hash same as hashsize
|
||||
cur int // cursor position for righmost currently open chunk
|
||||
segment []byte // the rightmost open segment (not complete)
|
||||
depth int // index of last level
|
||||
result chan []byte // result channel
|
||||
hash []byte // to record the result
|
||||
max int32 // max segments for SegmentWriter interface
|
||||
blockLength []byte // The block length that needes to be added in Sum
|
||||
} |
||||
|
||||
// New creates a reusable Hasher
|
||||
// implements the hash.Hash interface
|
||||
// pulls a new Tree from a resource pool for hashing each chunk
|
||||
func New(p *TreePool) *Hasher { |
||||
return &Hasher{ |
||||
pool: p, |
||||
depth: depth(p.SegmentCount), |
||||
size: p.SegmentSize, |
||||
blocksize: p.SegmentSize, |
||||
count: p.SegmentCount, |
||||
result: make(chan []byte), |
||||
} |
||||
} |
||||
|
||||
// Node is a reuseable segment hasher representing a node in a BMT
|
||||
// it allows for continued writes after a Sum
|
||||
// and is left in completely reusable state after Reset
|
||||
type Node struct { |
||||
level, index int // position of node for information/logging only
|
||||
initial bool // first and last node
|
||||
root bool // whether the node is root to a smaller BMT
|
||||
isLeft bool // whether it is left side of the parent double segment
|
||||
unbalanced bool // indicates if a node has only the left segment
|
||||
parent *Node // BMT connections
|
||||
state int32 // atomic increment impl concurrent boolean toggle
|
||||
left, right []byte |
||||
} |
||||
|
||||
// NewNode constructor for segment hasher nodes in the BMT
|
||||
func NewNode(level, index int, parent *Node) *Node { |
||||
return &Node{ |
||||
parent: parent, |
||||
level: level, |
||||
index: index, |
||||
initial: index == 0, |
||||
isLeft: index%2 == 0, |
||||
} |
||||
} |
||||
|
||||
// TreePool provides a pool of Trees used as resources by Hasher
|
||||
// a Tree popped from the pool is guaranteed to have clean state
|
||||
// for hashing a new chunk
|
||||
// Hasher Reset releases the Tree to the pool
|
||||
type TreePool struct { |
||||
lock sync.Mutex |
||||
c chan *Tree |
||||
hasher BaseHasher |
||||
SegmentSize int |
||||
SegmentCount int |
||||
Capacity int |
||||
count int |
||||
} |
||||
|
||||
// NewTreePool creates a Tree pool with hasher, segment size, segment count and capacity
|
||||
// on GetTree it reuses free Trees or creates a new one if size is not reached
|
||||
func NewTreePool(hasher BaseHasher, segmentCount, capacity int) *TreePool { |
||||
return &TreePool{ |
||||
c: make(chan *Tree, capacity), |
||||
hasher: hasher, |
||||
SegmentSize: hasher().Size(), |
||||
SegmentCount: segmentCount, |
||||
Capacity: capacity, |
||||
} |
||||
} |
||||
|
||||
// Drain drains the pool uptil it has no more than n resources
|
||||
func (self *TreePool) Drain(n int) { |
||||
self.lock.Lock() |
||||
defer self.lock.Unlock() |
||||
for len(self.c) > n { |
||||
<-self.c |
||||
self.count-- |
||||
} |
||||
} |
||||
|
||||
// Reserve is blocking until it returns an available Tree
|
||||
// it reuses free Trees or creates a new one if size is not reached
|
||||
func (self *TreePool) Reserve() *Tree { |
||||
self.lock.Lock() |
||||
defer self.lock.Unlock() |
||||
var t *Tree |
||||
if self.count == self.Capacity { |
||||
return <-self.c |
||||
} |
||||
select { |
||||
case t = <-self.c: |
||||
default: |
||||
t = NewTree(self.hasher, self.SegmentSize, self.SegmentCount) |
||||
self.count++ |
||||
} |
||||
return t |
||||
} |
||||
|
||||
// Release gives back a Tree to the pool.
|
||||
// This Tree is guaranteed to be in reusable state
|
||||
// does not need locking
|
||||
func (self *TreePool) Release(t *Tree) { |
||||
self.c <- t // can never fail but...
|
||||
} |
||||
|
||||
// Tree is a reusable control structure representing a BMT
|
||||
// organised in a binary tree
|
||||
// Hasher uses a TreePool to pick one for each chunk hash
|
||||
// the Tree is 'locked' while not in the pool
|
||||
type Tree struct { |
||||
leaves []*Node |
||||
} |
||||
|
||||
// Draw draws the BMT (badly)
|
||||
func (self *Tree) Draw(hash []byte, d int) string { |
||||
var left, right []string |
||||
var anc []*Node |
||||
for i, n := range self.leaves { |
||||
left = append(left, fmt.Sprintf("%v", hashstr(n.left))) |
||||
if i%2 == 0 { |
||||
anc = append(anc, n.parent) |
||||
} |
||||
right = append(right, fmt.Sprintf("%v", hashstr(n.right))) |
||||
} |
||||
anc = self.leaves |
||||
var hashes [][]string |
||||
for l := 0; len(anc) > 0; l++ { |
||||
var nodes []*Node |
||||
hash := []string{""} |
||||
for i, n := range anc { |
||||
hash = append(hash, fmt.Sprintf("%v|%v", hashstr(n.left), hashstr(n.right))) |
||||
if i%2 == 0 && n.parent != nil { |
||||
nodes = append(nodes, n.parent) |
||||
} |
||||
} |
||||
hash = append(hash, "") |
||||
hashes = append(hashes, hash) |
||||
anc = nodes |
||||
} |
||||
hashes = append(hashes, []string{"", fmt.Sprintf("%v", hashstr(hash)), ""}) |
||||
total := 60 |
||||
del := " " |
||||
var rows []string |
||||
for i := len(hashes) - 1; i >= 0; i-- { |
||||
var textlen int |
||||
hash := hashes[i] |
||||
for _, s := range hash { |
||||
textlen += len(s) |
||||
} |
||||
if total < textlen { |
||||
total = textlen + len(hash) |
||||
} |
||||
delsize := (total - textlen) / (len(hash) - 1) |
||||
if delsize > len(del) { |
||||
delsize = len(del) |
||||
} |
||||
row := fmt.Sprintf("%v: %v", len(hashes)-i-1, strings.Join(hash, del[:delsize])) |
||||
rows = append(rows, row) |
||||
|
||||
} |
||||
rows = append(rows, strings.Join(left, " ")) |
||||
rows = append(rows, strings.Join(right, " ")) |
||||
return strings.Join(rows, "\n") + "\n" |
||||
} |
||||
|
||||
// NewTree initialises the Tree by building up the nodes of a BMT
|
||||
// segment size is stipulated to be the size of the hash
|
||||
// segmentCount needs to be positive integer and does not need to be
|
||||
// a power of two and can even be an odd number
|
||||
// segmentSize * segmentCount determines the maximum chunk size
|
||||
// hashed using the tree
|
||||
func NewTree(hasher BaseHasher, segmentSize, segmentCount int) *Tree { |
||||
n := NewNode(0, 0, nil) |
||||
n.root = true |
||||
prevlevel := []*Node{n} |
||||
// iterate over levels and creates 2^level nodes
|
||||
level := 1 |
||||
count := 2 |
||||
for d := 1; d <= depth(segmentCount); d++ { |
||||
nodes := make([]*Node, count) |
||||
for i := 0; i < len(nodes); i++ { |
||||
var parent *Node |
||||
parent = prevlevel[i/2] |
||||
t := NewNode(level, i, parent) |
||||
nodes[i] = t |
||||
} |
||||
prevlevel = nodes |
||||
level++ |
||||
count *= 2 |
||||
} |
||||
// the datanode level is the nodes on the last level where
|
||||
return &Tree{ |
||||
leaves: prevlevel, |
||||
} |
||||
} |
||||
|
||||
// methods needed by hash.Hash
|
||||
|
||||
// Size returns the size
|
||||
func (self *Hasher) Size() int { |
||||
return self.size |
||||
} |
||||
|
||||
// BlockSize returns the block size
|
||||
func (self *Hasher) BlockSize() int { |
||||
return self.blocksize |
||||
} |
||||
|
||||
// Sum returns the hash of the buffer
|
||||
// hash.Hash interface Sum method appends the byte slice to the underlying
|
||||
// data before it calculates and returns the hash of the chunk
|
||||
func (self *Hasher) Sum(b []byte) (r []byte) { |
||||
t := self.bmt |
||||
i := self.cur |
||||
n := t.leaves[i] |
||||
j := i |
||||
// must run strictly before all nodes calculate
|
||||
// datanodes are guaranteed to have a parent
|
||||
if len(self.segment) > self.size && i > 0 && n.parent != nil { |
||||
n = n.parent |
||||
} else { |
||||
i *= 2 |
||||
} |
||||
d := self.finalise(n, i) |
||||
self.writeSegment(j, self.segment, d) |
||||
c := <-self.result |
||||
self.releaseTree() |
||||
|
||||
// sha3(length + BMT(pure_chunk))
|
||||
if self.blockLength == nil { |
||||
return c |
||||
} |
||||
res := self.pool.hasher() |
||||
res.Reset() |
||||
res.Write(self.blockLength) |
||||
res.Write(c) |
||||
return res.Sum(nil) |
||||
} |
||||
|
||||
// Hasher implements the SwarmHash interface
|
||||
|
||||
// Hash waits for the hasher result and returns it
|
||||
// caller must call this on a BMT Hasher being written to
|
||||
func (self *Hasher) Hash() []byte { |
||||
return <-self.result |
||||
} |
||||
|
||||
// Hasher implements the io.Writer interface
|
||||
|
||||
// Write fills the buffer to hash
|
||||
// with every full segment complete launches a hasher go routine
|
||||
// that shoots up the BMT
|
||||
func (self *Hasher) Write(b []byte) (int, error) { |
||||
l := len(b) |
||||
if l <= 0 { |
||||
return 0, nil |
||||
} |
||||
s := self.segment |
||||
i := self.cur |
||||
count := (self.count + 1) / 2 |
||||
need := self.count*self.size - self.cur*2*self.size |
||||
size := self.size |
||||
if need > size { |
||||
size *= 2 |
||||
} |
||||
if l < need { |
||||
need = l |
||||
} |
||||
// calculate missing bit to complete current open segment
|
||||
rest := size - len(s) |
||||
if need < rest { |
||||
rest = need |
||||
} |
||||
s = append(s, b[:rest]...) |
||||
need -= rest |
||||
// read full segments and the last possibly partial segment
|
||||
for need > 0 && i < count-1 { |
||||
// push all finished chunks we read
|
||||
self.writeSegment(i, s, self.depth) |
||||
need -= size |
||||
if need < 0 { |
||||
size += need |
||||
} |
||||
s = b[rest : rest+size] |
||||
rest += size |
||||
i++ |
||||
} |
||||
self.segment = s |
||||
self.cur = i |
||||
// otherwise, we can assume len(s) == 0, so all buffer is read and chunk is not yet full
|
||||
return l, nil |
||||
} |
||||
|
||||
// Hasher implements the io.ReaderFrom interface
|
||||
|
||||
// ReadFrom reads from io.Reader and appends to the data to hash using Write
|
||||
// it reads so that chunk to hash is maximum length or reader reaches EOF
|
||||
// caller must Reset the hasher prior to call
|
||||
func (self *Hasher) ReadFrom(r io.Reader) (m int64, err error) { |
||||
bufsize := self.size*self.count - self.size*self.cur - len(self.segment) |
||||
buf := make([]byte, bufsize) |
||||
var read int |
||||
for { |
||||
var n int |
||||
n, err = r.Read(buf) |
||||
read += n |
||||
if err == io.EOF || read == len(buf) { |
||||
hash := self.Sum(buf[:n]) |
||||
if read == len(buf) { |
||||
err = NewEOC(hash) |
||||
} |
||||
break |
||||
} |
||||
if err != nil { |
||||
break |
||||
} |
||||
n, err = self.Write(buf[:n]) |
||||
if err != nil { |
||||
break |
||||
} |
||||
} |
||||
return int64(read), err |
||||
} |
||||
|
||||
// Reset needs to be called before writing to the hasher
|
||||
func (self *Hasher) Reset() { |
||||
self.getTree() |
||||
self.blockLength = nil |
||||
} |
||||
|
||||
// Hasher implements the SwarmHash interface
|
||||
|
||||
// ResetWithLength needs to be called before writing to the hasher
|
||||
// the argument is supposed to be the byte slice binary representation of
|
||||
// the legth of the data subsumed under the hash
|
||||
func (self *Hasher) ResetWithLength(l []byte) { |
||||
self.Reset() |
||||
self.blockLength = l |
||||
|
||||
} |
||||
|
||||
// Release gives back the Tree to the pool whereby it unlocks
|
||||
// it resets tree, segment and index
|
||||
func (self *Hasher) releaseTree() { |
||||
if self.bmt != nil { |
||||
n := self.bmt.leaves[self.cur] |
||||
for ; n != nil; n = n.parent { |
||||
n.unbalanced = false |
||||
if n.parent != nil { |
||||
n.root = false |
||||
} |
||||
} |
||||
self.pool.Release(self.bmt) |
||||
self.bmt = nil |
||||
|
||||
} |
||||
self.cur = 0 |
||||
self.segment = nil |
||||
} |
||||
|
||||
func (self *Hasher) writeSegment(i int, s []byte, d int) { |
||||
h := self.pool.hasher() |
||||
n := self.bmt.leaves[i] |
||||
|
||||
if len(s) > self.size && n.parent != nil { |
||||
go func() { |
||||
h.Reset() |
||||
h.Write(s) |
||||
s = h.Sum(nil) |
||||
|
||||
if n.root { |
||||
self.result <- s |
||||
return |
||||
} |
||||
self.run(n.parent, h, d, n.index, s) |
||||
}() |
||||
return |
||||
} |
||||
go self.run(n, h, d, i*2, s) |
||||
} |
||||
|
||||
func (self *Hasher) run(n *Node, h hash.Hash, d int, i int, s []byte) { |
||||
isLeft := i%2 == 0 |
||||
for { |
||||
if isLeft { |
||||
n.left = s |
||||
} else { |
||||
n.right = s |
||||
} |
||||
if !n.unbalanced && n.toggle() { |
||||
return |
||||
} |
||||
if !n.unbalanced || !isLeft || i == 0 && d == 0 { |
||||
h.Reset() |
||||
h.Write(n.left) |
||||
h.Write(n.right) |
||||
s = h.Sum(nil) |
||||
|
||||
} else { |
||||
s = append(n.left, n.right...) |
||||
} |
||||
|
||||
self.hash = s |
||||
if n.root { |
||||
self.result <- s |
||||
return |
||||
} |
||||
|
||||
isLeft = n.isLeft |
||||
n = n.parent |
||||
i++ |
||||
} |
||||
} |
||||
|
||||
// getTree obtains a BMT resource by reserving one from the pool
|
||||
func (self *Hasher) getTree() *Tree { |
||||
if self.bmt != nil { |
||||
return self.bmt |
||||
} |
||||
t := self.pool.Reserve() |
||||
self.bmt = t |
||||
return t |
||||
} |
||||
|
||||
// atomic bool toggle implementing a concurrent reusable 2-state object
|
||||
// atomic addint with %2 implements atomic bool toggle
|
||||
// it returns true if the toggler just put it in the active/waiting state
|
||||
func (self *Node) toggle() bool { |
||||
return atomic.AddInt32(&self.state, 1)%2 == 1 |
||||
} |
||||
|
||||
func hashstr(b []byte) string { |
||||
end := len(b) |
||||
if end > 4 { |
||||
end = 4 |
||||
} |
||||
return fmt.Sprintf("%x", b[:end]) |
||||
} |
||||
|
||||
func depth(n int) (d int) { |
||||
for l := (n - 1) / 2; l > 0; l /= 2 { |
||||
d++ |
||||
} |
||||
return d |
||||
} |
||||
|
||||
// finalise is following the zigzags on the tree belonging
|
||||
// to the final datasegment
|
||||
func (self *Hasher) finalise(n *Node, i int) (d int) { |
||||
isLeft := i%2 == 0 |
||||
for { |
||||
// when the final segment's path is going via left segments
|
||||
// the incoming data is pushed to the parent upon pulling the left
|
||||
// we do not need toogle the state since this condition is
|
||||
// detectable
|
||||
n.unbalanced = isLeft |
||||
n.right = nil |
||||
if n.initial { |
||||
n.root = true |
||||
return d |
||||
} |
||||
isLeft = n.isLeft |
||||
n = n.parent |
||||
d++ |
||||
} |
||||
} |
||||
|
||||
// EOC (end of chunk) implements the error interface
|
||||
type EOC struct { |
||||
Hash []byte // read the hash of the chunk off the error
|
||||
} |
||||
|
||||
// Error returns the error string
|
||||
func (self *EOC) Error() string { |
||||
return fmt.Sprintf("hasher limit reached, chunk hash: %x", self.Hash) |
||||
} |
||||
|
||||
// NewEOC creates new end of chunk error with the hash
|
||||
func NewEOC(hash []byte) *EOC { |
||||
return &EOC{hash} |
||||
} |
@ -0,0 +1,85 @@ |
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// simple nonconcurrent reference implementation for hashsize segment based
|
||||
// Binary Merkle tree hash on arbitrary but fixed maximum chunksize
|
||||
//
|
||||
// This implementation does not take advantage of any paralellisms and uses
|
||||
// far more memory than necessary, but it is easy to see that it is correct.
|
||||
// It can be used for generating test cases for optimized implementations.
|
||||
// see testBMTHasherCorrectness function in bmt_test.go
|
||||
package bmt |
||||
|
||||
import ( |
||||
"hash" |
||||
) |
||||
|
||||
// RefHasher is the non-optimized easy to read reference implementation of BMT
|
||||
type RefHasher struct { |
||||
span int |
||||
section int |
||||
cap int |
||||
h hash.Hash |
||||
} |
||||
|
||||
// NewRefHasher returns a new RefHasher
|
||||
func NewRefHasher(hasher BaseHasher, count int) *RefHasher { |
||||
h := hasher() |
||||
hashsize := h.Size() |
||||
maxsize := hashsize * count |
||||
c := 2 |
||||
for ; c < count; c *= 2 { |
||||
} |
||||
if c > 2 { |
||||
c /= 2 |
||||
} |
||||
return &RefHasher{ |
||||
section: 2 * hashsize, |
||||
span: c * hashsize, |
||||
cap: maxsize, |
||||
h: h, |
||||
} |
||||
} |
||||
|
||||
// Hash returns the BMT hash of the byte slice
|
||||
// implements the SwarmHash interface
|
||||
func (rh *RefHasher) Hash(d []byte) []byte { |
||||
if len(d) > rh.cap { |
||||
d = d[:rh.cap] |
||||
} |
||||
|
||||
return rh.hash(d, rh.span) |
||||
} |
||||
|
||||
func (rh *RefHasher) hash(d []byte, s int) []byte { |
||||
l := len(d) |
||||
left := d |
||||
var right []byte |
||||
if l > rh.section { |
||||
for ; s >= l; s /= 2 { |
||||
} |
||||
left = rh.hash(d[:s], s) |
||||
right = d[s:] |
||||
if l-s > rh.section/2 { |
||||
right = rh.hash(right, s) |
||||
} |
||||
} |
||||
defer rh.h.Reset() |
||||
rh.h.Write(left) |
||||
rh.h.Write(right) |
||||
h := rh.h.Sum(nil) |
||||
return h |
||||
} |
@ -0,0 +1,481 @@ |
||||
// Copyright 2017 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package bmt |
||||
|
||||
import ( |
||||
"bytes" |
||||
crand "crypto/rand" |
||||
"fmt" |
||||
"hash" |
||||
"io" |
||||
"math/rand" |
||||
"sync" |
||||
"sync/atomic" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/crypto/sha3" |
||||
) |
||||
|
||||
const ( |
||||
maxproccnt = 8 |
||||
) |
||||
|
||||
// TestRefHasher tests that the RefHasher computes the expected BMT hash for
|
||||
// all data lengths between 0 and 256 bytes
|
||||
func TestRefHasher(t *testing.T) { |
||||
hashFunc := sha3.NewKeccak256 |
||||
|
||||
sha3 := func(data ...[]byte) []byte { |
||||
h := hashFunc() |
||||
for _, v := range data { |
||||
h.Write(v) |
||||
} |
||||
return h.Sum(nil) |
||||
} |
||||
|
||||
// the test struct is used to specify the expected BMT hash for data
|
||||
// lengths between "from" and "to"
|
||||
type test struct { |
||||
from int64 |
||||
to int64 |
||||
expected func([]byte) []byte |
||||
} |
||||
|
||||
var tests []*test |
||||
|
||||
// all lengths in [0,64] should be:
|
||||
//
|
||||
// sha3(data)
|
||||
//
|
||||
tests = append(tests, &test{ |
||||
from: 0, |
||||
to: 64, |
||||
expected: func(data []byte) []byte { |
||||
return sha3(data) |
||||
}, |
||||
}) |
||||
|
||||
// all lengths in [65,96] should be:
|
||||
//
|
||||
// sha3(
|
||||
// sha3(data[:64])
|
||||
// data[64:]
|
||||
// )
|
||||
//
|
||||
tests = append(tests, &test{ |
||||
from: 65, |
||||
to: 96, |
||||
expected: func(data []byte) []byte { |
||||
return sha3(sha3(data[:64]), data[64:]) |
||||
}, |
||||
}) |
||||
|
||||
// all lengths in [97,128] should be:
|
||||
//
|
||||
// sha3(
|
||||
// sha3(data[:64])
|
||||
// sha3(data[64:])
|
||||
// )
|
||||
//
|
||||
tests = append(tests, &test{ |
||||
from: 97, |
||||
to: 128, |
||||
expected: func(data []byte) []byte { |
||||
return sha3(sha3(data[:64]), sha3(data[64:])) |
||||
}, |
||||
}) |
||||
|
||||
// all lengths in [129,160] should be:
|
||||
//
|
||||
// sha3(
|
||||
// sha3(
|
||||
// sha3(data[:64])
|
||||
// sha3(data[64:128])
|
||||
// )
|
||||
// data[128:]
|
||||
// )
|
||||
//
|
||||
tests = append(tests, &test{ |
||||
from: 129, |
||||
to: 160, |
||||
expected: func(data []byte) []byte { |
||||
return sha3(sha3(sha3(data[:64]), sha3(data[64:128])), data[128:]) |
||||
}, |
||||
}) |
||||
|
||||
// all lengths in [161,192] should be:
|
||||
//
|
||||
// sha3(
|
||||
// sha3(
|
||||
// sha3(data[:64])
|
||||
// sha3(data[64:128])
|
||||
// )
|
||||
// sha3(data[128:])
|
||||
// )
|
||||
//
|
||||
tests = append(tests, &test{ |
||||
from: 161, |
||||
to: 192, |
||||
expected: func(data []byte) []byte { |
||||
return sha3(sha3(sha3(data[:64]), sha3(data[64:128])), sha3(data[128:])) |
||||
}, |
||||
}) |
||||
|
||||
// all lengths in [193,224] should be:
|
||||
//
|
||||
// sha3(
|
||||
// sha3(
|
||||
// sha3(data[:64])
|
||||
// sha3(data[64:128])
|
||||
// )
|
||||
// sha3(
|
||||
// sha3(data[128:192])
|
||||
// data[192:]
|
||||
// )
|
||||
// )
|
||||
//
|
||||
tests = append(tests, &test{ |
||||
from: 193, |
||||
to: 224, |
||||
expected: func(data []byte) []byte { |
||||
return sha3(sha3(sha3(data[:64]), sha3(data[64:128])), sha3(sha3(data[128:192]), data[192:])) |
||||
}, |
||||
}) |
||||
|
||||
// all lengths in [225,256] should be:
|
||||
//
|
||||
// sha3(
|
||||
// sha3(
|
||||
// sha3(data[:64])
|
||||
// sha3(data[64:128])
|
||||
// )
|
||||
// sha3(
|
||||
// sha3(data[128:192])
|
||||
// sha3(data[192:])
|
||||
// )
|
||||
// )
|
||||
//
|
||||
tests = append(tests, &test{ |
||||
from: 225, |
||||
to: 256, |
||||
expected: func(data []byte) []byte { |
||||
return sha3(sha3(sha3(data[:64]), sha3(data[64:128])), sha3(sha3(data[128:192]), sha3(data[192:]))) |
||||
}, |
||||
}) |
||||
|
||||
// run the tests
|
||||
for _, x := range tests { |
||||
for length := x.from; length <= x.to; length++ { |
||||
t.Run(fmt.Sprintf("%d_bytes", length), func(t *testing.T) { |
||||
data := make([]byte, length) |
||||
if _, err := io.ReadFull(crand.Reader, data); err != nil && err != io.EOF { |
||||
t.Fatal(err) |
||||
} |
||||
expected := x.expected(data) |
||||
actual := NewRefHasher(hashFunc, 128).Hash(data) |
||||
if !bytes.Equal(actual, expected) { |
||||
t.Fatalf("expected %x, got %x", expected, actual) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func testDataReader(l int) (r io.Reader) { |
||||
return io.LimitReader(crand.Reader, int64(l)) |
||||
} |
||||
|
||||
func TestHasherCorrectness(t *testing.T) { |
||||
err := testHasher(testBaseHasher) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
} |
||||
|
||||
func testHasher(f func(BaseHasher, []byte, int, int) error) error { |
||||
tdata := testDataReader(4128) |
||||
data := make([]byte, 4128) |
||||
tdata.Read(data) |
||||
hasher := sha3.NewKeccak256 |
||||
size := hasher().Size() |
||||
counts := []int{1, 2, 3, 4, 5, 8, 16, 32, 64, 128} |
||||
|
||||
var err error |
||||
for _, count := range counts { |
||||
max := count * size |
||||
incr := 1 |
||||
for n := 0; n <= max+incr; n += incr { |
||||
err = f(hasher, data, n, count) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func TestHasherReuseWithoutRelease(t *testing.T) { |
||||
testHasherReuse(1, t) |
||||
} |
||||
|
||||
func TestHasherReuseWithRelease(t *testing.T) { |
||||
testHasherReuse(maxproccnt, t) |
||||
} |
||||
|
||||
func testHasherReuse(i int, t *testing.T) { |
||||
hasher := sha3.NewKeccak256 |
||||
pool := NewTreePool(hasher, 128, i) |
||||
defer pool.Drain(0) |
||||
bmt := New(pool) |
||||
|
||||
for i := 0; i < 500; i++ { |
||||
n := rand.Intn(4096) |
||||
tdata := testDataReader(n) |
||||
data := make([]byte, n) |
||||
tdata.Read(data) |
||||
|
||||
err := testHasherCorrectness(bmt, hasher, data, n, 128) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestHasherConcurrency(t *testing.T) { |
||||
hasher := sha3.NewKeccak256 |
||||
pool := NewTreePool(hasher, 128, maxproccnt) |
||||
defer pool.Drain(0) |
||||
wg := sync.WaitGroup{} |
||||
cycles := 100 |
||||
wg.Add(maxproccnt * cycles) |
||||
errc := make(chan error) |
||||
|
||||
for p := 0; p < maxproccnt; p++ { |
||||
for i := 0; i < cycles; i++ { |
||||
go func() { |
||||
bmt := New(pool) |
||||
n := rand.Intn(4096) |
||||
tdata := testDataReader(n) |
||||
data := make([]byte, n) |
||||
tdata.Read(data) |
||||
err := testHasherCorrectness(bmt, hasher, data, n, 128) |
||||
wg.Done() |
||||
if err != nil { |
||||
errc <- err |
||||
} |
||||
}() |
||||
} |
||||
} |
||||
go func() { |
||||
wg.Wait() |
||||
close(errc) |
||||
}() |
||||
var err error |
||||
select { |
||||
case <-time.NewTimer(5 * time.Second).C: |
||||
err = fmt.Errorf("timed out") |
||||
case err = <-errc: |
||||
} |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
} |
||||
|
||||
func testBaseHasher(hasher BaseHasher, d []byte, n, count int) error { |
||||
pool := NewTreePool(hasher, count, 1) |
||||
defer pool.Drain(0) |
||||
bmt := New(pool) |
||||
return testHasherCorrectness(bmt, hasher, d, n, count) |
||||
} |
||||
|
||||
func testHasherCorrectness(bmt hash.Hash, hasher BaseHasher, d []byte, n, count int) (err error) { |
||||
data := d[:n] |
||||
rbmt := NewRefHasher(hasher, count) |
||||
exp := rbmt.Hash(data) |
||||
timeout := time.NewTimer(time.Second) |
||||
c := make(chan error) |
||||
|
||||
go func() { |
||||
bmt.Reset() |
||||
bmt.Write(data) |
||||
got := bmt.Sum(nil) |
||||
if !bytes.Equal(got, exp) { |
||||
c <- fmt.Errorf("wrong hash: expected %x, got %x", exp, got) |
||||
} |
||||
close(c) |
||||
}() |
||||
select { |
||||
case <-timeout.C: |
||||
err = fmt.Errorf("BMT hash calculation timed out") |
||||
case err = <-c: |
||||
} |
||||
return err |
||||
} |
||||
|
||||
func BenchmarkSHA3_4k(t *testing.B) { benchmarkSHA3(4096, t) } |
||||
func BenchmarkSHA3_2k(t *testing.B) { benchmarkSHA3(4096/2, t) } |
||||
func BenchmarkSHA3_1k(t *testing.B) { benchmarkSHA3(4096/4, t) } |
||||
func BenchmarkSHA3_512b(t *testing.B) { benchmarkSHA3(4096/8, t) } |
||||
func BenchmarkSHA3_256b(t *testing.B) { benchmarkSHA3(4096/16, t) } |
||||
func BenchmarkSHA3_128b(t *testing.B) { benchmarkSHA3(4096/32, t) } |
||||
|
||||
func BenchmarkBMTBaseline_4k(t *testing.B) { benchmarkBMTBaseline(4096, t) } |
||||
func BenchmarkBMTBaseline_2k(t *testing.B) { benchmarkBMTBaseline(4096/2, t) } |
||||
func BenchmarkBMTBaseline_1k(t *testing.B) { benchmarkBMTBaseline(4096/4, t) } |
||||
func BenchmarkBMTBaseline_512b(t *testing.B) { benchmarkBMTBaseline(4096/8, t) } |
||||
func BenchmarkBMTBaseline_256b(t *testing.B) { benchmarkBMTBaseline(4096/16, t) } |
||||
func BenchmarkBMTBaseline_128b(t *testing.B) { benchmarkBMTBaseline(4096/32, t) } |
||||
|
||||
func BenchmarkRefHasher_4k(t *testing.B) { benchmarkRefHasher(4096, t) } |
||||
func BenchmarkRefHasher_2k(t *testing.B) { benchmarkRefHasher(4096/2, t) } |
||||
func BenchmarkRefHasher_1k(t *testing.B) { benchmarkRefHasher(4096/4, t) } |
||||
func BenchmarkRefHasher_512b(t *testing.B) { benchmarkRefHasher(4096/8, t) } |
||||
func BenchmarkRefHasher_256b(t *testing.B) { benchmarkRefHasher(4096/16, t) } |
||||
func BenchmarkRefHasher_128b(t *testing.B) { benchmarkRefHasher(4096/32, t) } |
||||
|
||||
func BenchmarkHasher_4k(t *testing.B) { benchmarkHasher(4096, t) } |
||||
func BenchmarkHasher_2k(t *testing.B) { benchmarkHasher(4096/2, t) } |
||||
func BenchmarkHasher_1k(t *testing.B) { benchmarkHasher(4096/4, t) } |
||||
func BenchmarkHasher_512b(t *testing.B) { benchmarkHasher(4096/8, t) } |
||||
func BenchmarkHasher_256b(t *testing.B) { benchmarkHasher(4096/16, t) } |
||||
func BenchmarkHasher_128b(t *testing.B) { benchmarkHasher(4096/32, t) } |
||||
|
||||
func BenchmarkHasherNoReuse_4k(t *testing.B) { benchmarkHasherReuse(1, 4096, t) } |
||||
func BenchmarkHasherNoReuse_2k(t *testing.B) { benchmarkHasherReuse(1, 4096/2, t) } |
||||
func BenchmarkHasherNoReuse_1k(t *testing.B) { benchmarkHasherReuse(1, 4096/4, t) } |
||||
func BenchmarkHasherNoReuse_512b(t *testing.B) { benchmarkHasherReuse(1, 4096/8, t) } |
||||
func BenchmarkHasherNoReuse_256b(t *testing.B) { benchmarkHasherReuse(1, 4096/16, t) } |
||||
func BenchmarkHasherNoReuse_128b(t *testing.B) { benchmarkHasherReuse(1, 4096/32, t) } |
||||
|
||||
func BenchmarkHasherReuse_4k(t *testing.B) { benchmarkHasherReuse(16, 4096, t) } |
||||
func BenchmarkHasherReuse_2k(t *testing.B) { benchmarkHasherReuse(16, 4096/2, t) } |
||||
func BenchmarkHasherReuse_1k(t *testing.B) { benchmarkHasherReuse(16, 4096/4, t) } |
||||
func BenchmarkHasherReuse_512b(t *testing.B) { benchmarkHasherReuse(16, 4096/8, t) } |
||||
func BenchmarkHasherReuse_256b(t *testing.B) { benchmarkHasherReuse(16, 4096/16, t) } |
||||
func BenchmarkHasherReuse_128b(t *testing.B) { benchmarkHasherReuse(16, 4096/32, t) } |
||||
|
||||
// benchmarks the minimum hashing time for a balanced (for simplicity) BMT
|
||||
// by doing count/segmentsize parallel hashings of 2*segmentsize bytes
|
||||
// doing it on n maxproccnt each reusing the base hasher
|
||||
// the premise is that this is the minimum computation needed for a BMT
|
||||
// therefore this serves as a theoretical optimum for concurrent implementations
|
||||
func benchmarkBMTBaseline(n int, t *testing.B) { |
||||
tdata := testDataReader(64) |
||||
data := make([]byte, 64) |
||||
tdata.Read(data) |
||||
hasher := sha3.NewKeccak256 |
||||
|
||||
t.ReportAllocs() |
||||
t.ResetTimer() |
||||
for i := 0; i < t.N; i++ { |
||||
count := int32((n-1)/hasher().Size() + 1) |
||||
wg := sync.WaitGroup{} |
||||
wg.Add(maxproccnt) |
||||
var i int32 |
||||
for j := 0; j < maxproccnt; j++ { |
||||
go func() { |
||||
defer wg.Done() |
||||
h := hasher() |
||||
for atomic.AddInt32(&i, 1) < count { |
||||
h.Reset() |
||||
h.Write(data) |
||||
h.Sum(nil) |
||||
} |
||||
}() |
||||
} |
||||
wg.Wait() |
||||
} |
||||
} |
||||
|
||||
func benchmarkHasher(n int, t *testing.B) { |
||||
tdata := testDataReader(n) |
||||
data := make([]byte, n) |
||||
tdata.Read(data) |
||||
|
||||
size := 1 |
||||
hasher := sha3.NewKeccak256 |
||||
segmentCount := 128 |
||||
pool := NewTreePool(hasher, segmentCount, size) |
||||
bmt := New(pool) |
||||
|
||||
t.ReportAllocs() |
||||
t.ResetTimer() |
||||
for i := 0; i < t.N; i++ { |
||||
bmt.Reset() |
||||
bmt.Write(data) |
||||
bmt.Sum(nil) |
||||
} |
||||
} |
||||
|
||||
func benchmarkHasherReuse(poolsize, n int, t *testing.B) { |
||||
tdata := testDataReader(n) |
||||
data := make([]byte, n) |
||||
tdata.Read(data) |
||||
|
||||
hasher := sha3.NewKeccak256 |
||||
segmentCount := 128 |
||||
pool := NewTreePool(hasher, segmentCount, poolsize) |
||||
cycles := 200 |
||||
|
||||
t.ReportAllocs() |
||||
t.ResetTimer() |
||||
for i := 0; i < t.N; i++ { |
||||
wg := sync.WaitGroup{} |
||||
wg.Add(cycles) |
||||
for j := 0; j < cycles; j++ { |
||||
bmt := New(pool) |
||||
go func() { |
||||
defer wg.Done() |
||||
bmt.Reset() |
||||
bmt.Write(data) |
||||
bmt.Sum(nil) |
||||
}() |
||||
} |
||||
wg.Wait() |
||||
} |
||||
} |
||||
|
||||
func benchmarkSHA3(n int, t *testing.B) { |
||||
data := make([]byte, n) |
||||
tdata := testDataReader(n) |
||||
tdata.Read(data) |
||||
hasher := sha3.NewKeccak256 |
||||
h := hasher() |
||||
|
||||
t.ReportAllocs() |
||||
t.ResetTimer() |
||||
for i := 0; i < t.N; i++ { |
||||
h.Reset() |
||||
h.Write(data) |
||||
h.Sum(nil) |
||||
} |
||||
} |
||||
|
||||
func benchmarkRefHasher(n int, t *testing.B) { |
||||
data := make([]byte, n) |
||||
tdata := testDataReader(n) |
||||
tdata.Read(data) |
||||
hasher := sha3.NewKeccak256 |
||||
rbmt := NewRefHasher(hasher, 128) |
||||
|
||||
t.ReportAllocs() |
||||
t.ResetTimer() |
||||
for i := 0; i < t.N; i++ { |
||||
rbmt.Hash(data) |
||||
} |
||||
} |
Loading…
Reference in new issue