diff --git a/eth/api_admin.go b/eth/api_admin.go index 4a3ccb84e8..576780bd58 100644 --- a/eth/api_admin.go +++ b/eth/api_admin.go @@ -24,6 +24,7 @@ import ( "os" "strings" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" @@ -141,3 +142,15 @@ func (api *AdminAPI) ImportChain(file string) (bool, error) { } return true, nil } + +func (api *AdminAPI) SetFeeCurrencyBlocklistStatus(enabled bool) { + api.eth.Miner().SetFeeCurrencyBlocklistStatus(enabled) +} + +func (api *AdminAPI) UnblockTransaction(hash common.Hash) bool { + return api.eth.Miner().UnblockTransaction(hash) +} + +func (api *AdminAPI) UnblockFeeCurrency(address common.Address) bool { + return api.eth.Miner().UnblockFeeCurrency(address) +} diff --git a/miner/currency_blocklist.go b/miner/currency_blocklist.go index 4081a2eb81..9f0b57e279 100644 --- a/miner/currency_blocklist.go +++ b/miner/currency_blocklist.go @@ -2,6 +2,7 @@ package miner import ( "sync" + "sync/atomic" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -18,21 +19,40 @@ type AddressBlocklist struct { // will get evicted when evict() is called headerEvictionTimeoutSeconds uint64 oldestHeader *types.Header + + enactFilterEnabled *atomic.Bool } func NewAddressBlocklist() *AddressBlocklist { - return &AddressBlocklist{ + bl := &AddressBlocklist{ mux: &sync.RWMutex{}, currencies: map[common.Address]*types.Header{}, headerEvictionTimeoutSeconds: EvictionTimeoutSeconds, oldestHeader: nil, + enactFilterEnabled: &atomic.Bool{}, } + bl.enactFilterEnabled.Store(true) + return bl +} + +func (b *AddressBlocklist) GetEnableFilterStatus() bool { + return b.enactFilterEnabled.Load() +} + +// This only activates / deactivates wether the +// allowlist is filtered by the blocklist or not. +func (b *AddressBlocklist) SetEnableFilterStatus(enabled bool) { + b.enactFilterEnabled.Store(enabled) } func (b *AddressBlocklist) FilterAllowlist(allowlist common.AddressSet, latest *types.Header) common.AddressSet { b.mux.RLock() defer b.mux.RUnlock() + if !b.enactFilterEnabled.Load() { + return allowlist + } + filtered := common.AddressSet{} for a := range allowlist { if !b.isBlocked(a, latest) { @@ -64,14 +84,16 @@ func (b *AddressBlocklist) Remove(currency common.Address) bool { return ok } -func (b *AddressBlocklist) Add(currency common.Address, head types.Header) { +func (b *AddressBlocklist) Add(currency common.Address, head types.Header) bool { b.mux.Lock() defer b.mux.Unlock() + _, existed := b.currencies[currency] if b.oldestHeader == nil || b.oldestHeader.Time > head.Time { b.oldestHeader = &head } b.currencies[currency] = &head + return !existed } func (b *AddressBlocklist) Evict(latest *types.Header) []common.Address { diff --git a/miner/miner.go b/miner/miner.go index 080000a530..c88ae78de8 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -103,6 +103,7 @@ type Miner struct { lifeCtxCancel context.CancelFunc lifeCtx context.Context + blockedTxRingbuffer *TxRingBuffer feeCurrencyBlocklist *AddressBlocklist } @@ -121,6 +122,7 @@ func New(eth Backend, config Config, engine consensus.Engine) *Miner { lifeCtxCancel: cancel, lifeCtx: ctx, + blockedTxRingbuffer: NewTxRingBuffer(100000), feeCurrencyBlocklist: NewAddressBlocklist(), } } @@ -231,6 +233,26 @@ func (miner *Miner) getPending() *newPayloadResult { return ret } +func (miner *Miner) SetFeeCurrencyBlocklistStatus(enabled bool) { + miner.feeCurrencyBlocklist.SetEnableFilterStatus(enabled) +} + +func (miner *Miner) UnblockFeeCurrency(address common.Address) bool { + removed := miner.feeCurrencyBlocklist.Remove(address) + if removed { + feeCurrenciesInBlocklistCounter.Dec(1) + } + return removed +} + +func (miner *Miner) UnblockTransaction(hash common.Hash) bool { + removed := miner.blockedTxRingbuffer.Remove(hash) + if removed { + transactionsInBlocklistCounter.Dec(1) + } + return removed +} + func (miner *Miner) Close() { miner.lifeCtxCancel() } diff --git a/miner/tx_ringbuffer.go b/miner/tx_ringbuffer.go new file mode 100644 index 0000000000..06d47df2c7 --- /dev/null +++ b/miner/tx_ringbuffer.go @@ -0,0 +1,70 @@ +package miner + +import ( + "sync" + + "github.com/ethereum/go-ethereum/common" +) + +// TxRingBuffer is a ring buffer of transaction hashes, it has one method to add +// transaction hashes and another method to check if a transaction hash is in the buffer. +type TxRingBuffer struct { + ring []common.Hash + m map[common.Hash]struct{} + index int + mux *sync.RWMutex +} + +// NewTxRingBuffer creates a new transaction ring buffer with the given size. +func NewTxRingBuffer(size int) *TxRingBuffer { + return &TxRingBuffer{ + ring: make([]common.Hash, size), + m: make(map[common.Hash]struct{}), + mux: &sync.RWMutex{}, + } +} + +// Add adds a new transaction hash to the ring buffer, if the buffer is +// at capacity, the oldest transaction hash is overwritten. +func (b *TxRingBuffer) Add(tx common.Hash) bool { + b.mux.Lock() + defer b.mux.Unlock() + + if _, exists := b.m[tx]; exists { + return false + } + + // Clean up old transaction hash from map + oldHash := b.ring[b.index] + delete(b.m, oldHash) + + // Add new transaction hash to ring and map + b.ring[b.index] = tx + b.m[tx] = struct{}{} + + // Increment the index + b.index++ + if b.index == len(b.ring) { + b.index = 0 + } + return true +} + +func (b *TxRingBuffer) Has(tx common.Hash) bool { + b.mux.RLock() + defer b.mux.RUnlock() + + _, exists := b.m[tx] + return exists +} + +func (b *TxRingBuffer) Remove(tx common.Hash) bool { + b.mux.Lock() + defer b.mux.Unlock() + + if _, exists := b.m[tx]; !exists { + return false + } + delete(b.m, tx) + return true +} diff --git a/miner/worker.go b/miner/worker.go index 2515c99c1a..da8b2bbd36 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -61,6 +61,9 @@ var ( txConditionalMinedTimer = metrics.NewRegisteredTimer("miner/transactionConditional/elapsedtime", nil) txInteropRejectedCounter = metrics.NewRegisteredCounter("miner/transactionInterop/rejected", nil) + + feeCurrenciesInBlocklistCounter = metrics.NewRegisteredCounter("miner/blocklist/feeCurrency/blocked", nil) + transactionsInBlocklistCounter = metrics.NewRegisteredCounter("miner/blocklist/transactions/blocked", nil) ) // environment is the worker's current environment and holds all @@ -87,6 +90,7 @@ type environment struct { // Celo specific multiGasPool *core.MultiGasPool // available per-fee-currency gas used to pack transactions feeCurrencyAllowlist common.AddressSet + blockingAllowed bool feeCurrencyContext *common.FeeCurrencyContext } @@ -340,11 +344,13 @@ func (miner *Miner) prepareWork(genParams *generateParams, witness bool) (*envir "evicted-fee-currencies", evicted, "eviction-timeout-seconds", EvictionTimeoutSeconds, ) + feeCurrenciesInBlocklistCounter.Dec(int64(len(evicted))) } env.feeCurrencyAllowlist = miner.feeCurrencyBlocklist.FilterAllowlist( common.CurrencyAllowlist(env.feeCurrencyContext.ExchangeRates), header, ) + env.blockingAllowed = miner.feeCurrencyBlocklist.GetEnableFilterStatus() if header.ParentBeaconRoot != nil { core.ProcessBeaconBlockRoot(*header.ParentBeaconRoot, env.evm) @@ -418,13 +424,14 @@ func (miner *Miner) commitTransaction(env *environment, tx *types.Transaction) e receipt, err := miner.applyTransaction(env, tx) if err != nil { if errors.Is(err, contracts.ErrFeeCurrencyEVMCall) { + log.Warn( "fee-currency EVM execution error, temporarily blocking fee-currency in local txpools", "tx-hash", tx.Hash(), "fee-currency", tx.FeeCurrency(), "error", err.Error(), ) - miner.blockFeeCurrency(env, *tx.FeeCurrency(), err) + miner.registerFeeCurrencyTxFailure(env, tx, err) } return err } @@ -596,6 +603,12 @@ func (miner *Miner) commitTransactions(env *environment, plainTxs, blobTxs *tran continue } } + if miner.blockedTxRingbuffer.Has(ltx.Hash) { + log.Trace("Skipping tx execution, tx in blockedTxRingbuffer", "hash", ltx.Hash) + txs.Pop() + continue + } + // If we don't have enough space for the next transaction, skip the account. if env.gasPool.Gas() < ltx.Gas { log.Trace("Not enough gas left for transaction", "hash", ltx.Hash, "left", env.gasPool.Gas(), "needed", ltx.Gas) @@ -834,14 +847,23 @@ func (miner *Miner) validateParams(genParams *generateParams) (time.Duration, er return time.Duration(blockTime) * time.Second, nil } -func (miner *Miner) blockFeeCurrency(env *environment, feeCurrency common.Address, err error) { +func (miner *Miner) registerFeeCurrencyTxFailure(env *environment, tx *types.Transaction, err error) { // the fee-currency is still in the allowlist of this environment, // so set the fee-currency block gas limit to 0 to prevent other // transactions. - pool := env.multiGasPool.PoolFor(&feeCurrency) - pool.SetGas(0) + if env.blockingAllowed { + pool := env.multiGasPool.PoolFor(tx.FeeCurrency()) + pool.SetGas(0) + } // also add the fee-currency to a worker-wide blocklist, // so that they are not allowlisted in the following blocks // (only locally in the txpool, not consensus-critical) - miner.feeCurrencyBlocklist.Add(feeCurrency, *env.header) + if miner.feeCurrencyBlocklist.Add(*tx.FeeCurrency(), *env.header) { + feeCurrenciesInBlocklistCounter.Inc(1) + } + + // Add tx to block tx ringbuffer + if miner.blockedTxRingbuffer.Add(tx.Hash()) { + transactionsInBlocklistCounter.Inc(1) + } }