package merkle import ( "crypto/sha256" "hash" "math/bits" ) // HashFromByteSlices computes a Merkle tree where the leaves are the byte slice, // in the provided order. It follows RFC-6962. func HashFromByteSlices(items [][]byte) []byte { return hashFromByteSlices(sha256.New(), items) } func hashFromByteSlices(sha hash.Hash, items [][]byte) []byte { switch len(items) { case 0: return emptyHash() case 1: return leafHashOpt(sha, items[0]) default: k := getSplitPoint(int64(len(items))) left := hashFromByteSlices(sha, items[:k]) right := hashFromByteSlices(sha, items[k:]) return innerHashOpt(sha, left, right) } } // HashFromByteSliceIterative is an iterative alternative to // HashFromByteSlice motivated by potential performance improvements. // (#2611) had suggested that an iterative version of // HashFromByteSlice would be faster, presumably because // we can envision some overhead accumulating from stack // frames and function calls. Additionally, a recursive algorithm risks // hitting the stack limit and causing a stack overflow should the tree // be too large. // // Provided here is an iterative alternative, a test to assert // correctness and a benchmark. On the performance side, there appears to // be no overall difference: // // BenchmarkHashAlternatives/recursive-4 20000 77677 ns/op // BenchmarkHashAlternatives/iterative-4 20000 76802 ns/op // // On the surface it might seem that the additional overhead is due to // the different allocation patterns of the implementations. The recursive // version uses a single [][]byte slices which it then re-slices at each level of the tree. // The iterative version reproduces [][]byte once within the function and // then rewrites sub-slices of that array at each level of the tree. // // Experimenting by modifying the code to simply calculate the // hash and not store the result show little to no difference in performance. // // These preliminary results suggest: // // 1. The performance of the HashFromByteSlice is pretty good // 2. Go has low overhead for recursive functions // 3. The performance of the HashFromByteSlice routine is dominated // by the actual hashing of data // // Although this work is in no way exhaustive, point #3 suggests that // optimization of this routine would need to take an alternative // approach to make significant improvements on the current performance. // // Finally, considering that the recursive implementation is easier to // read, it might not be worthwhile to switch to a less intuitive // implementation for so little benefit. func HashFromByteSlicesIterative(input [][]byte) []byte { items := make([][]byte, len(input)) sha := sha256.New() for i, leaf := range input { items[i] = leafHash(leaf) } size := len(items) for { switch size { case 0: return emptyHash() case 1: return items[0] default: rp := 0 // read position wp := 0 // write position for rp < size { if rp+1 < size { items[wp] = innerHashOpt(sha, items[rp], items[rp+1]) rp += 2 } else { items[wp] = items[rp] rp++ } wp++ } size = wp } } } // getSplitPoint returns the largest power of 2 less than length func getSplitPoint(length int64) int64 { if length < 1 { panic("Trying to split a tree with size < 1") } uLength := uint(length) bitlen := bits.Len(uLength) k := int64(1 << uint(bitlen-1)) if k == length { k >>= 1 } return k }