Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

btcutil: reuse serialized tx during TxHash #2081

Merged
merged 3 commits into from
Dec 29, 2023
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
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
Loading