Skip to content
Merged
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
28 changes: 27 additions & 1 deletion rfc6962/rfc6962.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ package rfc6962

import (
"crypto"
_ "crypto/sha256" // SHA256 is the default algorithm.
"crypto/sha256"
)

// Domain separation prefixes
Expand All @@ -41,12 +41,18 @@ func New(h crypto.Hash) *Hasher {

// EmptyRoot returns a special case for an empty tree.
func (t *Hasher) EmptyRoot() []byte {
if t.Hash == crypto.SHA256 {
h := sha256.Sum256(nil)
return h[:]
}
return t.New().Sum(nil)
}

// HashLeaf returns the Merkle tree leaf hash of the data passed in through leaf.
// The data in leaf is prefixed by the LeafHashPrefix.
func (t *Hasher) HashLeaf(leaf []byte) []byte {
// Note: A SHA-256 fast path using sha256.Sum256 is possible here if leaf data is
// staged into an array on the stack, but requires an arbitrary buffer size threshold.
h := t.New()
h.Write([]byte{RFC6962LeafHashPrefix})
h.Write(leaf)
Expand All @@ -56,6 +62,10 @@ func (t *Hasher) HashLeaf(leaf []byte) []byte {
// HashChildren returns the inner Merkle tree node hash of the two child nodes l and r.
// The hashed structure is NodeHashPrefix||l||r.
func (t *Hasher) HashChildren(l, r []byte) []byte {
if t.Hash == crypto.SHA256 && len(l) == sha256.Size && len(r) == sha256.Size {

@pav-kv pav-kv Sep 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to optimize this for any 256 bit hash (t.Hash.Size() == 32), rather than only the specific SHA-256 one?

Optionally, any hash <= 512 bits too. The optimization seems to be about placing the buffer on stack, so it seems like it could be only based on the size and algorithm-agnostic?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Pavel, good to have the old gang back together for some low level Merkle Go optimization!

Take a look at the reply to Al above, and https://go.dev/play/p/MUT8x7Jb-7j which shows the different approaches. It isn't just the buffer on the stack. It's the interface and virtual function that gets you.

// Fast path for sha256 common case
return hashChildren256(l, r)
}
h := t.New()
b := append(append(append(
make([]byte, 0, 1+len(l)+len(r)),
Expand All @@ -66,3 +76,19 @@ func (t *Hasher) HashChildren(l, r []byte) []byte {
h.Write(b)
return h.Sum(nil)
}

// hashChildren256 hashes the fixed 65-byte RFC6962 interior node preimage on the stack.
//
// This optimization relies on using the concrete sha256.Sum256 function directly.
// Because the compiler has full visibility into Sum256, it can prove that b does
// not outlive the call, keeping b on the stack (1 allocation total for the returned slice).
// Passing b through an interface (e.g. t.New().Write(b)) would force b to escape to the
// heap because the compiler cannot prove an interface method won't retain it.
func hashChildren256(l, r []byte) []byte {
var b [1 + 2*sha256.Size]byte
b[0] = RFC6962NodeHashPrefix
copy(b[1:], l)
copy(b[1+sha256.Size:], r)
h := sha256.Sum256(b[:])
return h[:]
}
9 changes: 9 additions & 0 deletions rfc6962/rfc6962_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,12 @@ func BenchmarkHashChildren(b *testing.B) {
_ = h.HashChildren(l, r)
}
}

func BenchmarkHashLeaf(b *testing.B) {
h := DefaultHasher
data := []byte("benchmark leaf payload")
b.ResetTimer()
for range b.N {
_ = h.HashLeaf(data)
}
}
Loading