Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented the limit per fee currency #2203

Merged
merged 12 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ var (
utils.ProxyEnodeURLPairsFlag,
utils.LegacyProxyEnodeURLPairsFlag,
utils.ProxyAllowPrivateIPFlag,
utils.CeloFeeCurrencyDefault,
utils.CeloFeeCurrencyLimits,
}

rpcFlags = []cli.Flag{
Expand Down
2 changes: 2 additions & 0 deletions cmd/geth/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ var AppHelpFlagGroups = []flags.FlagGroup{
utils.MiningEnabledFlag,
utils.MinerValidatorFlag,
utils.MinerExtraDataFlag,
utils.CeloFeeCurrencyDefault,
utils.CeloFeeCurrencyLimits,
},
},
{
Expand Down
41 changes: 41 additions & 0 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,16 @@ var (
Name: "miner.extradata",
Usage: "Block extra data set by the miner (default = client version)",
}
CeloFeeCurrencyDefault = cli.Float64Flag{
Name: "celo.feecurrency.default",
Usage: "Default fraction of block gas limit available for TXs paid with a whitelisted alternative currency",
Value: 0.5,
}
CeloFeeCurrencyLimits = cli.StringFlag{
Name: "celo.feecurrency.limits",
Usage: "Comma separated currency address-to-block percentage mappings (<address>=<fraction>)",
}

// Account settings

UnlockedAccountFlag = cli.StringFlag{
Expand Down Expand Up @@ -1433,6 +1443,37 @@ func setMiner(ctx *cli.Context, cfg *miner.Config) {
if ctx.GlobalIsSet(MinerExtraDataFlag.Name) {
cfg.ExtraData = []byte(ctx.GlobalString(MinerExtraDataFlag.Name))
}

cfg.FeeCurrencyDefault = ctx.GlobalFloat64(CeloFeeCurrencyDefault.Name)

defaultLimits, ok := miner.DefaultFeeCurrencyLimits[getNetworkId(ctx)]
if !ok {
defaultLimits = make(map[common.Address]float64)
}

cfg.FeeCurrencyLimits = defaultLimits

if ctx.GlobalIsSet(CeloFeeCurrencyLimits.Name) {
feeCurrencyLimits := ctx.GlobalString(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 setWhitelist(ctx *cli.Context, cfg *ethconfig.Config) {
Expand Down
58 changes: 58 additions & 0 deletions core/celo_multi_gaspool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package core

import (
"github.com/celo-org/celo-blockchain/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 any unconfigured currencies and CELO
func NewMultiGasPool(
block_gas_limit 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(block_gas_limit) * fraction),
)
}

// A special case for CELO which doesn't have a limit
celoPool := new(GasPool).AddGas(block_gas_limit)

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 {
palango marked this conversation as resolved.
Show resolved Hide resolved
return mgp.defaultPool
}

return mgp.pools[*feeCurrency]
}
139 changes: 139 additions & 0 deletions core/celo_multi_gaspool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package core

import (
"testing"

"github.com/celo-org/celo-blockchain/common"
)

func TestMultiCurrencyGasPool(t *testing.T) {
block_gas_limit := uint64(1_000)
sub_gas_amount := 100

cusd_token := common.HexToAddress("0x765DE816845861e75A25fCA122bb6898B8B1282a")
ceur_token := common.HexToAddress("0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73")

testCases := []struct {
name string
whitelist []common.Address
feeCurrency *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, // block_gas_limit - sub_gas_amount
},
{
name: "Non-empty whitelist, non-empty mapping, CELO uses default pool",
feeCurrency: nil,
whitelist: []FeeCurrency{
cusd_token,
},
defaultLimit: 0.9,
limits: map[FeeCurrency]float64{
cusd_token: 0.5,
},
defaultPoolExpected: true,
expectedValue: 900, // block_gas_limit - sub_gas_amount
},
{
name: "Empty whitelist, empty mapping, non-whitelisted currency fallbacks to the default pool",
feeCurrency: &cusd_token,
whitelist: []FeeCurrency{},
defaultLimit: 0.9,
limits: map[FeeCurrency]float64{},
defaultPoolExpected: true,
expectedValue: 900, // block_gas_limit - sub_gas_amount
},
{
name: "Non-empty whitelist, non-empty mapping, non-whitelisted currency uses default pool",
feeCurrency: &ceur_token,
whitelist: []FeeCurrency{
cusd_token,
},
defaultLimit: 0.9,
limits: map[FeeCurrency]float64{
cusd_token: 0.5,
},
defaultPoolExpected: true,
expectedValue: 900, // block_gas_limit - sub_gas_amount
},
{
name: "Non-empty whitelist, empty mapping, whitelisted currency uses default limit",
feeCurrency: &cusd_token,
whitelist: []FeeCurrency{
cusd_token,
},
defaultLimit: 0.9,
limits: map[FeeCurrency]float64{},
defaultPoolExpected: false,
expectedValue: 800, // block_gas_limit * defaultLimit - sub_gas_amount
},
{
name: "Non-empty whitelist, non-empty mapping, configured whitelisted currency uses configured limits",
feeCurrency: &cusd_token,
whitelist: []FeeCurrency{
cusd_token,
},
defaultLimit: 0.9,
limits: map[FeeCurrency]float64{
cusd_token: 0.5,
},
defaultPoolExpected: false,
expectedValue: 400, // block_gas_limit * 0.5 - sub_gas_amount
},
{
name: "Non-empty whitelist, non-empty mapping, unconfigured whitelisted currency uses default limit",
feeCurrency: &ceur_token,
whitelist: []FeeCurrency{
cusd_token,
ceur_token,
},
defaultLimit: 0.9,
limits: map[FeeCurrency]float64{
cusd_token: 0.5,
},
defaultPoolExpected: false,
expectedValue: 800, // block_gas_limit * 0.5 - sub_gas_amount
},
}

for _, c := range testCases {
t.Run(c.name, func(t *testing.T) {
mgp := NewMultiGasPool(
block_gas_limit,
c.whitelist,
c.defaultLimit,
c.limits,
)

pool := mgp.PoolFor(c.feeCurrency)
pool.SubGas(uint64(sub_gas_amount))

if c.defaultPoolExpected {
result := mgp.defaultPool.Gas()
if result != c.expectedValue {
t.Error("Default pool expected", c.expectedValue, "got", result)
}
} else {
pool := mgp.pools[*c.feeCurrency]
result := pool.Gas()

if result != c.expectedValue {
t.Error(
"Expected pool", c.feeCurrency, "value", c.expectedValue,
"got", result,
)
}
}
})
}
}
11 changes: 11 additions & 0 deletions core/sys_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ func (sc *SysContractCallCtx) GetIntrinsicGasForAlternativeFeeCurrency() uint64
return sc.nonCeloCurrencyIntrinsicGas
}

// GetWhitelistedCurrencies retrieves intrinsic gas for non-native fee currencies.
func (sc *SysContractCallCtx) GetWhitelistedCurrencies() []common.Address {
whitelist := make([]common.Address, 0, len(sc.whitelistedCurrencies))

for c := range sc.whitelistedCurrencies {
whitelist = append(whitelist, c)
}

return whitelist
}

// IsWhitelisted indicates if the fee currency is whitelisted, or it's native token(CELO).
func (sc *SysContractCallCtx) IsWhitelisted(feeCurrency *common.Address) bool {
if feeCurrency == nil {
Expand Down
47 changes: 41 additions & 6 deletions miner/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ import (
type blockState 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
bytesBlock *core.BytesBlock // available bytes used to pack transactions
gasLimit uint64
sysCtx *core.SysContractCallCtx
state *state.StateDB // apply state changes here
tcount int // tx count in cycle
gasPool *core.GasPool // available gas used to pack transactions
bytesBlock *core.BytesBlock // available bytes used to pack transactions
multiGasPool core.MultiGasPool // available gas to pay for with currency
gasLimit uint64
sysCtx *core.SysContractCallCtx

header *types.Header
txs []*types.Transaction
Expand Down Expand Up @@ -112,6 +113,7 @@ func prepareBlock(w *worker) (*blockState, error) {
txFeeRecipient: txFeeRecipient,
}
b.gasPool = new(core.GasPool).AddGas(b.gasLimit)

if w.chainConfig.IsGingerbread(header.Number) {
header.GasLimit = b.gasLimit
header.Difficulty = big.NewInt(0)
Expand All @@ -127,6 +129,13 @@ func prepareBlock(w *worker) (*blockState, error) {
}
b.sysCtx = core.NewSysContractCallCtx(header, state.Copy(), w.chain)

b.multiGasPool = core.NewMultiGasPool(
b.gasLimit,
b.sysCtx.GetWhitelistedCurrencies(),
w.config.FeeCurrencyDefault,
w.config.FeeCurrencyLimits,
)

// Play our part in generating the random beacon.
if w.isRunning() && random.IsRunning(vmRunner) {
istanbul, ok := w.engine.(consensus.Istanbul)
Expand Down Expand Up @@ -250,6 +259,17 @@ loop:
if tx == nil {
break
}
// Short-circuit if the transaction is using more gas allocated for the
// given fee currency.
if b.multiGasPool.PoolFor(tx.FeeCurrency()).Gas() < tx.Gas() {
log.Trace(
"Skipping transaction which requires more gas than is left in the pool for a specific fee currency",
"currency", tx.FeeCurrency(), "tx hash", tx.Hash(),
"gas", b.multiGasPool.PoolFor(tx.FeeCurrency()).Gas(), "txgas", tx.Gas(),
)
txs.Pop()
continue
}
// Short-circuit if the transaction requires more gas than we have in the pool.
// If we didn't short-circuit here, we would get core.ErrGasLimitReached below.
// Short-circuiting here saves us the trouble of checking the GPM and so on when the tx can't be included
Expand Down Expand Up @@ -287,7 +307,10 @@ loop:
// Start executing the transaction
b.state.Prepare(tx.Hash(), b.tcount)

availableGas := b.gasPool.Gas()
logs, err := b.commitTransaction(w, tx, txFeeRecipient)
gasUsed := availableGas - b.gasPool.Gas()

switch {
case errors.Is(err, core.ErrGasLimitReached):
// Pop the current out-of-gas transaction without shifting in the next from the account
Expand Down Expand Up @@ -321,6 +344,18 @@ loop:
return err
}
}

err = b.multiGasPool.PoolFor(tx.FeeCurrency()).SubGas(gasUsed)
// Should never happen as we check it above
if err != nil {
log.Warn(
"Unexpectedly reached limit for fee currency",
"hash", tx.Hash(), "gas", b.multiGasPool.PoolFor(tx.FeeCurrency()).Gas(),
"tx gas used", gasUsed,
)
return err
}

txs.Shift()

default:
Expand Down
Loading
Loading