Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

txpool: Don't propagate transactions with duplicate authorizations #13137

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 22 additions & 18 deletions core/types/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ func (ath *Authorization) copy() *Authorization {
}

func (ath *Authorization) RecoverSigner(data *bytes.Buffer, b []byte) (*libcommon.Address, error) {
if ath.Nonce == 1<<64-1 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Expand All @@ -57,46 +61,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))

Expand Down
1 change: 1 addition & 0 deletions params/protocol_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 27 additions & 1 deletion txnprovider/txpool/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -211,6 +212,7 @@ func New(
maxBlobsPerBlock: maxBlobsPerBlock,
feeCalculator: feeCalculator,
logger: logger,
auths: map[common.Address]*metaTxn{},
}

if shanghaiTime != nil {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}

Expand Down
172 changes: 167 additions & 5 deletions txnprovider/txpool/pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions txnprovider/txpool/pool_txn_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -685,6 +688,7 @@ type TxnSlot struct {

// EIP-7702: set code tx
Authorizations []Signature
AuthRaw [][]byte // rlp encoded chainID+address+nonce
}

// nolint
Expand Down
Loading
Loading