From 905283b1ea2694136d5903a230dabcec7b6f46b0 Mon Sep 17 00:00:00 2001 From: Andrew Ashikhmin <34320705+yperbasis@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:33:53 +0200 Subject: [PATCH] Stricter validation of EIP-7702 transactions (#12201) Cherry pick #11885 --- core/types/authorization.go | 7 +- core/types/set_code_tx.go | 12 +- core/types/transaction.go | 4 +- core/types/transaction_signing.go | 6 +- core/vm/contracts.go | 5 +- crypto/crypto.go | 15 --- crypto/crypto_test.go | 50 +------ erigon-lib/crypto/secp256k1.go | 4 +- erigon-lib/crypto/secp256k1_test.go | 74 +++++++++++ erigon-lib/txpool/pool.go | 20 ++- erigon-lib/txpool/txpool_grpc_server.go | 7 +- erigon-lib/txpool/txpoolcfg/txpoolcfg.go | 75 ++++++----- erigon-lib/types/txn.go | 158 ++++++++++++++--------- erigon-lib/types/txn_test.go | 13 +- 14 files changed, 265 insertions(+), 185 deletions(-) create mode 100644 erigon-lib/crypto/secp256k1_test.go diff --git a/core/types/authorization.go b/core/types/authorization.go index a715cfa425d..71070671045 100644 --- a/core/types/authorization.go +++ b/core/types/authorization.go @@ -7,7 +7,10 @@ import ( "io" "github.com/holiman/uint256" + libcommon "github.com/ledgerwatch/erigon-lib/common" + libcrypto "github.com/ledgerwatch/erigon-lib/crypto" + "github.com/ledgerwatch/erigon-lib/common/length" rlp2 "github.com/ledgerwatch/erigon-lib/rlp" "github.com/ledgerwatch/erigon/common/u256" @@ -74,8 +77,8 @@ func (ath *Authorization) RecoverSigner(data *bytes.Buffer, b []byte) (*libcommo return nil, fmt.Errorf("invalid v value: %d", ath.V.Uint64()) } - if !crypto.ValidateSignatureValues(sig[64], &ath.R, &ath.S, false) { - return nil, fmt.Errorf("invalid signature") + if !libcrypto.TransactionSignatureIsValid(sig[64], &ath.R, &ath.S, false /* allowPreEip2s */) { + return nil, errors.New("invalid signature") } pubkey, err := crypto.Ecrecover(hash.Bytes(), sig[:]) diff --git a/core/types/set_code_tx.go b/core/types/set_code_tx.go index cd3412b75c2..60544f1fa58 100644 --- a/core/types/set_code_tx.go +++ b/core/types/set_code_tx.go @@ -132,7 +132,11 @@ func (tx *SetCodeTransaction) AsMessage(s Signer, baseFee *big.Int, rules *chain msg.gasPrice.Set(tx.FeeCap) } + if len(tx.Authorizations) == 0 { + return msg, errors.New("SetCodeTransaction without authorizations is invalid") + } msg.authorizations = tx.Authorizations + var err error msg.from, err = tx.Sender(s) return msg, err @@ -234,13 +238,11 @@ func (tx *SetCodeTransaction) DecodeRLP(s *rlp.Stream) error { if b, err = s.Bytes(); err != nil { return err } - if len(b) > 0 && len(b) != 20 { + if len(b) != 20 { return fmt.Errorf("wrong size for To: %d", len(b)) } - if len(b) > 0 { - tx.To = &libcommon.Address{} - copy((*tx.To)[:], b) - } + tx.To = &libcommon.Address{} + copy((*tx.To)[:], b) if b, err = s.Uint256Bytes(); err != nil { return err } diff --git a/core/types/transaction.go b/core/types/transaction.go index 2964d68b9fb..2a3e279defc 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -31,10 +31,10 @@ import ( "github.com/ledgerwatch/erigon-lib/chain" libcommon "github.com/ledgerwatch/erigon-lib/common" "github.com/ledgerwatch/erigon-lib/common/fixedgas" + libcrypto "github.com/ledgerwatch/erigon-lib/crypto" types2 "github.com/ledgerwatch/erigon-lib/types" "github.com/ledgerwatch/erigon/common/math" - "github.com/ledgerwatch/erigon/crypto" "github.com/ledgerwatch/erigon/rlp" ) @@ -280,7 +280,7 @@ func sanityCheckSignature(v *uint256.Int, r *uint256.Int, s *uint256.Int, maybeP // must already be equal to the recovery id. plainV = byte(v.Uint64()) } - if !crypto.ValidateSignatureValues(plainV, r, s, false) { + if !libcrypto.TransactionSignatureIsValid(plainV, r, s, true /* allowPreEip2s */) { return ErrInvalidSig } diff --git a/core/types/transaction_signing.go b/core/types/transaction_signing.go index fb88697bcd4..dadc1a69bca 100644 --- a/core/types/transaction_signing.go +++ b/core/types/transaction_signing.go @@ -23,9 +23,11 @@ import ( "math/big" "github.com/holiman/uint256" + "github.com/ledgerwatch/secp256k1" + "github.com/ledgerwatch/erigon-lib/chain" libcommon "github.com/ledgerwatch/erigon-lib/common" - "github.com/ledgerwatch/secp256k1" + libcrypto "github.com/ledgerwatch/erigon-lib/crypto" "github.com/ledgerwatch/erigon/common/u256" "github.com/ledgerwatch/erigon/crypto" @@ -359,7 +361,7 @@ func recoverPlain(context *secp256k1.Context, sighash libcommon.Hash, R, S, Vb * return libcommon.Address{}, ErrInvalidSig } V := byte(Vb.Uint64() - 27) - if !crypto.ValidateSignatureValues(V, R, S, homestead) { + if !libcrypto.TransactionSignatureIsValid(V, R, S, !homestead) { return libcommon.Address{}, ErrInvalidSig } // encode the signature in uncompressed format diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 8657d882b35..7cc287334e9 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -30,6 +30,7 @@ import ( "github.com/ledgerwatch/erigon-lib/chain" libcommon "github.com/ledgerwatch/erigon-lib/common" + libcrypto "github.com/ledgerwatch/erigon-lib/crypto" "github.com/ledgerwatch/erigon-lib/crypto/blake2b" libkzg "github.com/ledgerwatch/erigon-lib/crypto/kzg" @@ -238,8 +239,8 @@ func (c *ecrecover) Run(input []byte) ([]byte, error) { s := new(uint256.Int).SetBytes(input[96:128]) v := input[63] - 27 - // tighter sig s values input homestead only apply to tx sigs - if !allZero(input[32:63]) || !crypto.ValidateSignatureValues(v, r, s, false) { + // tighter sig s values input homestead only apply to txn sigs + if !allZero(input[32:63]) || !libcrypto.TransactionSignatureIsValid(v, r, s, true /* allowPreEip2s */) { return nil, nil } // We must make sure not to modify the 'input', so placing the 'v' along with diff --git a/crypto/crypto.go b/crypto/crypto.go index 6d385075d92..6ca99d07c9d 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -298,21 +298,6 @@ func GenerateKey() (*ecdsa.PrivateKey, error) { return ecdsa.GenerateKey(S256(), rand.Reader) } -// ValidateSignatureValues verifies whether the signature values are valid with -// the given chain rules. The v value is assumed to be either 0 or 1. -func ValidateSignatureValues(v byte, r, s *uint256.Int, homestead bool) bool { - if r.IsZero() || s.IsZero() { - return false - } - // reject upper range of s values (ECDSA malleability) - // see discussion in secp256k1/libsecp256k1/include/secp256k1.h - if homestead && s.Gt(secp256k1halfN) { - return false - } - // Frontier: allow s to be in full N range - return r.Lt(secp256k1N) && s.Lt(secp256k1N) && (v == 0 || v == 1) -} - // DESCRIBED: docs/programmers_guide/guide.md#address---identifier-of-an-account func PubkeyToAddress(p ecdsa.PublicKey) libcommon.Address { pubBytes := MarshalPubkey(&p) diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index b9a979bf8c2..c5167d70754 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -20,18 +20,16 @@ import ( "bytes" "crypto/ecdsa" "encoding/hex" - "github.com/ledgerwatch/erigon-lib/common/hexutil" "os" "reflect" "testing" "golang.org/x/crypto/sha3" - "github.com/holiman/uint256" libcommon "github.com/ledgerwatch/erigon-lib/common" + "github.com/ledgerwatch/erigon-lib/common/hexutil" "github.com/ledgerwatch/erigon/common" - "github.com/ledgerwatch/erigon/common/u256" ) var testAddrHex = "970e8128ab834e8eac17ab8e3812f010678cf791" @@ -276,52 +274,6 @@ func TestSaveECDSA(t *testing.T) { } } -func TestValidateSignatureValues(t *testing.T) { - check := func(expected bool, v byte, r, s *uint256.Int) { - if ValidateSignatureValues(v, r, s, false) != expected { - t.Errorf("mismatch for v: %d r: %d s: %d want: %v", v, r, s, expected) - } - } - minusOne := uint256.NewInt(0).SetAllOne() - one := u256.Num1 - zero := u256.Num0 - secp256k1nMinus1 := new(uint256.Int).Sub(secp256k1N, u256.Num1) - - // correct v,r,s - check(true, 0, one, one) - check(true, 1, one, one) - // incorrect v, correct r,s, - check(false, 2, one, one) - check(false, 3, one, one) - - // incorrect v, combinations of incorrect/correct r,s at lower limit - check(false, 2, zero, zero) - check(false, 2, zero, one) - check(false, 2, one, zero) - check(false, 2, one, one) - - // correct v for any combination of incorrect r,s - check(false, 0, zero, zero) - check(false, 0, zero, one) - check(false, 0, one, zero) - - check(false, 1, zero, zero) - check(false, 1, zero, one) - check(false, 1, one, zero) - - // correct sig with max r,s - check(true, 0, secp256k1nMinus1, secp256k1nMinus1) - // correct v, combinations of incorrect r,s at upper limit - check(false, 0, secp256k1N, secp256k1nMinus1) - check(false, 0, secp256k1nMinus1, secp256k1N) - check(false, 0, secp256k1N, secp256k1N) - - // current callers ensures r,s cannot be negative, but let's test for that too - // as crypto package could be used stand-alone - check(false, 0, minusOne, one) - check(false, 0, one, minusOne) -} - func checkhash(t *testing.T, name string, f func([]byte) []byte, msg, exp []byte) { sum := f(msg) if !bytes.Equal(exp, sum) { diff --git a/erigon-lib/crypto/secp256k1.go b/erigon-lib/crypto/secp256k1.go index 3e8f6dc2ff0..e004445787c 100644 --- a/erigon-lib/crypto/secp256k1.go +++ b/erigon-lib/crypto/secp256k1.go @@ -24,7 +24,7 @@ import ( var ( secp256k1N = new(uint256.Int).SetBytes(hexutility.MustDecodeHex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141")) - secp256k1halfN = new(uint256.Int).Rsh(secp256k1N, 1) + Secp256k1halfN = new(uint256.Int).Rsh(secp256k1N, 1) ) // See Appendix F "Signing Transactions" of the Yellow Paper @@ -34,7 +34,7 @@ func TransactionSignatureIsValid(v byte, r, s *uint256.Int, allowPreEip2s bool) } // See EIP-2: Homestead Hard-fork Changes - if !allowPreEip2s && s.Gt(secp256k1halfN) { + if !allowPreEip2s && s.Gt(Secp256k1halfN) { return false } diff --git a/erigon-lib/crypto/secp256k1_test.go b/erigon-lib/crypto/secp256k1_test.go new file mode 100644 index 00000000000..717ce7a06fb --- /dev/null +++ b/erigon-lib/crypto/secp256k1_test.go @@ -0,0 +1,74 @@ +// Copyright 2014 The go-ethereum Authors +// (original work) +// Copyright 2024 The Erigon Authors +// (modifications) +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package crypto + +import ( + "testing" + + "github.com/holiman/uint256" + + "github.com/ledgerwatch/erigon-lib/common/u256" +) + +func TestTransactionSignatureIsValid(t *testing.T) { + check := func(expected bool, v byte, r, s *uint256.Int) { + if TransactionSignatureIsValid(v, r, s, true) != expected { + t.Errorf("mismatch for v: %d r: %d s: %d want: %v", v, r, s, expected) + } + } + minusOne := uint256.NewInt(0).SetAllOne() + one := u256.N1 + zero := u256.N0 + secp256k1nMinus1 := new(uint256.Int).Sub(secp256k1N, u256.N1) + + // correct v,r,s + check(true, 0, one, one) + check(true, 1, one, one) + // incorrect v, correct r,s, + check(false, 2, one, one) + check(false, 3, one, one) + + // incorrect v, combinations of incorrect/correct r,s at lower limit + check(false, 2, zero, zero) + check(false, 2, zero, one) + check(false, 2, one, zero) + check(false, 2, one, one) + + // correct v for any combination of incorrect r,s + check(false, 0, zero, zero) + check(false, 0, zero, one) + check(false, 0, one, zero) + + check(false, 1, zero, zero) + check(false, 1, zero, one) + check(false, 1, one, zero) + + // correct sig with max r,s + check(true, 0, secp256k1nMinus1, secp256k1nMinus1) + // correct v, combinations of incorrect r,s at upper limit + check(false, 0, secp256k1N, secp256k1nMinus1) + check(false, 0, secp256k1nMinus1, secp256k1N) + check(false, 0, secp256k1N, secp256k1N) + + // current callers ensures r,s cannot be negative, but let's test for that too + // as crypto package could be used stand-alone + check(false, 0, minusOne, one) + check(false, 0, one, minusOne) +} diff --git a/erigon-lib/txpool/pool.go b/erigon-lib/txpool/pool.go index 53a8a3a7652..89dca8e1ec0 100644 --- a/erigon-lib/txpool/pool.go +++ b/erigon-lib/txpool/pool.go @@ -40,6 +40,7 @@ import ( "github.com/hashicorp/golang-lru/v2/simplelru" "github.com/holiman/uint256" "github.com/ledgerwatch/erigon-lib/common/hexutility" + "github.com/ledgerwatch/erigon-lib/crypto" "github.com/ledgerwatch/log/v3" "github.com/ledgerwatch/erigon-lib/chain" @@ -773,7 +774,8 @@ func (p *TxPool) best(n uint16, txs *types.TxsRlp, tx kv.Tx, onTopOf, availableG // make sure we have enough gas in the caller to add this transaction. // not an exact science using intrinsic gas but as close as we could hope for at // this stage - intrinsicGas, _ := txpoolcfg.CalcIntrinsicGas(uint64(mt.Tx.DataLen), uint64(mt.Tx.DataNonZeroLen), uint64(mt.Tx.AuthorizationLen), nil, mt.Tx.Creation, true, true, isShanghai) + authorizationLen := uint64(len(mt.Tx.Authorizations)) + intrinsicGas, _ := txpoolcfg.CalcIntrinsicGas(uint64(mt.Tx.DataLen), uint64(mt.Tx.DataNonZeroLen), authorizationLen, nil, mt.Tx.Creation, true, true, isShanghai) if intrinsicGas > availableGas { // we might find another TX with a low enough intrinsic gas to include so carry on continue @@ -853,7 +855,7 @@ func (p *TxPool) validateTx(txn *types.TxSlot, isLocal bool, stateCache kvcache. return txpoolcfg.TypeNotActivated } if txn.Creation { - return txpoolcfg.CreateBlobTxn + return txpoolcfg.InvalidCreateTxn } blobCount := uint64(len(txn.BlobHashes)) if blobCount == 0 { @@ -897,10 +899,22 @@ func (p *TxPool) validateTx(txn *types.TxSlot, isLocal bool, stateCache kvcache. } } + authorizationLen := len(txn.Authorizations) if txn.Type == types.SetCodeTxType { if !p.isPrague() { return txpoolcfg.TypeNotActivated } + if txn.Creation { + return txpoolcfg.InvalidCreateTxn + } + if authorizationLen == 0 { + return txpoolcfg.NoAuthorizations + } + for i := 0; i < authorizationLen; i++ { + if txn.Authorizations[i].S.Gt(crypto.Secp256k1halfN) { + return txpoolcfg.InvalidAuthorization + } + } } // Drop non-local transactions under our own minimal accepted gas price or tip @@ -910,7 +924,7 @@ func (p *TxPool) validateTx(txn *types.TxSlot, isLocal bool, stateCache kvcache. } return txpoolcfg.UnderPriced } - gas, reason := txpoolcfg.CalcIntrinsicGas(uint64(txn.DataLen), uint64(txn.DataNonZeroLen), uint64(txn.AuthorizationLen), nil, txn.Creation, true, true, isShanghai) + gas, reason := txpoolcfg.CalcIntrinsicGas(uint64(txn.DataLen), uint64(txn.DataNonZeroLen), uint64(authorizationLen), nil, txn.Creation, true, true, isShanghai) if txn.Traced { p.logger.Info(fmt.Sprintf("TX TRACING: validateTx intrinsic gas idHash=%x gas=%d", txn.IDHash, gas)) } diff --git a/erigon-lib/txpool/txpool_grpc_server.go b/erigon-lib/txpool/txpool_grpc_server.go index 9d7ee2b7bc6..c638d3b37e0 100644 --- a/erigon-lib/txpool/txpool_grpc_server.go +++ b/erigon-lib/txpool/txpool_grpc_server.go @@ -239,8 +239,11 @@ func mapDiscardReasonToProto(reason txpoolcfg.DiscardReason) txpool_proto.Import return txpool_proto.ImportResult_ALREADY_EXISTS case txpoolcfg.UnderPriced, txpoolcfg.ReplaceUnderpriced, txpoolcfg.FeeTooLow: return txpool_proto.ImportResult_FEE_TOO_LOW - case txpoolcfg.InvalidSender, txpoolcfg.NegativeValue, txpoolcfg.OversizedData, txpoolcfg.InitCodeTooLarge, txpoolcfg.RLPTooLong, txpoolcfg.CreateBlobTxn, txpoolcfg.NoBlobs, txpoolcfg.TooManyBlobs, txpoolcfg.TypeNotActivated, txpoolcfg.UnequalBlobTxExt, txpoolcfg.BlobHashCheckFail, txpoolcfg.UnmatchedBlobTxExt: - // TODO(eip-4844) TypeNotActivated may be transient (e.g. a blob transaction is submitted 1 sec prior to Cancun activation) + case txpoolcfg.InvalidSender, txpoolcfg.NegativeValue, txpoolcfg.OversizedData, txpoolcfg.InitCodeTooLarge, + txpoolcfg.RLPTooLong, txpoolcfg.InvalidCreateTxn, txpoolcfg.NoBlobs, txpoolcfg.TooManyBlobs, + txpoolcfg.TypeNotActivated, txpoolcfg.UnequalBlobTxExt, txpoolcfg.BlobHashCheckFail, + txpoolcfg.UnmatchedBlobTxExt, txpoolcfg.NoAuthorizations, txpoolcfg.InvalidAuthorization: + // TODO(EIP-7702) TypeNotActivated may be transient (e.g. a set code transaction is submitted 1 sec prior to the Pectra activation) return txpool_proto.ImportResult_INVALID default: return txpool_proto.ImportResult_INTERNAL_ERROR diff --git a/erigon-lib/txpool/txpoolcfg/txpoolcfg.go b/erigon-lib/txpool/txpoolcfg/txpoolcfg.go index a9f56f9700d..e5eabf3db65 100644 --- a/erigon-lib/txpool/txpoolcfg/txpoolcfg.go +++ b/erigon-lib/txpool/txpoolcfg/txpoolcfg.go @@ -84,39 +84,40 @@ var DefaultConfig = Config{ type DiscardReason uint8 const ( - NotSet DiscardReason = 0 // analog of "nil-value", means it will be set in future - Success DiscardReason = 1 - AlreadyKnown DiscardReason = 2 - Mined DiscardReason = 3 - ReplacedByHigherTip DiscardReason = 4 - UnderPriced DiscardReason = 5 - ReplaceUnderpriced DiscardReason = 6 // if a transaction is attempted to be replaced with a different one without the required price bump. - FeeTooLow DiscardReason = 7 - OversizedData DiscardReason = 8 - InvalidSender DiscardReason = 9 - NegativeValue DiscardReason = 10 // ensure no one is able to specify a transaction with a negative value. - Spammer DiscardReason = 11 - PendingPoolOverflow DiscardReason = 12 - BaseFeePoolOverflow DiscardReason = 13 - QueuedPoolOverflow DiscardReason = 14 - GasUintOverflow DiscardReason = 15 - IntrinsicGas DiscardReason = 16 - RLPTooLong DiscardReason = 17 - NonceTooLow DiscardReason = 18 - InsufficientFunds DiscardReason = 19 - NotReplaced DiscardReason = 20 // There was an existing transaction with the same sender and nonce, not enough price bump to replace - DuplicateHash DiscardReason = 21 // There was an existing transaction with the same hash - InitCodeTooLarge DiscardReason = 22 // EIP-3860 - transaction init code is too large - TypeNotActivated DiscardReason = 23 // For example, an EIP-4844 transaction is submitted before Cancun activation - CreateBlobTxn DiscardReason = 24 // Blob transactions cannot have the form of a create transaction - NoBlobs DiscardReason = 25 // Blob transactions must have at least one blob - TooManyBlobs DiscardReason = 26 // There's a limit on how many blobs a block (and thus any transaction) may have - UnequalBlobTxExt DiscardReason = 27 // blob_versioned_hashes, blobs, commitments and proofs must have equal number - BlobHashCheckFail DiscardReason = 28 // KZGcommitment's versioned hash has to be equal to blob_versioned_hash at the same index - UnmatchedBlobTxExt DiscardReason = 29 // KZGcommitments must match the corresponding blobs and proofs - BlobTxReplace DiscardReason = 30 // Cannot replace type-3 blob txn with another type of txn - BlobPoolOverflow DiscardReason = 31 // The total number of blobs (through blob txs) in the pool has reached its limit - + NotSet DiscardReason = 0 // analog of "nil-value", means it will be set in future + Success DiscardReason = 1 + AlreadyKnown DiscardReason = 2 + Mined DiscardReason = 3 + ReplacedByHigherTip DiscardReason = 4 + UnderPriced DiscardReason = 5 + ReplaceUnderpriced DiscardReason = 6 // if a transaction is attempted to be replaced with a different one without the required price bump. + FeeTooLow DiscardReason = 7 + OversizedData DiscardReason = 8 + InvalidSender DiscardReason = 9 + NegativeValue DiscardReason = 10 // ensure no one is able to specify a transaction with a negative value. + Spammer DiscardReason = 11 + PendingPoolOverflow DiscardReason = 12 + BaseFeePoolOverflow DiscardReason = 13 + QueuedPoolOverflow DiscardReason = 14 + GasUintOverflow DiscardReason = 15 + IntrinsicGas DiscardReason = 16 + RLPTooLong DiscardReason = 17 + NonceTooLow DiscardReason = 18 + InsufficientFunds DiscardReason = 19 + NotReplaced DiscardReason = 20 // There was an existing transaction with the same sender and nonce, not enough price bump to replace + DuplicateHash DiscardReason = 21 // There was an existing transaction with the same hash + InitCodeTooLarge DiscardReason = 22 // EIP-3860 - transaction init code is too large + TypeNotActivated DiscardReason = 23 // For example, an EIP-4844 transaction is submitted before Cancun activation + InvalidCreateTxn DiscardReason = 24 // EIP-4844 & 7702 transactions cannot have the form of a create transaction + NoBlobs DiscardReason = 25 // Blob transactions must have at least one blob + TooManyBlobs DiscardReason = 26 // There's a limit on how many blobs a block (and thus any transaction) may have + UnequalBlobTxExt DiscardReason = 27 // blob_versioned_hashes, blobs, commitments and proofs must have equal number + BlobHashCheckFail DiscardReason = 28 // KZGcommitment's versioned hash has to be equal to blob_versioned_hash at the same index + UnmatchedBlobTxExt DiscardReason = 29 // KZGcommitments must match the corresponding blobs and proofs + BlobTxReplace DiscardReason = 30 // Cannot replace type-3 blob txn with another type of txn + BlobPoolOverflow DiscardReason = 31 // The total number of blobs (through blob txs) in the pool has reached its limit + NoAuthorizations DiscardReason = 32 // EIP-7702 transactions with an empty authorization list are invalid + InvalidAuthorization DiscardReason = 33 // Authorization signature is invalid (EIP-7702) ) func (r DiscardReason) String() string { @@ -169,8 +170,8 @@ func (r DiscardReason) String() string { return "initcode too large" case TypeNotActivated: return "fork supporting this transaction type is not activated yet" - case CreateBlobTxn: - return "blob transactions cannot have the form of a create transaction" + case InvalidCreateTxn: + return "EIP-4844 & 7702 transactions cannot have the form of a create transaction" case NoBlobs: return "blob transactions must have at least one blob" case TooManyBlobs: @@ -179,6 +180,10 @@ func (r DiscardReason) String() string { return "can't replace blob-txn with a non-blob-txn" case BlobPoolOverflow: return "blobs limit in txpool is full" + case NoAuthorizations: + return "EIP-7702 transactions with an empty authorization list are invalid" + case InvalidAuthorization: + return "Authorization signature is invalid (EIP-7702)" default: panic(fmt.Sprintf("discard reason: %d", r)) } diff --git a/erigon-lib/types/txn.go b/erigon-lib/types/txn.go index e83788d976c..32939c93d2d 100644 --- a/erigon-lib/types/txn.go +++ b/erigon-lib/types/txn.go @@ -44,18 +44,20 @@ type TxParseConfig struct { ChainID uint256.Int } +type Signature struct { + ChainID uint256.Int + V uint256.Int + R uint256.Int + S uint256.Int +} + // TxParseContext is object that is required to parse transactions and turn transaction payload into TxSlot objects // usage of TxContext helps avoid extra memory allocations type TxParseContext struct { + Signature Keccak2 hash.Hash Keccak1 hash.Hash validateRlp func([]byte) error - ChainID uint256.Int // Signature values - R uint256.Int // Signature values - S uint256.Int // Signature values - V uint256.Int // Signature values - ChainIDMul uint256.Int - DeriveChainID uint256.Int // pre-allocated variable to calculate Sub(&ctx.v, &ctx.chainIDMul) cfg TxParseConfig buf [65]byte // buffer needs to be enough for hashes (32 bytes) and for public key (65 bytes) Sig [65]byte @@ -63,7 +65,6 @@ type TxParseContext struct { withSender bool allowPreEip2s bool // Allow s > secp256k1n/2; see EIP-2 chainIDRequired bool - IsProtected bool } func NewTxParseContext(chainID uint256.Int) *TxParseContext { @@ -78,7 +79,6 @@ func NewTxParseContext(chainID uint256.Int) *TxParseContext { // behave as of London enabled ctx.cfg.ChainID.Set(&chainID) - ctx.ChainIDMul.Mul(&chainID, u256.N2) return ctx } @@ -110,7 +110,7 @@ type TxSlot struct { Proofs []gokzg4844.KZGProof // EIP-7702: set code tx - AuthorizationLen int + Authorizations []Signature } const ( @@ -295,6 +295,54 @@ func (ctx *TxParseContext) ParseTransaction(payload []byte, pos int, slot *TxSlo return p, err } +func parseSignature(payload []byte, pos int, legacy bool, cfgChainId *uint256.Int, sig *Signature) (p int, yParity byte, err error) { + p = pos + + // Parse V / yParity + p, err = rlp.U256(payload, p, &sig.V) + if err != nil { + return 0, 0, fmt.Errorf("v: %w", err) + } + if legacy { + preEip155 := sig.V.Eq(u256.N27) || sig.V.Eq(u256.N28) + // Compute chainId from V + if preEip155 { + yParity = byte(sig.V.Uint64() - 27) + sig.ChainID.Set(cfgChainId) + } else { + // EIP-155: Simple replay attack protection + // V = ChainID * 2 + 35 + yParity + if sig.V.LtUint64(35) { + return 0, 0, fmt.Errorf("EIP-155 implies V>=35 (was %d)", sig.V.Uint64()) + } + sig.ChainID.Sub(&sig.V, u256.N35) + yParity = byte(sig.ChainID.Uint64() % 2) + sig.ChainID.Rsh(&sig.ChainID, 1) + if !sig.ChainID.Eq(cfgChainId) { + return 0, 0, fmt.Errorf("invalid chainID %s (expected %s)", &sig.ChainID, cfgChainId) + } + } + } else { + if sig.V.GtUint64(1) { + return 0, 0, fmt.Errorf("v is loo large: %s", &sig.V) + } + yParity = byte(sig.V.Uint64()) + } + + // Next follows R of the signature + p, err = rlp.U256(payload, p, &sig.R) + if err != nil { + return 0, 0, fmt.Errorf("r: %w", err) + } + // New follows S of the signature + p, err = rlp.U256(payload, p, &sig.S) + if err != nil { + return 0, 0, fmt.Errorf("s: %w", err) + } + + return p, yParity, nil +} + func (ctx *TxParseContext) parseTransactionBody(payload []byte, pos, p0 int, slot *TxSlot, sender []byte, validateHash func([]byte) error) (p int, err error) { p = p0 legacy := slot.Type == LegacyTxType @@ -427,19 +475,22 @@ func (ctx *TxParseContext) parseTransactionBody(payload []byte, pos, p0 int, slo if err != nil { return 0, fmt.Errorf("%w: storage key list len: %s", ErrParseTxn, err) //nolint } - skeyPos := storagePos - for skeyPos < storagePos+storageLen { - skeyPos, err = rlp.StringOfLen(payload, skeyPos, 32) + sKeyPos := storagePos + for sKeyPos < storagePos+storageLen { + sKeyPos, err = rlp.StringOfLen(payload, sKeyPos, 32) if err != nil { return 0, fmt.Errorf("%w: tuple storage key len: %s", ErrParseTxn, err) //nolint } slot.AlStorCount++ - skeyPos += 32 + sKeyPos += 32 } - if skeyPos != storagePos+storageLen { - return 0, fmt.Errorf("%w: extraneous space in the tuple after storage key list", ErrParseTxn) + if sKeyPos != storagePos+storageLen { + return 0, fmt.Errorf("%w: unexpected storage key items", ErrParseTxn) } tuplePos += tupleLen + if tuplePos != sKeyPos { + return 0, fmt.Errorf("%w: extraneous space in the tuple after storage key list", ErrParseTxn) + } } if tuplePos != dataPos+dataLen { return 0, fmt.Errorf("%w: extraneous space in the access list after all tuples", ErrParseTxn) @@ -452,14 +503,36 @@ func (ctx *TxParseContext) parseTransactionBody(payload []byte, pos, p0 int, slo return 0, fmt.Errorf("%w: authorizations len: %s", ErrParseTxn, err) //nolint } authPos := dataPos - var authLen int for authPos < dataPos+dataLen { + var authLen int authPos, authLen, err = rlp.List(payload, authPos) if err != nil { return 0, fmt.Errorf("%w: authorization: %s", ErrParseTxn, err) //nolint } - slot.AuthorizationLen++ + var sig Signature + p2 := authPos + p2, err = rlp.U256(payload, p2, &sig.ChainID) + if err != nil { + return 0, fmt.Errorf("%w: authorization chainId: %s", ErrParseTxn, err) //nolint + } + p2, err = rlp.StringOfLen(payload, p2, 20) // address + if err != nil { + return 0, fmt.Errorf("%w: authorization address: %s", ErrParseTxn, err) //nolint + } + p2 += 20 + p2, _, err = rlp.U64(payload, p2) // nonce + if err != nil { + return 0, fmt.Errorf("%w: authorization nonce: %s", ErrParseTxn, err) //nolint + } + p2, _, err = parseSignature(payload, p2, false /* legacy */, nil /* cfgChainId */, &sig) + if err != nil { + return 0, fmt.Errorf("%w: authorization signature: %s", ErrParseTxn, err) //nolint + } + slot.Authorizations = append(slot.Authorizations, sig) authPos += authLen + if authPos != p2 { + return 0, fmt.Errorf("%w: authorization: unexpected list items", ErrParseTxn) + } } if authPos != dataPos+dataLen { return 0, fmt.Errorf("%w: extraneous space in the authorizations", ErrParseTxn) @@ -490,29 +563,19 @@ func (ctx *TxParseContext) parseTransactionBody(payload []byte, pos, p0 int, slo p = dataPos + dataLen } // This is where the data for Sighash ends - // Next follows V of the signature + // Next follows the signature var vByte byte sigHashEnd := p sigHashLen := uint(sigHashEnd - sigHashPos) var chainIDBits, chainIDLen int - if legacy { - p, err = rlp.U256(payload, p, &ctx.V) - if err != nil { - return 0, fmt.Errorf("%w: V: %s", ErrParseTxn, err) //nolint - } - ctx.IsProtected = ctx.V.Eq(u256.N27) || ctx.V.Eq(u256.N28) - // Compute chainId from V - if ctx.IsProtected { - // Do not add chain id and two extra zeros - vByte = byte(ctx.V.Uint64() - 27) - ctx.ChainID.Set(&ctx.cfg.ChainID) - } else { - ctx.ChainID.Sub(&ctx.V, u256.N35) - ctx.ChainID.Rsh(&ctx.ChainID, 1) - if !ctx.ChainID.Eq(&ctx.cfg.ChainID) { - return 0, fmt.Errorf("%w: %s, %d (expected %d)", ErrParseTxn, "invalid chainID", ctx.ChainID.Uint64(), ctx.cfg.ChainID.Uint64()) - } + p, vByte, err = parseSignature(payload, p, legacy, &ctx.cfg.ChainID, &ctx.Signature) + if err != nil { + return 0, fmt.Errorf("%w: %s", ErrParseTxn, err) //nolint + } + if legacy { + preEip155 := ctx.V.Eq(u256.N27) || ctx.V.Eq(u256.N28) + if !preEip155 { chainIDBits = ctx.ChainID.BitLen() if chainIDBits <= 7 { chainIDLen = 1 @@ -522,32 +585,7 @@ func (ctx *TxParseContext) parseTransactionBody(payload []byte, pos, p0 int, slo } sigHashLen += uint(chainIDLen) // For chainId sigHashLen += 2 // For two extra zeros - - ctx.DeriveChainID.Sub(&ctx.V, &ctx.ChainIDMul) - vByte = byte(ctx.DeriveChainID.Sub(&ctx.DeriveChainID, u256.N8).Uint64() - 27) - } - } else { - var v uint64 - p, v, err = rlp.U64(payload, p) - if err != nil { - return 0, fmt.Errorf("%w: V: %s", ErrParseTxn, err) //nolint - } - if v > 1 { - return 0, fmt.Errorf("%w: V is loo large: %d", ErrParseTxn, v) } - vByte = byte(v) - ctx.IsProtected = true - } - - // Next follows R of the signature - p, err = rlp.U256(payload, p, &ctx.R) - if err != nil { - return 0, fmt.Errorf("%w: R: %s", ErrParseTxn, err) //nolint - } - // New follows S of the signature - p, err = rlp.U256(payload, p, &ctx.S) - if err != nil { - return 0, fmt.Errorf("%w: S: %s", ErrParseTxn, err) //nolint } // For legacy transactions, hash the full payload diff --git a/erigon-lib/types/txn_test.go b/erigon-lib/types/txn_test.go index 90b6ff91f07..99ce6b0cfaa 100644 --- a/erigon-lib/types/txn_test.go +++ b/erigon-lib/types/txn_test.go @@ -294,7 +294,7 @@ func TestBlobTxParsing(t *testing.T) { } func TestSetCodeTxParsing(t *testing.T) { - bodyRlxHex := "0x04f902b10188a804b97e55f1619888a08ae04b7dc9b296889262bfb381a9852c88c3ed5816717427719426b97a6e638930cd51d0482d4c3908e171c848ca88e7fcd96deec354e0b8b45c84e7ba97d4d60285eebbf3f7be79aed25dfe2a9f086c69c8ae152aa777b806f45ab602f4b354a6537154e24b4f6b85b58535726876fa885dba96b202417326bb4e4ba5e0bcccd9b4e4df6096401c19e7d38df3599157249a72ac3cf095c39cfde8d4233303823f5341ccaa9ebaf78cd8dd06ec61af9924df9a2f97d13c88ae737a017c914d21d3390984a6102c51293b3b2cec8214e6be2ee033ed4795f1158d9103c9ab5f50786729dd9baf395bb20c71456cf8d5f89b94f8032107975d3fbd90ffa74185a0cb5ab43afe85f884a037e36aea1770355c584876f969c7014017aa51a5e287c9679f4402d1a878b6e3a0be3cdb54e9c5fc3032535d262e6e9ce6a092e068ad0c95f19b4022c1111652b0a0562f7754a0c1d29acfbdaad8ae779125ccc6afec0ec1177056391479b25cee72a069a8be541415e284e16be88ecdb73d5e14ae0e0ade0db635a8d717b70d98293ef794cad5e3980e2d53db6d79d5e6821cff73bef01803e1a0415787a09d11b750dfd34dfe0868ab2c7e6bd8d7ef1a66720f2ea6c7f6e9bb01f8ccf201943d8b4362475b3d0504ccd9e63cddd351b88fa052c088832c50d9581133828864e3d39680cff14988237c283c57f04c54f201940e7ceefb1855e91bd085d85852b2a9df4f9da4f0c088a87ef261eb89b837882d717143b8cb5e718854d879dc9f18304ef2019477be91ff1fb94eb618aebb4d829e9f8eeec4301bc088343c738cf5c7f5b1880521e62bff507ec288ce2f51e36cb6d54ef20194c32daf3ad4597567184d790ab83d7bf34cf0e446c088c04ac61fbe29181988f8c0d967f0799fb988772265a4be2b26ab0188c91023f53ea594d7881c46b9feb3b7cbc6" + bodyRlxHex := "0x04f9041701880124ec419e9796da8868b499f209983df888bb35ca86e3d9ea47882486c24309101b0e94e4ec12c49d6bcf7cc1325aa50afff92a561229fe880c716dca0e3e3d28b902b6779e563691f1ca8a86a02efdd93db261215047dad430a475d0e191f66b580d6e759a7c7a739532455e65160acf92dc1e1cc11970e7851277278e9d5d2549e451de8c8dd98ebdd3c55e73cd0b465875b72ea6d54917474f7ddfbd1f66d1a929694becc69bc3064c79c32b2db2a094844b400133724e046d9a96f2b6c7888fe008e6a667a970068487ce9a8e6c1260973956b26c1b78235f3452e21c5ed6d47507023ec4072b9ebea8ea9bde77ea64352ef7a6a8efb2ca61fbd0cf7c31491a4c38e3081dfc7b5e8066fca60d8f57b641032f23119a67a37ad0514529df22ba73b4028dc4a6aef0b26161371d731a81d8ac20ea90515b924f2534e32c240d0b75b5d1683e1bc7ecf8b82b73fb4c40d7cfc38e8c32f2c4d3424a86ba8c6e867f13328be201dd8d5e8ee47e03c1d9096968b71228b068cc21514f6bab7867a0d0a2651f40e927079b008c3ef11d571eb5f71d729ee9cfb3d2a99d258c10371fa1df271f4588e031498b155244295490fd842b3055e240ea89843a188b7f15be53252367761b9a8d21818d2c756822c0383246e167dd645722aefe4ecc5e78608bcc851dc5a51255a3f91e908bb5fa53063596458f45c6e25a712de4b2a5b36eea57f5b772c84f1d0f2f2ae103445fb7f2d38493041ca452f1e846c34331bea7b5b350d02306fa3a15b50e978b4efebccce8a3479479d51c95a08e0cab0732fc4f8095337d7502c6a962199342ed127701a6f5b0e54cbdd88f23556aab406a3a7ef49f848c3efbf4cf62052999bde1940abf4944158aefc5472f4ec9e23308cfb63deedc79e9a4f39d8b353c7e6f15d36f4c63987ae6f32701c6579e68f05f9ae86b6fbbc8d57bc17e5c2f3e5389ea75d102017767205c10d6bf5cf6e33a94ad9e6cfac5accf56d61dcee39f2e954ea89b7241e480e6021fa099a81bc9d28d6ca58a11d36f406b212be70c721bd8a4d1d643fa2bf30ebd59a4f838f794fbba2afaae8cabd778b6e151b0431e3fef0a033ce1a07081820b2a08cc2ed4355811644547f23597f7ebe516538baac51d97cbccee97f8ccf201941d994a07f0b3e925d332d4eae10c9ba474da3d8a8806320d2ae09c60e880887dbf8422d2f6549088321947f20ebcbfeff20194327d773bdc6c27cd28a533e81074372dc33a8afd884ef63dce09c5e56c8088cb702ac89cff765f88d26fe11c3d471949f20194f61ffc773a97207c8124c29526a59e6fa0b34a52880e563a787da952ab808884f2a19b171abfb2882d473907f3ada086f20194c1d608bb39e078a99086e7564e89a7625ed86dca88e8a0ab45821912e88088df6c3d43080350518895a828c35680a0278088e2487fd89ca40b3488689accdbeb8d4d2e" bodyRlx := hexutility.MustDecodeHex(bodyRlxHex) hasEnvelope := false @@ -308,7 +308,7 @@ func TestSetCodeTxParsing(t *testing.T) { _, err = ctx.ParseTransaction(bodyRlx, 0, &tx, nil, hasEnvelope, false, nil) require.NoError(t, err) - assert.Equal(t, 4, tx.AuthorizationLen) + assert.Equal(t, 4, len(tx.Authorizations)) assert.Equal(t, SetCodeTxType, tx.Type) // test empty authorizations @@ -324,7 +324,7 @@ func TestSetCodeTxParsing(t *testing.T) { _, err = ctx.ParseTransaction(bodyRlx, 0, &tx2, nil, hasEnvelope, false, nil) require.NoError(t, err) - assert.Equal(t, 0, tx2.AuthorizationLen) + assert.Equal(t, 0, len(tx2.Authorizations)) assert.Equal(t, SetCodeTxType, tx2.Type) // generated using this in encdec_test.go @@ -340,9 +340,10 @@ func TestSetCodeTxParsing(t *testing.T) { tx.GetChainID().SetUint64(1) - for _, auth := range tx.(*SetCodeTransaction).GetAuthorizations() { - auth.ChainID.SetUint64(1) - auth.V.SetUint64(uint64(randIntInRange(0, 2))) + auths := tx.(*SetCodeTransaction).GetAuthorizations() + for i := range auths { + auths[i].ChainID.SetUint64(1) + auths[i].V.SetUint64(uint64(randIntInRange(0, 2))) } w := bytes.NewBuffer(nil) if err := tx.MarshalBinary(w); err != nil {