From 7d0fbb43869c59f9355c88ccdd84adf63056a8cc Mon Sep 17 00:00:00 2001
From: Maximilian Langenfeld <15726643+ezdac@users.noreply.github.com>
Date: Tue, 7 May 2024 11:17:56 +0200
Subject: [PATCH] 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).
However there is a small difference:
The `MultiGasPool` does not consider the currency whitelist here because
fee-currency validity will be checked by the transaction-validation
logic upon adding transactions to the tx-pool.
This means that the `GetPool` method will create a GasPool on the
fly, if it is not memoized in the internal map yet, no matter
if the feeCurrency is whitelisted or not.
In practice, this will never happen for non-whitelisted
currencies, because those transactions will not make it to
the tx-pool.
---
cmd/geth/main.go | 2 +
cmd/utils/flags.go | 46 +++++++++++
core/celo_multi_gaspool.go | 66 +++++++++++++++
core/celo_multi_gaspool_test.go | 118 +++++++++++++++++++++++++++
core/txpool/legacypool/legacypool.go | 17 ++--
core/txpool/subpool.go | 3 +
eth/handler_test.go | 15 ++--
miner/celo_defaults.go | 25 ++++++
miner/miner.go | 6 ++
miner/worker.go | 57 +++++++++++--
params/config.go | 1 +
11 files changed, 336 insertions(+), 20 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 45f8c655f0..bf0c4dbe0b 100644
--- a/cmd/geth/main.go
+++ b/cmd/geth/main.go
@@ -127,6 +127,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 cd79ba8ee3..754ba94cef 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -534,6 +534,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{
@@ -1679,6 +1690,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 == "" {
@@ -2011,6 +2055,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/core/celo_multi_gaspool.go b/core/celo_multi_gaspool.go
new file mode 100644
index 0000000000..db8c7e8425
--- /dev/null
+++ b/core/celo_multi_gaspool.go
@@ -0,0 +1,66 @@
+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
+
+ blockGasLimit uint64
+ defaultLimit float64
+}
+
+type FeeCurrencyLimitMapping = map[FeeCurrency]float64
+
+// NewMultiGasPool creates a multi-fee currency gas pool and a default fallback
+// pool for CELO
+func NewMultiGasPool(
+ blockGasLimit uint64,
+ defaultLimit float64,
+ limitsMapping FeeCurrencyLimitMapping,
+) *MultiGasPool {
+ pools := make(map[FeeCurrency]*GasPool, len(limitsMapping))
+ // A special case for CELO which doesn't have a limit
+ celoPool := new(GasPool).AddGas(blockGasLimit)
+ mgp := &MultiGasPool{
+ pools: pools,
+ defaultPool: celoPool,
+ blockGasLimit: blockGasLimit,
+ defaultLimit: defaultLimit,
+ }
+ for feeCurrency, fraction := range limitsMapping {
+ mgp.getOrInitPool(feeCurrency, &fraction)
+ }
+ return mgp
+}
+
+func (mgp MultiGasPool) getOrInitPool(c FeeCurrency, fraction *float64) *GasPool {
+ if gp, ok := mgp.pools[c]; ok {
+ return gp
+ }
+ if fraction == nil {
+ fraction = &mgp.defaultLimit
+ }
+ gp := new(GasPool).AddGas(
+ uint64(float64(mgp.blockGasLimit) * *fraction),
+ )
+ mgp.pools[c] = gp
+ return gp
+}
+
+// GetPool returns an initialised pool for the given fee currency or
+// initialises and returns a new pool with a default limit.
+// For a `nil` FeeCurrency value, it returns the default pool.
+func (mgp MultiGasPool) GetPool(c *FeeCurrency) *GasPool {
+ if c == nil {
+ return mgp.defaultPool
+ }
+ // Use the default fraction here because the configured limits'
+ // pools have been created already in the constructor.
+ return mgp.getOrInitPool(*c, nil)
+}
diff --git a/core/celo_multi_gaspool_test.go b/core/celo_multi_gaspool_test.go
new file mode 100644
index 0000000000..d45af2558b
--- /dev/null
+++ b/core/celo_multi_gaspool_test.go
@@ -0,0 +1,118 @@
+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
+ defaultLimit float64
+ limits FeeCurrencyLimitMapping
+ defaultPoolExpected bool
+ expectedValue uint64
+ }{
+ {
+ name: "Empty mapping, CELO uses default pool",
+ feeCurrency: nil,
+ defaultLimit: 0.9,
+ limits: map[FeeCurrency]float64{},
+ defaultPoolExpected: true,
+ expectedValue: 900, // blockGasLimit - subGasAmount
+ },
+ {
+ name: "Non-empty mapping, CELO uses default pool",
+ feeCurrency: nil,
+ defaultLimit: 0.9,
+ limits: map[FeeCurrency]float64{
+ cUSDToken: 0.5,
+ },
+ defaultPoolExpected: true,
+ expectedValue: 900, // blockGasLimit - subGasAmount
+ },
+ {
+ name: "Empty mapping, currency fallbacks to the default limit",
+ feeCurrency: &cUSDToken,
+ defaultLimit: 0.9,
+ limits: map[FeeCurrency]float64{},
+ defaultPoolExpected: false,
+ expectedValue: 800, // blockGasLimit * defaultLimit- subGasAmount
+ },
+ {
+ name: "Non-empty mapping, currency uses default limit",
+ feeCurrency: &cEURToken,
+ defaultLimit: 0.9,
+ limits: map[FeeCurrency]float64{
+ cUSDToken: 0.5,
+ },
+ defaultPoolExpected: false,
+ expectedValue: 800, // blockGasLimit * defaultLimit - subGasAmount
+ },
+ {
+ name: "Non-empty whitelist, empty mapping, currency uses default limit",
+ feeCurrency: &cUSDToken,
+ defaultLimit: 0.9,
+ limits: map[FeeCurrency]float64{},
+ defaultPoolExpected: false,
+ expectedValue: 800, // blockGasLimit * defaultLimit - subGasAmount
+ },
+ {
+ name: "Non-empty mapping, configured currency uses configured limits",
+ feeCurrency: &cUSDToken,
+ defaultLimit: 0.9,
+ limits: map[FeeCurrency]float64{
+ cUSDToken: 0.5,
+ },
+ defaultPoolExpected: false,
+ expectedValue: 400, // blockGasLimit * 0.5 - subGasAmount
+ },
+ {
+ name: "Non-empty mapping, unconfigured currency uses default limit",
+ feeCurrency: &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.defaultLimit,
+ c.limits,
+ )
+
+ pool := mgp.GetPool(c.feeCurrency)
+ pool.SubGas(uint64(subGasAmount))
+
+ if c.defaultPoolExpected {
+ result := mgp.GetPool(nil).Gas()
+ if result != c.expectedValue {
+ t.Error("Default pool expected", c.expectedValue, "got", result)
+ }
+ } else {
+ result := mgp.GetPool(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 3fe9f9f5e6..6b8ef9fe82 100644
--- a/core/txpool/legacypool/legacypool.go
+++ b/core/txpool/legacypool/legacypool.go
@@ -558,14 +558,15 @@ func (pool *LegacyPool) Pending(enforceTips bool) map[common.Address][]*txpool.L
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: txs[i].GasFeeCap(),
- GasTipCap: 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: txs[i].GasFeeCap(),
+ GasTipCap: 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 de05b38d43..1f4f658064 100644
--- a/core/txpool/subpool.go
+++ b/core/txpool/subpool.go
@@ -40,6 +40,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 6d6132ee4c..2d58df35a5 100644
--- a/eth/handler_test.go
+++ b/eth/handler_test.go
@@ -108,13 +108,14 @@ func (p *testTxPool) Pending(enforceTips bool) map[common.Address][]*txpool.Lazy
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: tx.GasFeeCap(),
- GasTipCap: tx.GasTipCap(),
- Gas: tx.Gas(),
- BlobGas: tx.BlobGas(),
+ Hash: tx.Hash(),
+ Tx: tx,
+ Time: tx.Time(),
+ GasFeeCap: tx.GasFeeCap(),
+ GasTipCap: 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..8c1d0ec22e
--- /dev/null
+++ b/miner/celo_defaults.go
@@ -0,0 +1,25 @@
+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")
+)
+
+// default limits default fraction
+const DefaultFeeCurrencyDefault = 0.5
+
+// default limits configuration
+var DefaultFeeCurrencyLimits = map[uint64]map[common.Address]float64{
+ params.CeloMainnetChainID: {
+ cUSD_TOKEN: 0.9,
+ cEUR_TOKEN: 0.5,
+ cREAL_TOKEN: 0.5,
+ },
+}
diff --git a/miner/miner.go b/miner/miner.go
index 82d353a336..250083f042 100644
--- a/miner/miner.go
+++ b/miner/miner.go
@@ -61,6 +61,10 @@ type Config struct {
NewPayloadTimeout time.Duration // The maximum time allowance for creating a new payload
RollupComputePendingBlock bool // Compute the pending block from tx-pool, instead of copying the latest-block
+
+ // 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.
@@ -74,6 +78,8 @@ var DefaultConfig = Config{
// run 3 rounds.
Recommit: 2 * time.Second,
NewPayloadTimeout: 2 * time.Second,
+
+ FeeCurrencyDefault: DefaultFeeCurrencyDefault,
}
// Miner creates blocks and searches for proof-of-work values.
diff --git a/miner/worker.go b/miner/worker.go
index 87e1373192..a5dc2b7b66 100644
--- a/miner/worker.go
+++ b/miner/worker.go
@@ -86,11 +86,12 @@ 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
+ coinbase common.Address
header *types.Header
txs []*types.Transaction
@@ -839,6 +840,13 @@ func (w *worker) commitTransactions(env *environment, txs *transactionsByPriceAn
if env.gasPool == nil {
env.gasPool = new(core.GasPool).AddGas(gasLimit)
}
+ if env.multiGasPool == nil {
+ env.multiGasPool = core.NewMultiGasPool(
+ env.header.GasLimit,
+ w.config.FeeCurrencyDefault,
+ w.config.FeeCurrencyLimits,
+ )
+ }
var coalescedLogs []*types.Log
for {
@@ -869,6 +877,15 @@ func (w *worker) commitTransactions(env *environment, txs *transactionsByPriceAn
txs.Pop()
continue
}
+ if left := env.multiGasPool.GetPool(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 {
@@ -890,7 +907,9 @@ func (w *worker) commitTransactions(env *environment, txs *transactionsByPriceAn
// 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
@@ -898,6 +917,23 @@ func (w *worker) commitTransactions(env *environment, txs *transactionsByPriceAn
txs.Shift()
case errors.Is(err, nil):
+ err := env.multiGasPool.GetPool(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.GetPool(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++
@@ -1107,6 +1143,13 @@ func (w *worker) generateWork(genParams *generateParams) *newPayloadResult {
if work.gasPool == nil {
work.gasPool = new(core.GasPool).AddGas(work.header.GasLimit)
}
+ if work.multiGasPool == nil {
+ work.multiGasPool = core.NewMultiGasPool(
+ work.header.GasLimit,
+ w.config.FeeCurrencyDefault,
+ w.config.FeeCurrencyLimits,
+ )
+ }
misc.EnsureCreate2Deployer(w.chainConfig, work.header.Time, work.state)
@@ -1117,6 +1160,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.GetPool(nil).SubGas(tx.Gas())
work.tcount++
}
diff --git a/params/config.go b/params/config.go
index 382ab3e100..a3639b4ff7 100644
--- a/params/config.go
+++ b/params/config.go
@@ -34,6 +34,7 @@ var (
const (
OPMainnetChainID = 10
OPGoerliChainID = 420
+ CeloMainnetChainID = 42220
BaseMainnetChainID = 8453
BaseGoerliChainID = 84531
baseSepoliaChainID = 84532