Skip to content

Commit

Permalink
Merge pull request #2081 from Roasbeef/dont-serialize-tx-for-txhash
Browse files Browse the repository at this point in the history
btcutil: reuse serialized tx during TxHash
  • Loading branch information
Roasbeef authored Dec 29, 2023
2 parents 8766bfd + e102a81 commit 790c570
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 9 deletions.
80 changes: 80 additions & 0 deletions btcutil/bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package btcutil_test

import (
"testing"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
)

var (
bencHash *chainhash.Hash
)

// BenchmarkTxHash benchmarks the performance of calculating the hash of a
// transaction.
func BenchmarkTxHash(b *testing.B) {
// Make a new block from the test block, we'll then call the Bytes
// function to cache the serialized block. Afterwards we all
// Transactions to populate the serialization cache.
testBlock := btcutil.NewBlock(&Block100000)
_, _ = testBlock.Bytes()

// The second transaction in the block has no witness data. The first
// does however.
testTx := testBlock.Transactions()[1]
testTx2 := testBlock.Transactions()[0]

// Run a benchmark for the portion that needs to strip the non-witness
// data from the transaction.
b.Run("tx_hash_has_witness", func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()

var txHash *chainhash.Hash
for i := 0; i < b.N; i++ {
txHash = testTx2.Hash()
}

bencHash = txHash
})

// Next, run it for the portion that can just hash the bytes directly.
b.Run("tx_hash_no_witness", func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()

var txHash *chainhash.Hash
for i := 0; i < b.N; i++ {
txHash = testTx.Hash()
}

bencHash = txHash
})

}

// BenchmarkTxWitnessHash benchmarks the performance of calculating the hash of
// a transaction.
func BenchmarkTxWitnessHash(b *testing.B) {
// Make a new block from the test block, we'll then call the Bytes
// function to cache the serialized block. Afterwards we all
// Transactions to populate the serialization cache.
testBlock := btcutil.NewBlock(&Block100000)
_, _ = testBlock.Bytes()

// The first transaction in the block has been modified to have witness
// data.
testTx := testBlock.Transactions()[0]

b.ResetTimer()
b.ReportAllocs()

var txHash *chainhash.Hash
for i := 0; i < b.N; i++ {
txHash = testTx.WitnessHash()
}

bencHash = txHash

}
39 changes: 37 additions & 2 deletions btcutil/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,32 @@ func (b *Block) Transactions() []*Tx {
b.transactions = make([]*Tx, len(b.msgBlock.Transactions))
}

// Offset of each tx. 80 accounts for the block header size.
offset := 80 + wire.VarIntSerializeSize(
uint64(len(b.msgBlock.Transactions)),
)

// Generate and cache the wrapped transactions for all that haven't
// already been done.
for i, tx := range b.transactions {
if tx == nil {
newTx := NewTx(b.msgBlock.Transactions[i])
newTx.SetIndex(i)

size := b.msgBlock.Transactions[i].SerializeSize()

// The block may not always have the serializedBlock.
if len(b.serializedBlock) > 0 {
// This allows for the reuse of the already
// serialized tx.
newTx.setBytes(
b.serializedBlock[offset : offset+size],
)

// Increment offset for this block.
offset += size
}

b.transactions[i] = newTx
}
}
Expand Down Expand Up @@ -234,6 +254,12 @@ func NewBlockFromBytes(serializedBlock []byte) (*Block, error) {
return nil, err
}
b.serializedBlock = serializedBlock

// This initializes []btcutil.Tx to have the serialized raw
// transactions cached. Helps speed up things like generating the
// txhash.
b.Transactions()

return b, nil
}

