Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ require (
github.com/ipfs/go-log/v2 v2.6.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/klauspost/cpuid/v2 v2.3.0
github.com/koron/go-ssdp v0.0.6 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/libdns/libdns v0.2.2 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -527,8 +527,8 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW
github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg=
github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/reedsolomon v1.11.8 h1:s8RpUW5TK4hjr+djiOpbZJB4ksx+TdYbRH7vHQpwPOY=
Expand Down
26 changes: 24 additions & 2 deletions pkg/bmt/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ func BenchmarkBMT(b *testing.B) {
b.Run(fmt.Sprintf("%v_size_%v", "BMT", size), func(b *testing.B) {
benchmarkBMT(b, size)
})
b.Run(fmt.Sprintf("%v_size_%v", "BMT_NoSIMD", size), func(b *testing.B) {
benchmarkBMTNoSIMD(b, size)
})
}
}

Expand Down Expand Up @@ -87,7 +90,7 @@ func benchmarkBMT(b *testing.B, n int) {

testData := testutil.RandBytesWithSeed(b, 4096, seed)

pool := bmt.NewPool(bmt.NewConf(swarm.NewHasher, testSegmentCount, testPoolSize))
pool := bmt.NewPool(bmt.NewConf(testSegmentCount, testPoolSize))
h := pool.Get()
defer pool.Put(h)

Expand All @@ -106,7 +109,7 @@ func benchmarkPool(b *testing.B, poolsize int) {

testData := testutil.RandBytesWithSeed(b, 4096, seed)

pool := bmt.NewPool(bmt.NewConf(swarm.NewHasher, testSegmentCount, poolsize))
pool := bmt.NewPool(bmt.NewConf(testSegmentCount, poolsize))
cycles := 100

b.ReportAllocs()
Expand All @@ -127,6 +130,25 @@ func benchmarkPool(b *testing.B, poolsize int) {
}
}

// benchmarks BMT Hasher with SIMD disabled
func benchmarkBMTNoSIMD(b *testing.B, n int) {
b.Helper()

testData := testutil.RandBytesWithSeed(b, 4096, seed)

pool := bmt.NewPool(bmt.NewConfNoSIMD(testSegmentCount, testPoolSize))
h := pool.Get()
defer pool.Put(h)

b.ReportAllocs()

for b.Loop() {
if _, err := syncHash(h, testData[:n]); err != nil {
b.Fatalf("seed %d: %v", seed, err)
}
}
}

// benchmarks the reference hasher
func benchmarkRefHasher(b *testing.B, n int) {
b.Helper()
Expand Down
204 changes: 37 additions & 167 deletions pkg/bmt/bmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,28 @@ var (
// Sum 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 {
*Conf // configuration
bmt *tree // prebuilt BMT resource for flowcontrol and proofs
size int // bytes written to Hasher since last Reset()
pos int // index of rightmost currently open segment
result chan []byte // result channel
errc chan error // error channel
span []byte // The span of the data subsumed under the chunk
*Conf // configuration
bmt *tree // prebuilt BMT resource for flowcontrol and proofs
size int // bytes written to Hasher since last Reset()
span []byte // The span of the data subsumed under the chunk
}

// NewHasher gives back an instance of a Hasher struct
func NewHasher(hasherFact func() hash.Hash) *Hasher {
conf := NewConf(hasherFact, swarm.BmtBranches, 32)
func NewHasher() *Hasher {
return newHasherWithConf(NewConf(swarm.BmtBranches, 32))
}

// NewPrefixHasher gives back an instance of a Hasher struct with the given prefix
// prepended to every hash operation.
func NewPrefixHasher(prefix []byte) *Hasher {
return newHasherWithConf(NewConfWithPrefix(prefix, swarm.BmtBranches, 32))
}

func newHasherWithConf(conf *Conf) *Hasher {
return &Hasher{
Conf: conf,
result: make(chan []byte),
errc: make(chan error, 1),
span: make([]byte, SpanSize),
bmt: newTree(conf.maxSize, conf.depth, conf.hasher),
Conf: conf,
span: make([]byte, SpanSize),
bmt: newTree(conf.maxSize, conf.depth, conf.baseHasher, conf.prefix),
}
}

Expand Down Expand Up @@ -95,17 +98,27 @@ func (h *Hasher) BlockSize() int {
// using Hash presupposes sequential synchronous writes (io.Writer interface).
func (h *Hasher) Hash(b []byte) ([]byte, error) {
if h.size == 0 {
return doHash(h.hasher(), h.span, h.zerohashes[h.depth])
return doHash(h.baseHasher(), h.span, h.zerohashes[h.depth])
}
// zero-fill remainder so all sections have deterministic input
for i := h.size; i < h.maxSize; i++ {
h.bmt.buffer[i] = 0
}
if len(h.bmt.levels) == 1 {
// single-level tree: hash the only section directly
secsize := 2 * h.segmentSize
root := h.bmt.levels[0][0]
rootHash, err := doHash(root.hasher, h.bmt.buffer[:secsize])
if err != nil {
return nil, err
}
return doHash(h.baseHasher(), h.span, rootHash)
}
copy(h.bmt.buffer[h.size:], zerosection)
// write the last section with final flag set to true
go h.processSection(h.pos, true)
select {
case result := <-h.result:
return doHash(h.hasher(), h.span, result)
case err := <-h.errc:
rootHash, err := h.hashSIMD()
if err != nil {
return nil, err
}
return doHash(h.baseHasher(), h.span, rootHash)
}

// Sum returns the BMT root hash of the buffer, unsafe version of Hash
Expand All @@ -114,168 +127,25 @@ func (h *Hasher) Sum(b []byte) []byte {
return s
}

// Write calls sequentially add to the buffer to be hashed,
// with every full segment calls processSection in a go routine.
// Write calls sequentially add to the buffer to be hashed.
// All hashing is deferred to Hash().
func (h *Hasher) Write(b []byte) (int, error) {
l := len(b)
maxVal := h.maxSize - h.size
if l > maxVal {
l = maxVal
}
copy(h.bmt.buffer[h.size:], b)
secsize := 2 * h.segmentSize
from := h.size / secsize
h.size += l
to := h.size / secsize
if l == maxVal {
to--
}
h.pos = to
for i := from; i < to; i++ {
go h.processSection(i, false)
}
return l, nil
}

// Reset prepares the Hasher for reuse
func (h *Hasher) Reset() {
h.pos = 0
h.size = 0
copy(h.span, zerospan)
}

// processSection writes the hash of i-th section into level 1 node of the BMT tree.
func (h *Hasher) processSection(i int, final bool) {
secsize := 2 * h.segmentSize
offset := i * secsize
level := 1
// select the leaf node for the section
n := h.bmt.leaves[i]
isLeft := n.isLeft
hasher := n.hasher
n = n.parent
// hash the section
section, err := doHash(hasher, h.bmt.buffer[offset:offset+secsize])
if err != nil {
select {
case h.errc <- err:
default:
}
return
}
// write hash into parent node
if final {
// for the last segment use writeFinalNode
h.writeFinalNode(level, n, isLeft, section)
} else {
h.writeNode(n, isLeft, section)
}
}

// writeNode pushes the data to the node.
// if it is the first of 2 sisters written, the routine terminates.
// if it is the second, it calculates the hash and writes it
// to the parent node recursively.
// since hashing the parent is synchronous the same hasher can be used.
func (h *Hasher) writeNode(n *node, isLeft bool, s []byte) {
var err error
for {
// at the root of the bmt just write the result to the result channel
if n == nil {
h.result <- s
return
}
// otherwise assign child hash to left or right segment
if isLeft {
n.left = s
} else {
n.right = s
}
// the child-thread first arriving will terminate
if n.toggle() {
return
}
// the thread coming second now can be sure both left and right children are written
// so it calculates the hash of left|right and pushes it to the parent
s, err = doHash(n.hasher, n.left, n.right)
if err != nil {
select {
case h.errc <- err:
default:
}
return
}
isLeft = n.isLeft
n = n.parent
}
}

// writeFinalNode is following the path starting from the final datasegment to the
// BMT root via parents.
// For unbalanced trees it fills in the missing right sister nodes using
// the pool's lookup table for BMT subtree root hashes for all-zero sections.
// Otherwise behaves like `writeNode`.
func (h *Hasher) writeFinalNode(level int, n *node, isLeft bool, s []byte) {
var err error
for {
// at the root of the bmt just write the result to the result channel
if n == nil {
if s != nil {
h.result <- s
}
return
}
var noHash bool
if isLeft {
// coming from left sister branch
// when the final section's path is going via left child node
// we include an all-zero subtree hash for the right level and toggle the node.
n.right = h.zerohashes[level]
if s != nil {
n.left = s
// if a left final node carries a hash, it must be the first (and only thread)
// so the toggle is already in passive state no need no call
// yet thread needs to carry on pushing hash to parent
noHash = false
} else {
// if again first thread then propagate nil and calculate no hash
noHash = n.toggle()
}
} else {
// right sister branch
if s != nil {
// if hash was pushed from right child node, write right segment change state
n.right = s
// if toggle is true, we arrived first so no hashing just push nil to parent
noHash = n.toggle()
} else {
// if s is nil, then thread arrived first at previous node and here there will be two,
// so no need to do anything and keep s = nil for parent
noHash = true
}
}
// the child-thread first arriving will just continue resetting s to nil
// the second thread now can be sure both left and right children are written
// it calculates the hash of left|right and pushes it to the parent
if noHash {
s = nil
} else {
s, err = doHash(n.hasher, n.left, n.right)
if err != nil {
select {
case h.errc <- err:
default:
}
return
}
}
// iterate to parent
isLeft = n.isLeft
n = n.parent
level++
}
}

// calculates the Keccak256 SHA3 hash of the data
func sha3hash(data ...[]byte) ([]byte, error) {
return doHash(swarm.NewHasher(), data...)
Expand Down
Loading
Loading