From 64c7e68e1a2e251f8a3a61c94d2570d4a9d678fd Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Tue, 6 Jan 2026 19:38:36 +0000 Subject: [PATCH] wire: Optimize writes to hashers This adds two additional type cases to the optimized writes for BLAKE-256 (used by transactions and the original PoW algorithm) and BLAKE-3 (used by the current PoW algorithm). It also updates both the block header and transaction code to use these hashers during the calculation of block and transaction hashes. --- wire/bench_test.go | 23 +++++++++++++++++ wire/blockheader.go | 28 +++++++++++---------- wire/common.go | 16 ++++++++++++ wire/go.mod | 6 ++--- wire/msgtx.go | 61 ++++++++++++++++++++++++--------------------- 5 files changed, 89 insertions(+), 45 deletions(-) diff --git a/wire/bench_test.go b/wire/bench_test.go index 14fad50c3..59778067a 100644 --- a/wire/bench_test.go +++ b/wire/bench_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/crypto/blake256" ) // genesisCoinbaseTx is the coinbase transaction for the genesis blocks for @@ -634,6 +635,28 @@ func BenchmarkTxHash(b *testing.B) { } } +// BenchmarkTxHashReuseHasher performs a benchmark on how long it takes to hash a +// transaction by reusing the *blake256.Hasher256 object. +func BenchmarkTxHashReuseHasher(b *testing.B) { + h := blake256.NewHasher256() + + txHash := func(h *blake256.Hasher256, tx *MsgTx) chainhash.Hash { + txCopy := *tx + txCopy.SerType = TxSerializeNoWitness + err := txCopy.Serialize(h) + if err != nil { + panic(err) + } + return h.Sum256() + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + h.Reset() + _ = txHash(h, &genesisCoinbaseTx) + } +} + // BenchmarkHashB performs a benchmark on how long it takes to perform a hash // returning a byte slice. func BenchmarkHashB(b *testing.B) { diff --git a/wire/blockheader.go b/wire/blockheader.go index e420194a8..33fb8047b 100644 --- a/wire/blockheader.go +++ b/wire/blockheader.go @@ -11,6 +11,7 @@ import ( "time" "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/crypto/blake256" "lukechampine.com/blake3" ) @@ -88,19 +89,19 @@ type BlockHeader struct { // header. const blockHeaderLen = 180 -// BlockHash computes the block identifier hash for the given block header. +// BlockHash computes the BLAKE-256 block identifier hash for the given block +// header. func (h *BlockHeader) BlockHash() chainhash.Hash { // Encode the header and hash everything prior to the number of // transactions. Ignore the error returns since there is no way the encode // could fail except being out of memory which would cause a run-time panic. - buf := bytes.NewBuffer(make([]byte, 0, MaxBlockHeaderPayload)) - _ = writeBlockHeader(buf, 0, h) - - return chainhash.HashH(buf.Bytes()) + hasher := blake256.NewHasher256() + _ = writeBlockHeader(hasher, 0, h) + return hasher.Sum256() } -// PowHashV1 calculates and returns the version 1 proof of work hash for the -// block header. +// PowHashV1 calculates and returns the version 1 proof of work BLAKE-256 hash +// for the block header. // // NOTE: This is the original proof of work hash function used at Decred launch // and applies to all blocks prior to the activation of DCP0011. @@ -108,16 +109,17 @@ func (h *BlockHeader) PowHashV1() chainhash.Hash { return h.BlockHash() } -// PowHashV2 calculates and returns the version 2 proof of work hash as defined -// in DCP0011 for the block header. +// PowHashV2 calculates and returns the version 2 proof of work BLAKE3 hash as +// defined in DCP0011 for the block header. func (h *BlockHeader) PowHashV2() chainhash.Hash { // Encode the header and hash everything prior to the number of // transactions. Ignore the error returns since there is no way the encode // could fail except being out of memory which would cause a run-time panic. - buf := bytes.NewBuffer(make([]byte, 0, MaxBlockHeaderPayload)) - _ = writeBlockHeader(buf, 0, h) - - return blake3.Sum256(buf.Bytes()) + var digest chainhash.Hash + hasher := blake3.New(len(digest), nil) + _ = writeBlockHeader(hasher, 0, h) + hasher.Sum(digest[:0]) + return digest } // BtcDecode decodes r using the bitcoin protocol encoding into the receiver. diff --git a/wire/common.go b/wire/common.go index b508802ba..7c6218bcb 100644 --- a/wire/common.go +++ b/wire/common.go @@ -15,6 +15,8 @@ import ( "time" "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/crypto/blake256" + "lukechampine.com/blake3" ) const ( @@ -382,6 +384,18 @@ func shortWrite(w io.Writer, cb func() (data [8]byte, size int)) error { w.Write(data[:size]) return nil + // Hashing transactions can be optimized by writing directly to the + // BLAKE-256 hasher. + case *blake256.Hasher256: + w.Write(data[:size]) + return nil + + // Hashing block headers can be optimized by writing directly to the + // BLAKE-3 hasher. + case *blake3.Hasher: + w.Write(data[:size]) + return nil + default: p := binarySerializer.Borrow()[:size] copy(p, data[:size]) @@ -813,6 +827,8 @@ func WriteVarString(w io.Writer, pver uint32, str string) error { switch w := w.(type) { case *bytes.Buffer: _, err = w.WriteString(str) + case *blake256.Hasher256: + w.WriteString(str) default: _, err = w.Write([]byte(str)) } diff --git a/wire/go.mod b/wire/go.mod index 85fa1d68b..191ff3af0 100644 --- a/wire/go.mod +++ b/wire/go.mod @@ -5,10 +5,8 @@ go 1.17 require ( github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/chaincfg/chainhash v1.0.5 + github.com/decred/dcrd/crypto/blake256 v1.1.0 lukechampine.com/blake3 v1.3.0 ) -require ( - github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect - github.com/klauspost/cpuid/v2 v2.0.9 // indirect -) +require github.com/klauspost/cpuid/v2 v2.0.9 // indirect diff --git a/wire/msgtx.go b/wire/msgtx.go index 05af4fdf2..01c071b42 100644 --- a/wire/msgtx.go +++ b/wire/msgtx.go @@ -12,6 +12,7 @@ import ( "strconv" "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/crypto/blake256" ) const ( @@ -391,24 +392,28 @@ func (msg *MsgTx) serialize(serType TxSerializeType) ([]byte, error) { return buf.Bytes(), nil } -// mustSerialize returns the serialization of the transaction for the provided -// serialization type without modifying the original transaction. It will panic -// if any errors occur. -func (msg *MsgTx) mustSerialize(serType TxSerializeType) []byte { - serialized, err := msg.serialize(serType) +// mustHash returns the hash of the transaction for the provided +// serialization type without modifying the original transaction. +// It will panic if serialization fails. +func (msg *MsgTx) mustHash(hasher *blake256.Hasher256, serType TxSerializeType) chainhash.Hash { + // Shallow copy so the serialization type can be changed without + // modifying the original transaction. + mtxCopy := *msg + mtxCopy.SerType = serType + err := mtxCopy.Serialize(hasher) if err != nil { panic(fmt.Sprintf("MsgTx failed serializing for type %v", serType)) } - return serialized + return hasher.Sum256() } -// TxHash generates the hash for the transaction prefix. Since it does not -// contain any witness data, it is not malleable and therefore is stable for -// use in unconfirmed transaction chains. +// TxHash generates the BLAKE-256 hash for the transaction prefix. Since it +// does not contain any witness data, it is not malleable and therefore is +// stable for use in unconfirmed transaction chains. func (msg *MsgTx) TxHash() chainhash.Hash { // TxHash should always calculate a non-witnessed hash. - return chainhash.HashH(msg.mustSerialize(TxSerializeNoWitness)) + return msg.mustHash(blake256.NewHasher256(), TxSerializeNoWitness) } // CachedTxHash is equivalent to calling TxHash, however it caches the result so @@ -433,29 +438,29 @@ func (msg *MsgTx) RecacheTxHash() *chainhash.Hash { return msg.CachedHash } -// TxHashWitness generates the hash for the transaction witness. +// TxHashWitness generates the BLAKE-256 hash for the transaction witness. func (msg *MsgTx) TxHashWitness() chainhash.Hash { // TxHashWitness should always calculate a witnessed hash. - return chainhash.HashH(msg.mustSerialize(TxSerializeOnlyWitness)) + return msg.mustHash(blake256.NewHasher256(), TxSerializeOnlyWitness) } -// TxHashFull generates the hash for the transaction prefix || witness. It first -// obtains the hashes for both the transaction prefix and witness, then -// concatenates them and hashes the result. +// TxHashFull generates the hash for the transaction prefix || witness. This +// is the BLAKE-256 hash of the concatenation of the individual prefix and +// witness hashes (and not the hash of the full serialization). func (msg *MsgTx) TxHashFull() chainhash.Hash { - // Note that the inputs to the hashes, the serialized prefix and - // witness, have different serialized versions because the serialized - // encoding of the version includes the real transaction version in the - // lower 16 bits and the transaction serialization type in the upper 16 - // bits. The real transaction version (lower 16 bits) will be the same - // in both serializations. - concat := make([]byte, chainhash.HashSize*2) - prefixHash := msg.TxHash() - witnessHash := msg.TxHashWitness() - copy(concat[0:], prefixHash[:]) - copy(concat[chainhash.HashSize:], witnessHash[:]) - - return chainhash.HashH(concat) + // Even for a transaction that has neither prefix nor witness (and + // would otherwise hash to the same result), the prefix and witness + // hashes will still differ due to the serialization type being + // encoded into the upper 16 bits of the transaction version. + hasher := blake256.NewHasher256() + prefixHash := msg.mustHash(hasher, TxSerializeNoWitness) + hasher.Reset() + witnessHash := msg.mustHash(hasher, TxSerializeOnlyWitness) + hasher.Reset() + + hasher.WriteBytes(prefixHash[:]) + hasher.WriteBytes(witnessHash[:]) + return hasher.Sum256() } // Copy creates a deep copy of a transaction so that the original does not get