diff --git a/btcutil/bench_test.go b/btcutil/bench_test.go new file mode 100644 index 0000000000..c1f52da5b7 --- /dev/null +++ b/btcutil/bench_test.go @@ -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 + +} diff --git a/btcutil/block.go b/btcutil/block.go index 7d38abc4a0..7f8d8786e3 100644 --- a/btcutil/block.go +++ b/btcutil/block.go @@ -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 } } @@ -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 } @@ -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 } diff --git a/btcutil/tx.go b/btcutil/tx.go index 5633fef90e..4f26befe32 100644 --- a/btcutil/tx.go +++ b/btcutil/tx.go @@ -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. @@ -35,24 +36,82 @@ 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 { @@ -60,7 +119,13 @@ func (t *Tx) WitnessHash() *chainhash.Hash { } // 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 } @@ -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) {