Skip to content

Commit

Permalink
core/types: faster RLP encoding of Header, StateAcccount, ReceiptForS…
Browse files Browse the repository at this point in the history
…torage (#24420)

This change makes use of the new code generator rlp/rlpgen to improve the
performance of RLP encoding for Header and StateAccount. It also speeds up
encoding of ReceiptForStorage using the new rlp.EncoderBuffer API.

The change is much less transparent than I wanted it to be, because Header and
StateAccount now have an EncodeRLP method defined with pointer receiver. It
used to be possible to encode non-pointer values of these types, but the new
method prevents that and attempting to encode unadressable values (even if
part of another value) will return an error. The error can be surprising and may
pop up in places that previously didn't expect any errors.

To make things work, I also needed to update all code paths (mostly in unit tests)
that lead to encoding of non-pointer values, and pass a pointer instead.

Benchmark results:

    name                             old time/op    new time/op    delta
    EncodeRLP/legacy-header-8           328ns ± 0%     237ns ± 1%   -27.63%  (p=0.000 n=8+8)
    EncodeRLP/london-header-8           353ns ± 0%     247ns ± 1%   -30.06%  (p=0.000 n=8+8)
    EncodeRLP/receipt-for-storage-8     237ns ± 0%     123ns ± 0%   -47.86%  (p=0.000 n=8+7)
    EncodeRLP/receipt-full-8            297ns ± 0%     301ns ± 1%    +1.39%  (p=0.000 n=8+8)

    name                             old speed      new speed      delta
    EncodeRLP/legacy-header-8        1.66GB/s ± 0%  2.29GB/s ± 1%   +38.19%  (p=0.000 n=8+8)
    EncodeRLP/london-header-8        1.55GB/s ± 0%  2.22GB/s ± 1%   +42.99%  (p=0.000 n=8+8)
    EncodeRLP/receipt-for-storage-8  38.0MB/s ± 0%  64.8MB/s ± 0%   +70.48%  (p=0.000 n=8+7)
    EncodeRLP/receipt-full-8          910MB/s ± 0%   897MB/s ± 1%    -1.37%  (p=0.000 n=8+8)

    name                             old alloc/op   new alloc/op   delta
    EncodeRLP/legacy-header-8           0.00B          0.00B           ~     (all equal)
    EncodeRLP/london-header-8           0.00B          0.00B           ~     (all equal)
    EncodeRLP/receipt-for-storage-8     64.0B ± 0%      0.0B       -100.00%  (p=0.000 n=8+8)
    EncodeRLP/receipt-full-8             320B ± 0%      320B ± 0%      ~     (all equal)
  • Loading branch information
fjl committed Feb 18, 2022
1 parent 06aaeed commit d6f49bf
Show file tree
Hide file tree
Showing 16 changed files with 163 additions and 55 deletions.
2 changes: 1 addition & 1 deletion core/types/access_list_tx.go
Expand Up @@ -22,7 +22,7 @@ import (
"github.com/ethereum/go-ethereum/common"
)

//go:generate gencodec -type AccessTuple -out gen_access_tuple.go
//go:generate go run github.com/fjl/gencodec@latest -type AccessTuple -out gen_access_tuple.go

// AccessList is an EIP-2930 access list.
type AccessList []AccessTuple
Expand Down
3 changes: 2 additions & 1 deletion core/types/block.go
Expand Up @@ -63,7 +63,8 @@ func (n *BlockNonce) UnmarshalText(input []byte) error {
return hexutil.UnmarshalFixedText("BlockNonce", input, n[:])
}

//go:generate gencodec -type Header -field-override headerMarshaling -out gen_header_json.go
//go:generate go run github.com/fjl/gencodec@latest -type Header -field-override headerMarshaling -out gen_header_json.go
//go:generate go run ../../rlp/rlpgen -type Header -out gen_header_rlp.go

// Header represents a block header in the Ethereum blockchain.
type Header struct {
Expand Down
6 changes: 3 additions & 3 deletions core/types/block_test.go
Expand Up @@ -285,7 +285,7 @@ func makeBenchBlock() *Block {
func TestRlpDecodeParentHash(t *testing.T) {
// A minimum one
want := common.HexToHash("0x112233445566778899001122334455667788990011223344556677889900aabb")
if rlpData, err := rlp.EncodeToBytes(Header{ParentHash: want}); err != nil {
if rlpData, err := rlp.EncodeToBytes(&Header{ParentHash: want}); err != nil {
t.Fatal(err)
} else {
if have := HeaderParentHashFromRLP(rlpData); have != want {
Expand All @@ -299,7 +299,7 @@ func TestRlpDecodeParentHash(t *testing.T) {
// | BaseFee | dynamic| *big.Int | 64 bits |
mainnetTd := new(big.Int)
mainnetTd.SetString("5ad3c2c71bbff854908", 16)
if rlpData, err := rlp.EncodeToBytes(Header{
if rlpData, err := rlp.EncodeToBytes(&Header{
ParentHash: want,
Difficulty: mainnetTd,
Number: new(big.Int).SetUint64(math.MaxUint64),
Expand All @@ -316,7 +316,7 @@ func TestRlpDecodeParentHash(t *testing.T) {
{
// The rlp-encoding of the heder belowCauses _total_ length of 65540,
// which is the first to blow the fast-path.
h := Header{
h := &Header{
ParentHash: want,
Extra: make([]byte, 65041),
}
Expand Down
27 changes: 27 additions & 0 deletions core/types/gen_account_rlp.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions core/types/gen_header_rlp.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions core/types/gen_log_rlp.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 8 additions & 11 deletions core/types/log.go
Expand Up @@ -24,7 +24,7 @@ import (
"github.com/ethereum/go-ethereum/rlp"
)

//go:generate gencodec -type Log -field-override logMarshaling -out gen_log_json.go
//go:generate go run github.com/fjl/gencodec@latest -type Log -field-override logMarshaling -out gen_log_json.go

// Log represents a contract log event. These events are generated by the LOG opcode and
// stored/indexed by the node.
Expand Down Expand Up @@ -62,15 +62,14 @@ type logMarshaling struct {
Index hexutil.Uint
}

//go:generate go run ../../rlp/rlpgen -type rlpLog -out gen_log_rlp.go

type rlpLog struct {
Address common.Address
Topics []common.Hash
Data []byte
}

// rlpStorageLog is the storage encoding of a log.
type rlpStorageLog rlpLog

// legacyRlpStorageLog is the previous storage encoding of a log including some redundant fields.
type legacyRlpStorageLog struct {
Address common.Address
Expand All @@ -85,7 +84,8 @@ type legacyRlpStorageLog struct {

// EncodeRLP implements rlp.Encoder.
func (l *Log) EncodeRLP(w io.Writer) error {
return rlp.Encode(w, rlpLog{Address: l.Address, Topics: l.Topics, Data: l.Data})
rl := rlpLog{Address: l.Address, Topics: l.Topics, Data: l.Data}
return rlp.Encode(w, &rl)
}

// DecodeRLP implements rlp.Decoder.
Expand All @@ -104,11 +104,8 @@ type LogForStorage Log

// EncodeRLP implements rlp.Encoder.
func (l *LogForStorage) EncodeRLP(w io.Writer) error {
return rlp.Encode(w, rlpStorageLog{
Address: l.Address,
Topics: l.Topics,
Data: l.Data,
})
rl := rlpLog{Address: l.Address, Topics: l.Topics, Data: l.Data}
return rlp.Encode(w, &rl)
}

// DecodeRLP implements rlp.Decoder.
Expand All @@ -119,7 +116,7 @@ func (l *LogForStorage) DecodeRLP(s *rlp.Stream) error {
if err != nil {
return err
}
var dec rlpStorageLog
var dec rlpLog
err = rlp.DecodeBytes(blob, &dec)
if err == nil {
*l = LogForStorage{
Expand Down
24 changes: 14 additions & 10 deletions core/types/receipt.go
Expand Up @@ -31,7 +31,7 @@ import (
"github.com/ethereum/go-ethereum/rlp"
)

//go:generate gencodec -type Receipt -field-override receiptMarshaling -out gen_receipt_json.go
//go:generate go run github.com/fjl/gencodec@latest -type Receipt -field-override receiptMarshaling -out gen_receipt_json.go

var (
receiptStatusFailedRLP = []byte{}
Expand Down Expand Up @@ -287,16 +287,20 @@ type ReceiptForStorage Receipt

// EncodeRLP implements rlp.Encoder, and flattens all content fields of a receipt
// into an RLP stream.
func (r *ReceiptForStorage) EncodeRLP(w io.Writer) error {
enc := &storedReceiptRLP{
PostStateOrStatus: (*Receipt)(r).statusEncoding(),
CumulativeGasUsed: r.CumulativeGasUsed,
Logs: make([]*LogForStorage, len(r.Logs)),
}
for i, log := range r.Logs {
enc.Logs[i] = (*LogForStorage)(log)
func (r *ReceiptForStorage) EncodeRLP(_w io.Writer) error {
w := rlp.NewEncoderBuffer(_w)
outerList := w.List()
w.WriteBytes((*Receipt)(r).statusEncoding())
w.WriteUint64(r.CumulativeGasUsed)
logList := w.List()
for _, log := range r.Logs {
if err := rlp.Encode(w, log); err != nil {
return err
}
}
return rlp.Encode(w, enc)
w.ListEnd(logList)
w.ListEnd(outerList)
return w.Flush()
}

// DecodeRLP implements rlp.Decoder, and loads both consensus and implementation
Expand Down
2 changes: 2 additions & 0 deletions core/types/state_account.go
Expand Up @@ -22,6 +22,8 @@ import (
"github.com/ethereum/go-ethereum/common"
)

//go:generate go run ../../rlp/rlpgen -type StateAccount -out gen_account_rlp.go

// StateAccount is the Ethereum consensus representation of accounts.
// These objects are stored in the main account trie.
type StateAccount struct {
Expand Down
2 changes: 1 addition & 1 deletion eth/handler_eth_test.go
Expand Up @@ -570,7 +570,7 @@ func testCheckpointChallenge(t *testing.T, syncmode downloader.SyncMode, checkpo
t.Fatalf("failed to answer challenge: %v", err)
}
} else {
responseRlp, _ := rlp.EncodeToBytes(types.Header{Number: response.Number})
responseRlp, _ := rlp.EncodeToBytes(&types.Header{Number: response.Number})
if err := remote.ReplyBlockHeadersRLP(request.RequestId, []rlp.RawValue{responseRlp}); err != nil {
t.Fatalf("failed to answer challenge: %v", err)
}
Expand Down
22 changes: 10 additions & 12 deletions eth/protocols/eth/handler_test.go
Expand Up @@ -264,11 +264,11 @@ func testGetBlockHeaders(t *testing.T, protocol uint) {
headers = append(headers, backend.chain.GetBlockByHash(hash).Header())
}
// Send the hash request and verify the response
p2p.Send(peer.app, GetBlockHeadersMsg, GetBlockHeadersPacket66{
p2p.Send(peer.app, GetBlockHeadersMsg, &GetBlockHeadersPacket66{
RequestId: 123,
GetBlockHeadersPacket: tt.query,
})
if err := p2p.ExpectMsg(peer.app, BlockHeadersMsg, BlockHeadersPacket66{
if err := p2p.ExpectMsg(peer.app, BlockHeadersMsg, &BlockHeadersPacket66{
RequestId: 123,
BlockHeadersPacket: headers,
}); err != nil {
Expand All @@ -279,14 +279,12 @@ func testGetBlockHeaders(t *testing.T, protocol uint) {
if origin := backend.chain.GetBlockByNumber(tt.query.Origin.Number); origin != nil {
tt.query.Origin.Hash, tt.query.Origin.Number = origin.Hash(), 0

p2p.Send(peer.app, GetBlockHeadersMsg, GetBlockHeadersPacket66{
p2p.Send(peer.app, GetBlockHeadersMsg, &GetBlockHeadersPacket66{
RequestId: 456,
GetBlockHeadersPacket: tt.query,
})
if err := p2p.ExpectMsg(peer.app, BlockHeadersMsg, BlockHeadersPacket66{
RequestId: 456,
BlockHeadersPacket: headers,
}); err != nil {
expected := &BlockHeadersPacket66{RequestId: 456, BlockHeadersPacket: headers}
if err := p2p.ExpectMsg(peer.app, BlockHeadersMsg, expected); err != nil {
t.Errorf("test %d by hash: headers mismatch: %v", i, err)
}
}
Expand Down Expand Up @@ -364,11 +362,11 @@ func testGetBlockBodies(t *testing.T, protocol uint) {
}
}
// Send the hash request and verify the response
p2p.Send(peer.app, GetBlockBodiesMsg, GetBlockBodiesPacket66{
p2p.Send(peer.app, GetBlockBodiesMsg, &GetBlockBodiesPacket66{
RequestId: 123,
GetBlockBodiesPacket: hashes,
})
if err := p2p.ExpectMsg(peer.app, BlockBodiesMsg, BlockBodiesPacket66{
if err := p2p.ExpectMsg(peer.app, BlockBodiesMsg, &BlockBodiesPacket66{
RequestId: 123,
BlockBodiesPacket: bodies,
}); err != nil {
Expand Down Expand Up @@ -436,7 +434,7 @@ func testGetNodeData(t *testing.T, protocol uint) {
it.Release()

// Request all hashes.
p2p.Send(peer.app, GetNodeDataMsg, GetNodeDataPacket66{
p2p.Send(peer.app, GetNodeDataMsg, &GetNodeDataPacket66{
RequestId: 123,
GetNodeDataPacket: hashes,
})
Expand Down Expand Up @@ -546,11 +544,11 @@ func testGetBlockReceipts(t *testing.T, protocol uint) {
receipts = append(receipts, backend.chain.GetReceiptsByHash(block.Hash()))
}
// Send the hash request and verify the response
p2p.Send(peer.app, GetReceiptsMsg, GetReceiptsPacket66{
p2p.Send(peer.app, GetReceiptsMsg, &GetReceiptsPacket66{
RequestId: 123,
GetReceiptsPacket: hashes,
})
if err := p2p.ExpectMsg(peer.app, ReceiptsMsg, ReceiptsPacket66{
if err := p2p.ExpectMsg(peer.app, ReceiptsMsg, &ReceiptsPacket66{
RequestId: 123,
ReceiptsPacket: receipts,
}); err != nil {
Expand Down
10 changes: 5 additions & 5 deletions eth/protocols/eth/peer.go
Expand Up @@ -241,7 +241,7 @@ func (p *Peer) ReplyPooledTransactionsRLP(id uint64, hashes []common.Hash, txs [
p.knownTxs.Add(hashes...)

// Not packed into PooledTransactionsPacket to avoid RLP decoding
return p2p.Send(p.rw, PooledTransactionsMsg, PooledTransactionsRLPPacket66{
return p2p.Send(p.rw, PooledTransactionsMsg, &PooledTransactionsRLPPacket66{
RequestId: id,
PooledTransactionsRLPPacket: txs,
})
Expand Down Expand Up @@ -298,7 +298,7 @@ func (p *Peer) AsyncSendNewBlock(block *types.Block, td *big.Int) {

// ReplyBlockHeaders is the eth/66 version of SendBlockHeaders.
func (p *Peer) ReplyBlockHeadersRLP(id uint64, headers []rlp.RawValue) error {
return p2p.Send(p.rw, BlockHeadersMsg, BlockHeadersRLPPacket66{
return p2p.Send(p.rw, BlockHeadersMsg, &BlockHeadersRLPPacket66{
RequestId: id,
BlockHeadersRLPPacket: headers,
})
Expand All @@ -307,23 +307,23 @@ func (p *Peer) ReplyBlockHeadersRLP(id uint64, headers []rlp.RawValue) error {
// ReplyBlockBodiesRLP is the eth/66 version of SendBlockBodiesRLP.
func (p *Peer) ReplyBlockBodiesRLP(id uint64, bodies []rlp.RawValue) error {
// Not packed into BlockBodiesPacket to avoid RLP decoding
return p2p.Send(p.rw, BlockBodiesMsg, BlockBodiesRLPPacket66{
return p2p.Send(p.rw, BlockBodiesMsg, &BlockBodiesRLPPacket66{
RequestId: id,
BlockBodiesRLPPacket: bodies,
})
}

// ReplyNodeData is the eth/66 response to GetNodeData.
func (p *Peer) ReplyNodeData(id uint64, data [][]byte) error {
return p2p.Send(p.rw, NodeDataMsg, NodeDataPacket66{
return p2p.Send(p.rw, NodeDataMsg, &NodeDataPacket66{
RequestId: id,
NodeDataPacket: data,
})
}

// ReplyReceiptsRLP is the eth/66 response to GetReceipts.
func (p *Peer) ReplyReceiptsRLP(id uint64, receipts []rlp.RawValue) error {
return p2p.Send(p.rw, ReceiptsMsg, ReceiptsRLPPacket66{
return p2p.Send(p.rw, ReceiptsMsg, &ReceiptsRLPPacket66{
RequestId: id,
ReceiptsRLPPacket: receipts,
})
Expand Down

0 comments on commit d6f49bf

Please sign in to comment.