github.com/lazyledger/lazyledger-core@v0.35.0-dev.0.20210613111200-4c651f053571/crypto/merkle/tree.go (about) 1 package merkle 2 3 import ( 4 "crypto/sha256" 5 "hash" 6 "math/bits" 7 ) 8 9 // HashFromByteSlices computes a Merkle tree where the leaves are the byte slice, 10 // in the provided order. It follows RFC-6962. 11 func HashFromByteSlices(items [][]byte) []byte { 12 return hashFromByteSlices(sha256.New(), items) 13 } 14 func hashFromByteSlices(sha hash.Hash, items [][]byte) []byte { 15 switch len(items) { 16 case 0: 17 return emptyHash() 18 case 1: 19 return leafHashOpt(sha, items[0]) 20 default: 21 k := getSplitPoint(int64(len(items))) 22 left := hashFromByteSlices(sha, items[:k]) 23 right := hashFromByteSlices(sha, items[k:]) 24 return innerHashOpt(sha, left, right) 25 } 26 } 27 28 // HashFromByteSliceIterative is an iterative alternative to 29 // HashFromByteSlice motivated by potential performance improvements. 30 // (#2611) had suggested that an iterative version of 31 // HashFromByteSlice would be faster, presumably because 32 // we can envision some overhead accumulating from stack 33 // frames and function calls. Additionally, a recursive algorithm risks 34 // hitting the stack limit and causing a stack overflow should the tree 35 // be too large. 36 // 37 // Provided here is an iterative alternative, a test to assert 38 // correctness and a benchmark. On the performance side, there appears to 39 // be no overall difference: 40 // 41 // BenchmarkHashAlternatives/recursive-4 20000 77677 ns/op 42 // BenchmarkHashAlternatives/iterative-4 20000 76802 ns/op 43 // 44 // On the surface it might seem that the additional overhead is due to 45 // the different allocation patterns of the implementations. The recursive 46 // version uses a single [][]byte slices which it then re-slices at each level of the tree. 47 // The iterative version reproduces [][]byte once within the function and 48 // then rewrites sub-slices of that array at each level of the tree. 49 // 50 // Experimenting by modifying the code to simply calculate the 51 // hash and not store the result show little to no difference in performance. 52 // 53 // These preliminary results suggest: 54 // 55 // 1. The performance of the HashFromByteSlice is pretty good 56 // 2. Go has low overhead for recursive functions 57 // 3. The performance of the HashFromByteSlice routine is dominated 58 // by the actual hashing of data 59 // 60 // Although this work is in no way exhaustive, point #3 suggests that 61 // optimization of this routine would need to take an alternative 62 // approach to make significant improvements on the current performance. 63 // 64 // Finally, considering that the recursive implementation is easier to 65 // read, it might not be worthwhile to switch to a less intuitive 66 // implementation for so little benefit. 67 func HashFromByteSlicesIterative(input [][]byte) []byte { 68 sha := sha256.New() 69 items := make([][]byte, len(input)) 70 71 for i, leaf := range input { 72 items[i] = leafHashOpt(sha, leaf) 73 } 74 75 size := len(items) 76 for { 77 switch size { 78 case 0: 79 return emptyHash() 80 case 1: 81 return items[0] 82 default: 83 rp := 0 // read position 84 wp := 0 // write position 85 for rp < size { 86 if rp+1 < size { 87 items[wp] = innerHashOpt(sha, items[rp], items[rp+1]) 88 rp += 2 89 } else { 90 items[wp] = items[rp] 91 rp++ 92 } 93 wp++ 94 } 95 size = wp 96 } 97 } 98 } 99 100 // getSplitPoint returns the largest power of 2 less than length 101 func getSplitPoint(length int64) int64 { 102 if length < 1 { 103 panic("Trying to split a tree with size < 1") 104 } 105 uLength := uint(length) 106 bitlen := bits.Len(uLength) 107 k := int64(1 << uint(bitlen-1)) 108 if k == length { 109 k >>= 1 110 } 111 return k 112 }