Skip to content

Commit 9aa279c

Browse files
authored
perf: streaming TxHash and block-level scratch buffer for deserialization (#106)
* perf: streaming TxHash and block-level scratch buffer for deserialization TxHash optimization: - Write tx data directly to sha256.New() instead of serializing into an intermediate bytes.Buffer, eliminating a per-tx buffer allocation proportional to the transaction size. - Cache computed hash in MsgTx.cachedHash field, invalidated by AddTxIn/AddTxOut. Applied to both MsgTx and MsgExtendedTx. Block deserialization optimization: - Add bsvdecodeWithScratch method that reads all scripts into a shared scratch buffer (reused across transactions) instead of allocating a fresh buffer per large script via scriptFreeList.Borrow. - MsgBlock.Bsvdecode passes a shared scratch buffer to each tx, growing it only when a script larger than any previous one is encountered. After each tx, the buffer is reset (len=0) but capacity is preserved. - Pre-allocate MsgTx structs contiguously in MsgBlock.Bsvdecode (one slice instead of per-tx heap allocations). For a 3.64 GB testnet block (28,672 txs): - Block deserialization: 7,514 MB → 3,790 MB (-49.6%) - TxHash: 3,758 MB → 0 MB (eliminated from allocation profile) * fix: resolve govet shadow lint warning in bsvdecodeWithScratch Pre-declare scriptLen variable to avoid shadowing err from outer scope in the output script reading loop.
1 parent 0c3dba1 commit 9aa279c

File tree

5 files changed

+239
-60
lines changed

5 files changed

+239
-60
lines changed

msg_block.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,21 @@ func (msg *MsgBlock) Bsvdecode(r io.Reader, pver uint32, enc MessageEncoding) er
8383
return messageError("MsgBlock.Bsvdecode", str)
8484
}
8585

86-
msg.Transactions = make([]*MsgTx, 0, txCount)
86+
// Pre-allocate all MsgTx structs contiguously and use a shared scratch
87+
// buffer for reading script data across all transactions, avoiding a
88+
// fresh heap allocation per large script.
89+
txs := make([]MsgTx, txCount)
90+
msg.Transactions = make([]*MsgTx, txCount)
91+
92+
var scratch []byte
8793

8894
for i := uint64(0); i < txCount; i++ {
89-
tx := MsgTx{}
95+
msg.Transactions[i] = &txs[i]
9096

91-
err := tx.Bsvdecode(r, pver, enc)
97+
err := txs[i].bsvdecode(r, pver, enc, &scratch)
9298
if err != nil {
9399
return err
94100
}
95-
96-
msg.Transactions = append(msg.Transactions, &tx)
97101
}
98102

99103
return nil

msg_block_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ import (
1616
"github.com/davecgh/go-spew/spew"
1717
)
1818

19+
// clearBlockTxCaches clears cached hashes on all transactions in a block
20+
// so that reflect.DeepEqual comparisons are not affected by cache state.
21+
func clearBlockTxCaches(b *MsgBlock) {
22+
for _, tx := range b.Transactions {
23+
tx.cachedHash = nil
24+
}
25+
}
26+
1927
// TestBlock tests the MsgBlock API.
2028
func TestBlock(t *testing.T) {
2129
pver := ProtocolVersion
@@ -57,6 +65,8 @@ func TestBlock(t *testing.T) {
5765
tx := blockOne.Transactions[0].Copy()
5866
_ = msg.AddTransaction(tx)
5967

68+
clearBlockTxCaches(msg)
69+
clearBlockTxCaches(&blockOne)
6070
if !reflect.DeepEqual(msg.Transactions, blockOne.Transactions) {
6171
t.Errorf("AddTransaction: wrong transactions - got %v, want %v",
6272
spew.Sdump(msg.Transactions),
@@ -207,6 +217,8 @@ func TestBlockWire(t *testing.T) {
207217
continue
208218
}
209219

220+
clearBlockTxCaches(&msg)
221+
clearBlockTxCaches(test.out)
210222
if !reflect.DeepEqual(&msg, test.out) {
211223
t.Errorf("Bsvdecode #%d\n got: %s want: %s", i,
212224
spew.Sdump(&msg), spew.Sdump(test.out))
@@ -322,6 +334,8 @@ func TestBlockSerialize(t *testing.T) {
322334
continue
323335
}
324336

337+
clearBlockTxCaches(&block)
338+
clearBlockTxCaches(test.out)
325339
if !reflect.DeepEqual(&block, test.out) {
326340
t.Errorf("Deserialize #%d\n got: %s want: %s", i,
327341
spew.Sdump(&block), spew.Sdump(test.out))
@@ -340,6 +354,8 @@ func TestBlockSerialize(t *testing.T) {
340354
continue
341355
}
342356

357+
clearBlockTxCaches(&txLocBlock)
358+
clearBlockTxCaches(test.out)
343359
if !reflect.DeepEqual(&txLocBlock, test.out) {
344360
t.Errorf("DeserializeTxLoc #%d\n got: %s want: %s", i,
345361
spew.Sdump(&txLocBlock), spew.Sdump(test.out))

msg_extended_tx.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package wire
66

77
import (
88
"bytes"
9+
"crypto/sha256"
910
"fmt"
1011
"io"
1112

@@ -70,14 +71,13 @@ func (msg *MsgExtendedTx) AddTxOut(to *TxOut) {
7071

7172
// TxHash generates the Hash for the transaction.
7273
func (msg *MsgExtendedTx) TxHash() chainhash.Hash {
73-
// Encode the transaction and calculate double sha256 on the result.
74-
// Ignore the error returns since the only way the encode could fail
75-
// is being out of memory or due to nil pointers, both of which would
76-
// cause a run-time panic.
77-
buf := bytes.NewBuffer(make([]byte, 0, msg.SerializeSize()))
78-
_ = msg.Serialize(buf)
79-
80-
return chainhash.DoubleHashH(buf.Bytes())
74+
// Serialize directly into a SHA-256 hasher to avoid allocating an
75+
// intermediate buffer. Double SHA-256 is computed as sha256(sha256(tx)).
76+
h := sha256.New()
77+
_ = msg.Serialize(h)
78+
var first [32]byte
79+
h.Sum(first[:0])
80+
return chainhash.Hash(sha256.Sum256(first[:]))
8181
}
8282

8383
// Copy creates a deep copy of a transaction so that the original does not get

0 commit comments

Comments
 (0)