diff --git a/common/types.go b/common/types.go index bf74e43716..8c517a5834 100644 --- a/common/types.go +++ b/common/types.go @@ -472,3 +472,5 @@ func (d *Decimal) UnmarshalJSON(input []byte) error { return err } } + +type ExchangeRates = map[Address]*big.Rat diff --git a/core/celo_backend.go b/core/celo_backend.go index 1d5a367d31..a54b1bf58c 100644 --- a/core/celo_backend.go +++ b/core/celo_backend.go @@ -14,14 +14,14 @@ import ( // CeloBackend provide a partial ContractBackend implementation, so that we can // access core contracts during block processing. type CeloBackend struct { - chainConfig *params.ChainConfig - state *state.StateDB + ChainConfig *params.ChainConfig + State *state.StateDB } // ContractCaller implementation func (b *CeloBackend) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { - return b.state.GetCode(contract), nil + return b.State.GetCode(contract), nil } func (b *CeloBackend) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { @@ -44,7 +44,7 @@ func (b *CeloBackend) CallContract(ctx context.Context, call ethereum.CallMsg, b txCtx := vm.TxContext{} vmConfig := vm.Config{} - evm := vm.NewEVM(blockCtx, txCtx, b.state, b.chainConfig, vmConfig) + evm := vm.NewEVM(blockCtx, txCtx, b.State, b.ChainConfig, vmConfig) ret, _, err := evm.StaticCall(vm.AccountRef(evm.Origin), *call.To, call.Data, call.Gas) return ret, err diff --git a/core/celo_evm.go b/core/celo_evm.go index 4db27bed00..0fb33078a4 100644 --- a/core/celo_evm.go +++ b/core/celo_evm.go @@ -16,7 +16,7 @@ import ( ) // Returns the exchange rates for all gas currencies from CELO -func getExchangeRates(caller *CeloBackend) (map[common.Address]*big.Rat, error) { +func GetExchangeRates(caller bind.ContractCaller) (common.ExchangeRates, error) { exchangeRates := map[common.Address]*big.Rat{} whitelist, err := abigen.NewFeeCurrencyWhitelistCaller(contracts.FeeCurrencyWhitelistAddress, caller) if err != nil { @@ -57,7 +57,7 @@ func setCeloFieldsInBlockContext(blockContext *vm.BlockContext, header *types.He // Add fee currency exchange rates var err error - blockContext.ExchangeRates, err = getExchangeRates(caller) + blockContext.ExchangeRates, err = GetExchangeRates(caller) if err != nil { log.Error("Error fetching exchange rates!", "err", err) } diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index c48f6c0f5f..31d862baaf 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -1035,8 +1035,7 @@ func (p *BlobPool) validateTx(tx *types.Transaction) error { MaxSize: txMaxSize, MinTip: p.gasTip.ToBig(), } - var fcv txpool.FeeCurrencyValidator = nil // TODO: create with proper value - if err := txpool.CeloValidateTransaction(tx, p.head, p.signer, baseOpts, p.state, fcv); err != nil { + if err := txpool.CeloValidateTransaction(tx, p.head, p.signer, baseOpts, p.state, p.feeCurrencyValidator); err != nil { return err } // Ensure the transaction adheres to the stateful pool filters (nonce, balance) diff --git a/core/txpool/celo_validation.go b/core/txpool/celo_validation.go index 5c0f7b27ac..ea1da82692 100644 --- a/core/txpool/celo_validation.go +++ b/core/txpool/celo_validation.go @@ -19,6 +19,9 @@ type FeeCurrencyValidator interface { // Balance returns the feeCurrency balance of the address specified, in the given state. // If feeCurrency is nil, the native currency balance has to be returned. Balance(st *state.StateDB, address common.Address, feeCurrency *common.Address) *big.Int + + // ToCurrencyValue + ToCurrencyValue(st *state.StateDB, fromNativeValue *big.Int, toFeeCurrency *common.Address) *big.Int } func NewFeeCurrencyValidator() FeeCurrencyValidator { @@ -82,7 +85,7 @@ func CeloValidateTransaction(tx *types.Transaction, head *types.Header, return err } if IsFeeCurrencyTx(tx) { - if !fcv.IsWhitelisted(st, tx.FeeCurrency()) { + if !fcv.IsWhitelisted(st, tx.FeeCurrency()) { // TODO: change to celoContext return NonWhitelistedFeeCurrencyError } } diff --git a/core/txpool/legacypool/celo.go b/core/txpool/legacypool/celo.go new file mode 100644 index 0000000000..86a527db68 --- /dev/null +++ b/core/txpool/legacypool/celo.go @@ -0,0 +1,123 @@ +package legacypool + +import ( + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/contracts/celo/abigen" + "github.com/ethereum/go-ethereum/core/state" +) + +var ( + unitRate = big.NewRat(1, 1) +) + +// IsWhitelisted checks if a given fee currency is whitelisted +func IsWhitelisted(exchangeRates common.ExchangeRates, feeCurrency *common.Address) bool { + if feeCurrency == nil { + return true + } + _, ok := exchangeRates[*feeCurrency] + return ok +} + +func TranslateValue(exchangeRates common.ExchangeRates, val *big.Int, fromFeeCurrency, toFeeCurrency *common.Address) (*big.Int, error) { + // TODO: implement me + return val, nil +} + +func CurrencyBaseFee(exchangeRates common.ExchangeRates, feeCurrency *common.Address) *big.Int { + // TODO: implement me + return nil +} + +func CurrencyBaseFeeAt(st *state.StateDB, feeCurrency *common.Address) *big.Int { + var exchangeRates common.ExchangeRates + return CurrencyBaseFee(exchangeRates, feeCurrency) +} + +// Compares values in different currencies +// nil currency => native currency +func CompareValue(exchangeRates common.ExchangeRates, val1 *big.Int, feeCurrency1 *common.Address, val2 *big.Int, feeCurrency2 *common.Address) (int, error) { + // Short circuit if the fee currency is the same. + if areEqualAddresses(feeCurrency1, feeCurrency2) { + return val1.Cmp(val2), nil + } + + var exchangeRate1, exchangeRate2 *big.Rat + var ok bool + if feeCurrency1 == nil { + exchangeRate1 = unitRate + } else { + exchangeRate1, ok = exchangeRates[*feeCurrency1] + if !ok { + return 0, fmt.Errorf("fee currency not registered: %s", feeCurrency1.Hex()) + } + } + + if feeCurrency2 == nil { + exchangeRate2 = unitRate + } else { + exchangeRate2, ok = exchangeRates[*feeCurrency2] + if !ok { + return 0, fmt.Errorf("fee currency not registered: %s", feeCurrency1.Hex()) + } + } + + // Below code block is basically evaluating this comparison: + // val1 * exchangeRate1.denominator / exchangeRate1.numerator < val2 * exchangeRate2.denominator / exchangeRate2.numerator + // It will transform that comparison to this, to remove having to deal with fractional values. + // val1 * exchangeRate1.denominator * exchangeRate2.numerator < val2 * exchangeRate2.denominator * exchangeRate1.numerator + leftSide := new(big.Int).Mul( + val1, + new(big.Int).Mul( + exchangeRate1.Denom(), + exchangeRate2.Num(), + ), + ) + rightSide := new(big.Int).Mul( + val2, + new(big.Int).Mul( + exchangeRate2.Denom(), + exchangeRate1.Num(), + ), + ) + + return leftSide.Cmp(rightSide), nil +} + +func CompareValueAt(st *state.StateDB, val1 *big.Int, curr1 *common.Address, val2 *big.Int, curr2 *common.Address) int { + // TODO: Get exchangeRates from statedb + var exchangeRates common.ExchangeRates + ret, err := CompareValue(exchangeRates, val1, curr1, val2, curr2) + // Err should not be possible if the pruning of non whitelisted currencies + // was made properly (and exchange rates are available) + if err != nil { + // TODO: LOG + // Compare with no currencies (Panic could be an option too) + r2, _ := CompareValue(exchangeRates, val1, nil, val2, nil) + return r2 + } + return ret +} + +func areEqualAddresses(addr1, addr2 *common.Address) bool { + return (addr1 == nil && addr2 == nil) || (addr1 != nil && addr2 != nil && *addr1 == *addr2) +} + +func GetBalanceOf(backend *bind.ContractCaller, account common.Address, feeCurrency common.Address) (*big.Int, error) { + token, err := abigen.NewFeeCurrencyCaller(feeCurrency, *backend) + if err != nil { + return nil, errors.New("failed to access fee currency token") + } + + balance, err := token.BalanceOf(&bind.CallOpts{}, account) + if err != nil { + return nil, errors.New("failed to access token balance") + } + + return balance, nil +} diff --git a/core/txpool/legacypool/celo_legacypool.go b/core/txpool/legacypool/celo_legacypool.go new file mode 100644 index 0000000000..fde9bb15a9 --- /dev/null +++ b/core/txpool/legacypool/celo_legacypool.go @@ -0,0 +1,24 @@ +package legacypool + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// filter Filters transactions from the given list, according to remaining balance (per currency, minus l1Cost) +// and gasLimit. Returns drops and invalid txs. +func (pool *LegacyPool) filter(list *celo_list, addr common.Address, l1Cost *big.Int, gasLimit uint64) (types.Transactions, types.Transactions) { + st := pool.currentState + fcv := pool.feeCurrencyValidator + // CELO: drop all transactions that no longer have a whitelisted currency + dropsWhitelist := list.FilterWhitelisted(st, pool.all, fcv) + + drops, invalids := list.FilterBalance(st, addr, l1Cost, gasLimit, + fcv) + totalDrops := make(types.Transactions, 0, len(dropsWhitelist)+len(drops)) + totalDrops = append(totalDrops, dropsWhitelist...) + totalDrops = append(totalDrops, drops...) + return totalDrops, invalids +} diff --git a/core/txpool/legacypool/celo_list.go b/core/txpool/legacypool/celo_list.go new file mode 100644 index 0000000000..edf339ad99 --- /dev/null +++ b/core/txpool/legacypool/celo_list.go @@ -0,0 +1,283 @@ +package legacypool + +import ( + "math" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/core/types" +) + +type TxComparator interface { + GasFeeCapCmp(*types.Transaction, *types.Transaction) int + GasTipCapCmp(*types.Transaction, *types.Transaction) int + EffectiveGasTipCmp(*types.Transaction, *types.Transaction, *big.Int) int + UpdateState(st *state.StateDB) +} + +type txcomp struct { + // Comparisons take place in the context of a state + st *state.StateDB +} + +func (t *txcomp) UpdateState(st *state.StateDB) { + t.st = st +} + +func (t *txcomp) GasFeeCapCmp(tx *types.Transaction, other *types.Transaction) int { + // Excessive copying of inner values during reheap. TODO: modify to user the inner value directly. + return CompareValueAt(t.st, tx.GasFeeCap(), tx.FeeCurrency(), other.GasFeeCap(), other.FeeCurrency()) +} +func (t *txcomp) GasTipCapCmp(tx *types.Transaction, other *types.Transaction) int { + return CompareValueAt(t.st, tx.GasTipCap(), tx.FeeCurrency(), other.GasTipCap(), other.FeeCurrency()) +} +func (t *txcomp) EffectiveGasTipCmp(tx *types.Transaction, other *types.Transaction, baseFee *big.Int) int { + if baseFee == nil { + return t.GasTipCapCmp(tx, other) + } + baseFee1 := CurrencyBaseFeeAt(t.st, tx.FeeCurrency()) + baseFee2 := CurrencyBaseFeeAt(t.st, other.FeeCurrency()) + effectiveTip1 := tx.EffectiveGasTipValue(baseFee1) + effectiveTip2 := other.EffectiveGasTipValue(baseFee2) + return CompareValueAt(t.st, effectiveTip1, tx.FeeCurrency(), effectiveTip2, other.FeeCurrency()) +} + +type celo_list struct { + list *list + totalCost map[common.Address]*big.Int + + // Pointer reference to inner list + txs *sortedMap +} + +func newCeloList(strict bool) *celo_list { + inner_list := newList(strict) + return &celo_list{ + list: inner_list, + totalCost: make(map[common.Address]*big.Int), + + txs: inner_list.txs, + } +} + +func (c *celo_list) TotalCostFor(feeCurrency *common.Address) *big.Int { + if feeCurrency == nil { + return c.list.totalcost + } + if tc, ok := c.totalCost[*feeCurrency]; ok { + return tc + } + return new(big.Int) +} + +// TotalCost Returns the total cost for transactions with the same fee currency. +func (c *celo_list) TotalCost(tx *types.Transaction) *big.Int { + if !txpool.IsFeeCurrencyTx(tx) { + return c.TotalCostFor(nil) + } + return c.TotalCostFor(tx.FeeCurrency()) +} + +func (c *celo_list) addTotalCost(tx *types.Transaction) { + if txpool.IsFeeCurrencyTx(tx) { + feeCurrency := tx.FeeCurrency() + if _, ok := c.totalCost[*feeCurrency]; !ok { + c.totalCost[*feeCurrency] = big.NewInt(0) + } + c.totalCost[*feeCurrency].Add(c.totalCost[*feeCurrency], tx.Cost()) + } else { + c.list.totalcost.Add(c.list.totalcost, tx.Cost()) + } +} + +func (c *celo_list) subTotalCost(txs types.Transactions) { + for _, tx := range txs { + if txpool.IsFeeCurrencyTx(tx) { + feeCurrency := tx.FeeCurrency() + c.totalCost[*feeCurrency].Sub(c.totalCost[*feeCurrency], tx.Cost()) + } else { + c.list.totalcost.Sub(c.list.totalcost, tx.Cost()) + } + } +} + +func (c *celo_list) FilterWhitelisted(st *state.StateDB, all *lookup, fcv txpool.FeeCurrencyValidator) types.Transactions { + removed := c.list.txs.Filter(func(tx *types.Transaction) bool { + return txpool.IsFeeCurrencyTx(tx) && fcv.IsWhitelisted(st, tx.FeeCurrency()) + }) + c.subTotalCost(removed) + return removed +} + +func balanceMinusL1Cost(st *state.StateDB, l1Cost *big.Int, + feeCurrency *common.Address, addr common.Address, + fcv txpool.FeeCurrencyValidator) *big.Int { + balance := fcv.Balance(st, addr, feeCurrency) + currencyL1Cost := fcv.ToCurrencyValue(st, l1Cost, feeCurrency) + return new(big.Int).Sub(balance, currencyL1Cost) +} + +// FilterBalance executes the same filter as legacypool.list.Filter(costcap, gascap). Since +// celo_list has txs of multiple currencies, the costcap is not sent, but calculated +// for every balance of different fees. +func (c *celo_list) FilterBalance(st *state.StateDB, addr common.Address, l1Cost *big.Int, + gasLimit uint64, + fcv txpool.FeeCurrencyValidator) (types.Transactions, types.Transactions) { + + // costcap && gascap are not used in celo_list. + + balanceNative := balanceMinusL1Cost(st, l1Cost, nil, addr, fcv) + balances := make(map[common.Address]*big.Int) + + // Filter out all the transactions above the account's funds + removed := c.list.txs.Filter(func(tx *types.Transaction) bool { + var feeCurrency *common.Address = nil + var costLimit *big.Int = nil + if txpool.IsFeeCurrencyTx(tx) { + feeCurrency = tx.FeeCurrency() + if _, ok := balances[*feeCurrency]; !ok { + balances[*feeCurrency] = balanceMinusL1Cost(st, l1Cost, feeCurrency, addr, fcv) + } + costLimit = balances[*feeCurrency] + } else { + costLimit = balanceNative + } + return tx.Gas() > gasLimit || tx.Cost().Cmp(costLimit) > 0 + }) + if len(removed) == 0 { + return nil, nil + } + var invalids types.Transactions + // If the list was strict, filter anything above the lowest nonce + if c.list.strict { + lowest := uint64(math.MaxUint64) + for _, tx := range removed { + if nonce := tx.Nonce(); lowest > nonce { + lowest = nonce + } + } + invalids = c.list.txs.filter(func(tx *types.Transaction) bool { return tx.Nonce() > lowest }) + } + // Reset total cost + c.subTotalCost(removed) + c.subTotalCost(invalids) + c.list.txs.reheap() + return removed, invalids +} + +// Add tries to insert a new transaction into the list, returning whether the +// transaction was accepted, and if yes, any previous transaction it replaced. +// +// If the new transaction is accepted into the list, the lists' cost and gas +// thresholds are also potentially updated. +func (c *celo_list) Add(tx *types.Transaction, priceBump uint64, l1CostFn txpool.L1CostFunc) (bool, *types.Transaction) { + oldNativeTotalCost := big.NewInt(0).Set(c.list.totalcost) + added, oldTx := c.list.Add(tx, priceBump, l1CostFn) + if !added { + return false, nil + } + if !txpool.IsFeeCurrencyTx(tx) && oldTx != nil && !txpool.IsFeeCurrencyTx(oldTx) { + // both the tx and the replacement are native currency, nothing to do + return true, oldTx + } + // undo change in native totalcost + c.list.totalcost.Set(oldNativeTotalCost) + // Recalculate + c.addTotalCost(tx) + // TODO: Add rollup cost, translated to the feecurrency of the tx + // TODO(hbandura): op-geth impl doesn't remove the l1cost from the removed tx + // is this intentional? + // Remove replaced tx cost + if oldTx != nil { + c.subTotalCost(types.Transactions{oldTx}) + } + return added, oldTx + +} + +// Forward removes all transactions from the list with a nonce lower than the +// provided threshold. Every removed transaction is returned for any post-removal +// maintenance. +func (c *celo_list) Forward(threshold uint64) types.Transactions { + txs := c.list.txs.Forward(threshold) + // Goes through celo_list subtotalcost to remove currency specific balances. + c.subTotalCost(txs) + return txs +} + +// Cap places a hard limit on the number of items, returning all transactions +// exceeding that limit. +func (c *celo_list) Cap(threshold int) types.Transactions { + txs := c.list.txs.Cap(threshold) + // Goes through celo_list subtotalcost to remove currency specific balances. + c.subTotalCost(txs) + return txs +} + +// Remove deletes a transaction from the maintained list, returning whether the +// transaction was found, and also returning any transaction invalidated due to +// the deletion (strict mode only). +func (c *celo_list) Remove(tx *types.Transaction) (bool, types.Transactions) { + // Remove the transaction from the set + nonce := tx.Nonce() + if removed := c.txs.Remove(nonce); !removed { + return false, nil + } + // Goes through celo_list subtotalcost to remove currency specific balances. + c.subTotalCost([]*types.Transaction{tx}) + // In strict mode, filter out non-executable transactions + if c.list.strict { + txs := c.txs.Filter(func(tx *types.Transaction) bool { return tx.Nonce() > nonce }) + // Goes through celo_list subtotalcost to remove currency specific balances. + c.subTotalCost(txs) + return true, txs + } + return true, nil +} + +// Ready retrieves a sequentially increasing list of transactions starting at the +// provided nonce that is ready for processing. The returned transactions will be +// removed from the list. +// +// Note, all transactions with nonces lower than start will also be returned to +// prevent getting into and invalid state. This is not something that should ever +// happen but better to be self correcting than failing! +func (c *celo_list) Ready(start uint64) types.Transactions { + txs := c.txs.Ready(start) + // Goes through celo_list subtotalcost to remove currency specific balances. + c.subTotalCost(txs) + return txs +} + +// Contains returns whether the list contains a transaction +// with the provided nonce. +func (c *celo_list) Contains(nonce uint64) bool { + return c.list.Contains(nonce) +} + +// *** Forwarded Methods *** + +// Len returns the length of the transaction list. +func (c *celo_list) Len() int { + return c.list.Len() +} + +// Empty returns whether the list of transactions is empty or not. +func (c *celo_list) Empty() bool { + return c.list.Empty() +} + +// Flatten creates a nonce-sorted slice of transactions based on the loosely +// sorted internal representation. The result of the sorting is cached in case +// it's requested again before any modifications are made to the contents. +func (c *celo_list) Flatten() types.Transactions { + return c.list.Flatten() +} + +// LastElement returns the last element of a flattened list, thus, the +// transaction with the highest nonce +func (c *celo_list) LastElement() *types.Transaction { + return c.list.LastElement() +} diff --git a/core/txpool/legacypool/celo_list_test.go b/core/txpool/legacypool/celo_list_test.go new file mode 100644 index 0000000000..5ce8741c45 --- /dev/null +++ b/core/txpool/legacypool/celo_list_test.go @@ -0,0 +1,185 @@ +package legacypool + +import ( + "container/heap" + "math/big" + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" +) + +// Tests that transactions can be added to strict lists and list contents and +// nonce boundaries are correctly maintained. +func TestStrictCeloListAdd(t *testing.T) { + // Generate a list of transactions to insert + key, _ := crypto.GenerateKey() + + txs := make(types.Transactions, 1024) + for i := 0; i < len(txs); i++ { + txs[i] = transaction(uint64(i), 0, key) + } + // Insert the transactions in a random order + list := newCeloList(true) + for _, v := range rand.Perm(len(txs)) { + list.Add(txs[v], DefaultConfig.PriceBump, nil) + } + // Verify internal state + if len(list.list.txs.items) != len(txs) { + t.Errorf("transaction count mismatch: have %d, want %d", len(list.txs.items), len(txs)) + } + for i, tx := range txs { + if list.list.txs.items[tx.Nonce()] != tx { + t.Errorf("item %d: transaction mismatch: have %v, want %v", i, list.txs.items[tx.Nonce()], tx) + } + } +} + +func BenchmarkCeloListAdd(b *testing.B) { + // Generate a list of transactions to insert + key, _ := crypto.GenerateKey() + + txs := make(types.Transactions, 100000) + for i := 0; i < len(txs); i++ { + txs[i] = transaction(uint64(i), 0, key) + } + // Insert the transactions in a random order + priceLimit := big.NewInt(int64(DefaultConfig.PriceLimit)) + b.ResetTimer() + for i := 0; i < b.N; i++ { + list := newCeloList(true) + for _, v := range rand.Perm(len(txs)) { + list.Add(txs[v], DefaultConfig.PriceBump, nil) + // Filter is invalid from celo_list since it does not work with costcap + list.list.Filter(priceLimit, DefaultConfig.PriceBump) // TODO: change to actual celo_list benchmark + } + } +} + +// Priceheap tests + +func legacytx(price int) *types.Transaction { + return types.NewTx(&types.LegacyTx{GasPrice: big.NewInt(int64(price))}) +} + +func celotx(fee int, tip int) *types.Transaction { + return celotxcurrency(fee, tip, nil) +} + +func celotxcurrency(fee int, tip int, currency *common.Address) *types.Transaction { + return types.NewTx(&types.CeloDynamicFeeTx{ + GasFeeCap: big.NewInt(int64(fee)), + GasTipCap: big.NewInt(int64(tip)), + FeeCurrency: currency, + }) +} + +type testNominalTxComparator struct{} + +func (t *testNominalTxComparator) GasFeeCapCmp(a *types.Transaction, b *types.Transaction) int { + return a.GasFeeCapCmp(b) +} + +func (t *testNominalTxComparator) GasTipCapCmp(a *types.Transaction, b *types.Transaction) int { + return a.GasTipCapCmp(b) +} + +func (t *testNominalTxComparator) EffectiveGasTipCmp(a *types.Transaction, b *types.Transaction, baseFee *big.Int) int { + return a.EffectiveGasTipCmp(b, baseFee) +} + +type testMultiplierTxComparator struct { + mults map[common.Address]int +} + +func testCmpMults(vA *big.Int, mA int, vB *big.Int, mB int) int { + if mA == 0 { + mA = 1 + } + if mB == 0 { + mB = 1 + } + rA := int(vA.Uint64()) * mA + rB := int(vB.Uint64()) * mB + if (rA - rB) < 0 { + return -1 + } + if (rA - rB) > 0 { + return 1 + } + return 0 +} + +func (t *testMultiplierTxComparator) GasFeeCapCmp(a *types.Transaction, b *types.Transaction) int { + return testCmpMults(a.GasFeeCap(), t.mults[*a.FeeCurrency()], b.GasFeeCap(), t.mults[*b.FeeCurrency()]) +} + +func (t *testMultiplierTxComparator) GasTipCapCmp(a *types.Transaction, b *types.Transaction) int { + return testCmpMults(a.GasTipCap(), t.mults[*a.FeeCurrency()], b.GasTipCap(), t.mults[*b.FeeCurrency()]) +} + +func (t *testMultiplierTxComparator) EffectiveGasTipCmp(a *types.Transaction, b *types.Transaction, baseFee *big.Int) int { + return testCmpMults(a.EffectiveGasTipValue(baseFee), t.mults[*a.FeeCurrency()], b.EffectiveGasTipValue(baseFee), t.mults[*b.FeeCurrency()]) +} + +func newTestPriceHeap() *priceHeap { + return &priceHeap{ + txComparator: &testNominalTxComparator{}, + } +} + +func TestLegacyPushes(t *testing.T) { + m := newTestPriceHeap() + heap.Push(m, legacytx(100)) + heap.Push(m, legacytx(50)) + heap.Push(m, legacytx(200)) + heap.Push(m, legacytx(75)) + assert.Equal(t, 4, m.Len()) + v := heap.Pop(m) + tm, _ := v.(*types.Transaction) + assert.Equal(t, big.NewInt(50), tm.GasPrice()) + assert.Equal(t, 3, m.Len()) +} + +func TestCeloPushes(t *testing.T) { + m := newTestPriceHeap() + heap.Push(m, celotx(100, 0)) + heap.Push(m, celotx(50, 3)) + heap.Push(m, celotx(200, 0)) + heap.Push(m, celotx(75, 0)) + assert.Equal(t, 4, m.Len()) + v := heap.Pop(m) + tm, _ := v.(*types.Transaction) + assert.Equal(t, big.NewInt(50), tm.GasFeeCap()) + assert.Equal(t, big.NewInt(3), tm.GasTipCap()) + assert.Equal(t, 3, m.Len()) +} + +func TestCurrencyAdds(t *testing.T) { + c1 := common.BigToAddress(big.NewInt(2)) + c2 := common.BigToAddress(big.NewInt(3)) + tmc := &testMultiplierTxComparator{ + mults: map[common.Address]int{ + c1: 2, + c2: 3, + }} + m := newTestPriceHeap() + m.txComparator = tmc + heap.Push(m, celotxcurrency(100, 0, &c1)) // 200 + heap.Push(m, celotxcurrency(250, 0, &c2)) // 750 + heap.Push(m, celotxcurrency(50, 0, &c1)) // 100 + heap.Push(m, celotxcurrency(75, 0, &c2)) // 225 + heap.Push(m, celotxcurrency(200, 0, &c1)) // 400 + + assert.Equal(t, 5, m.Len()) + + tm := heap.Pop(m).(*types.Transaction) + assert.Equal(t, big.NewInt(50), tm.GasPrice()) + assert.Equal(t, 4, m.Len()) + + tm2 := heap.Pop(m).(*types.Transaction) + assert.Equal(t, big.NewInt(100), tm2.GasPrice()) +} diff --git a/core/txpool/legacypool/celo_test.go b/core/txpool/legacypool/celo_test.go new file mode 100644 index 0000000000..49fa943f70 --- /dev/null +++ b/core/txpool/legacypool/celo_test.go @@ -0,0 +1,175 @@ +package legacypool + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +var ( + currA = common.HexToAddress("0xA") + currB = common.HexToAddress("0xB") + currX = common.HexToAddress("0xF") + exchangeRates = common.ExchangeRates{ + currA: big.NewRat(47, 100), + currB: big.NewRat(45, 100), + } +) + +func TestCompareFees(t *testing.T) { + type args struct { + val1 *big.Int + feeCurrency1 *common.Address + val2 *big.Int + feeCurrency2 *common.Address + } + tests := []struct { + name string + args args + wantResult int + wantErr bool + }{ + // Native currency + { + name: "Same amount of native currency", + args: args{ + val1: big.NewInt(1), + feeCurrency1: nil, + val2: big.NewInt(1), + feeCurrency2: nil, + }, + wantResult: 0, + }, { + name: "Different amounts of native currency 1", + args: args{ + val1: big.NewInt(2), + feeCurrency1: nil, + val2: big.NewInt(1), + feeCurrency2: nil, + }, + wantResult: 1, + }, { + name: "Different amounts of native currency 2", + args: args{ + val1: big.NewInt(1), + feeCurrency1: nil, + val2: big.NewInt(5), + feeCurrency2: nil, + }, + wantResult: -1, + }, + // Mixed currency + { + name: "Same amount of mixed currency", + args: args{ + val1: big.NewInt(1), + feeCurrency1: nil, + val2: big.NewInt(1), + feeCurrency2: &currA, + }, + wantResult: -1, + }, { + name: "Different amounts of mixed currency 1", + args: args{ + val1: big.NewInt(100), + feeCurrency1: nil, + val2: big.NewInt(47), + feeCurrency2: &currA, + }, + wantResult: 0, + }, { + name: "Different amounts of mixed currency 2", + args: args{ + val1: big.NewInt(100), + feeCurrency1: nil, + val2: big.NewInt(45), + feeCurrency2: &currB, + }, + wantResult: 0, + }, + // Two fee currencies + { + name: "Same amount of same currency", + args: args{ + val1: big.NewInt(1), + feeCurrency1: &currA, + val2: big.NewInt(1), + feeCurrency2: &currA, + }, + wantResult: 0, + }, { + name: "Different amounts of same currency 1", + args: args{ + val1: big.NewInt(3), + feeCurrency1: &currA, + val2: big.NewInt(1), + feeCurrency2: &currA, + }, + wantResult: 1, + }, { + name: "Different amounts of same currency 2", + args: args{ + val1: big.NewInt(1), + feeCurrency1: &currA, + val2: big.NewInt(7), + feeCurrency2: &currA, + }, + wantResult: -1, + }, + // Unregistered fee currency + { + name: "Different amounts of different currencies", + args: args{ + val1: big.NewInt(1), + feeCurrency1: &currA, + val2: big.NewInt(1), + feeCurrency2: &currX, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := CompareValue(exchangeRates, tt.args.val1, tt.args.feeCurrency1, tt.args.val2, tt.args.feeCurrency2) + + if tt.wantErr && err == nil { + t.Error("Expected error in celoContextImpl.CompareFees") + } + if got != tt.wantResult { + t.Errorf("celoContextImpl.CompareFees() = %v, want %v", got, tt.wantResult) + } + }) + } +} + +func TestIsWhitelisted(t *testing.T) { + tests := []struct { + name string + feeCurrency *common.Address + want bool + }{ + { + name: "no fee currency", + feeCurrency: nil, + want: true, + }, + { + name: "valid fee currency", + feeCurrency: &currA, + want: true, + }, + { + name: "invalid fee currency", + feeCurrency: &currX, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsWhitelisted(exchangeRates, tt.feeCurrency); got != tt.want { + t.Errorf("IsWhitelisted() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 709283abc2..a9348de857 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -223,12 +223,12 @@ type LegacyPool struct { locals *accountSet // Set of local transaction to exempt from eviction rules journal *journal // Journal of local transaction to back up to disk - reserve txpool.AddressReserver // Address reserver to ensure exclusivity across subpools - pending map[common.Address]*list // All currently processable transactions - queue map[common.Address]*list // Queued but non-processable transactions - beats map[common.Address]time.Time // Last heartbeat from each known account - all *lookup // All transactions to allow lookups - priced *pricedList // All transactions sorted by price + reserve txpool.AddressReserver // Address reserver to ensure exclusivity across subpools + pending map[common.Address]*celo_list // All currently processable transactions + queue map[common.Address]*celo_list // Queued but non-processable transactions + beats map[common.Address]time.Time // Last heartbeat from each known account + all *lookup // All transactions to allow lookups + priced *pricedList // All transactions sorted by price reqResetCh chan *txpoolResetRequest reqPromoteCh chan *accountSet @@ -244,6 +244,7 @@ type LegacyPool struct { // Celo feeCurrencyValidator txpool.FeeCurrencyValidator + txComparator TxComparator } type txpoolResetRequest struct { @@ -262,8 +263,8 @@ func New(config Config, chain BlockChain) *LegacyPool { chain: chain, chainconfig: chain.Config(), signer: types.LatestSigner(chain.Config()), - pending: make(map[common.Address]*list), - queue: make(map[common.Address]*list), + pending: make(map[common.Address]*celo_list), + queue: make(map[common.Address]*celo_list), beats: make(map[common.Address]time.Time), all: newLookup(), reqResetCh: make(chan *txpoolResetRequest), @@ -281,7 +282,7 @@ func New(config Config, chain BlockChain) *LegacyPool { log.Info("Setting new local account", "address", addr) pool.locals.add(addr) } - pool.priced = newPricedList(pool.all) + pool.priced = newPricedList(pool.all, pool.txComparator) if (!config.NoLocals || config.JournalRemote) && config.Journal != "" { pool.journal = newTxJournal(config.Journal) @@ -647,7 +648,7 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction, local bool) error { }, ExistingExpenditure: func(addr common.Address) *big.Int { if list := pool.pending[addr]; list != nil { - return list.totalcost + return list.TotalCost(tx) } return new(big.Int) }, @@ -867,7 +868,7 @@ func (pool *LegacyPool) enqueueTx(hash common.Hash, tx *types.Transaction, local // Try to insert the transaction into the future queue from, _ := types.Sender(pool.signer, tx) // already validated if pool.queue[from] == nil { - pool.queue[from] = newList(false) + pool.queue[from] = newCeloList(false) } inserted, old := pool.queue[from].Add(tx, pool.config.PriceBump, pool.l1CostFn) if !inserted { @@ -919,7 +920,7 @@ func (pool *LegacyPool) journalTx(from common.Address, tx *types.Transaction) { func (pool *LegacyPool) promoteTx(addr common.Address, hash common.Hash, tx *types.Transaction) bool { // Try to insert the transaction into the pending queue if pool.pending[addr] == nil { - pool.pending[addr] = newList(true) + pool.pending[addr] = newCeloList(true) } list := pool.pending[addr] @@ -1446,6 +1447,7 @@ func (pool *LegacyPool) reset(oldHead, newHead *types.Header) { } pool.currentHead.Store(newHead) pool.currentState = statedb + pool.txComparator.UpdateState(pool.currentState) pool.pendingNonces = newNoncer(statedb) costFn := types.NewL1CostFunc(pool.chainconfig, statedb) @@ -1480,16 +1482,16 @@ func (pool *LegacyPool) promoteExecutables(accounts []common.Address) []*types.T pool.all.Remove(hash) } log.Trace("Removed old queued transactions", "count", len(forwards)) - balance := pool.currentState.GetBalance(addr) + // Drop all transactions that are too costly (low balance or out of gas) + + var l1Cost *big.Int if !list.Empty() && pool.l1CostFn != nil { // Reduce the cost-cap by L1 rollup cost of the first tx if necessary. Other txs will get filtered out afterwards. el := list.txs.FirstElement() - if l1Cost := pool.l1CostFn(el.RollupDataGas()); l1Cost != nil { - balance = new(big.Int).Sub(balance, l1Cost) // negative big int is fine - } + l1Cost = pool.l1CostFn(el.RollupDataGas()) } // Drop all transactions that are too costly (low balance or out of gas) - drops, _ := list.Filter(balance, gasLimit) + drops, _ := pool.filter(list, addr, l1Cost, gasLimit) for _, tx := range drops { hash := tx.Hash() pool.all.Remove(hash) @@ -1689,16 +1691,14 @@ func (pool *LegacyPool) demoteUnexecutables() { pool.all.Remove(hash) log.Trace("Removed old pending transaction", "hash", hash) } - balance := pool.currentState.GetBalance(addr) + var l1Cost *big.Int if !list.Empty() && pool.l1CostFn != nil { // Reduce the cost-cap by L1 rollup cost of the first tx if necessary. Other txs will get filtered out afterwards. el := list.txs.FirstElement() - if l1Cost := pool.l1CostFn(el.RollupDataGas()); l1Cost != nil { - balance = new(big.Int).Sub(balance, l1Cost) // negative big int is fine - } + l1Cost = pool.l1CostFn(el.RollupDataGas()) } // Drop all transactions that are too costly (low balance or out of gas), and queue any invalids back for later - drops, invalids := list.Filter(balance, gasLimit) + drops, invalids := pool.filter(list, addr, l1Cost, gasLimit) for _, tx := range drops { hash := tx.Hash() log.Trace("Removed unpayable pending transaction", "hash", hash) diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go index 43dfeee92c..9517419ae7 100644 --- a/core/txpool/legacypool/legacypool_test.go +++ b/core/txpool/legacypool/legacypool_test.go @@ -198,8 +198,8 @@ func validatePoolInternals(pool *LegacyPool) error { if nonce := pool.pendingNonces.get(addr); nonce != last+1 { return fmt.Errorf("pending nonce mismatch: have %v, want %v", nonce, last+1) } - if txs.totalcost.Cmp(common.Big0) < 0 { - return fmt.Errorf("totalcost went negative: %v", txs.totalcost) + if txs.TotalCostFor(nil).Cmp(common.Big0) < 0 { + return fmt.Errorf("totalcost went negative: %v", txs.TotalCostFor(nil)) } } return nil diff --git a/core/txpool/legacypool/list.go b/core/txpool/legacypool/list.go index fe100834e0..5f4946ec0f 100644 --- a/core/txpool/legacypool/list.go +++ b/core/txpool/legacypool/list.go @@ -482,6 +482,9 @@ func (l *list) subTotalCost(txs []*types.Transaction) { type priceHeap struct { baseFee *big.Int // heap should always be re-sorted after baseFee is changed list []*types.Transaction + + // Celo currency comparator + txComparator TxComparator } func (h *priceHeap) Len() int { return len(h.list) } @@ -501,16 +504,17 @@ func (h *priceHeap) Less(i, j int) bool { func (h *priceHeap) cmp(a, b *types.Transaction) int { if h.baseFee != nil { // Compare effective tips if baseFee is specified - if c := a.EffectiveGasTipCmp(b, h.baseFee); c != 0 { + if c := h.txComparator.EffectiveGasTipCmp(a, b, h.baseFee); c != 0 { return c } } + // Celo modification, using a txComparator (which uses currency exchange rates) // Compare fee caps if baseFee is not specified or effective tips are equal - if c := a.GasFeeCapCmp(b); c != 0 { + if c := h.txComparator.GasFeeCapCmp(a, b); c != 0 { return c } // Compare tips if effective tips and fee caps are equal - return a.GasTipCapCmp(b) + return h.txComparator.GasTipCapCmp(a, b) } func (h *priceHeap) Push(x interface{}) { @@ -554,10 +558,13 @@ const ( ) // newPricedList creates a new price-sorted transaction heap. -func newPricedList(all *lookup) *pricedList { - return &pricedList{ +func newPricedList(all *lookup, txComparator TxComparator) *pricedList { + ret := &pricedList{ all: all, } + ret.urgent.txComparator = txComparator + ret.floating.txComparator = txComparator + return ret } // Put inserts a new transaction into the heap. diff --git a/core/vm/evm.go b/core/vm/evm.go index cb65356349..e1844b5f3e 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -93,7 +93,7 @@ type BlockContext struct { ExcessBlobGas *uint64 // ExcessBlobGas field in the header, needed to compute the data // Celo specific information - ExchangeRates map[common.Address]*big.Rat + ExchangeRates common.ExchangeRates } // TxContext provides the EVM with information about a transaction.