Skip to content

Commit a34b90c

Browse files
committed
core/types: fix immutability guarantees in Block
This change rearranges the accessor methods in block.go and fixes some minor issues with the copy-on-write logic of block data. As a refresher, the rules around block immutability are as follows: - We copy all data when the block is constructed. This makes the references held inside the block independent of whatever value was passed in. - We copy all header data on access. This is because any change to the header would mess up the cached hash and size values in the block. Calling code is expected to take advantage of this to avoid over-allocating! - When new body data is attached to the block, we create a shallow copy of the block. This ensures block modifications are race-free. - We do not copy body data on access because it does not affect the caches, and also because it would be too expensive. The change corrects these issues: - Block.WithWithdrawals did not create a shallow copy of the block. - Block.WithBody copied the header unnecessarily, and did not preserve withdrawals. However, the bugs did not affect any code in go-ethereum because blocks are *always* created using NewBlockWithHeader().WithBody().WithWithdrawals().
1 parent 6e934f4 commit a34b90c

File tree

1 file changed

+63
-41
lines changed

1 file changed

+63
-41
lines changed

core/types/block.go

Lines changed: 63 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,23 @@ type Body struct {
170170
Withdrawals []*Withdrawal `rlp:"optional"`
171171
}
172172

173-
// Block represents an entire block in the Ethereum blockchain.
173+
// Block represents an Ethereum block.
174+
//
175+
// Note the Block type tries to be 'immutable', and contains certain caches that rely
176+
// on that. The rules around block immutability are as follows:
177+
//
178+
// - We copy all data when the block is constructed. This makes references held inside
179+
// the block independent of whatever value was passed in.
180+
//
181+
// - We copy all header data on access. This is because any change to the header would mess
182+
// up the cached hash and size values in the block. Calling code is expected to take
183+
// advantage of this to avoid over-allocating!
184+
//
185+
// - When new body data is attached to the block, a shallow copy of the block is returned.
186+
// This ensures block modifications are race-free.
187+
//
188+
// - We do not copy body data on access because it does not affect the caches, and also
189+
// because it would be too expensive.
174190
type Block struct {
175191
header *Header
176192
uncles []*Header
@@ -195,9 +211,8 @@ type extblock struct {
195211
Withdrawals []*Withdrawal `rlp:"optional"`
196212
}
197213

198-
// NewBlock creates a new block. The input data is copied,
199-
// changes to header and to the field values will not affect the
200-
// block.
214+
// NewBlock creates a new block. The input data is copied, changes to header and to the
215+
// field values will not affect the block.
201216
//
202217
// The values of TxHash, UncleHash, ReceiptHash and Bloom in header
203218
// are ignored and set to values derived from the given txs, uncles
@@ -234,13 +249,11 @@ func NewBlock(header *Header, txs []*Transaction, uncles []*Header, receipts []*
234249
return b
235250
}
236251

237-
// NewBlockWithWithdrawals creates a new block with withdrawals. The input data
238-
// is copied, changes to header and to the field values will not
239-
// affect the block.
252+
// NewBlockWithWithdrawals creates a new block with withdrawals. The input data is copied,
253+
// changes to header and to the field values will not affect the block.
240254
//
241-
// The values of TxHash, UncleHash, ReceiptHash and Bloom in header
242-
// are ignored and set to values derived from the given txs, uncles
243-
// and receipts.
255+
// The values of TxHash, UncleHash, ReceiptHash and Bloom in header are ignored and set to
256+
// values derived from the given txs, uncles and receipts.
244257
func NewBlockWithWithdrawals(header *Header, txs []*Transaction, uncles []*Header, receipts []*Receipt, withdrawals []*Withdrawal, hasher TrieHasher) *Block {
245258
b := NewBlock(header, txs, uncles, receipts, hasher)
246259

@@ -256,15 +269,7 @@ func NewBlockWithWithdrawals(header *Header, txs []*Transaction, uncles []*Heade
256269
return b.WithWithdrawals(withdrawals)
257270
}
258271

259-
// NewBlockWithHeader creates a block with the given header data. The
260-
// header data is copied, changes to header and to the field values
261-
// will not affect the block.
262-
func NewBlockWithHeader(header *Header) *Block {
263-
return &Block{header: CopyHeader(header)}
264-
}
265-
266-
// CopyHeader creates a deep copy of a block header to prevent side effects from
267-
// modifying a header variable.
272+
// CopyHeader creates a deep copy of a block header.
268273
func CopyHeader(h *Header) *Header {
269274
cpy := *h
270275
if cpy.Difficulty = new(big.Int); h.Difficulty != nil {
@@ -295,7 +300,7 @@ func CopyHeader(h *Header) *Header {
295300
return &cpy
296301
}
297302

298-
// DecodeRLP decodes the Ethereum
303+
// DecodeRLP decodes a block from RLP.
299304
func (b *Block) DecodeRLP(s *rlp.Stream) error {
300305
var eb extblock
301306
_, size, _ := s.Kind()
@@ -307,20 +312,28 @@ func (b *Block) DecodeRLP(s *rlp.Stream) error {
307312
return nil
308313
}
309314

310-
// EncodeRLP serializes b into the Ethereum RLP block format.
315+
// EncodeRLP serializes a block as RLP.
311316
func (b *Block) EncodeRLP(w io.Writer) error {
312-
return rlp.Encode(w, extblock{
317+
return rlp.Encode(w, &extblock{
313318
Header: b.header,
314319
Txs: b.transactions,
315320
Uncles: b.uncles,
316321
Withdrawals: b.withdrawals,
317322
})
318323
}
319324

320-
// TODO: copies
325+
// Body returns the non-header content of the block.
326+
// Note the returned data is not an independent copy.
327+
func (b *Block) Body() *Body {
328+
return &Body{b.transactions, b.uncles, b.withdrawals}
329+
}
330+
331+
// Accessors for body data. These do not return a copy because the content
332+
// of the body slices does not affect the cached hash/size in block.
321333

322334
func (b *Block) Uncles() []*Header { return b.uncles }
323335
func (b *Block) Transactions() Transactions { return b.transactions }
336+
func (b *Block) Withdrawals() Withdrawals { return b.withdrawals }
324337

325338
func (b *Block) Transaction(hash common.Hash) *Transaction {
326339
for _, transaction := range b.transactions {
@@ -331,6 +344,13 @@ func (b *Block) Transaction(hash common.Hash) *Transaction {
331344
return nil
332345
}
333346

347+
// Header returns the block header (as a copy).
348+
func (b *Block) Header() *Header {
349+
return CopyHeader(b.header)
350+
}
351+
352+
// Header value accessors. These do copy!
353+
334354
func (b *Block) Number() *big.Int { return new(big.Int).Set(b.header.Number) }
335355
func (b *Block) GasLimit() uint64 { return b.header.GasLimit }
336356
func (b *Block) GasUsed() uint64 { return b.header.GasUsed }
@@ -356,10 +376,6 @@ func (b *Block) BaseFee() *big.Int {
356376
return new(big.Int).Set(b.header.BaseFee)
357377
}
358378

359-
func (b *Block) Withdrawals() Withdrawals {
360-
return b.withdrawals
361-
}
362-
363379
func (b *Block) ExcessBlobGas() *uint64 {
364380
var excessBlobGas *uint64
365381
if b.header.ExcessBlobGas != nil {
@@ -378,11 +394,6 @@ func (b *Block) BlobGasUsed() *uint64 {
378394
return blobGasUsed
379395
}
380396

381-
func (b *Block) Header() *Header { return CopyHeader(b.header) }
382-
383-
// Body returns the non-header content of the block.
384-
func (b *Block) Body() *Body { return &Body{b.transactions, b.uncles, b.withdrawals} }
385-
386397
// Size returns the true RLP encoded storage size of the block, either by encoding
387398
// and returning it, or returning a previously cached value.
388399
func (b *Block) Size() uint64 {
@@ -415,25 +426,31 @@ func CalcUncleHash(uncles []*Header) common.Hash {
415426
return rlpHash(uncles)
416427
}
417428

429+
// NewBlockWithHeader creates a block with the given header data. The
430+
// header data is copied, changes to header and to the field values
431+
// will not affect the block.
432+
func NewBlockWithHeader(header *Header) *Block {
433+
return &Block{header: CopyHeader(header)}
434+
}
435+
418436
// WithSeal returns a new block with the data from b but the header replaced with
419437
// the sealed one.
420438
func (b *Block) WithSeal(header *Header) *Block {
421-
cpy := *header
422-
423439
return &Block{
424-
header: &cpy,
440+
header: CopyHeader(header),
425441
transactions: b.transactions,
426442
uncles: b.uncles,
427443
withdrawals: b.withdrawals,
428444
}
429445
}
430446

431-
// WithBody returns a new block with the given transaction and uncle contents.
447+
// WithBody returns a copy of the block with the given transaction and uncle contents.
432448
func (b *Block) WithBody(transactions []*Transaction, uncles []*Header) *Block {
433449
block := &Block{
434-
header: CopyHeader(b.header),
450+
header: b.header,
435451
transactions: make([]*Transaction, len(transactions)),
436452
uncles: make([]*Header, len(uncles)),
453+
withdrawals: b.withdrawals,
437454
}
438455
copy(block.transactions, transactions)
439456
for i := range uncles {
@@ -442,13 +459,18 @@ func (b *Block) WithBody(transactions []*Transaction, uncles []*Header) *Block {
442459
return block
443460
}
444461

445-
// WithWithdrawals sets the withdrawal contents of a block, does not return a new block.
462+
// WithWithdrawals returns a copy of the block containing the given withdrawals.
446463
func (b *Block) WithWithdrawals(withdrawals []*Withdrawal) *Block {
464+
block := &Block{
465+
header: b.header,
466+
transactions: b.transactions,
467+
uncles: b.uncles,
468+
}
447469
if withdrawals != nil {
448-
b.withdrawals = make([]*Withdrawal, len(withdrawals))
449-
copy(b.withdrawals, withdrawals)
470+
block.withdrawals = make([]*Withdrawal, len(withdrawals))
471+
copy(block.withdrawals, withdrawals)
450472
}
451-
return b
473+
return block
452474
}
453475

454476
// Hash returns the keccak256 hash of b's header.

0 commit comments

Comments
 (0)