// 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 . package bmt import ( "bytes" "encoding/binary" "fmt" "math/rand" "sync" "sync/atomic" "testing" "time" "github.com/ethereum/go-ethereum/swarm/testutil" "golang.org/x/crypto/sha3" ) // the actual data length generated (could be longer than max datalength of the BMT) const BufferSize = 4128 const ( // segmentCount is the maximum number of segments of the underlying chunk // Should be equal to max-chunk-data-size / hash-size // Currently set to 128 == 4096 (default chunk size) / 32 (sha3.keccak256 size) segmentCount = 128 ) var counts = []int{1, 2, 3, 4, 5, 8, 9, 15, 16, 17, 32, 37, 42, 53, 63, 64, 65, 111, 127, 128} // calculates the Keccak256 SHA3 hash of the data func sha3hash(data ...[]byte) []byte { h := sha3.NewLegacyKeccak256() return doSum(h, nil, data...) } // TestRefHasher tests that the RefHasher computes the expected BMT hash for // some small data lengths func TestRefHasher(t *testing.T) { // the test struct is used to specify the expected BMT hash for // segment counts between from and to and lengths from 1 to datalength type test struct { from int to int expected func([]byte) []byte } var tests []*test // all lengths in [0,64] should be: // // sha3hash(data) // tests = append(tests, &test{ from: 1, to: 2, expected: func(d []byte) []byte { data := make([]byte, 64) copy(data, d) return sha3hash(data) }, }) // all lengths in [3,4] should be: // // sha3hash( // sha3hash(data[:64]) // sha3hash(data[64:]) // ) // tests = append(tests, &test{ from: 3, to: 4, expected: func(d []byte) []byte { data := make([]byte, 128) copy(data, d) return sha3hash(sha3hash(data[:64]), sha3hash(data[64:])) }, }) // all segmentCounts in [5,8] should be: // // sha3hash( // sha3hash( // sha3hash(data[:64]) // sha3hash(data[64:128]) // ) // sha3hash( // sha3hash(data[128:192]) // sha3hash(data[192:]) // ) // ) // tests = append(tests, &test{ from: 5, to: 8, expected: func(d []byte) []byte { data := make([]byte, 256) copy(data, d) return sha3hash(sha3hash(sha3hash(data[:64]), sha3hash(data[64:128])), sha3hash(sha3hash(data[128:192]), sha3hash(data[192:]))) }, }) // run the tests for i, x := range tests { for segmentCount := x.from; segmentCount <= x.to; segmentCount++ { for length := 1; length <= segmentCount*32; length++ { t.Run(fmt.Sprintf("%d_segments_%d_bytes", segmentCount, length), func(t *testing.T) { data := testutil.RandomBytes(i, length) expected := x.expected(data) actual := NewRefHasher(sha3.NewLegacyKeccak256, segmentCount).Hash(data) if !bytes.Equal(actual, expected) { t.Fatalf("expected %x, got %x", expected, actual) } }) } } } } // tests if hasher responds with correct hash comparing the reference implementation return value func TestHasherEmptyData(t *testing.T) { hasher := sha3.NewLegacyKeccak256 var data []byte for _, count := range counts { t.Run(fmt.Sprintf("%d_segments", count), func(t *testing.T) { pool := NewTreePool(hasher, count, PoolSize) defer pool.Drain(0) bmt := New(pool) rbmt := NewRefHasher(hasher, count) refHash := rbmt.Hash(data) expHash := syncHash(bmt, nil, data) if !bytes.Equal(expHash, refHash) { t.Fatalf("hash mismatch with reference. expected %x, got %x", refHash, expHash) } }) } } // tests sequential write with entire max size written in one go func TestSyncHasherCorrectness(t *testing.T) { data := testutil.RandomBytes(1, BufferSize) hasher := sha3.NewLegacyKeccak256 size := hasher().Size() var err error for _, count := range counts { t.Run(fmt.Sprintf("segments_%v", count), func(t *testing.T) { max := count * size var incr int capacity := 1 pool := NewTreePool(hasher, count, capacity) defer pool.Drain(0) for n := 0; n <= max; n += incr { incr = 1 + rand.Intn(5) bmt := New(pool) err = testHasherCorrectness(bmt, hasher, data, n, count) if err != nil { t.Fatal(err) } } }) } } // tests order-neutral concurrent writes with entire max size written in one go func TestAsyncCorrectness(t *testing.T) { data := testutil.RandomBytes(1, BufferSize) hasher := sha3.NewLegacyKeccak256 size := hasher().Size() whs := []whenHash{first, last, random} for _, double := range []bool{false, true} { for _, wh := range whs { for _, count := range counts { t.Run(fmt.Sprintf("double_%v_hash_when_%v_segments_%v", double, wh, count), func(t *testing.T) { max := count * size var incr int capacity := 1 pool := NewTreePool(hasher, count, capacity) defer pool.Drain(0) for n := 1; n <= max; n += incr { incr = 1 + rand.Intn(5) bmt := New(pool) d := data[:n] rbmt := NewRefHasher(hasher, count) exp := rbmt.Hash(d) got := syncHash(bmt, nil, d) if !bytes.Equal(got, exp) { t.Fatalf("wrong sync hash for datalength %v: expected %x (ref), got %x", n, exp, got) } sw := bmt.NewAsyncWriter(double) got = asyncHashRandom(sw, nil, d, wh) if !bytes.Equal(got, exp) { t.Fatalf("wrong async hash for datalength %v: expected %x, got %x", n, exp, got) } } }) } } } } // Tests that the BMT hasher can be synchronously reused with poolsizes 1 and PoolSize func TestHasherReuse(t *testing.T) { t.Run(fmt.Sprintf("poolsize_%d", 1), func(t *testing.T) { testHasherReuse(1, t) }) t.Run(fmt.Sprintf("poolsize_%d", PoolSize), func(t *testing.T) { testHasherReuse(PoolSize, t) }) } // tests if bmt reuse is not corrupting result func testHasherReuse(poolsize int, t *testing.T) { hasher := sha3.NewLegacyKeccak256 pool := NewTreePool(hasher, segmentCount, poolsize) defer pool.Drain(0) bmt := New(pool) for i := 0; i < 100; i++ { data := testutil.RandomBytes(1, BufferSize) n := rand.Intn(bmt.Size()) err := testHasherCorrectness(bmt, hasher, data, n, segmentCount) if err != nil { t.Fatal(err) } } } // Tests if pool can be cleanly reused even in concurrent use by several hasher func TestBMTConcurrentUse(t *testing.T) { hasher := sha3.NewLegacyKeccak256 pool := NewTreePool(hasher, segmentCount, PoolSize) defer pool.Drain(0) cycles := 100 errc := make(chan error) for i := 0; i < cycles; i++ { go func() { bmt := New(pool) data := testutil.RandomBytes(1, BufferSize) n := rand.Intn(bmt.Size()) errc <- testHasherCorrectness(bmt, hasher, data, n, 128) }() } LOOP: for { select { case <-time.NewTimer(5 * time.Second).C: t.Fatal("timed out") case err := <-errc: if err != nil { t.Fatal(err) } cycles-- if cycles == 0 { break LOOP } } } } // Tests BMT Hasher io.Writer interface is working correctly // even multiple short random write buffers func TestBMTWriterBuffers(t *testing.T) { hasher := sha3.NewLegacyKeccak256 for _, count := range counts { t.Run(fmt.Sprintf("%d_segments", count), func(t *testing.T) { errc := make(chan error) pool := NewTreePool(hasher, count, PoolSize) defer pool.Drain(0) n := count * 32 bmt := New(pool) data := testutil.RandomBytes(1, n) rbmt := NewRefHasher(hasher, count) refHash := rbmt.Hash(data) expHash := syncHash(bmt, nil, data) if !bytes.Equal(expHash, refHash) { t.Fatalf("hash mismatch with reference. expected %x, got %x", refHash, expHash) } attempts := 10 f := func() error { bmt := New(pool) bmt.Reset() var buflen int for offset := 0; offset < n; offset += buflen { buflen = rand.Intn(n-offset) + 1 read, err := bmt.Write(data[offset : offset+buflen]) if err != nil { return err } if read != buflen { return fmt.Errorf("incorrect read. expected %v bytes, got %v", buflen, read) } } hash := bmt.Sum(nil) if !bytes.Equal(hash, expHash) { return fmt.Errorf("hash mismatch. expected %x, got %x", hash, expHash) } return nil } for j := 0; j < attempts; j++ { go func() { errc <- f() }() } timeout := time.NewTimer(2 * time.Second) for { select { case err := <-errc: if err != nil { t.Fatal(err) } attempts-- if attempts == 0 { return } case <-timeout.C: t.Fatalf("timeout") } } }) } } // helper function that compares reference and optimised implementations on // correctness func testHasherCorrectness(bmt *Hasher, hasher BaseHasherFunc, d []byte, n, count int) (err error) { span := make([]byte, 8) if len(d) < n { n = len(d) } binary.BigEndian.PutUint64(span, uint64(n)) data := d[:n] rbmt := NewRefHasher(hasher, count) exp := sha3hash(span, rbmt.Hash(data)) got := syncHash(bmt, span, data) if !bytes.Equal(got, exp) { return fmt.Errorf("wrong hash: expected %x, got %x", exp, got) } return err } // func BenchmarkBMT(t *testing.B) { for size := 4096; size >= 128; size /= 2 { t.Run(fmt.Sprintf("%v_size_%v", "SHA3", size), func(t *testing.B) { benchmarkSHA3(t, size) }) t.Run(fmt.Sprintf("%v_size_%v", "Baseline", size), func(t *testing.B) { benchmarkBMTBaseline(t, size) }) t.Run(fmt.Sprintf("%v_size_%v", "REF", size), func(t *testing.B) { benchmarkRefHasher(t, size) }) t.Run(fmt.Sprintf("%v_size_%v", "BMT", size), func(t *testing.B) { benchmarkBMT(t, size) }) } } type whenHash = int const ( first whenHash = iota last random ) func BenchmarkBMTAsync(t *testing.B) { whs := []whenHash{first, last, random} for size := 4096; size >= 128; size /= 2 { for _, wh := range whs { for _, double := range []bool{false, true} { t.Run(fmt.Sprintf("double_%v_hash_when_%v_size_%v", double, wh, size), func(t *testing.B) { benchmarkBMTAsync(t, size, wh, double) }) } } } } func BenchmarkPool(t *testing.B) { caps := []int{1, PoolSize} for size := 4096; size >= 128; size /= 2 { for _, c := range caps { t.Run(fmt.Sprintf("poolsize_%v_size_%v", c, size), func(t *testing.B) { benchmarkPool(t, c, size) }) } } } // benchmarks simple sha3 hash on chunks func benchmarkSHA3(t *testing.B, n int) { data := testutil.RandomBytes(1, n) hasher := sha3.NewLegacyKeccak256 h := hasher() t.ReportAllocs() t.ResetTimer() for i := 0; i < t.N; i++ { doSum(h, nil, data) } } // 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 PoolSize 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(t *testing.B, n int) { hasher := sha3.NewLegacyKeccak256 hashSize := hasher().Size() data := testutil.RandomBytes(1, hashSize) t.ReportAllocs() t.ResetTimer() for i := 0; i < t.N; i++ { count := int32((n-1)/hashSize + 1) wg := sync.WaitGroup{} wg.Add(PoolSize) var i int32 for j := 0; j < PoolSize; j++ { go func() { defer wg.Done() h := hasher() for atomic.AddInt32(&i, 1) < count { doSum(h, nil, data) } }() } wg.Wait() } } // benchmarks BMT Hasher func benchmarkBMT(t *testing.B, n int) { data := testutil.RandomBytes(1, n) hasher := sha3.NewLegacyKeccak256 pool := NewTreePool(hasher, segmentCount, PoolSize) bmt := New(pool) t.ReportAllocs() t.ResetTimer() for i := 0; i < t.N; i++ { syncHash(bmt, nil, data) } } // benchmarks BMT hasher with asynchronous concurrent segment/section writes func benchmarkBMTAsync(t *testing.B, n int, wh whenHash, double bool) { data := testutil.RandomBytes(1, n) hasher := sha3.NewLegacyKeccak256 pool := NewTreePool(hasher, segmentCount, PoolSize) bmt := New(pool).NewAsyncWriter(double) idxs, segments := splitAndShuffle(bmt.SectionSize(), data) rand.Shuffle(len(idxs), func(i int, j int) { idxs[i], idxs[j] = idxs[j], idxs[i] }) t.ReportAllocs() t.ResetTimer() for i := 0; i < t.N; i++ { asyncHash(bmt, nil, n, wh, idxs, segments) } } // benchmarks 100 concurrent bmt hashes with pool capacity func benchmarkPool(t *testing.B, poolsize, n int) { data := testutil.RandomBytes(1, n) hasher := sha3.NewLegacyKeccak256 pool := NewTreePool(hasher, segmentCount, poolsize) cycles := 100 t.ReportAllocs() t.ResetTimer() wg := sync.WaitGroup{} for i := 0; i < t.N; i++ { wg.Add(cycles) for j := 0; j < cycles; j++ { go func() { defer wg.Done() bmt := New(pool) syncHash(bmt, nil, data) }() } wg.Wait() } } // benchmarks the reference hasher func benchmarkRefHasher(t *testing.B, n int) { data := testutil.RandomBytes(1, n) hasher := sha3.NewLegacyKeccak256 rbmt := NewRefHasher(hasher, 128) t.ReportAllocs() t.ResetTimer() for i := 0; i < t.N; i++ { rbmt.Hash(data) } } // Hash hashes the data and the span using the bmt hasher func syncHash(h *Hasher, span, data []byte) []byte { h.ResetWithLength(span) h.Write(data) return h.Sum(nil) } func splitAndShuffle(secsize int, data []byte) (idxs []int, segments [][]byte) { l := len(data) n := l / secsize if l%secsize > 0 { n++ } for i := 0; i < n; i++ { idxs = append(idxs, i) end := (i + 1) * secsize if end > l { end = l } section := data[i*secsize : end] segments = append(segments, section) } rand.Shuffle(n, func(i int, j int) { idxs[i], idxs[j] = idxs[j], idxs[i] }) return idxs, segments } // splits the input data performs a random shuffle to mock async section writes func asyncHashRandom(bmt SectionWriter, span []byte, data []byte, wh whenHash) (s []byte) { idxs, segments := splitAndShuffle(bmt.SectionSize(), data) return asyncHash(bmt, span, len(data), wh, idxs, segments) } // mock for async section writes for BMT SectionWriter // requires a permutation (a random shuffle) of list of all indexes of segments // and writes them in order to the appropriate section // the Sum function is called according to the wh parameter (first, last, random [relative to segment writes]) func asyncHash(bmt SectionWriter, span []byte, l int, wh whenHash, idxs []int, segments [][]byte) (s []byte) { bmt.Reset() if l == 0 { return bmt.Sum(nil, l, span) } c := make(chan []byte, 1) hashf := func() { c <- bmt.Sum(nil, l, span) } maxsize := len(idxs) var r int if wh == random { r = rand.Intn(maxsize) } for i, idx := range idxs { bmt.Write(idx, segments[idx]) if (wh == first || wh == random) && i == r { go hashf() } } if wh == last { return bmt.Sum(nil, l, span) } return <-c }