From b47e4d5b38b34c045cb10af6c0b5603c285310cd Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Wed, 19 Mar 2025 07:21:40 +0100 Subject: [PATCH] core/types: reduce allocs in transaction signing (#31258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR roughly halves the number of allocations needed to compute the sigHash for a transaction. This sigHash is used whenever we recover a signature of a transaction, so quite often. During a recent benchmark full syncing on Holesky, roughly 2.8% of all allocations were happening here because the fields from the transaction would be copied multiple times. ``` 66168733 153175654 (flat, cum) 2.80% of Total . . 368:func (s londonSigner) Hash(tx *Transaction) common.Hash { . . 369: if tx.Type() != DynamicFeeTxType { . . 370: return s.eip2930Signer.Hash(tx) . . 371: } . 19169966 372: return prefixedRlpHash( . . 373: tx.Type(), 26442187 26442187 374: []interface{}{ . . 375: s.chainId, 6848616 6848616 376: tx.Nonce(), . 19694077 377: tx.GasTipCap(), . 18956774 378: tx.GasFeeCap(), 6357089 6357089 379: tx.Gas(), . 12321050 380: tx.To(), . 16865054 381: tx.Value(), 13435187 13435187 382: tx.Data(), 13085654 13085654 383: tx.AccessList(), . . 384: }) . . 385:} ``` This PR reduces the allocations and speeds up the computation of the sigHash by ~22%, which is quite significantly given that this operation involves a call to Keccak ``` // BenchmarkHash-8 440082 2639 ns/op 384 B/op 13 allocs/op // BenchmarkHash-8 493566 2033 ns/op 240 B/op 6 allocs/op ``` ``` Hash-8 2.691µ ± 8% 2.097µ ± 9% -22.07% (p=0.000 n=10) ``` It also kinda cleans up stuff in my opinion, since the transaction should itself know best how to compute the sighash ![Screenshot_2025-02-25_13-52-41](https://github.com/user-attachments/assets/e2b268aa-e137-417d-926b-f3619daef748) --------- Co-authored-by: Gary Rong --- core/types/transaction.go | 3 ++ core/types/transaction_signing.go | 70 +++---------------------------- core/types/transaction_test.go | 17 ++++++++ core/types/tx_access_list.go | 15 +++++++ core/types/tx_blob.go | 18 ++++++++ core/types/tx_dynamic_fee.go | 16 +++++++ core/types/tx_legacy.go | 13 ++++++ core/types/tx_setcode.go | 17 ++++++++ 8 files changed, 105 insertions(+), 64 deletions(-) diff --git a/core/types/transaction.go b/core/types/transaction.go index 4e48248b4a..a2f4104635 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -100,6 +100,9 @@ type TxData interface { encode(*bytes.Buffer) error decode([]byte) error + + // sigHash returns the hash of the transaction that is ought to be signed + sigHash(*big.Int) common.Hash } // EncodeRLP implements rlp.Encoder diff --git a/core/types/transaction_signing.go b/core/types/transaction_signing.go index 030fc472a0..89c08aeddd 100644 --- a/core/types/transaction_signing.go +++ b/core/types/transaction_signing.go @@ -233,20 +233,7 @@ func (s pragueSigner) Hash(tx *Transaction) common.Hash { if tx.Type() != SetCodeTxType { return s.cancunSigner.Hash(tx) } - return prefixedRlpHash( - tx.Type(), - []interface{}{ - s.chainId, - tx.Nonce(), - tx.GasTipCap(), - tx.GasFeeCap(), - tx.Gas(), - tx.To(), - tx.Value(), - tx.Data(), - tx.AccessList(), - tx.SetCodeAuthorizations(), - }) + return tx.inner.sigHash(s.chainId) } type cancunSigner struct{ londonSigner } @@ -301,21 +288,7 @@ func (s cancunSigner) Hash(tx *Transaction) common.Hash { if tx.Type() != BlobTxType { return s.londonSigner.Hash(tx) } - return prefixedRlpHash( - tx.Type(), - []interface{}{ - s.chainId, - tx.Nonce(), - tx.GasTipCap(), - tx.GasFeeCap(), - tx.Gas(), - tx.To(), - tx.Value(), - tx.Data(), - tx.AccessList(), - tx.BlobGasFeeCap(), - tx.BlobHashes(), - }) + return tx.inner.sigHash(s.chainId) } type londonSigner struct{ eip2930Signer } @@ -369,19 +342,7 @@ func (s londonSigner) Hash(tx *Transaction) common.Hash { if tx.Type() != DynamicFeeTxType { return s.eip2930Signer.Hash(tx) } - return prefixedRlpHash( - tx.Type(), - []interface{}{ - s.chainId, - tx.Nonce(), - tx.GasTipCap(), - tx.GasFeeCap(), - tx.Gas(), - tx.To(), - tx.Value(), - tx.Data(), - tx.AccessList(), - }) + return tx.inner.sigHash(s.chainId) } type eip2930Signer struct{ EIP155Signer } @@ -444,18 +405,7 @@ func (s eip2930Signer) Hash(tx *Transaction) common.Hash { case LegacyTxType: return s.EIP155Signer.Hash(tx) case AccessListTxType: - return prefixedRlpHash( - tx.Type(), - []interface{}{ - s.chainId, - tx.Nonce(), - tx.GasPrice(), - tx.Gas(), - tx.To(), - tx.Value(), - tx.Data(), - tx.AccessList(), - }) + return tx.inner.sigHash(s.chainId) default: // This _should_ not happen, but in case someone sends in a bad // json struct via RPC, it's probably more prudent to return an @@ -525,15 +475,7 @@ func (s EIP155Signer) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big // Hash returns the hash to be signed by the sender. // It does not uniquely identify the transaction. func (s EIP155Signer) Hash(tx *Transaction) common.Hash { - return rlpHash([]interface{}{ - tx.Nonce(), - tx.GasPrice(), - tx.Gas(), - tx.To(), - tx.Value(), - tx.Data(), - s.chainId, uint(0), uint(0), - }) + return tx.inner.sigHash(s.chainId) } // HomesteadSigner implements Signer interface using the @@ -597,7 +539,7 @@ func (fs FrontierSigner) SignatureValues(tx *Transaction, sig []byte) (r, s, v * // Hash returns the hash to be signed by the sender. // It does not uniquely identify the transaction. func (fs FrontierSigner) Hash(tx *Transaction) common.Hash { - return rlpHash([]interface{}{ + return rlpHash([]any{ tx.Nonce(), tx.GasPrice(), tx.Gas(), diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go index 17a7dda357..8922448d97 100644 --- a/core/types/transaction_test.go +++ b/core/types/transaction_test.go @@ -576,3 +576,20 @@ func TestYParityJSONUnmarshalling(t *testing.T) { } } } + +func BenchmarkHash(b *testing.B) { + signer := NewLondonSigner(big.NewInt(1)) + to := common.Address{} + tx := NewTx(&DynamicFeeTx{ + ChainID: big.NewInt(123), + Nonce: 1, + Gas: 1000000, + To: &to, + Value: big.NewInt(1), + GasTipCap: big.NewInt(500), + GasFeeCap: big.NewInt(500), + }) + for i := 0; i < b.N; i++ { + signer.Hash(tx) + } +} diff --git a/core/types/tx_access_list.go b/core/types/tx_access_list.go index 730a77b752..915de9a8ab 100644 --- a/core/types/tx_access_list.go +++ b/core/types/tx_access_list.go @@ -127,3 +127,18 @@ func (tx *AccessListTx) encode(b *bytes.Buffer) error { func (tx *AccessListTx) decode(input []byte) error { return rlp.DecodeBytes(input, tx) } + +func (tx *AccessListTx) sigHash(chainID *big.Int) common.Hash { + return prefixedRlpHash( + AccessListTxType, + []any{ + chainID, + tx.Nonce, + tx.GasPrice, + tx.Gas, + tx.To, + tx.Value, + tx.Data, + tx.AccessList, + }) +} diff --git a/core/types/tx_blob.go b/core/types/tx_blob.go index 32401db101..9b1d53958f 100644 --- a/core/types/tx_blob.go +++ b/core/types/tx_blob.go @@ -259,3 +259,21 @@ func (tx *BlobTx) decode(input []byte) error { } return nil } + +func (tx *BlobTx) sigHash(chainID *big.Int) common.Hash { + return prefixedRlpHash( + BlobTxType, + []any{ + chainID, + tx.Nonce, + tx.GasTipCap, + tx.GasFeeCap, + tx.Gas, + tx.To, + tx.Value, + tx.Data, + tx.AccessList, + tx.BlobFeeCap, + tx.BlobHashes, + }) +} diff --git a/core/types/tx_dynamic_fee.go b/core/types/tx_dynamic_fee.go index 981755cf70..bba81464f8 100644 --- a/core/types/tx_dynamic_fee.go +++ b/core/types/tx_dynamic_fee.go @@ -123,3 +123,19 @@ func (tx *DynamicFeeTx) encode(b *bytes.Buffer) error { func (tx *DynamicFeeTx) decode(input []byte) error { return rlp.DecodeBytes(input, tx) } + +func (tx *DynamicFeeTx) sigHash(chainID *big.Int) common.Hash { + return prefixedRlpHash( + DynamicFeeTxType, + []any{ + chainID, + tx.Nonce, + tx.GasTipCap, + tx.GasFeeCap, + tx.Gas, + tx.To, + tx.Value, + tx.Data, + tx.AccessList, + }) +} diff --git a/core/types/tx_legacy.go b/core/types/tx_legacy.go index 71025b78fc..49f0a98809 100644 --- a/core/types/tx_legacy.go +++ b/core/types/tx_legacy.go @@ -123,3 +123,16 @@ func (tx *LegacyTx) encode(*bytes.Buffer) error { func (tx *LegacyTx) decode([]byte) error { panic("decode called on LegacyTx)") } + +// OBS: This is the post-EIP155 hash, the pre-EIP155 does not contain a chainID. +func (tx *LegacyTx) sigHash(chainID *big.Int) common.Hash { + return rlpHash([]any{ + tx.Nonce, + tx.GasPrice, + tx.Gas, + tx.To, + tx.Value, + tx.Data, + chainID, uint(0), uint(0), + }) +} diff --git a/core/types/tx_setcode.go b/core/types/tx_setcode.go index 894bac10a3..b8e38ef1f7 100644 --- a/core/types/tx_setcode.go +++ b/core/types/tx_setcode.go @@ -223,3 +223,20 @@ func (tx *SetCodeTx) encode(b *bytes.Buffer) error { func (tx *SetCodeTx) decode(input []byte) error { return rlp.DecodeBytes(input, tx) } + +func (tx *SetCodeTx) sigHash(chainID *big.Int) common.Hash { + return prefixedRlpHash( + SetCodeTxType, + []any{ + chainID, + tx.Nonce, + tx.GasTipCap, + tx.GasFeeCap, + tx.Gas, + tx.To, + tx.Value, + tx.Data, + tx.AccessList, + tx.AuthList, + }) +}