From 7bac1501d71750477c28555ce8494215c08b65b6 Mon Sep 17 00:00:00 2001 From: Maximilian Langenfeld <15726643+ezdac@users.noreply.github.com> Date: Tue, 4 Jun 2024 10:27:27 +0200 Subject: [PATCH] Port the block limit per fee currency (#119) * Port the block limit per fee currency feature Closes #65 This implements the block-limit per fee-currency feature. Some parts of this have been directly ported from celo-blockchain (https://github.com/celo-org/celo-blockchain/commit/dc45bdc00). * Copy MultiGasPool in miner environment * Add USDT and USDC to block-limit per currency defaults * Initialize MultiGasPool with currency whitelist --- cmd/geth/main.go | 2 + cmd/utils/flags.go | 46 +++++++++ common/celo_types.go | 10 ++ core/celo_multi_gaspool.go | 71 ++++++++++++++ core/celo_multi_gaspool_test.go | 138 +++++++++++++++++++++++++++ core/txpool/legacypool/legacypool.go | 17 ++-- core/txpool/subpool.go | 3 + eth/handler_test.go | 15 +-- miner/celo_defaults.go | 29 ++++++ miner/miner.go | 6 ++ miner/worker.go | 66 +++++++++++-- params/config.go | 1 + 12 files changed, 383 insertions(+), 21 deletions(-) create mode 100644 core/celo_multi_gaspool.go create mode 100644 core/celo_multi_gaspool_test.go create mode 100644 miner/celo_defaults.go diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 0418dd685a..e91ca41a21 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -130,6 +130,8 @@ var ( utils.MinerExtraDataFlag, utils.MinerRecommitIntervalFlag, utils.MinerNewPayloadTimeout, + utils.CeloFeeCurrencyDefault, + utils.CeloFeeCurrencyLimits, utils.NATFlag, utils.NoDiscoverFlag, utils.DiscoveryV4Flag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 0c5e4414eb..960f598bf7 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -509,6 +509,17 @@ var ( Value: ethconfig.Defaults.Miner.NewPayloadTimeout, Category: flags.MinerCategory, } + CeloFeeCurrencyDefault = &cli.Float64Flag{ + Name: "celo.feecurrency.default", + Usage: "Default fraction of block gas limit available for TXs paid with a whitelisted alternative currency", + Value: ethconfig.Defaults.Miner.FeeCurrencyDefault, + Category: flags.MinerCategory, + } + CeloFeeCurrencyLimits = &cli.StringFlag{ + Name: "celo.feecurrency.limits", + Usage: "Comma separated currency address-to-block percentage mappings (
=)", + Category: flags.MinerCategory, + } // Account settings UnlockedAccountFlag = &cli.StringFlag{ @@ -1642,6 +1653,39 @@ func setMiner(ctx *cli.Context, cfg *miner.Config) { } } +func setCeloMiner(ctx *cli.Context, cfg *miner.Config, networkId uint64) { + cfg.FeeCurrencyDefault = ctx.Float64(CeloFeeCurrencyDefault.Name) + + defaultLimits, ok := miner.DefaultFeeCurrencyLimits[networkId] + if !ok { + defaultLimits = make(map[common.Address]float64) + } + + cfg.FeeCurrencyLimits = defaultLimits + + if ctx.IsSet(CeloFeeCurrencyLimits.Name) { + feeCurrencyLimits := ctx.String(CeloFeeCurrencyLimits.Name) + + for _, entry := range strings.Split(feeCurrencyLimits, ",") { + parts := strings.Split(entry, "=") + if len(parts) != 2 { + Fatalf("Invalid fee currency limits entry: %s", entry) + } + var address common.Address + if err := address.UnmarshalText([]byte(parts[0])); err != nil { + Fatalf("Invalid fee currency address hash %s: %v", parts[0], err) + } + + fraction, err := strconv.ParseFloat(parts[1], 64) + if err != nil { + Fatalf("Invalid block limit fraction %s: %v", parts[1], err) + } + + cfg.FeeCurrencyLimits[address] = fraction + } + } +} + func setRequiredBlocks(ctx *cli.Context, cfg *ethconfig.Config) { requiredBlocks := ctx.String(EthRequiredBlocksFlag.Name) if requiredBlocks == "" { @@ -1991,6 +2035,8 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if err := kzg4844.UseCKZG(ctx.String(CryptoKZGFlag.Name) == "ckzg"); err != nil { Fatalf("Failed to set KZG library implementation to %s: %v", ctx.String(CryptoKZGFlag.Name), err) } + + setCeloMiner(ctx, &cfg.Miner, cfg.NetworkId) } // SetDNSDiscoveryDefaults configures DNS discovery with the given URL if diff --git a/common/celo_types.go b/common/celo_types.go index 44ac2ef072..3ae8a2935a 100644 --- a/common/celo_types.go +++ b/common/celo_types.go @@ -10,6 +10,16 @@ var ( type ExchangeRates = map[Address]*big.Rat +func CurrencyWhitelist(exchangeRates ExchangeRates) []Address { + addrs := make([]Address, len(exchangeRates)) + i := 0 + for k := range exchangeRates { + addrs[i] = k + i++ + } + return addrs +} + func IsCurrencyWhitelisted(exchangeRates ExchangeRates, feeCurrency *Address) bool { if feeCurrency == nil { return true diff --git a/core/celo_multi_gaspool.go b/core/celo_multi_gaspool.go new file mode 100644 index 0000000000..6cec29a1c9 --- /dev/null +++ b/core/celo_multi_gaspool.go @@ -0,0 +1,71 @@ +package core + +import ( + "github.com/ethereum/go-ethereum/common" +) + +type FeeCurrency = common.Address + +// MultiGasPool tracks the amount of gas available during execution +// of the transactions in a block per fee currency. The zero value is a pool +// with zero gas available. +type MultiGasPool struct { + pools map[FeeCurrency]*GasPool + defaultPool *GasPool +} + +type FeeCurrencyLimitMapping = map[FeeCurrency]float64 + +// NewMultiGasPool creates a multi-fee currency gas pool and a default fallback +// pool for CELO +func NewMultiGasPool( + blockGasLimit uint64, + whitelist []FeeCurrency, + defaultLimit float64, + limitsMapping FeeCurrencyLimitMapping, +) *MultiGasPool { + pools := make(map[FeeCurrency]*GasPool, len(whitelist)) + + for i := range whitelist { + currency := whitelist[i] + fraction, ok := limitsMapping[currency] + if !ok { + fraction = defaultLimit + } + + pools[currency] = new(GasPool).AddGas( + uint64(float64(blockGasLimit) * fraction), + ) + } + + // A special case for CELO which doesn't have a limit + celoPool := new(GasPool).AddGas(blockGasLimit) + + return &MultiGasPool{ + pools: pools, + defaultPool: celoPool, + } +} + +// PoolFor returns a configured pool for the given fee currency or the default +// one otherwise +func (mgp MultiGasPool) PoolFor(feeCurrency *FeeCurrency) *GasPool { + if feeCurrency == nil || mgp.pools[*feeCurrency] == nil { + return mgp.defaultPool + } + + return mgp.pools[*feeCurrency] +} + +func (mgp MultiGasPool) Copy() *MultiGasPool { + pools := make(map[FeeCurrency]*GasPool, len(mgp.pools)) + for fc, gp := range mgp.pools { + gpCpy := *gp + pools[fc] = &gpCpy + } + gpCpy := *mgp.defaultPool + return &MultiGasPool{ + pools: pools, + defaultPool: &gpCpy, + } +} diff --git a/core/celo_multi_gaspool_test.go b/core/celo_multi_gaspool_test.go new file mode 100644 index 0000000000..ec79703329 --- /dev/null +++ b/core/celo_multi_gaspool_test.go @@ -0,0 +1,138 @@ +package core + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +func TestMultiCurrencyGasPool(t *testing.T) { + blockGasLimit := uint64(1_000) + subGasAmount := 100 + + cUSDToken := common.HexToAddress("0x765DE816845861e75A25fCA122bb6898B8B1282a") + cEURToken := common.HexToAddress("0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73") + + testCases := []struct { + name string + feeCurrency *FeeCurrency + whitelist []FeeCurrency + defaultLimit float64 + limits FeeCurrencyLimitMapping + defaultPoolExpected bool + expectedValue uint64 + }{ + { + name: "Empty whitelist, empty mapping, CELO uses default pool", + feeCurrency: nil, + whitelist: []FeeCurrency{}, + defaultLimit: 0.9, + limits: map[FeeCurrency]float64{}, + defaultPoolExpected: true, + expectedValue: 900, // blockGasLimit - subGasAmount + }, + { + name: "Non-empty whitelist, non-empty mapping, CELO uses default pool", + feeCurrency: nil, + whitelist: []FeeCurrency{ + cUSDToken, + }, + defaultLimit: 0.9, + limits: map[FeeCurrency]float64{ + cUSDToken: 0.5, + }, + defaultPoolExpected: true, + expectedValue: 900, // blockGasLimit - subGasAmount + }, + { + name: "Empty whitelist, empty mapping, non-whitelisted currency fallbacks to the default pool", + feeCurrency: &cUSDToken, + whitelist: []FeeCurrency{}, + defaultLimit: 0.9, + limits: map[FeeCurrency]float64{}, + defaultPoolExpected: true, + expectedValue: 900, // blockGasLimit - subGasAmount + }, + { + name: "Non-empty whitelist, non-empty mapping, non-whitelisted currency uses default pool", + feeCurrency: &cEURToken, + whitelist: []FeeCurrency{ + cUSDToken, + }, + defaultLimit: 0.9, + limits: map[FeeCurrency]float64{ + cUSDToken: 0.5, + }, + defaultPoolExpected: true, + expectedValue: 900, // blockGasLimit - subGasAmount + }, + { + name: "Non-empty whitelist, empty mapping, whitelisted currency uses default limit", + feeCurrency: &cUSDToken, + whitelist: []FeeCurrency{ + cUSDToken, + }, + defaultLimit: 0.9, + limits: map[FeeCurrency]float64{}, + defaultPoolExpected: false, + expectedValue: 800, // blockGasLimit * defaultLimit - subGasAmount + }, + { + name: "Non-empty whitelist, non-empty mapping, configured whitelisted currency uses configured limits", + feeCurrency: &cUSDToken, + whitelist: []FeeCurrency{ + cUSDToken, + }, + defaultLimit: 0.9, + limits: map[FeeCurrency]float64{ + cUSDToken: 0.5, + }, + defaultPoolExpected: false, + expectedValue: 400, // blockGasLimit * 0.5 - subGasAmount + }, + { + name: "Non-empty whitelist, non-empty mapping, unconfigured whitelisted currency uses default limit", + feeCurrency: &cEURToken, + whitelist: []FeeCurrency{ + cUSDToken, + cEURToken, + }, + defaultLimit: 0.9, + limits: map[FeeCurrency]float64{ + cUSDToken: 0.5, + }, + defaultPoolExpected: false, + expectedValue: 800, // blockGasLimit * 0.5 - subGasAmount + }, + } + + for _, c := range testCases { + t.Run(c.name, func(t *testing.T) { + mgp := NewMultiGasPool( + blockGasLimit, + c.whitelist, + c.defaultLimit, + c.limits, + ) + + pool := mgp.PoolFor(c.feeCurrency) + pool.SubGas(uint64(subGasAmount)) + + if c.defaultPoolExpected { + result := mgp.PoolFor(nil).Gas() + if result != c.expectedValue { + t.Error("Default pool expected", c.expectedValue, "got", result) + } + } else { + result := mgp.PoolFor(c.feeCurrency).Gas() + + if result != c.expectedValue { + t.Error( + "Expected pool", c.feeCurrency, "value", c.expectedValue, + "got", result, + ) + } + } + }) + } +} diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 0e40c0c05d..a2717d8cfa 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -573,14 +573,15 @@ func (pool *LegacyPool) Pending(filter txpool.PendingFilter) map[common.Address] lazies := make([]*txpool.LazyTransaction, len(txs)) for i := 0; i < len(txs); i++ { lazies[i] = &txpool.LazyTransaction{ - Pool: pool, - Hash: txs[i].Hash(), - Tx: txs[i], - Time: txs[i].Time(), - GasFeeCap: uint256.MustFromBig(txs[i].GasFeeCap()), - GasTipCap: uint256.MustFromBig(txs[i].GasTipCap()), - Gas: txs[i].Gas(), - BlobGas: txs[i].BlobGas(), + Pool: pool, + Hash: txs[i].Hash(), + Tx: txs[i], + Time: txs[i].Time(), + GasFeeCap: uint256.MustFromBig(txs[i].GasFeeCap()), + GasTipCap: uint256.MustFromBig(txs[i].GasTipCap()), + Gas: txs[i].Gas(), + BlobGas: txs[i].BlobGas(), + FeeCurrency: txs[i].FeeCurrency(), } } pending[addr] = lazies diff --git a/core/txpool/subpool.go b/core/txpool/subpool.go index 9881ed1b8f..5e0c850fd0 100644 --- a/core/txpool/subpool.go +++ b/core/txpool/subpool.go @@ -41,6 +41,9 @@ type LazyTransaction struct { Gas uint64 // Amount of gas required by the transaction BlobGas uint64 // Amount of blob gas required by the transaction + + // Celo + FeeCurrency *common.Address } // Resolve retrieves the full transaction belonging to a lazy handle if it is still diff --git a/eth/handler_test.go b/eth/handler_test.go index 58353f6b64..8c1ba1ef5f 100644 --- a/eth/handler_test.go +++ b/eth/handler_test.go @@ -109,13 +109,14 @@ func (p *testTxPool) Pending(filter txpool.PendingFilter) map[common.Address][]* for addr, batch := range batches { for _, tx := range batch { pending[addr] = append(pending[addr], &txpool.LazyTransaction{ - Hash: tx.Hash(), - Tx: tx, - Time: tx.Time(), - GasFeeCap: uint256.MustFromBig(tx.GasFeeCap()), - GasTipCap: uint256.MustFromBig(tx.GasTipCap()), - Gas: tx.Gas(), - BlobGas: tx.BlobGas(), + Hash: tx.Hash(), + Tx: tx, + Time: tx.Time(), + GasFeeCap: uint256.MustFromBig(tx.GasFeeCap()), + GasTipCap: uint256.MustFromBig(tx.GasTipCap()), + Gas: tx.Gas(), + BlobGas: tx.BlobGas(), + FeeCurrency: tx.FeeCurrency(), }) } } diff --git a/miner/celo_defaults.go b/miner/celo_defaults.go new file mode 100644 index 0000000000..be843d1bc9 --- /dev/null +++ b/miner/celo_defaults.go @@ -0,0 +1,29 @@ +package miner + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" +) + +// cStables addresses on mainnet +var ( + cUSD_TOKEN = common.HexToAddress("0x765DE816845861e75A25fCA122bb6898B8B1282a") + cEUR_TOKEN = common.HexToAddress("0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73") + cREAL_TOKEN = common.HexToAddress("0xe8537a3d056DA446677B9E9d6c5dB704EaAb4787") + USDC_TOKEN = common.HexToAddress("0xcebA9300f2b948710d2653dD7B07f33A8B32118C") + USDT_TOKEN = common.HexToAddress("0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e") +) + +// default limits default fraction +const DefaultFeeCurrencyLimit = 0.5 + +// default limits configuration +var DefaultFeeCurrencyLimits = map[uint64]map[common.Address]float64{ + params.CeloMainnetChainID: { + cUSD_TOKEN: 0.9, + USDT_TOKEN: 0.9, + USDC_TOKEN: 0.9, + cEUR_TOKEN: 0.5, + cREAL_TOKEN: 0.5, + }, +} diff --git a/miner/miner.go b/miner/miner.go index 85b3d8d13d..2b9c1a416c 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -62,6 +62,10 @@ type Config struct { RollupComputePendingBlock bool // Compute the pending block from tx-pool, instead of copying the latest-block EffectiveGasCeil uint64 // if non-zero, a gas ceiling to apply independent of the header's gaslimit value + + // Celo: + FeeCurrencyDefault float64 // Default fraction of block gas limit + FeeCurrencyLimits map[common.Address]float64 // Fee currency-to-limit fraction mapping } // DefaultConfig contains default settings for miner. @@ -75,6 +79,8 @@ var DefaultConfig = Config{ // run 3 rounds. Recommit: 2 * time.Second, NewPayloadTimeout: 2 * time.Second, + + FeeCurrencyDefault: DefaultFeeCurrencyLimit, } // Miner creates blocks and searches for proof-of-work values. diff --git a/miner/worker.go b/miner/worker.go index a69e564410..7a21bef643 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -87,11 +87,13 @@ var ( // environment is the worker's current environment and holds all // information of the sealing block generation. type environment struct { - signer types.Signer - state *state.StateDB // apply state changes here - tcount int // tx count in cycle - gasPool *core.GasPool // available gas used to pack transactions - coinbase common.Address + signer types.Signer + state *state.StateDB // apply state changes here + tcount int // tx count in cycle + gasPool *core.GasPool // available gas used to pack transactions + multiGasPool *core.MultiGasPool // available per-fee-currency gas used to pack transactions + feeCurrencyWhitelist []common.Address + coinbase common.Address header *types.Header txs []*types.Transaction @@ -114,6 +116,9 @@ func (env *environment) copy() *environment { gasPool := *env.gasPool cpy.gasPool = &gasPool } + if env.multiGasPool != nil { + cpy.multiGasPool = env.multiGasPool.Copy() + } cpy.txs = make([]*types.Transaction, len(env.txs)) copy(cpy.txs, env.txs) @@ -851,6 +856,14 @@ func (w *worker) commitTransactions(env *environment, plainTxs, blobTxs *transac if env.gasPool == nil { env.gasPool = new(core.GasPool).AddGas(gasLimit) } + if env.multiGasPool == nil { + env.multiGasPool = core.NewMultiGasPool( + env.header.GasLimit, + env.feeCurrencyWhitelist, + w.config.FeeCurrencyDefault, + w.config.FeeCurrencyLimits, + ) + } var coalescedLogs []*types.Log for { @@ -906,6 +919,15 @@ func (w *worker) commitTransactions(env *environment, plainTxs, blobTxs *transac txs.Pop() continue } + if left := env.multiGasPool.PoolFor(ltx.FeeCurrency).Gas(); left < ltx.Gas { + log.Trace( + "Not enough specific fee-currency gas left for transaction", + "currency", ltx.FeeCurrency, "hash", ltx.Hash, + "left", left, "needed", ltx.Gas, + ) + txs.Pop() + continue + } // Transaction seems to fit, pull it up from the pool tx := ltx.Resolve() if tx == nil { @@ -927,7 +949,9 @@ func (w *worker) commitTransactions(env *environment, plainTxs, blobTxs *transac // Start executing the transaction env.state.SetTxContext(tx.Hash(), env.tcount) + availableGas := env.gasPool.Gas() logs, err := w.commitTransaction(env, tx) + gasUsed := availableGas - env.gasPool.Gas() switch { case errors.Is(err, core.ErrNonceTooLow): // New head notification data race between the transaction pool and miner, shift @@ -935,6 +959,23 @@ func (w *worker) commitTransactions(env *environment, plainTxs, blobTxs *transac txs.Shift() case errors.Is(err, nil): + err := env.multiGasPool.PoolFor(tx.FeeCurrency()).SubGas(gasUsed) + if err != nil { + // Should never happen as we check it above + log.Warn( + "Unexpectedly reached limit for fee currency, but tx will not be skipped", + "hash", tx.Hash(), "gas", env.multiGasPool.PoolFor(tx.FeeCurrency()).Gas(), + "tx gas used", gasUsed, + ) + // If we reach this codepath, we want to still include the transaction, + // since the "global" gasPool in the commitTransaction accepted it and we + // would have to roll the transaction back now, introducing unnecessary + // complexity. + // Since we shouldn't reach this point anyways and the + // block gas limit per fee currency is enforced voluntarily and not + // included in the consensus this is fine. + } + // Everything ok, collect the logs and shift in the next transaction from the same account coalescedLogs = append(coalescedLogs, logs...) env.tcount++ @@ -1095,8 +1136,9 @@ func (w *worker) prepareWork(genParams *generateParams) (*environment, error) { log.Error("Failed to create sealing context", "err", err) return nil, err } + context := core.NewEVMBlockContext(header, w.chain, nil, w.chainConfig, env.state) + env.feeCurrencyWhitelist = common.CurrencyWhitelist(context.ExchangeRates) if header.ParentBeaconRoot != nil { - context := core.NewEVMBlockContext(header, w.chain, nil, w.chainConfig, env.state) vmenv := vm.NewEVM(context, vm.TxContext{}, env.state, w.chainConfig, vm.Config{}) core.ProcessBeaconBlockRoot(*header.ParentBeaconRoot, vmenv, env.state) } @@ -1175,6 +1217,14 @@ func (w *worker) generateWork(genParams *generateParams) *newPayloadResult { } work.gasPool = new(core.GasPool).AddGas(gasLimit) } + if work.multiGasPool == nil { + work.multiGasPool = core.NewMultiGasPool( + work.header.GasLimit, + work.feeCurrencyWhitelist, + w.config.FeeCurrencyDefault, + w.config.FeeCurrencyLimits, + ) + } misc.EnsureCreate2Deployer(w.chainConfig, work.header.Time, work.state) @@ -1185,6 +1235,10 @@ func (w *worker) generateWork(genParams *generateParams) *newPayloadResult { if err != nil { return &newPayloadResult{err: fmt.Errorf("failed to force-include tx: %s type: %d sender: %s nonce: %d, err: %w", tx.Hash(), tx.Type(), from, tx.Nonce(), err)} } + // the non-fee currency pool in the multipool is not used, but for consistency + // subtract the gas. Don't check the error either, this has been checked already + // with the work.gasPool. + work.multiGasPool.PoolFor(nil).SubGas(tx.Gas()) work.tcount++ } diff --git a/params/config.go b/params/config.go index f067b37a23..823ace9fb2 100644 --- a/params/config.go +++ b/params/config.go @@ -35,6 +35,7 @@ var ( const ( OPMainnetChainID = 10 OPGoerliChainID = 420 + CeloMainnetChainID = 42220 BaseMainnetChainID = 8453 BaseGoerliChainID = 84531 baseSepoliaChainID = 84532