Skip to content

Commit

Permalink
core/types: fix immutability guarantees in Block
Browse files Browse the repository at this point in the history
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().
  • Loading branch information
fjl committed Aug 3, 2023
1 parent 6e934f4 commit a34b90c
Showing 1 changed file with 63 additions and 41 deletions.
104 changes: 63 additions & 41 deletions core/types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,23 @@ type Body struct {
Withdrawals []*Withdrawal `rlp:"optional"`
}

// Block represents an entire block in the Ethereum blockchain.
// Block represents an Ethereum block.
//
// Note the Block type tries to be 'immutable', and contains certain caches that rely
// on that. The rules around block immutability are as follows:
//
// - We copy all data when the block is constructed. This makes 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, a shallow copy of the block is returned.
// 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.
type Block struct {
header *Header
uncles []*Header
Expand All @@ -195,9 +211,8 @@ type extblock struct {
Withdrawals []*Withdrawal `rlp:"optional"`
}

// NewBlock creates a new block. The input data is copied,
// changes to header and to the field values will not affect the
// block.
// NewBlock creates a new block. The input data is copied, changes to header and to the
// field values will not affect the block.
//
// The values of TxHash, UncleHash, ReceiptHash and Bloom in header
// are ignored and set to values derived from the given txs, uncles
Expand Down Expand Up @@ -234,13 +249,11 @@ func NewBlock(header *Header, txs []*Transaction, uncles []*Header, receipts []*
return b
}

// NewBlockWithWithdrawals creates a new block with withdrawals. The input data
// is copied, changes to header and to the field values will not
// affect the block.
// NewBlockWithWithdrawals creates a new block with withdrawals. The input data is copied,
// changes to header and to the field values will not affect the block.
//
// The values of TxHash, UncleHash, ReceiptHash and Bloom in header
// are ignored and set to values derived from the given txs, uncles
// and receipts.
// The values of TxHash, UncleHash, ReceiptHash and Bloom in header are ignored and set to
// values derived from the given txs, uncles and receipts.
func NewBlockWithWithdrawals(header *Header, txs []*Transaction, uncles []*Header, receipts []*Receipt, withdrawals []*Withdrawal, hasher TrieHasher) *Block {
b := NewBlock(header, txs, uncles, receipts, hasher)

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

// NewBlockWithHeader creates a block with the given header data. The
// header data is copied, changes to header and to the field values
// will not affect the block.
func NewBlockWithHeader(header *Header) *Block {
return &Block{header: CopyHeader(header)}
}

// CopyHeader creates a deep copy of a block header to prevent side effects from
// modifying a header variable.
// CopyHeader creates a deep copy of a block header.
func CopyHeader(h *Header) *Header {
cpy := *h
if cpy.Difficulty = new(big.Int); h.Difficulty != nil {
Expand Down Expand Up @@ -295,7 +300,7 @@ func CopyHeader(h *Header) *Header {
return &cpy
}

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

// EncodeRLP serializes b into the Ethereum RLP block format.
// EncodeRLP serializes a block as RLP.
func (b *Block) EncodeRLP(w io.Writer) error {
return rlp.Encode(w, extblock{
return rlp.Encode(w, &extblock{
Header: b.header,
Txs: b.transactions,
Uncles: b.uncles,
Withdrawals: b.withdrawals,
})
}

// TODO: copies
// Body returns the non-header content of the block.
// Note the returned data is not an independent copy.
func (b *Block) Body() *Body {
return &Body{b.transactions, b.uncles, b.withdrawals}
}

// Accessors for body data. These do not return a copy because the content
// of the body slices does not affect the cached hash/size in block.

func (b *Block) Uncles() []*Header { return b.uncles }
func (b *Block) Transactions() Transactions { return b.transactions }
func (b *Block) Withdrawals() Withdrawals { return b.withdrawals }

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

// Header returns the block header (as a copy).
func (b *Block) Header() *Header {
return CopyHeader(b.header)
}

// Header value accessors. These do copy!

func (b *Block) Number() *big.Int { return new(big.Int).Set(b.header.Number) }
func (b *Block) GasLimit() uint64 { return b.header.GasLimit }
func (b *Block) GasUsed() uint64 { return b.header.GasUsed }
Expand All @@ -356,10 +376,6 @@ func (b *Block) BaseFee() *big.Int {
return new(big.Int).Set(b.header.BaseFee)
}

func (b *Block) Withdrawals() Withdrawals {
return b.withdrawals
}

func (b *Block) ExcessBlobGas() *uint64 {
var excessBlobGas *uint64
if b.header.ExcessBlobGas != nil {
Expand All @@ -378,11 +394,6 @@ func (b *Block) BlobGasUsed() *uint64 {
return blobGasUsed
}

func (b *Block) Header() *Header { return CopyHeader(b.header) }

// Body returns the non-header content of the block.
func (b *Block) Body() *Body { return &Body{b.transactions, b.uncles, b.withdrawals} }

// Size returns the true RLP encoded storage size of the block, either by encoding
// and returning it, or returning a previously cached value.
func (b *Block) Size() uint64 {
Expand Down Expand Up @@ -415,25 +426,31 @@ func CalcUncleHash(uncles []*Header) common.Hash {
return rlpHash(uncles)
}

// NewBlockWithHeader creates a block with the given header data. The
// header data is copied, changes to header and to the field values
// will not affect the block.
func NewBlockWithHeader(header *Header) *Block {
return &Block{header: CopyHeader(header)}
}

// WithSeal returns a new block with the data from b but the header replaced with
// the sealed one.
func (b *Block) WithSeal(header *Header) *Block {
cpy := *header

return &Block{
header: &cpy,
header: CopyHeader(header),
transactions: b.transactions,
uncles: b.uncles,
withdrawals: b.withdrawals,
}
}

// WithBody returns a new block with the given transaction and uncle contents.
// WithBody returns a copy of the block with the given transaction and uncle contents.
func (b *Block) WithBody(transactions []*Transaction, uncles []*Header) *Block {
block := &Block{
header: CopyHeader(b.header),
header: b.header,
transactions: make([]*Transaction, len(transactions)),
uncles: make([]*Header, len(uncles)),
withdrawals: b.withdrawals,
}
copy(block.transactions, transactions)
for i := range uncles {
Expand All @@ -442,13 +459,18 @@ func (b *Block) WithBody(transactions []*Transaction, uncles []*Header) *Block {
return block
}

// WithWithdrawals sets the withdrawal contents of a block, does not return a new block.
// WithWithdrawals returns a copy of the block containing the given withdrawals.
func (b *Block) WithWithdrawals(withdrawals []*Withdrawal) *Block {
block := &Block{
header: b.header,
transactions: b.transactions,
uncles: b.uncles,
}
if withdrawals != nil {
b.withdrawals = make([]*Withdrawal, len(withdrawals))
copy(b.withdrawals, withdrawals)
block.withdrawals = make([]*Withdrawal, len(withdrawals))
copy(block.withdrawals, withdrawals)
}
return b
return block
}

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

0 comments on commit a34b90c

Please sign in to comment.