diff --git a/chain/params.go b/chain/params.go index 1c2a7729ce..29c4f3a53a 100644 --- a/chain/params.go +++ b/chain/params.go @@ -90,6 +90,7 @@ const ( EIP155 = "EIP155" QuorumCalcAlignment = "quorumcalcalignment" TxHashWithType = "txHashWithType" + LondonFix = "londonfix" ) // Forks is map which contains all forks and their starting blocks from genesis @@ -125,6 +126,7 @@ func (f *Forks) At(block uint64) ForksInTime { EIP155: f.IsActive(EIP155, block), QuorumCalcAlignment: f.IsActive(QuorumCalcAlignment, block), TxHashWithType: f.IsActive(TxHashWithType, block), + LondonFix: f.IsActive(LondonFix, block), } } @@ -153,7 +155,8 @@ type ForksInTime struct { EIP158, EIP155, QuorumCalcAlignment, - TxHashWithType bool + TxHashWithType, + LondonFix bool } // AllForksEnabled should contain all supported forks by current edge version @@ -169,4 +172,5 @@ var AllForksEnabled = &Forks{ London: NewFork(0), QuorumCalcAlignment: NewFork(0), TxHashWithType: NewFork(0), + LondonFix: NewFork(0), } diff --git a/e2e-polybft/e2e/consensus_test.go b/e2e-polybft/e2e/consensus_test.go index 98863cbc23..cb7a409e0c 100644 --- a/e2e-polybft/e2e/consensus_test.go +++ b/e2e-polybft/e2e/consensus_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/umbracle/ethgo" "github.com/umbracle/ethgo/abi" @@ -618,3 +619,122 @@ func TestE2E_Consensus_CustomRewardToken(t *testing.T) { require.NoError(t, err) require.True(t, validatorInfo.WithdrawableRewards.Cmp(big.NewInt(0)) > 0) } + +// TestE2E_Consensus_EIP1559Check sends a legacy and a dynamic tx to the cluster +// and check if balance of sender, receiver, burn contract and miner is updates correctly +// in accordance with EIP-1559 specifications +func TestE2E_Consensus_EIP1559Check(t *testing.T) { + sender1, err := wallet.GenerateKey() + require.NoError(t, err) + + sender2, err := wallet.GenerateKey() + require.NoError(t, err) + + recipientKey, err := wallet.GenerateKey() + require.NoError(t, err) + + recipient := recipientKey.Address() + + // first account should have some matics premined + cluster := framework.NewTestCluster(t, 5, + framework.WithNativeTokenConfig(fmt.Sprintf(nativeTokenMintableTestCfg, sender1.Address())), + framework.WithPremine(types.Address(sender1.Address()), types.Address(sender2.Address())), + framework.WithBurnContract(&polybft.BurnContractInfo{BlockNumber: 0, Address: types.ZeroAddress}), + ) + defer cluster.Stop() + + cluster.WaitForReady(t) + + client := cluster.Servers[0].JSONRPC().Eth() + + waitUntilBalancesChanged := func(acct ethgo.Address, bal *big.Int) error { + err := cluster.WaitUntil(30*time.Second, 2*time.Second, func() bool { + balance, err := client.GetBalance(recipient, ethgo.Latest) + if err != nil { + return true + } + + return balance.Cmp(bal) > 0 + }) + + return err + } + + relayer, err := txrelayer.NewTxRelayer(txrelayer.WithIPAddress(cluster.Servers[0].JSONRPCAddr())) + require.NoError(t, err) + + // create and send tx + sendAmount := ethgo.Gwei(1) + + txn := []*ethgo.Transaction{ + { + Value: sendAmount, + To: &recipient, + Gas: 21000, + Nonce: uint64(0), + GasPrice: ethgo.Gwei(1).Uint64(), + }, + { + Value: sendAmount, + To: &recipient, + Gas: 21000, + Nonce: uint64(0), + Type: ethgo.TransactionDynamicFee, + MaxFeePerGas: ethgo.Gwei(1), + MaxPriorityFeePerGas: ethgo.Gwei(1), + }, + } + + initialMinerBalance := big.NewInt(0) + + var prevMiner ethgo.Address + + for i := 0; i < 2; i++ { + curTxn := txn[i] + + senderInitialBalance, _ := client.GetBalance(sender1.Address(), ethgo.Latest) + receiverInitialBalance, _ := client.GetBalance(recipient, ethgo.Latest) + burnContractInitialBalance, _ := client.GetBalance(ethgo.Address(types.ZeroAddress), ethgo.Latest) + + receipt, err := relayer.SendTransaction(curTxn, sender1) + require.NoError(t, err) + + // wait for balance to get changed + err = waitUntilBalancesChanged(recipient, receiverInitialBalance) + require.NoError(t, err) + + // Retrieve the transaction receipt + txReceipt, err := client.GetTransactionByHash(receipt.TransactionHash) + require.NoError(t, err) + + block, _ := client.GetBlockByHash(txReceipt.BlockHash, true) + finalMinerFinalBalance, _ := client.GetBalance(block.Miner, ethgo.Latest) + + if i == 0 { + prevMiner = block.Miner + } + + senderFinalBalance, _ := client.GetBalance(sender1.Address(), ethgo.Latest) + receiverFinalBalance, _ := client.GetBalance(recipient, ethgo.Latest) + burnContractFinalBalance, _ := client.GetBalance(ethgo.Address(types.ZeroAddress), ethgo.Latest) + + diffReciverBalance := new(big.Int).Sub(receiverFinalBalance, receiverInitialBalance) + assert.Equal(t, sendAmount, diffReciverBalance, "Receiver balance should be increased by send amount") + + if i == 1 && prevMiner != block.Miner { + initialMinerBalance = big.NewInt(0) + } + + diffBurnContractBalance := new(big.Int).Sub(burnContractFinalBalance, burnContractInitialBalance) + diffSenderBalance := new(big.Int).Sub(senderInitialBalance, senderFinalBalance) + diffMinerBalance := new(big.Int).Sub(finalMinerFinalBalance, initialMinerBalance) + + diffSenderBalance.Sub(diffSenderBalance, diffReciverBalance) + diffSenderBalance.Sub(diffSenderBalance, diffBurnContractBalance) + diffSenderBalance.Sub(diffSenderBalance, diffMinerBalance) + + assert.Zero(t, diffSenderBalance.Int64(), "Sender balance should be decreased by send amount + gas") + + initialMinerBalance = finalMinerFinalBalance + } +} diff --git a/e2e-polybft/e2e/migration_test.go b/e2e-polybft/e2e/migration_test.go index 4b8f29e462..caccb2210f 100644 --- a/e2e-polybft/e2e/migration_test.go +++ b/e2e-polybft/e2e/migration_test.go @@ -60,18 +60,20 @@ func TestE2E_Migration(t *testing.T) { //send transaction to user2 sendAmount := ethgo.Gwei(10000) receipt, err := relayer.SendTransaction(ðgo.Transaction{ - From: userAddr, - To: &userAddr2, - Gas: 1000000, - Value: sendAmount, + From: userAddr, + To: &userAddr2, + Gas: 1000000, + Value: sendAmount, + GasPrice: ethgo.Gwei(2).Uint64(), }, userKey) require.NoError(t, err) require.NotNil(t, receipt) receipt, err = relayer.SendTransaction(ðgo.Transaction{ - From: userAddr, - Gas: 1000000, - Input: contractsapi.TestWriteBlockMetadata.Bytecode, + From: userAddr, + Gas: 1000000, + GasPrice: ethgo.Gwei(2).Uint64(), + Input: contractsapi.TestWriteBlockMetadata.Bytecode, }, userKey) require.NoError(t, err) require.NotNil(t, receipt) diff --git a/e2e-polybft/e2e/txpool_test.go b/e2e-polybft/e2e/txpool_test.go index d8b5017cd4..fba7b5314b 100644 --- a/e2e-polybft/e2e/txpool_test.go +++ b/e2e-polybft/e2e/txpool_test.go @@ -68,11 +68,8 @@ func TestE2E_TxPool_Transfer(t *testing.T) { txn.MaxFeePerGas = big.NewInt(1000000000) txn.MaxPriorityFeePerGas = big.NewInt(100000000) } else { - gasPrice, err := client.GasPrice() - require.NoError(t, err) - txn.Type = ethgo.TransactionLegacy - txn.GasPrice = gasPrice + txn.GasPrice = ethgo.Gwei(2).Uint64() } sendTransaction(t, client, sender, txn) @@ -115,10 +112,6 @@ func TestE2E_TxPool_Transfer_Linear(t *testing.T) { client := cluster.Servers[0].JSONRPC().Eth() - // estimate gas price - gasPrice, err := client.GasPrice() - require.NoError(t, err) - waitUntilBalancesChanged := func(acct ethgo.Address) error { err := cluster.WaitUntil(30*time.Second, 2*time.Second, func() bool { balance, err := client.GetBalance(acct, ethgo.Latest) @@ -136,10 +129,10 @@ func TestE2E_TxPool_Transfer_Linear(t *testing.T) { if i%2 == 0 { txn.Type = ethgo.TransactionDynamicFee txn.MaxFeePerGas = big.NewInt(1000000000) - txn.MaxPriorityFeePerGas = big.NewInt(100000000) + txn.MaxPriorityFeePerGas = big.NewInt(1000000000) } else { txn.Type = ethgo.TransactionLegacy - txn.GasPrice = gasPrice + txn.GasPrice = ethgo.Gwei(1).Uint64() } } @@ -177,11 +170,8 @@ func TestE2E_TxPool_Transfer_Linear(t *testing.T) { populateTxFees(txn, i-1) // Add remaining fees to finish the cycle - for j := i; j < num; j++ { - copyTxn := txn.Copy() - populateTxFees(copyTxn, j) - txn.Value = txn.Value.Add(txn.Value, txCost(copyTxn)) - } + gasCostTotal := new(big.Int).Mul(txCost(txn), new(big.Int).SetInt64(int64(num-i-1))) + txn.Value = txn.Value.Add(txn.Value, gasCostTotal) sendTransaction(t, client, receivers[i-1], txn) @@ -274,11 +264,8 @@ func TestE2E_TxPool_BroadcastTransactions(t *testing.T) { txn.MaxFeePerGas = big.NewInt(1000000000) txn.MaxPriorityFeePerGas = big.NewInt(100000000) } else { - gasPrice, err := client.GasPrice() - require.NoError(t, err) - txn.Type = ethgo.TransactionLegacy - txn.GasPrice = gasPrice + txn.GasPrice = ethgo.Gwei(2).Uint64() } sendTransaction(t, client, sender, txn) diff --git a/server/server.go b/server/server.go index 7b4bb42a99..736a337b1a 100644 --- a/server/server.go +++ b/server/server.go @@ -1044,6 +1044,11 @@ func initForkManager(engineName string, config *chain.Chain) error { return err } + // Register Handler for London fork fix + if err := state.RegisterLondonFixFork(chain.LondonFix); err != nil { + return err + } + if factory := forkManagerFactory[ConsensusType(engineName)]; factory != nil { if err := factory(config.Params.Forks); err != nil { return err diff --git a/state/executor.go b/state/executor.go index 13d7be53c9..539ee64443 100644 --- a/state/executor.go +++ b/state/executor.go @@ -11,7 +11,6 @@ import ( "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/contracts" "github.com/0xPolygon/polygon-edge/crypto" - "github.com/0xPolygon/polygon-edge/helper/common" "github.com/0xPolygon/polygon-edge/state/runtime" "github.com/0xPolygon/polygon-edge/state/runtime/addresslist" "github.com/0xPolygon/polygon-edge/state/runtime/evm" @@ -454,18 +453,7 @@ func (t *Transition) ContextPtr() *runtime.TxContext { } func (t *Transition) subGasLimitPrice(msg *types.Transaction) error { - upfrontGasCost := new(big.Int).SetUint64(msg.Gas) - - factor := new(big.Int) - if msg.GasFeeCap != nil && msg.GasFeeCap.BitLen() > 0 { - // Apply EIP-1559 tx cost calculation factor - factor = factor.Set(msg.GasFeeCap) - } else { - // Apply legacy tx cost calculation factor - factor = factor.Set(msg.GasPrice) - } - - upfrontGasCost = upfrontGasCost.Mul(upfrontGasCost, factor) + upfrontGasCost := GetLondonFixHandler(uint64(t.ctx.Number)).getUpfrontGasCost(msg, t.ctx.BaseFee) if err := t.state.SubBalance(msg.From, upfrontGasCost); err != nil { if errors.Is(err, runtime.ErrNotEnoughFunds) { @@ -489,39 +477,9 @@ func (t *Transition) nonceCheck(msg *types.Transaction) error { } // checkDynamicFees checks correctness of the EIP-1559 feature-related fields. -// Basically, makes sure gas tip cap and gas fee cap are good. +// Basically, makes sure gas tip cap and gas fee cap are good for dynamic and legacy transactions func (t *Transition) checkDynamicFees(msg *types.Transaction) error { - if msg.Type != types.DynamicFeeTx { - return nil - } - - if msg.GasFeeCap.BitLen() == 0 && msg.GasTipCap.BitLen() == 0 { - return nil - } - - if l := msg.GasFeeCap.BitLen(); l > 256 { - return fmt.Errorf("%w: address %v, GasFeeCap bit length: %d", ErrFeeCapVeryHigh, - msg.From.String(), l) - } - - if l := msg.GasTipCap.BitLen(); l > 256 { - return fmt.Errorf("%w: address %v, GasTipCap bit length: %d", ErrTipVeryHigh, - msg.From.String(), l) - } - - if msg.GasFeeCap.Cmp(msg.GasTipCap) < 0 { - return fmt.Errorf("%w: address %v, GasTipCap: %s, GasFeeCap: %s", ErrTipAboveFeeCap, - msg.From.String(), msg.GasTipCap, msg.GasFeeCap) - } - - // This will panic if baseFee is nil, but basefee presence is verified - // as part of header validation. - if msg.GasFeeCap.Cmp(t.ctx.BaseFee) < 0 { - return fmt.Errorf("%w: address %v, GasFeeCap: %s, BaseFee: %s", ErrFeeCapTooLow, - msg.From.String(), msg.GasFeeCap, t.ctx.BaseFee) - } - - return nil + return GetLondonFixHandler(uint64(t.ctx.Number)).checkDynamicFees(msg, t) } // errors that can originate in the consensus rules checks of the apply method below @@ -642,13 +600,9 @@ func (t *Transition) apply(msg *types.Transaction) (*runtime.ExecutionResult, er // Define effective tip based on tx type. // We use EIP-1559 fields of the tx if the london hardfork is enabled. // Effective tip became to be either gas tip cap or (gas fee cap - current base fee) - effectiveTip := new(big.Int).Set(gasPrice) - if t.config.London && msg.Type == types.DynamicFeeTx { - effectiveTip = common.BigMin( - new(big.Int).Sub(msg.GasFeeCap, t.ctx.BaseFee), - new(big.Int).Set(msg.GasTipCap), - ) - } + effectiveTip := GetLondonFixHandler(uint64(t.ctx.Number)).getEffectiveTip( + msg, gasPrice, t.ctx.BaseFee, t.config.London, + ) // Pay the coinbase fee as a miner reward using the calculated effective tip. coinbaseFee := new(big.Int).Mul(new(big.Int).SetUint64(result.GasUsed), effectiveTip) diff --git a/state/executor_test.go b/state/executor_test.go index 79ae3c96e3..161e29363b 100644 --- a/state/executor_test.go +++ b/state/executor_test.go @@ -147,6 +147,9 @@ func Test_Transition_checkDynamicFees(t *testing.T) { ctx: runtime.TxContext{ BaseFee: tt.baseFee, }, + config: chain.ForksInTime{ + London: true, + }, } err := tr.checkDynamicFees(tt.tx) diff --git a/state/londonFix_fork.go b/state/londonFix_fork.go new file mode 100644 index 0000000000..9f296a75d0 --- /dev/null +++ b/state/londonFix_fork.go @@ -0,0 +1,164 @@ +package state + +import ( + "fmt" + "math/big" + + "github.com/0xPolygon/polygon-edge/forkmanager" + "github.com/0xPolygon/polygon-edge/helper/common" + "github.com/0xPolygon/polygon-edge/types" +) + +const LondonFixHandler forkmanager.HandlerDesc = "LondonFixHandler" + +type LondonFixFork interface { + checkDynamicFees(*types.Transaction, *Transition) error + getUpfrontGasCost(msg *types.Transaction, baseFee *big.Int) *big.Int + getEffectiveTip(msg *types.Transaction, gasPrice *big.Int, + baseFee *big.Int, isLondonForkEnabled bool) *big.Int +} + +type LondonFixForkV1 struct{} + +// checkDynamicFees checks correctness of the EIP-1559 feature-related fields. +// Basically, makes sure gas tip cap and gas fee cap are good for dynamic and legacy transactions +// and that GasFeeCap/GasPrice cap is not lower than base fee when London fork is active. +func (l *LondonFixForkV1) checkDynamicFees(msg *types.Transaction, t *Transition) error { + if msg.Type != types.DynamicFeeTx { + return nil + } + + if msg.GasFeeCap.BitLen() == 0 && msg.GasTipCap.BitLen() == 0 { + return nil + } + + if l := msg.GasFeeCap.BitLen(); l > 256 { + return fmt.Errorf("%w: address %v, GasFeeCap bit length: %d", ErrFeeCapVeryHigh, + msg.From.String(), l) + } + + if l := msg.GasTipCap.BitLen(); l > 256 { + return fmt.Errorf("%w: address %v, GasTipCap bit length: %d", ErrTipVeryHigh, + msg.From.String(), l) + } + + if msg.GasFeeCap.Cmp(msg.GasTipCap) < 0 { + return fmt.Errorf("%w: address %v, GasTipCap: %s, GasFeeCap: %s", ErrTipAboveFeeCap, + msg.From.String(), msg.GasTipCap, msg.GasFeeCap) + } + + // This will panic if baseFee is nil, but basefee presence is verified + // as part of header validation. + if msg.GasFeeCap.Cmp(t.ctx.BaseFee) < 0 { + return fmt.Errorf("%w: address %v, GasFeeCap: %s, BaseFee: %s", ErrFeeCapTooLow, + msg.From.String(), msg.GasFeeCap, t.ctx.BaseFee) + } + + return nil +} + +func (l *LondonFixForkV1) getUpfrontGasCost(msg *types.Transaction, baseFee *big.Int) *big.Int { + upfrontGasCost := new(big.Int).SetUint64(msg.Gas) + + factor := new(big.Int) + if msg.GasFeeCap != nil && msg.GasFeeCap.BitLen() > 0 { + // Apply EIP-1559 tx cost calculation factor + factor = factor.Set(msg.GasFeeCap) + } else { + // Apply legacy tx cost calculation factor + factor = factor.Set(msg.GasPrice) + } + + return upfrontGasCost.Mul(upfrontGasCost, factor) +} + +func (l *LondonFixForkV1) getEffectiveTip(msg *types.Transaction, gasPrice *big.Int, + baseFee *big.Int, isLondonForkEnabled bool) *big.Int { + if isLondonForkEnabled && msg.Type == types.DynamicFeeTx { + return common.BigMin( + new(big.Int).Sub(msg.GasFeeCap, baseFee), + new(big.Int).Set(msg.GasTipCap), + ) + } + + return new(big.Int).Set(gasPrice) +} + +type LondonFixForkV2 struct{} + +func (l *LondonFixForkV2) checkDynamicFees(msg *types.Transaction, t *Transition) error { + if !t.config.London { + return nil + } + + if msg.Type == types.DynamicFeeTx { + if msg.GasFeeCap.BitLen() == 0 && msg.GasTipCap.BitLen() == 0 { + return nil + } + + if l := msg.GasFeeCap.BitLen(); l > 256 { + return fmt.Errorf("%w: address %v, GasFeeCap bit length: %d", ErrFeeCapVeryHigh, + msg.From.String(), l) + } + + if l := msg.GasTipCap.BitLen(); l > 256 { + return fmt.Errorf("%w: address %v, GasTipCap bit length: %d", ErrTipVeryHigh, + msg.From.String(), l) + } + + if msg.GasFeeCap.Cmp(msg.GasTipCap) < 0 { + return fmt.Errorf("%w: address %v, GasTipCap: %s, GasFeeCap: %s", ErrTipAboveFeeCap, + msg.From.String(), msg.GasTipCap, msg.GasFeeCap) + } + } + + // This will panic if baseFee is nil, but basefee presence is verified + // as part of header validation. + if msg.GetGasFeeCap().Cmp(t.ctx.BaseFee) < 0 { + return fmt.Errorf("%w: address %v, GasFeeCap: %s, BaseFee: %s", ErrFeeCapTooLow, + msg.From.String(), msg.GasFeeCap, t.ctx.BaseFee) + } + + return nil +} + +func (l *LondonFixForkV2) getUpfrontGasCost(msg *types.Transaction, baseFee *big.Int) *big.Int { + return new(big.Int).Mul(new(big.Int).SetUint64(msg.Gas), msg.GetGasPrice(baseFee.Uint64())) +} + +func (l *LondonFixForkV2) getEffectiveTip(msg *types.Transaction, gasPrice *big.Int, + baseFee *big.Int, isLondonForkEnabled bool) *big.Int { + if isLondonForkEnabled { + return msg.EffectiveGasTip(baseFee) + } + + return new(big.Int).Set(gasPrice) +} + +func RegisterLondonFixFork(londonFixFork string) error { + fh := forkmanager.GetInstance() + + if err := fh.RegisterHandler( + forkmanager.InitialFork, LondonFixHandler, &LondonFixForkV1{}); err != nil { + return err + } + + if fh.IsForkRegistered(londonFixFork) { + if err := fh.RegisterHandler( + londonFixFork, LondonFixHandler, &LondonFixForkV2{}); err != nil { + return err + } + } + + return nil +} + +func GetLondonFixHandler(blockNumber uint64) LondonFixFork { + if h := forkmanager.GetInstance().GetHandler(LondonFixHandler, blockNumber); h != nil { + //nolint:forcetypeassert + return h.(LondonFixFork) + } + + // for tests + return &LondonFixForkV2{} +} diff --git a/state/transition_test.go b/state/transition_test.go index de94c84cb0..d8e507c8be 100644 --- a/state/transition_test.go +++ b/state/transition_test.go @@ -18,6 +18,7 @@ func newTestTransition(preState map[types.Address]*PreState) *Transition { return &Transition{ logger: hclog.NewNullLogger(), state: newTestTxn(preState), + ctx: runtime.TxContext{BaseFee: big.NewInt(0)}, } } diff --git a/txpool/txpool.go b/txpool/txpool.go index 297b684b69..564cfc96fe 100644 --- a/txpool/txpool.go +++ b/txpool/txpool.go @@ -661,6 +661,13 @@ func (p *TxPool) validateTx(tx *types.Transaction) error { if tx.GasFeeCap.Cmp(new(big.Int).SetUint64(baseFee)) < 0 { metrics.IncrCounter([]string{txPoolMetrics, "underpriced_tx"}, 1) + return ErrUnderpriced + } + } else { + // Legacy approach to check if the given tx is not underpriced when london hardfork is enabled + if forks.London && tx.GasPrice.Cmp(new(big.Int).SetUint64(baseFee)) < 0 { + metrics.IncrCounter([]string{txPoolMetrics, "underpriced_tx"}, 1) + return ErrUnderpriced } }