Skip to content

Commit

Permalink
Port the block limit per fee currency feature
Browse files Browse the repository at this point in the history
Closes #65

This implements the block-limit per fee-currency feature.
Some parts of this have been directly ported from celo-blockchain
(celo-org/celo-blockchain@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.
  • Loading branch information
ezdac committed May 7, 2024
1 parent b510efe commit 7d0fbb4
Show file tree
Hide file tree
Showing 11 changed files with 336 additions and 20 deletions.
2 changes: 2 additions & 0 deletions cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ var (
utils.MinerExtraDataFlag,
utils.MinerRecommitIntervalFlag,
utils.MinerNewPayloadTimeout,
utils.CeloFeeCurrencyDefault,
utils.CeloFeeCurrencyLimits,
utils.NATFlag,
utils.NoDiscoverFlag,
utils.DiscoveryV4Flag,
Expand Down
46 changes: 46 additions & 0 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (<address>=<fraction>)",
Category: flags.MinerCategory,
}

// Account settings
UnlockedAccountFlag = &cli.StringFlag{
Expand Down Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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
Expand Down
66 changes: 66 additions & 0 deletions core/celo_multi_gaspool.go
Original file line number Diff line number Diff line change
@@ -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)
}
118 changes: 118 additions & 0 deletions core/celo_multi_gaspool_test.go
Original file line number Diff line number Diff line change
@@ -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,
)
}
}
})
}
}
17 changes: 9 additions & 8 deletions core/txpool/legacypool/legacypool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions core/txpool/subpool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 8 additions & 7 deletions eth/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
})
}
}
Expand Down
25 changes: 25 additions & 0 deletions miner/celo_defaults.go
Original file line number Diff line number Diff line change
@@ -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,
},
}
6 changes: 6 additions & 0 deletions miner/miner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
Loading

0 comments on commit 7d0fbb4

Please sign in to comment.