diff --git a/core/types/authorization.go b/core/types/authorization.go index 602df7bb805..735e57d3e1c 100644 --- a/core/types/authorization.go +++ b/core/types/authorization.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "math" "github.com/holiman/uint256" @@ -36,8 +37,12 @@ func (ath *Authorization) copy() *Authorization { } func (ath *Authorization) RecoverSigner(data *bytes.Buffer, b []byte) (*libcommon.Address, error) { + if ath.Nonce > math.MaxUint64 { + return nil, errors.New("failed assertion: auth.nonce < 2**64 - 1") + } + authLen := rlp.U64Len(ath.ChainID) - authLen += (1 + length.Addr) + authLen += 1 + length.Addr authLen += rlp.U64Len(ath.Nonce) if err := rlp.EncodeStructSizePrefix(authLen, data, b); err != nil { @@ -57,46 +62,46 @@ func (ath *Authorization) RecoverSigner(data *bytes.Buffer, b []byte) (*libcommo return nil, err } + return RecoverSignerFromRLP(data.Bytes(), ath.YParity, ath.R, ath.S) +} + +func RecoverSignerFromRLP(rlp []byte, yParity uint8, r uint256.Int, s uint256.Int) (*libcommon.Address, error) { hashData := []byte{params.SetCodeMagicPrefix} - hashData = append(hashData, data.Bytes()...) + hashData = append(hashData, rlp...) hash := crypto.Keccak256Hash(hashData) var sig [65]byte - r := ath.R.Bytes() - s := ath.S.Bytes() - copy(sig[32-len(r):32], r) - copy(sig[64-len(s):64], s) + rBytes := r.Bytes() + sBytes := s.Bytes() + copy(sig[32-len(rBytes):32], rBytes) + copy(sig[64-len(sBytes):64], sBytes) - if ath.Nonce == 1<<64-1 { - return nil, errors.New("failed assertion: auth.nonce < 2**64 - 1") - } - if ath.YParity == 0 || ath.YParity == 1 { - sig[64] = ath.YParity - } else { - return nil, fmt.Errorf("invalid y parity value: %d", ath.YParity) + if yParity > 1 { + return nil, fmt.Errorf("invalid y parity value: %d", yParity) } + sig[64] = yParity - if !crypto.TransactionSignatureIsValid(sig[64], &ath.R, &ath.S, false /* allowPreEip2s */) { + if !crypto.TransactionSignatureIsValid(sig[64], &r, &s, false /* allowPreEip2s */) { return nil, errors.New("invalid signature") } - pubkey, err := crypto.Ecrecover(hash.Bytes(), sig[:]) + pubKey, err := crypto.Ecrecover(hash.Bytes(), sig[:]) if err != nil { return nil, err } - if len(pubkey) == 0 || pubkey[0] != 4 { + if len(pubKey) == 0 || pubKey[0] != 4 { return nil, errors.New("invalid public key") } var authority libcommon.Address - copy(authority[:], crypto.Keccak256(pubkey[1:])[12:]) + copy(authority[:], crypto.Keccak256(pubKey[1:])[12:]) return &authority, nil } func authorizationSize(auth Authorization) (authLen int) { authLen = rlp.U64Len(auth.ChainID) authLen += rlp.U64Len(auth.Nonce) - authLen += (1 + length.Addr) + authLen += 1 + length.Addr authLen += rlp.U64Len(uint64(auth.YParity)) + (1 + rlp.Uint256LenExcludingHead(&auth.R)) + (1 + rlp.Uint256LenExcludingHead(&auth.S)) diff --git a/params/protocol_params.go b/params/protocol_params.go index fc3c878f181..e9b33bdad40 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -184,6 +184,7 @@ const ( SetCodeMagicPrefix = byte(0x05) ) +// EIP-7702: Set EOA account code var DelegatedDesignationPrefix = []byte{0xef, 0x01, 0x00} // EIP-4788: Beacon block root in the EVM diff --git a/txnprovider/txpool/pool.go b/txnprovider/txpool/pool.go index d0600b3476a..23a82210be9 100644 --- a/txnprovider/txpool/pool.go +++ b/txnprovider/txpool/pool.go @@ -143,6 +143,7 @@ type TxPool struct { maxBlobsPerBlock uint64 feeCalculator FeeCalculator logger log.Logger + auths map[common.Address]*metaTxn // All accounts with a pooled authorization } type FeeCalculator interface { @@ -211,6 +212,7 @@ func New( maxBlobsPerBlock: maxBlobsPerBlock, feeCalculator: feeCalculator, logger: logger, + auths: map[common.Address]*metaTxn{}, } if shanghaiTime != nil { @@ -1237,6 +1239,30 @@ func (p *TxPool) addTxns(blockNum uint64, cacheView kvcache.CacheView, senders * continue } mt := newMetaTxn(txn, newTxns.IsLocal[i], blockNum) + if mt.TxnSlot.Type == SetCodeTxnType { + numAuths := len(mt.TxnSlot.AuthRaw) + foundDuplicate := false + for i := range numAuths { + signature := mt.TxnSlot.Authorizations[i] + signer, err := types.RecoverSignerFromRLP(mt.TxnSlot.AuthRaw[i], uint8(signature.V.Uint64()), signature.R, signature.S) + if err != nil { + continue + } + + if _, ok := p.auths[*signer]; ok { + foundDuplicate = true + break + } + + p.auths[*signer] = mt + } + + if foundDuplicate { + discardReasons[i] = txpoolcfg.ErrAuthorityReserved + continue + } + } + if reason := p.addLocked(mt, &announcements); reason != txpoolcfg.NotSet { discardReasons[i] = reason continue @@ -1504,7 +1530,7 @@ func (p *TxPool) removeMined(byNonce *BySenderAndNonce, minedTxns []*TxnSlot) er for _, txn := range minedTxns { nonce, ok := noncesToRemove[txn.SenderID] if !ok || txn.Nonce > nonce { - noncesToRemove[txn.SenderID] = txn.Nonce + noncesToRemove[txn.SenderID] = txn.Nonce // TODO: after 7702 nonce can be incremented more than once, may affect this } } diff --git a/txnprovider/txpool/pool_test.go b/txnprovider/txpool/pool_test.go index 3d2f15b8db6..d245f76743a 100644 --- a/txnprovider/txpool/pool_test.go +++ b/txnprovider/txpool/pool_test.go @@ -28,21 +28,25 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erigontech/erigon-lib/common/datadir" - "github.com/erigontech/erigon-lib/kv/temporal/temporaltest" - "github.com/erigontech/erigon-lib/log/v3" - "github.com/erigontech/erigon/core/types/typestest" - "github.com/erigontech/erigon-lib/common" + "github.com/erigontech/erigon-lib/common/datadir" "github.com/erigontech/erigon-lib/common/fixedgas" + "github.com/erigontech/erigon-lib/common/length" "github.com/erigontech/erigon-lib/common/u256" + "github.com/erigontech/erigon-lib/crypto" "github.com/erigontech/erigon-lib/crypto/kzg" "github.com/erigontech/erigon-lib/gointerfaces" remote "github.com/erigontech/erigon-lib/gointerfaces/remoteproto" "github.com/erigontech/erigon-lib/kv" "github.com/erigontech/erigon-lib/kv/kvcache" "github.com/erigontech/erigon-lib/kv/memdb" + "github.com/erigontech/erigon-lib/kv/temporal/temporaltest" + "github.com/erigontech/erigon-lib/log/v3" + "github.com/erigontech/erigon-lib/rlp" types2 "github.com/erigontech/erigon-lib/types" + "github.com/erigontech/erigon/core/types" + "github.com/erigontech/erigon/core/types/typestest" + "github.com/erigontech/erigon/params" "github.com/erigontech/erigon/txnprovider/txpool/txpoolcfg" ) @@ -167,6 +171,164 @@ func TestNonceFromAddress(t *testing.T) { } } +func TestMultipleAuthorizations(t *testing.T) { + ch := make(chan Announcements, 100) + coreDB, _ := temporaltest.NewTestDB(t, datadir.New(t.TempDir())) + db := memdb.NewTestPoolDB(t) + + cfg := txpoolcfg.DefaultConfig + sendersCache := kvcache.New(kvcache.DefaultCoherentConfig) + pool, err := New(ch, db, coreDB, cfg, sendersCache, *u256.N1, common.Big0 /* shanghaiTime */, nil, /* agraBlock */ + common.Big0 /* cancunTime */, common.Big0 /* pragueTime */, fixedgas.DefaultMaxBlobsPerBlock, nil, log.New()) + assert.NoError(t, err) + require.True(t, pool != nil) + ctx := context.Background() + var stateVersionID uint64 = 0 + pendingBaseFee := uint64(200000) + // start blocks from 0, set empty hash - then kvcache will also work on this + h1 := gointerfaces.ConvertHashToH256([32]byte{}) + change := &remote.StateChangeBatch{ + StateVersionId: stateVersionID, + PendingBlockBaseFee: pendingBaseFee, + BlockGasLimit: 1000000, + ChangeBatch: []*remote.StateChange{ + {BlockHeight: 0, BlockHash: h1}, + }, + } + + var addr1, addr2 [20]byte + addr2[0] = 1 + v := types2.EncodeAccountBytesV3(0, uint256.NewInt(1*common.Ether), make([]byte, 32), 1) + change.ChangeBatch[0].Changes = append(change.ChangeBatch[0].Changes, &remote.AccountChange{ + Action: remote.Action_UPSERT, + Address: gointerfaces.ConvertAddressToH160(addr1), + Data: v, + }) + change.ChangeBatch[0].Changes = append(change.ChangeBatch[0].Changes, &remote.AccountChange{ + Action: remote.Action_UPSERT, + Address: gointerfaces.ConvertAddressToH160(addr2), + Data: v, + }) + tx, err := db.BeginRw(ctx) + require.NoError(t, err) + defer tx.Rollback() + err = pool.OnNewBlock(ctx, change, TxnSlots{}, TxnSlots{}, TxnSlots{}) + assert.NoError(t, err) + + chainID := uint64(7078815900) + privateKey, err := crypto.GenerateKey() + assert.NoError(t, err) + authAddress := crypto.PubkeyToAddress(privateKey.PublicKey) + + var b [33]byte + data := bytes.NewBuffer(b[:]) + data.Reset() + + authLen := rlp.U64Len(chainID) + authLen += 1 + length.Addr + authLen += rlp.U64Len(0) + assert.NoError(t, rlp.EncodeStructSizePrefix(authLen, data, b[:])) + assert.NoError(t, rlp.EncodeInt(chainID, data, b[:])) + assert.NoError(t, rlp.EncodeOptionalAddress(&authAddress, data, b[:])) + assert.NoError(t, rlp.EncodeInt(0, data, b[:])) + + hashData := []byte{params.SetCodeMagicPrefix} + hashData = append(hashData, data.Bytes()...) + hash := crypto.Keccak256Hash(hashData) + + sig, err := crypto.Sign(hash.Bytes(), privateKey) + assert.NoError(t, err) + + r := uint256.NewInt(0).SetBytes(sig[:32]) + s := uint256.NewInt(0).SetBytes(sig[32:64]) + yParity := sig[64] + + var auth Signature + auth.ChainID.Set(uint256.NewInt(chainID)) + auth.V.Set(uint256.NewInt(uint64(yParity))) + auth.R.Set(r) + auth.S.Set(s) + + { + var txnSlots TxnSlots + txnSlot1 := &TxnSlot{ + Tip: *uint256.NewInt(300000), + FeeCap: *uint256.NewInt(300000), + Gas: 100000, + Nonce: 0, + Authorizations: []Signature{auth}, + AuthRaw: [][]byte{data.Bytes()}, + Type: SetCodeTxnType, + } + txnSlot1.IDHash[0] = 1 + txnSlots.Append(txnSlot1, addr1[:], true) + + txnSlot2 := &TxnSlot{ + Tip: *uint256.NewInt(300000), + FeeCap: *uint256.NewInt(300000), + Gas: 100000, + Nonce: 0, + Authorizations: []Signature{auth}, + AuthRaw: [][]byte{data.Bytes()}, + Type: SetCodeTxnType, + } + txnSlot2.IDHash[0] = 2 + txnSlots.Append(txnSlot2, addr2[:], true) + + logger := log.New() + + if err = pool.senders.registerNewSenders(&txnSlots, logger); err != nil { + t.Error(err) + } + + reasons, err := pool.AddLocalTxns(ctx, txnSlots) + assert.NoError(t, err) + assert.Equal(t, reasons, []txpoolcfg.DiscardReason{txpoolcfg.Success, txpoolcfg.ErrAuthorityReserved}) + } +} + +func TestRecoverSignerFromRLP_ValidData(t *testing.T) { + privateKey, err := crypto.GenerateKey() + assert.NoError(t, err) + pubKey := crypto.PubkeyToAddress(privateKey.PublicKey) + chainID := uint64(7078815900) + + var b [33]byte + data := bytes.NewBuffer(b[:]) + data.Reset() + + // Encode RLP data exactly as in the previous implementation + authLen := rlp.U64Len(chainID) + authLen += 1 + length.Addr + authLen += rlp.U64Len(0) // nonce + assert.NoError(t, rlp.EncodeStructSizePrefix(authLen, data, b[:])) + assert.NoError(t, rlp.EncodeInt(chainID, data, b[:])) + assert.NoError(t, rlp.EncodeOptionalAddress(&pubKey, data, b[:])) + assert.NoError(t, rlp.EncodeInt(0, data, b[:])) + + // Prepare hash data exactly as before + hashData := []byte{params.SetCodeMagicPrefix} + hashData = append(hashData, data.Bytes()...) + hash := crypto.Keccak256Hash(hashData) + + // Sign the hash + sig, err := crypto.Sign(hash.Bytes(), privateKey) + assert.NoError(t, err) + + // Separate signature components + r := uint256.NewInt(0).SetBytes(sig[:32]) + s := uint256.NewInt(0).SetBytes(sig[32:64]) + yParity := sig[64] + + // Recover signer using the explicit RecoverSignerFromRLP function + recoveredAddress, err := types.RecoverSignerFromRLP(data.Bytes(), yParity, *r, *s) + assert.NoError(t, err) + assert.NotNil(t, recoveredAddress) + + // Verify the recovered address matches the original public key address + assert.Equal(t, pubKey, *recoveredAddress) +} + func TestReplaceWithHigherFee(t *testing.T) { assert, require := assert.New(t), require.New(t) ch := make(chan Announcements, 100) diff --git a/txnprovider/txpool/pool_txn_parser.go b/txnprovider/txpool/pool_txn_parser.go index 654bfd96b02..686b99d6cd3 100644 --- a/txnprovider/txpool/pool_txn_parser.go +++ b/txnprovider/txpool/pool_txn_parser.go @@ -476,6 +476,7 @@ func (ctx *TxnParseContext) parseTransactionBody(payload []byte, pos, p0 int, sl } var sig Signature p2 := authPos + rawStart := p2 p2, err = rlp.ParseU256(payload, p2, &sig.ChainID) if err != nil { return 0, fmt.Errorf("%w: authorization chainId: %s", ErrParseTxn, err) //nolint @@ -493,11 +494,13 @@ func (ctx *TxnParseContext) parseTransactionBody(payload []byte, pos, p0 int, sl if err != nil { return 0, fmt.Errorf("%w: authorization nonce: %s", ErrParseTxn, err) //nolint } + rawEnd := p2 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) + slot.AuthRaw = append(slot.AuthRaw, payload[rawStart:rawEnd+1]) // TODO: check if we need +1 authPos += authLen if authPos != p2 { return 0, fmt.Errorf("%w: authorization: unexpected list items", ErrParseTxn) @@ -685,6 +688,7 @@ type TxnSlot struct { // EIP-7702: set code tx Authorizations []Signature + AuthRaw [][]byte // rlp encoded chainID+address+nonce } // nolint diff --git a/txnprovider/txpool/txpoolcfg/txpoolcfg.go b/txnprovider/txpool/txpoolcfg/txpoolcfg.go index 27ef3a979b0..c32acb39d0e 100644 --- a/txnprovider/txpool/txpoolcfg/txpoolcfg.go +++ b/txnprovider/txpool/txpoolcfg/txpoolcfg.go @@ -86,39 +86,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 - 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 txns) in the pool has reached its limit - NoAuthorizations DiscardReason = 32 // EIP-7702 transactions with an empty authorization list are invalid + 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 txns) in the pool has reached its limit + NoAuthorizations DiscardReason = 32 // EIP-7702 transactions with an empty authorization list are invalid + ErrAuthorityReserved DiscardReason = 33 // EIP-7702 transaction with authority already reserved ) func (r DiscardReason) String() string { @@ -183,6 +184,8 @@ func (r DiscardReason) String() string { return "blobs limit in txpool is full" case NoAuthorizations: return "EIP-7702 transactions with an empty authorization list are invalid" + case ErrAuthorityReserved: + return "EIP-7702 authority already reserved" default: panic(fmt.Sprintf("discard reason: %d", r)) }