diff --git a/core/txpool/errors.go b/core/txpool/errors.go index 4b87137123..469b79b968 100644 --- a/core/txpool/errors.go +++ b/core/txpool/errors.go @@ -51,10 +51,6 @@ var ( // making the transaction invalid, rather a DOS protection. ErrOversizedData = errors.New("oversized data") - // ErrFutureReplacePending is returned if a future transaction replaces a pending - // one. Future transactions should only be able to replace other future transactions. - ErrFutureReplacePending = errors.New("future transaction tries to replace pending") - // ErrAlreadyReserved is returned if the sender address has a pending transaction // in a different subpool. For example, this error is returned in response to any // input transaction of non-blob type when a blob transaction from this sender @@ -63,13 +59,4 @@ var ( // ErrInBlackList is returned if the transaction send by banned address ErrInBlackList = errors.New("sender or to in black list") - - // ErrAuthorityReserved is returned if a transaction has an authorization - // signed by an address which already has in-flight transactions known to the - // pool. - ErrAuthorityReserved = errors.New("authority already reserved") - - // ErrAuthorityNonce is returned if a transaction has an authorization with - // a nonce that is not currently valid for the authority. - ErrAuthorityNonceTooLow = errors.New("authority nonce too low") ) diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 02a475482d..56b3fc7329 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -65,6 +65,19 @@ var ( // ErrTxPoolOverflow is returned if the transaction pool is full and can't accept // another remote transaction. ErrTxPoolOverflow = errors.New("txpool is full") + + // ErrInflightTxLimitReached is returned when the maximum number of in-flight + // transactions is reached for specific accounts. + ErrInflightTxLimitReached = errors.New("in-flight transaction limit reached for delegated accounts") + + // ErrAuthorityReserved is returned if a transaction has an authorization + // signed by an address which already has in-flight transactions known to the + // pool. + ErrAuthorityReserved = errors.New("authority already reserved") + + // ErrFutureReplacePending is returned if a future transaction replaces a pending + // one. Future transactions should only be able to replace other future transactions. + ErrFutureReplacePending = errors.New("future transaction tries to replace pending") ) var ( @@ -657,22 +670,8 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction) error { opts := &txpool.ValidationOptionsWithState{ State: pool.currentState, - FirstNonceGap: nil, // Pool allows arbitrary arrival order, don't invalidate nonce gaps - UsedAndLeftSlots: func(addr common.Address) (int, int) { - var have int - if list := pool.pending[addr]; list != nil { - have += list.Len() - } - if list := pool.queue[addr]; list != nil { - have += list.Len() - } - if pool.currentState.GetCodeHash(addr) != types.EmptyCodeHash || len(pool.all.auths[addr]) != 0 { - // Allow at most one in-flight tx for delegated accounts or those with - // a pending authorization. - return have, max(0, 1-have) - } - return have, math.MaxInt - }, + FirstNonceGap: nil, // Pool allows arbitrary arrival order, don't invalidate nonce gaps + UsedAndLeftSlots: nil, // Pool has own mechanism to limit the number of transactions ExistingExpenditure: func(addr common.Address) *big.Int { if list := pool.pending[addr]; list != nil { return list.totalcost.ToBig() @@ -687,22 +686,49 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction) error { } return nil }, - KnownConflicts: func(from common.Address, auths []common.Address) []common.Address { - var conflicts []common.Address - // Authorities cannot conflict with any pending or queued transactions. - for _, addr := range auths { - if list := pool.pending[addr]; list != nil { - conflicts = append(conflicts, addr) - } else if list := pool.queue[addr]; list != nil { - conflicts = append(conflicts, addr) - } - } - return conflicts - }, } if err := txpool.ValidateTransactionWithState(tx, pool.signer, opts); err != nil { return err } + return pool.validateAuth(tx) +} + +// validateAuth verifies that the transaction complies with code authorization +// restrictions brought by SetCode transaction type. +func (pool *LegacyPool) validateAuth(tx *types.Transaction) error { + from, _ := types.Sender(pool.signer, tx) // validated + + // Allow at most one in-flight tx for delegated accounts or those with a + // pending authorization. + if pool.currentState.GetCodeHash(from) != types.EmptyCodeHash || len(pool.all.auths[from]) != 0 { + var ( + count int + exists bool + ) + pending := pool.pending[from] + if pending != nil { + count += pending.Len() + exists = pending.Contains(tx.Nonce()) + } + queue := pool.queue[from] + if queue != nil { + count += queue.Len() + exists = exists || queue.Contains(tx.Nonce()) + } + // Replace the existing in-flight transaction for delegated accounts + // are still supported + if count >= 1 && !exists { + return ErrInflightTxLimitReached + } + } + // Authorities cannot conflict with any pending or queued transactions. + if auths := tx.SetCodeAuthorities(); len(auths) > 0 { + for _, auth := range auths { + if pool.pending[auth] != nil || pool.queue[auth] != nil { + return ErrAuthorityReserved + } + } + } return nil } @@ -794,7 +820,7 @@ func (pool *LegacyPool) add(tx *types.Transaction) (replaced bool, err error) { pool.priced.Put(dropTx) } log.Trace("Discarding future transaction replacing pending tx", "hash", hash) - return false, txpool.ErrFutureReplacePending + return false, ErrFutureReplacePending } } @@ -1849,12 +1875,12 @@ func (t *lookup) Remove(hash common.Hash) { t.lock.Lock() defer t.lock.Unlock() - t.removeAuthorities(hash) tx, ok := t.txs[hash] if !ok { log.Error("No transaction found to be deleted", "hash", hash) return } + t.removeAuthorities(tx) t.slots -= numSlots(tx) slotsGauge.Update(int64(t.slots)) @@ -1892,8 +1918,9 @@ func (t *lookup) addAuthorities(tx *types.Transaction) { // removeAuthorities stops tracking the supplied tx in relation to its // authorities. -func (t *lookup) removeAuthorities(hash common.Hash) { - for addr := range t.auths { +func (t *lookup) removeAuthorities(tx *types.Transaction) { + hash := tx.Hash() + for _, addr := range tx.SetCodeAuthorities() { list := t.auths[addr] // Remove tx from tracker. if i := slices.Index(list, hash); i >= 0 { diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go index 0818c28fb0..8e3b6dc45a 100644 --- a/core/txpool/legacypool/legacypool_test.go +++ b/core/txpool/legacypool/legacypool_test.go @@ -23,6 +23,7 @@ import ( "fmt" "math/big" "math/rand" + "slices" "sync" "sync/atomic" "testing" @@ -150,6 +151,10 @@ func pricedSetCodeTx(nonce uint64, gaslimit uint64, gasFee, tip *uint256.Int, ke }) authList = append(authList, auth) } + return pricedSetCodeTxWithAuth(nonce, gaslimit, gasFee, tip, key, authList) +} + +func pricedSetCodeTxWithAuth(nonce uint64, gaslimit uint64, gasFee, tip *uint256.Int, key *ecdsa.PrivateKey, authList []types.SetCodeAuthorization) *types.Transaction { return types.MustSignNewTx(key, types.LatestSignerForChainID(params.TestChainConfig.ChainID), &types.SetCodeTx{ ChainID: uint256.MustFromBig(params.TestChainConfig.ChainID), Nonce: nonce, @@ -235,6 +240,23 @@ func validatePoolInternals(pool *LegacyPool) error { return fmt.Errorf("pending nonce mismatch: have %v, want %v", nonce, last+1) } } + // Ensure all auths in pool are tracked + for _, tx := range pool.all.txs { + for _, addr := range tx.SetCodeAuthorities() { + list := pool.all.auths[addr] + if i := slices.Index(list, tx.Hash()); i < 0 { + return fmt.Errorf("authority not tracked: addr %s, tx %s", addr, tx.Hash()) + } + } + } + // Ensure all auths in pool have an associated tx. + for addr, hashes := range pool.all.auths { + for _, hash := range hashes { + if _, ok := pool.all.txs[hash]; !ok { + return fmt.Errorf("dangling authority, missing originating tx: addr %s, hash %s", addr, hash.Hex()) + } + } + } return nil } @@ -1643,8 +1665,8 @@ func TestUnderpricing(t *testing.T) { t.Fatalf("failed to add well priced transaction: %v", err) } // Ensure that replacing a pending transaction with a future transaction fails - if err := pool.addRemoteSync(pricedTransaction(5, 100000, big.NewInt(6), keys[1])); err != txpool.ErrFutureReplacePending { - t.Fatalf("adding future replace transaction error mismatch: have %v, want %v", err, txpool.ErrFutureReplacePending) + if err := pool.addRemoteSync(pricedTransaction(5, 100000, big.NewInt(6), keys[1])); err != ErrFutureReplacePending { + t.Fatalf("adding future replace transaction error mismatch: have %v, want %v", err, ErrFutureReplacePending) } pending, queued = pool.Stats() if pending != 4 { @@ -2345,12 +2367,12 @@ func TestSetCodeTransactions(t *testing.T) { if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), keyA)); err != nil { t.Fatalf("%s: failed to add remote transaction: %v", name, err) } - if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyA)); !errors.Is(err, txpool.ErrAccountLimitExceeded) { - t.Fatalf("%s: error mismatch: want %v, have %v", name, txpool.ErrAccountLimitExceeded, err) + if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyA)); !errors.Is(err, ErrInflightTxLimitReached) { + t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrInflightTxLimitReached, err) } // Also check gapped transaction. - if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1), keyA)); !errors.Is(err, txpool.ErrAccountLimitExceeded) { - t.Fatalf("%s: error mismatch: want %v, have %v", name, txpool.ErrAccountLimitExceeded, err) + if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1), keyA)); !errors.Is(err, ErrInflightTxLimitReached) { + t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrInflightTxLimitReached, err) } // Replace by fee. if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(10), keyA)); err != nil { @@ -2384,8 +2406,8 @@ func TestSetCodeTransactions(t *testing.T) { t.Fatalf("%s: failed to add with pending delegatio: %v", name, err) } // Also check gapped transaction is rejected. - if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyC)); !errors.Is(err, txpool.ErrAccountLimitExceeded) { - t.Fatalf("%s: error mismatch: want %v, have %v", name, txpool.ErrAccountLimitExceeded, err) + if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyC)); !errors.Is(err, ErrInflightTxLimitReached) { + t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrInflightTxLimitReached, err) } }, }, @@ -2460,7 +2482,7 @@ func TestSetCodeTransactions(t *testing.T) { if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1000), keyC)); err != nil { t.Fatalf("%s: failed to added single pooled for account with pending delegation: %v", name, err) } - if err, want := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1000), keyC)), txpool.ErrAccountLimitExceeded; !errors.Is(err, want) { + if err, want := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1000), keyC)), ErrInflightTxLimitReached; !errors.Is(err, want) { t.Fatalf("%s: error mismatch: want %v, have %v", name, want, err) } }, @@ -2473,11 +2495,37 @@ func TestSetCodeTransactions(t *testing.T) { if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1000), keyC)); err != nil { t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) } - if err, want := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{1, keyC}})), txpool.ErrAuthorityReserved; !errors.Is(err, want) { + if err, want := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{1, keyC}})), ErrAuthorityReserved; !errors.Is(err, want) { t.Fatalf("%s: error mismatch: want %v, have %v", name, want, err) } }, }, + { + name: "remove-hash-from-authority-tracker", + pending: 10, + run: func(name string) { + var keys []*ecdsa.PrivateKey + for i := 0; i < 30; i++ { + key, _ := crypto.GenerateKey() + keys = append(keys, key) + addr := crypto.PubkeyToAddress(key.PublicKey) + testAddBalance(pool, addr, big.NewInt(params.Ether)) + } + // Create a transactions with 3 unique auths so the lookup's auth map is + // filled with addresses. + for i := 0; i < 30; i += 3 { + if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(10), uint256.NewInt(3), keys[i], []unsignedAuth{{0, keys[i]}, {0, keys[i+1]}, {0, keys[i+2]}})); err != nil { + t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) + } + } + // Replace one of the transactions with a normal transaction so that the + // original hash is removed from the tracker. The hash should be + // associated with 3 different authorities. + if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1000), keys[0])); err != nil { + t.Fatalf("%s: failed to replace with remote transaction: %v", name, err) + } + }, + }, } { tt.run(tt.name) pending, queued := pool.Stats() @@ -2494,6 +2542,65 @@ func TestSetCodeTransactions(t *testing.T) { } } +func TestSetCodeTransactionsReorg(t *testing.T) { + t.Parallel() + + // Create the pool to test the status retrievals with + statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) + blockchain := newTestBlockChain(params.MergedTestChainConfig, 1000000, statedb, new(event.Feed)) + + pool := New(testTxPoolConfig, blockchain) + pool.Init(testTxPoolConfig.PriceLimit, blockchain.CurrentBlock(), makeAddressReserver()) + defer pool.Close() + + // Create the test accounts + var ( + keyA, _ = crypto.GenerateKey() + addrA = crypto.PubkeyToAddress(keyA.PublicKey) + ) + testAddBalance(pool, addrA, big.NewInt(params.Ether)) + // Send an authorization for 0x42 + var authList []types.SetCodeAuthorization + auth, _ := types.SignSetCode(keyA, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(params.TestChainConfig.ChainID), + Address: common.Address{0x42}, + Nonce: 0, + }) + authList = append(authList, auth) + if err := pool.addRemoteSync(pricedSetCodeTxWithAuth(0, 250000, uint256.NewInt(10), uint256.NewInt(3), keyA, authList)); err != nil { + t.Fatalf("failed to add with remote setcode transaction: %v", err) + } + // Simulate the chain moving + blockchain.statedb.SetNonce(addrA, 1, tracing.NonceChangeAuthorization) + blockchain.statedb.SetCode(addrA, types.AddressToDelegation(auth.Address)) + <-pool.requestReset(nil, nil) + // Set an authorization for 0x00 + auth, _ = types.SignSetCode(keyA, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(params.TestChainConfig.ChainID), + Address: common.Address{}, + Nonce: 0, + }) + authList = append(authList, auth) + if err := pool.addRemoteSync(pricedSetCodeTxWithAuth(1, 250000, uint256.NewInt(10), uint256.NewInt(3), keyA, authList)); err != nil { + t.Fatalf("failed to add with remote setcode transaction: %v", err) + } + // Try to add a transactions in + if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1000), keyA)); !errors.Is(err, ErrInflightTxLimitReached) { + t.Fatalf("unexpected error %v, expecting %v", err, ErrInflightTxLimitReached) + } + // Simulate the chain moving + blockchain.statedb.SetNonce(addrA, 2, tracing.NonceChangeAuthorization) + blockchain.statedb.SetCode(addrA, nil) + <-pool.requestReset(nil, nil) + // Now send two transactions from addrA + if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1000), keyA)); err != nil { + t.Fatalf("failed to added single transaction: %v", err) + } + if err := pool.addRemoteSync(pricedTransaction(3, 100000, big.NewInt(1000), keyA)); err != nil { + t.Fatalf("failed to added single transaction: %v", err) + } +} + // Benchmarks the speed of validating the contents of the pending queue of the // transaction pool. func BenchmarkPendingDemotion100(b *testing.B) { benchmarkPendingDemotion(b, 100) } diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 043270811c..01de860a46 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -213,7 +213,7 @@ type ValidationOptionsWithState struct { // nonce gaps will be ignored and permitted. FirstNonceGap func(addr common.Address) uint64 - // UsedAndLeftSlots is a mandatory callback to retrieve the number of tx slots + // UsedAndLeftSlots is an optional callback to retrieve the number of tx slots // used and the number still permitted for an account. New transactions will // be rejected once the number of remaining slots reaches zero. UsedAndLeftSlots func(addr common.Address) (int, int) @@ -225,11 +225,6 @@ type ValidationOptionsWithState struct { // ExistingCost is a mandatory callback to retrieve an already pooled // transaction's cost with the given nonce to check for overdrafts. ExistingCost func(addr common.Address, nonce uint64) *big.Int - - // KnownConflicts is an optional callback which iterates over the list of - // addresses and returns all addresses known to the pool with in-flight - // transactions. - KnownConflicts func(sender common.Address, authorizers []common.Address) []common.Address } // ValidateTransactionWithState is a helper method to check whether a transaction @@ -280,15 +275,9 @@ func ValidateTransactionWithState(tx *types.Transaction, signer types.Signer, op // Transaction takes a new nonce value out of the pool. Ensure it doesn't // overflow the number of permitted transactions from a single account // (i.e. max cancellable via out-of-bound transaction). - if used, left := opts.UsedAndLeftSlots(from); left <= 0 { - return fmt.Errorf("%w: pooled %d txs", ErrAccountLimitExceeded, used) - } - - // Verify no authorizations will invalidate existing transactions known to - // the pool. - if opts.KnownConflicts != nil { - if conflicts := opts.KnownConflicts(from, tx.SetCodeAuthorities()); len(conflicts) > 0 { - return fmt.Errorf("%w: authorization conflicts with other known tx", ErrAuthorityReserved) + if opts.UsedAndLeftSlots != nil { + if used, left := opts.UsedAndLeftSlots(from); left <= 0 { + return fmt.Errorf("%w: pooled %d txs", ErrAccountLimitExceeded, used) } } } diff --git a/core/types/transaction.go b/core/types/transaction.go index 00c3e1c8d5..f47d354056 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -493,15 +493,23 @@ func (tx *Transaction) SetCodeAuthorizations() []SetCodeAuthorization { return setcodetx.AuthList } -// SetCodeAuthorities returns a list of each authorization's corresponding authority. +// SetCodeAuthorities returns a list of unique authorities from the +// authorization list. func (tx *Transaction) SetCodeAuthorities() []common.Address { setcodetx, ok := tx.inner.(*SetCodeTx) if !ok { return nil } - auths := make([]common.Address, 0, len(setcodetx.AuthList)) + var ( + marks = make(map[common.Address]bool) + auths = make([]common.Address, 0, len(setcodetx.AuthList)) + ) for _, auth := range setcodetx.AuthList { if addr, err := auth.Authority(); err == nil { + if marks[addr] { + continue + } + marks[addr] = true auths = append(auths, addr) } } diff --git a/eth/gasprice/feehistory.go b/eth/gasprice/feehistory.go index fe84950c50..59830e9fe8 100644 --- a/eth/gasprice/feehistory.go +++ b/eth/gasprice/feehistory.go @@ -107,8 +107,8 @@ func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) { // Compute gas used ratio for normal and blob gas. bf.results.gasUsedRatio = float64(bf.header.GasUsed) / float64(bf.header.GasLimit) if blobGasUsed := bf.header.BlobGasUsed; blobGasUsed != nil { - maxBlobs := eip4844.MaxBlobsPerBlock(config, bf.header.Time) - bf.results.blobGasUsedRatio = float64(*blobGasUsed) / float64(maxBlobs) + maxBlobGas := eip4844.MaxBlobGasPerBlock(config, bf.header.Time) + bf.results.blobGasUsedRatio = float64(*blobGasUsed) / float64(maxBlobGas) } if len(percentiles) == 0 { diff --git a/signer/core/apitypes/types.go b/signer/core/apitypes/types.go index 082e5009e2..9687906698 100644 --- a/signer/core/apitypes/types.go +++ b/signer/core/apitypes/types.go @@ -505,7 +505,16 @@ func (typedData *TypedData) encodeArrayValue(encValue interface{}, encType strin for _, item := range arrayValue { if reflect.TypeOf(item).Kind() == reflect.Slice || reflect.TypeOf(item).Kind() == reflect.Array { - encodedData, err := typedData.encodeArrayValue(item, parsedType, depth+1) + var ( + encodedData hexutil.Bytes + err error + ) + if reflect.TypeOf(item).Elem().Kind() == reflect.Uint8 { + // the item type is bytes. encode the bytes array directly instead of recursing. + encodedData, err = typedData.EncodePrimitiveValue(parsedType, item, depth+1) + } else { + encodedData, err = typedData.encodeArrayValue(item, parsedType, depth+1) + } if err != nil { return nil, err } diff --git a/signer/core/signed_data_test.go b/signer/core/signed_data_test.go index d0637010ba..b6c080736c 100644 --- a/signer/core/signed_data_test.go +++ b/signer/core/signed_data_test.go @@ -1014,3 +1014,49 @@ func TestComplexTypedDataWithLowercaseReftype(t *testing.T) { t.Fatalf("Error, got %x, wanted %x", sighash, expSigHash) } } + +var recursiveBytesTypesStandard = apitypes.Types{ + "EIP712Domain": { + { + Name: "name", + Type: "string", + }, + { + Name: "version", + Type: "string", + }, + { + Name: "chainId", + Type: "uint256", + }, + { + Name: "verifyingContract", + Type: "address", + }, + }, + "Val": { + { + Name: "field", + Type: "bytes[][]", + }, + }, +} + +var recursiveBytesMessageStandard = map[string]interface{}{ + "field": [][][]byte{{{1}, {2}}, {{3}, {4}}}, +} + +var recursiveBytesTypedData = apitypes.TypedData{ + Types: recursiveBytesTypesStandard, + PrimaryType: "Val", + Domain: domainStandard, + Message: recursiveBytesMessageStandard, +} + +func TestEncodeDataRecursiveBytes(t *testing.T) { + typedData := recursiveBytesTypedData + _, err := typedData.EncodeData(typedData.PrimaryType, typedData.Message, 0) + if err != nil { + t.Fatalf("got err %v", err) + } +}