Expand All @@ -256,10 +282,19 @@ func NewBlockFromReader(r io.Reader) (*Block, error) {

// NewBlockFromBlockAndBytes returns a new instance of a bitcoin block given
// an underlying wire.MsgBlock and the serialized bytes for it. See Block.
func NewBlockFromBlockAndBytes(msgBlock *wire.MsgBlock, serializedBlock []byte) *Block {
return &Block{
func NewBlockFromBlockAndBytes(msgBlock *wire.MsgBlock,
serializedBlock []byte) *Block {

b := &Block{
msgBlock: msgBlock,
serializedBlock: serializedBlock,
blockHeight: BlockHeightUnknown,
}

// This initializes []btcutil.Tx to have the serialized raw
// transactions cached. Helps speed up things like generating the
// txhash.
b.Transactions()

return b
}
84 changes: 77 additions & 7 deletions btcutil/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Tx struct {
txHashWitness *chainhash.Hash // Cached transaction witness hash
txHasWitness *bool // If the transaction has witness data
txIndex int // Position within a block or TxIndexUnknown
rawBytes []byte // Raw bytes for the tx in the raw block.
}

// MsgTx returns the underlying wire.MsgTx for the transaction.
Expand All @@ -35,32 +36,96 @@ func (t *Tx) MsgTx() *wire.MsgTx {
return t.msgTx
}

// Hash returns the hash of the transaction. This is equivalent to
// calling TxHash on the underlying wire.MsgTx, however it caches the
// result so subsequent calls are more efficient.
// Hash returns the hash of the transaction. This is equivalent to calling
// TxHash on the underlying wire.MsgTx, however it caches the result so
// subsequent calls are more efficient. If the Tx has the raw bytes of the tx
// cached, it will use that and skip serialization.
func (t *Tx) Hash() *chainhash.Hash {
// Return the cached hash if it has already been generated.
if t.txHash != nil {
return t.txHash
}

// Cache the hash and return it.
hash := t.msgTx.TxHash()
// If the rawBytes aren't available, call msgtx.TxHash.
if t.rawBytes == nil {
hash := t.msgTx.TxHash()
t.txHash = &hash
return &hash
}

// If we have the raw bytes, then don't call msgTx.TxHash as that has
// the overhead of serialization. Instead, we can take the existing
// serialized bytes and hash them to speed things up.
var hash chainhash.Hash
if t.HasWitness() {
// If the raw bytes contain the witness, we must strip it out
// before calculating the hash.
baseSize := t.msgTx.SerializeSizeStripped()
nonWitnessBytes := make([]byte, 0, baseSize)

// Append the version bytes.
offset := 4
nonWitnessBytes = append(
nonWitnessBytes, t.rawBytes[:offset]...,
)

// Append the input and output bytes. -8 to account for the
// version bytes and the locktime bytes.
//
// Skip the 2 bytes for the witness encoding.
offset += 2
nonWitnessBytes = append(
nonWitnessBytes,
t.rawBytes[offset:offset+baseSize-8]...,
)

// Append the last 4 bytes which are the locktime bytes.
nonWitnessBytes = append(
nonWitnessBytes, t.rawBytes[len(t.rawBytes)-4:]...,
)

// We purposely call doublehashh here instead of doublehashraw
// as we don't have the serialization overhead and avoiding the
// 1 alloc is better in this case.
hash = chainhash.DoubleHashRaw(func(w io.Writer) error {
_, err := w.Write(nonWitnessBytes)
return err
})
} else {
// If the raw bytes don't have the witness, we can use it
// directly.
//
// We purposely call doublehashh here instead of doublehashraw
// as we don't have the serialization overhead and avoiding the
// 1 alloc is better in this case.
hash = chainhash.DoubleHashRaw(func(w io.Writer) error {
_, err := w.Write(t.rawBytes)
return err
})
}

t.txHash = &hash
return &hash
}

// WitnessHash returns the witness hash (wtxid) of the transaction. This is
// equivalent to calling WitnessHash on the underlying wire.MsgTx, however it
// caches the result so subsequent calls are more efficient.
// caches the result so subsequent calls are more efficient. If the Tx has the
// raw bytes of the tx cached, it will use that and skip serialization.
func (t *Tx) WitnessHash() *chainhash.Hash {
// Return the cached hash if it has already been generated.
if t.txHashWitness != nil {
return t.txHashWitness
}

// Cache the hash and return it.
hash := t.msgTx.WitnessHash()
var hash chainhash.Hash
if len(t.rawBytes) > 0 {
hash = chainhash.DoubleHashH(t.rawBytes)
} else {
hash = t.msgTx.WitnessHash()
}

t.txHashWitness = &hash
return &hash
}
Expand Down Expand Up @@ -99,6 +164,11 @@ func NewTx(msgTx *wire.MsgTx) *Tx {
}
}

// setBytes sets the raw bytes of the tx.
func (t *Tx) setBytes(bytes []byte) {
t.rawBytes = bytes
}

// NewTxFromBytes returns a new instance of a bitcoin transaction given the
// serialized bytes. See Tx.
func NewTxFromBytes(serializedTx []byte) (*Tx, error) {
Expand Down

0 comments on commit 790c570

Please sign in to comment.