diff --git a/blockchain/blockchain.go b/blockchain/blockchain.go index 6682c2b0a4..2645b3261f 100644 --- a/blockchain/blockchain.go +++ b/blockchain/blockchain.go @@ -19,8 +19,14 @@ import ( ) const ( - BlockGasTargetDivisor uint64 = 1024 // The bound divisor of the gas limit, used in update calculations - defaultCacheSize int = 100 // The default size for Blockchain LRU cache structures + // defaultBaseFeeChangeDenom is the value to bound the amount the base fee can change between blocks. + defaultBaseFeeChangeDenom = 8 + + // blockGasTargetDivisor is the bound divisor of the gas limit, used in update calculations + blockGasTargetDivisor uint64 = 1024 + + // defaultCacheSize is the default size for Blockchain LRU cache structures + defaultCacheSize int = 100 ) var ( @@ -378,7 +384,7 @@ func (b *Blockchain) calculateGasLimit(parentGasLimit uint64) uint64 { return blockGasTarget } - delta := parentGasLimit * 1 / BlockGasTargetDivisor + delta := parentGasLimit * 1 / blockGasTargetDivisor if parentGasLimit < blockGasTarget { // The gas limit is lower than the gas target, so it should // increase towards the target @@ -1033,7 +1039,7 @@ func (b *Blockchain) updateGasPriceAvgWithBlock(block *types.Block) { gasPrices := make([]*big.Int, len(block.Transactions)) for i, transaction := range block.Transactions { - gasPrices[i] = transaction.GasPrice + gasPrices[i] = transaction.GetGasPrice(block.Header.BaseFee) } b.updateGasPriceAvg(gasPrices) @@ -1135,7 +1141,7 @@ func (b *Blockchain) verifyGasLimit(header *types.Header, parentHeader *types.He diff *= -1 } - limit := parentHeader.GasLimit / BlockGasTargetDivisor + limit := parentHeader.GasLimit / blockGasTargetDivisor if uint64(diff) > limit { return fmt.Errorf( "invalid gas limit, limit = %d, want %d +- %d", @@ -1414,3 +1420,37 @@ func (b *Blockchain) GetBlockByNumber(blockNumber uint64, full bool) (*types.Blo func (b *Blockchain) Close() error { return b.db.Close() } + +// CalculateBaseFee calculates the basefee of the header. +func (b *Blockchain) CalculateBaseFee(parent *types.Header) uint64 { + if !b.config.Params.Forks.IsLondon(parent.Number) { + return chain.GenesisBaseFee + } + + parentGasTarget := parent.GasLimit / b.config.Genesis.BaseFeeEM + + // If the parent gasUsed is the same as the target, the baseFee remains unchanged. + if parent.GasUsed == parentGasTarget { + return parent.BaseFee + } + + // If the parent block used more gas than its target, the baseFee should increase. + if parent.GasUsed > parentGasTarget { + gasUsedDelta := parent.GasUsed - parentGasTarget + baseFeeDelta := calcBaseFeeDelta(gasUsedDelta, parentGasTarget, parent.BaseFee) + + return parent.BaseFee + common.Max(baseFeeDelta, 1) + } + + // Otherwise, if the parent block used less gas than its target, the baseFee should decrease. + gasUsedDelta := parentGasTarget - parent.GasUsed + baseFeeDelta := calcBaseFeeDelta(gasUsedDelta, parentGasTarget, parent.BaseFee) + + return common.Max(parent.BaseFee-baseFeeDelta, 0) +} + +func calcBaseFeeDelta(gasUsedDelta, parentGasTarget, baseFee uint64) uint64 { + y := baseFee * gasUsedDelta / parentGasTarget + + return y / defaultBaseFeeChangeDenom +} diff --git a/blockchain/blockchain_test.go b/blockchain/blockchain_test.go index 1646c49660..b8de3dcffd 100644 --- a/blockchain/blockchain_test.go +++ b/blockchain/blockchain_test.go @@ -1335,3 +1335,59 @@ func TestBlockchain_VerifyBlockBody(t *testing.T) { assert.ErrorIs(t, err, errUnableToExecute) }) } + +func TestBlockchain_CalculateBaseFee(t *testing.T) { + t.Parallel() + + tests := []struct { + blockNumber uint64 + parentBaseFee uint64 + parentGasLimit uint64 + parentGasUsed uint64 + expectedBaseFee uint64 + elasticityMultiplier uint64 + }{ + {6, chain.GenesisBaseFee, 20000000, 10000000, chain.GenesisBaseFee, 2}, // usage == target + {6, chain.GenesisBaseFee, 20000000, 10000000, 1125000000, 4}, // usage == target + {6, chain.GenesisBaseFee, 20000000, 9000000, 987500000, 2}, // usage below target + {6, chain.GenesisBaseFee, 20000000, 9000000, 1100000000, 4}, // usage below target + {6, chain.GenesisBaseFee, 20000000, 11000000, 1012500000, 2}, // usage above target + {6, chain.GenesisBaseFee, 20000000, 11000000, 1150000000, 4}, // usage above target + {6, chain.GenesisBaseFee, 20000000, 20000000, 1125000000, 2}, // usage full + {6, chain.GenesisBaseFee, 20000000, 20000000, 1375000000, 4}, // usage full + {6, chain.GenesisBaseFee, 20000000, 0, 875000000, 2}, // usage 0 + {6, chain.GenesisBaseFee, 20000000, 0, 875000000, 4}, // usage 0 + } + + for i, test := range tests { + test := test + + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Parallel() + + fork := chain.Fork(5) + blockchain := Blockchain{ + config: &chain.Chain{ + Params: &chain.Params{ + Forks: &chain.Forks{ + London: &fork, + }, + }, + Genesis: &chain.Genesis{ + BaseFeeEM: test.elasticityMultiplier, + }, + }, + } + + parent := &types.Header{ + Number: test.blockNumber, + GasLimit: test.parentGasLimit, + GasUsed: test.parentGasUsed, + BaseFee: test.parentBaseFee, + } + + got := blockchain.CalculateBaseFee(parent) + assert.Equal(t, test.expectedBaseFee, got, fmt.Sprintf("expected %d, got %d", test.expectedBaseFee, got)) + }) + } +} diff --git a/chain/chain.go b/chain/chain.go index 67f216a09d..46044d9a10 100644 --- a/chain/chain.go +++ b/chain/chain.go @@ -7,17 +7,28 @@ import ( "math/big" "os" + "github.com/hashicorp/go-multierror" + "github.com/umbracle/ethgo" + + "github.com/0xPolygon/polygon-edge/helper/common" "github.com/0xPolygon/polygon-edge/helper/hex" "github.com/0xPolygon/polygon-edge/types" - "github.com/hashicorp/go-multierror" ) -var ( +const ( + // GenesisBaseFeeEM is the initial base fee elasticity multiplier for EIP-1559 blocks. + GenesisBaseFeeEM = 2 + // GenesisGasLimit is the default gas limit of the Genesis block. GenesisGasLimit uint64 = 4712388 // GenesisDifficulty is the default difficulty of the Genesis block. - GenesisDifficulty = big.NewInt(131072) + GenesisDifficulty uint64 = 131072 +) + +var ( + // GenesisBaseFee is the initial base fee for EIP-1559 blocks. + GenesisBaseFee = ethgo.Gwei(1).Uint64() ) // Chain is the blockchain chain configuration @@ -40,6 +51,8 @@ type Genesis struct { Mixhash types.Hash `json:"mixHash"` Coinbase types.Address `json:"coinbase"` Alloc map[types.Address]*GenesisAccount `json:"alloc,omitempty"` + BaseFee uint64 `json:"baseFee"` + BaseFeeEM uint64 `json:"baseFeeEM"` // Override StateRoot types.Hash @@ -66,6 +79,7 @@ func (g *Genesis) GenesisHeader() *types.Header { ExtraData: g.ExtraData, GasLimit: g.GasLimit, GasUsed: g.GasUsed, + BaseFee: g.BaseFee, Difficulty: g.Difficulty, MixHash: g.Mixhash, Miner: g.Coinbase.Bytes(), @@ -81,7 +95,11 @@ func (g *Genesis) GenesisHeader() *types.Header { } if g.Difficulty == 0 { - head.Difficulty = GenesisDifficulty.Uint64() + head.Difficulty = GenesisDifficulty + } + + if g.BaseFee == 0 { + head.BaseFee = GenesisBaseFee } return head @@ -109,6 +127,8 @@ func (g *Genesis) MarshalJSON() ([]byte, error) { Number *string `json:"number,omitempty"` GasUsed *string `json:"gasUsed,omitempty"` ParentHash types.Hash `json:"parentHash"` + BaseFee *string `json:"baseFee"` + BaseFeeEM *string `json:"baseFeeEM"` } var enc Genesis @@ -119,6 +139,8 @@ func (g *Genesis) MarshalJSON() ([]byte, error) { enc.GasLimit = types.EncodeUint64(g.GasLimit) enc.Difficulty = types.EncodeUint64(g.Difficulty) + enc.BaseFee = types.EncodeUint64(g.BaseFee) + enc.BaseFeeEM = types.EncodeUint64(g.BaseFeeEM) enc.Mixhash = g.Mixhash enc.Coinbase = g.Coinbase @@ -153,6 +175,8 @@ func (g *Genesis) UnmarshalJSON(data []byte) error { Number *string `json:"number"` GasUsed *string `json:"gasUsed"` ParentHash *types.Hash `json:"parentHash"` + BaseFee *string `json:"baseFee"` + BaseFeeEM *string `json:"baseFeeEM"` } var dec Genesis @@ -166,14 +190,14 @@ func (g *Genesis) UnmarshalJSON(data []byte) error { err = multierror.Append(err, fmt.Errorf("%s: %w", field, subErr)) } - nonce, subErr := types.ParseUint64orHex(dec.Nonce) + nonce, subErr := common.ParseUint64orHex(dec.Nonce) if subErr != nil { parseError("nonce", subErr) } binary.BigEndian.PutUint64(g.Nonce[:], nonce) - g.Timestamp, subErr = types.ParseUint64orHex(dec.Timestamp) + g.Timestamp, subErr = common.ParseUint64orHex(dec.Timestamp) if subErr != nil { parseError("timestamp", subErr) } @@ -189,16 +213,26 @@ func (g *Genesis) UnmarshalJSON(data []byte) error { return fmt.Errorf("field 'gaslimit' is required") } - g.GasLimit, subErr = types.ParseUint64orHex(dec.GasLimit) + g.GasLimit, subErr = common.ParseUint64orHex(dec.GasLimit) if subErr != nil { parseError("gaslimit", subErr) } - g.Difficulty, subErr = types.ParseUint64orHex(dec.Difficulty) + g.Difficulty, subErr = common.ParseUint64orHex(dec.Difficulty) if subErr != nil { parseError("difficulty", subErr) } + g.BaseFee, subErr = common.ParseUint64orHex(dec.BaseFee) + if subErr != nil { + parseError("baseFee", subErr) + } + + g.BaseFeeEM, subErr = common.ParseUint64orHex(dec.BaseFeeEM) + if subErr != nil { + parseError("baseFeeEM", subErr) + } + if dec.Mixhash != nil { g.Mixhash = *dec.Mixhash } @@ -214,12 +248,12 @@ func (g *Genesis) UnmarshalJSON(data []byte) error { } } - g.Number, subErr = types.ParseUint64orHex(dec.Number) + g.Number, subErr = common.ParseUint64orHex(dec.Number) if subErr != nil { parseError("number", subErr) } - g.GasUsed, subErr = types.ParseUint64orHex(dec.GasUsed) + g.GasUsed, subErr = common.ParseUint64orHex(dec.GasUsed) if subErr != nil { parseError("gasused", subErr) } @@ -318,7 +352,7 @@ func (g *GenesisAccount) UnmarshalJSON(data []byte) error { parseError("balance", subErr) } - g.Nonce, subErr = types.ParseUint64orHex(dec.Nonce) + g.Nonce, subErr = common.ParseUint64orHex(dec.Nonce) if subErr != nil { parseError("nonce", subErr) diff --git a/chain/params.go b/chain/params.go index 7d901cbd4a..e577296977 100644 --- a/chain/params.go +++ b/chain/params.go @@ -1,11 +1,18 @@ package chain import ( + "errors" "math/big" + "sort" "github.com/0xPolygon/polygon-edge/types" ) +var ( + // ErrBurnContractAddressMissing is the error when a contract address is not provided + ErrBurnContractAddressMissing = errors.New("burn contract address missing") +) + // Params are all the set of params for the chain type Params struct { Forks *Forks `json:"forks"` @@ -19,6 +26,9 @@ type Params struct { ContractDeployerBlockList *AddressListConfig `json:"contractDeployerBlockList,omitempty"` TransactionsAllowList *AddressListConfig `json:"transactionsAllowList,omitempty"` TransactionsBlockList *AddressListConfig `json:"transactionsBlockList,omitempty"` + + // Governance contract where the token will be sent to and burn in london fork + BurnContract map[uint64]string `json:"burnContract"` } type AddressListConfig struct { @@ -29,6 +39,31 @@ type AddressListConfig struct { EnabledAddresses []types.Address `json:"enabledAddresses,omitempty"` } +// CalculateBurnContract calculates burn contract address for the given block number +func (p *Params) CalculateBurnContract(block uint64) (types.Address, error) { + blocks := make([]uint64, 0, len(p.BurnContract)) + + for startBlock := range p.BurnContract { + blocks = append(blocks, startBlock) + } + + if len(blocks) == 0 { + return types.ZeroAddress, ErrBurnContractAddressMissing + } + + sort.Slice(blocks, func(i, j int) bool { + return blocks[i] < blocks[j] + }) + + for i := 0; i < len(blocks)-1; i++ { + if block >= blocks[i] && block < blocks[i+1] { + return types.StringToAddress(p.BurnContract[blocks[i]]), nil + } + } + + return types.StringToAddress(p.BurnContract[blocks[len(blocks)-1]]), nil +} + func (p *Params) GetEngine() string { // We know there is already one for k := range p.Engine { diff --git a/chain/params_test.go b/chain/params_test.go index be27a2a4ad..9c72d2764a 100644 --- a/chain/params_test.go +++ b/chain/params_test.go @@ -4,6 +4,10 @@ import ( "encoding/json" "reflect" "testing" + + "github.com/stretchr/testify/require" + + "github.com/0xPolygon/polygon-edge/types" ) func TestParamsForks(t *testing.T) { @@ -54,3 +58,73 @@ func TestParamsForksInTime(t *testing.T) { expect("constantinople", ff.Constantinople, false) expect("eip150", ff.EIP150, false) } + +func TestParams_CalculateBurnContract(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + burnContract map[uint64]string + block uint64 + want types.Address + wantErr bool + }{ + { + name: "no addresses in the list", + burnContract: map[uint64]string{}, + block: 10, + want: types.ZeroAddress, + wantErr: true, + }, + { + name: "last address is used", + burnContract: map[uint64]string{ + 15: "0x8888f1f195afa192cfee860698584c030f4c9db1", + }, + block: 10, + want: types.StringToAddress("0x8888f1f195afa192cfee860698584c030f4c9db1"), + wantErr: false, + }, + { + name: "first address is used", + burnContract: map[uint64]string{ + 5: "0x8888f1f195afa192cfee860698584c030f4c9db2", + 15: "0x8888f1f195afa192cfee860698584c030f4c9db1", + }, + block: 10, + want: types.StringToAddress("0x8888f1f195afa192cfee860698584c030f4c9db2"), + wantErr: false, + }, + { + name: "same block as key", + burnContract: map[uint64]string{ + 5: "0x8888f1f195afa192cfee860698584c030f4c4db2", + 10: "0x8888f1f195afa192cfee860698584c030f4c5db1", + 15: "0x8888f1f195afa192cfee860698584c030f4c6db0", + }, + block: 10, + want: types.StringToAddress("0x8888f1f195afa192cfee860698584c030f4c5db1"), + wantErr: false, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + p := &Params{ + BurnContract: tt.burnContract, + } + + got, err := p.CalculateBurnContract(tt.block) + if tt.wantErr { + require.Error(t, err, "CalculateBurnContract() error = %v, wantErr %v", err, tt.wantErr) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got, "CalculateBurnContract() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/command/default.go b/command/default.go index 6ba242fa25..ab6af9d7d4 100644 --- a/command/default.go +++ b/command/default.go @@ -1,22 +1,26 @@ package command import ( - "github.com/0xPolygon/polygon-edge/server" "github.com/umbracle/ethgo" + + "github.com/0xPolygon/polygon-edge/chain" + "github.com/0xPolygon/polygon-edge/server" ) const ( - DefaultGenesisFileName = "genesis.json" - DefaultChainName = "polygon-edge" - DefaultChainID = 100 - DefaultConsensus = server.PolyBFTConsensus - DefaultGenesisGasUsed = 458752 // 0x70000 - DefaultGenesisGasLimit = 5242880 // 0x500000 + DefaultGenesisFileName = "genesis.json" + DefaultChainName = "polygon-edge" + DefaultChainID = 100 + DefaultConsensus = server.PolyBFTConsensus + DefaultGenesisGasUsed = 458752 // 0x70000 + DefaultGenesisGasLimit = 5242880 // 0x500000 + DefaultGenesisBaseFeeEM = chain.GenesisBaseFeeEM ) var ( DefaultStake = ethgo.Ether(1e6) DefaultPremineBalance = ethgo.Ether(1e6) + DefaultGenesisBaseFee = chain.GenesisBaseFee ) const ( diff --git a/command/genesis/genesis.go b/command/genesis/genesis.go index 1ea4186a78..1ad107836d 100644 --- a/command/genesis/genesis.go +++ b/command/genesis/genesis.go @@ -72,6 +72,13 @@ func setFlags(cmd *cobra.Command) { "the maximum amount of gas used by all transactions in a block", ) + cmd.Flags().StringArrayVar( + ¶ms.burnContracts, + burnContractFlag, + []string{}, + "the burn contract blocks and addresses (format: :
)", + ) + cmd.Flags().StringArrayVar( ¶ms.bootnodes, command.BootnodeFlag, diff --git a/command/genesis/params.go b/command/genesis/params.go index f0e185b7a8..87d8f3f8e4 100644 --- a/command/genesis/params.go +++ b/command/genesis/params.go @@ -31,6 +31,7 @@ const ( epochSizeFlag = "epoch-size" epochRewardFlag = "epoch-reward" blockGasLimitFlag = "block-gas-limit" + burnContractFlag = "burn-contract" posFlag = "pos" minValidatorCount = "min-validator-count" maxValidatorCount = "max-validator-count" @@ -77,6 +78,8 @@ type genesisParams struct { blockGasLimit uint64 isPos bool + burnContracts []string + minNumValidators uint64 maxNumValidators uint64 @@ -357,15 +360,27 @@ func (p *genesisParams) initGenesisConfig() error { Alloc: map[types.Address]*chain.GenesisAccount{}, ExtraData: p.extraData, GasUsed: command.DefaultGenesisGasUsed, + BaseFee: command.DefaultGenesisBaseFee, + BaseFeeEM: command.DefaultGenesisBaseFeeEM, }, Params: &chain.Params{ - ChainID: int64(p.chainID), - Forks: chain.AllForksEnabled, - Engine: p.consensusEngineConfig, + ChainID: int64(p.chainID), + Forks: chain.AllForksEnabled, + Engine: p.consensusEngineConfig, + BurnContract: map[uint64]string{}, }, Bootnodes: p.bootnodes, } + for _, burnContract := range p.burnContracts { + block, address, err := parseBurnContractInfo(burnContract) + if err != nil { + return err + } + + chainConfig.Params.BurnContract[block] = address.String() + } + // Predeploy staking smart contract if needed if p.shouldPredeployStakingSC() { stakingAccount, err := p.predeployStakingSC() diff --git a/command/genesis/polybft_params.go b/command/genesis/polybft_params.go index 159ee21fc5..92ca6bb813 100644 --- a/command/genesis/polybft_params.go +++ b/command/genesis/polybft_params.go @@ -39,7 +39,6 @@ const ( defaultSprintSize = uint64(5) defaultValidatorSetSize = 100 defaultBlockTime = 2 * time.Second - defaultBridge = false defaultEpochReward = 1 contractDeployerAllowListAdminFlag = "contract-deployer-allow-list-admin" @@ -116,6 +115,7 @@ func (p *genesisParams) generatePolyBftChainConfig(o command.OutputFormatter) er Engine: map[string]interface{}{ string(server.PolyBFTConsensus): polyBftConfig, }, + BurnContract: map[uint64]string{}, }, Bootnodes: p.bootnodes, } @@ -152,6 +152,15 @@ func (p *genesisParams) generatePolyBftChainConfig(o command.OutputFormatter) er } } + for _, burnContract := range p.burnContracts { + block, addr, err := parseBurnContractInfo(burnContract) + if err != nil { + return err + } + + chainConfig.Params.BurnContract[block] = addr.String() + } + validatorMetadata := make([]*polybft.ValidatorMetadata, len(initialValidators)) for i, validator := range initialValidators { @@ -182,6 +191,8 @@ func (p *genesisParams) generatePolyBftChainConfig(o command.OutputFormatter) er ExtraData: genesisExtraData, GasUsed: command.DefaultGenesisGasUsed, Mixhash: polybft.PolyBFTMixDigest, + BaseFee: chain.GenesisBaseFee, + BaseFeeEM: chain.GenesisBaseFeeEM, } if len(p.contractDeployerAllowListAdmin) != 0 { diff --git a/command/genesis/utils.go b/command/genesis/utils.go index fb810b495e..497ba9db5d 100644 --- a/command/genesis/utils.go +++ b/command/genesis/utils.go @@ -120,6 +120,24 @@ func parseTrackerStartBlocks(trackerStartBlocksRaw []string) (map[types.Address] return trackerStartBlocksConfig, nil } +// parseBurnContractInfo parses provided burn contract information and returns burn contract block and address +func parseBurnContractInfo(burnContractInfoRaw string) (uint64, types.Address, error) { + // :
+ burnContractParts := strings.Split(burnContractInfoRaw, ":") + if len(burnContractParts) != 2 { + return 0, types.ZeroAddress, fmt.Errorf("expected format: :
") + } + + blockRaw := burnContractParts[0] + + block, err := types.ParseUint256orHex(&blockRaw) + if err != nil { + return 0, types.ZeroAddress, fmt.Errorf("failed to parse amount %s: %w", blockRaw, err) + } + + return block.Uint64(), types.StringToAddress(burnContractParts[1]), nil +} + // GetValidatorKeyFiles returns file names which has validator secrets func GetValidatorKeyFiles(rootDir, filePrefix string) ([]string, error) { if rootDir == "" { diff --git a/command/polybftsecrets/utils.go b/command/polybftsecrets/utils.go index 9507c0cb31..4dfaddc832 100644 --- a/command/polybftsecrets/utils.go +++ b/command/polybftsecrets/utils.go @@ -20,7 +20,6 @@ const ( // common errors for all polybft commands var ( ErrInvalidNum = fmt.Errorf("num flag value should be between 1 and %d", maxInitNum) - ErrInvalidConfig = errors.New("invalid secrets configuration") ErrInvalidParams = errors.New("no config file or data directory passed in") ErrUnsupportedType = errors.New("unsupported secrets manager") ErrSecureLocalStoreNotImplemented = errors.New( @@ -35,9 +34,7 @@ func GetSecretsManager(dataPath, configPath string, insecureLocalStore bool) (se if configPath != "" { secretsConfig, readErr := secrets.ReadConfig(configPath) if readErr != nil { - invalidConfigErr := ErrInvalidConfig.Error() - - return nil, fmt.Errorf("%s: %w", invalidConfigErr, readErr) + return nil, fmt.Errorf("invalid secrets configuration: %w", readErr) } if !secrets.SupportedServiceManager(secretsConfig.Type) { diff --git a/consensus/dev/dev.go b/consensus/dev/dev.go index 344cbcbb8f..ca10891878 100644 --- a/consensus/dev/dev.go +++ b/consensus/dev/dev.go @@ -109,10 +109,10 @@ type transitionInterface interface { Write(txn *types.Transaction) error } -func (d *Dev) writeTransactions(gasLimit uint64, transition transitionInterface) []*types.Transaction { +func (d *Dev) writeTransactions(baseFee, gasLimit uint64, transition transitionInterface) []*types.Transaction { var successful []*types.Transaction - d.txpool.Prepare() + d.txpool.Prepare(baseFee) for { tx := d.txpool.Peek() @@ -120,7 +120,7 @@ func (d *Dev) writeTransactions(gasLimit uint64, transition transitionInterface) break } - if tx.ExceedsBlockGasLimit(gasLimit) { + if tx.Gas > gasLimit { d.txpool.Drop(tx) continue @@ -167,7 +167,10 @@ func (d *Dev) writeNewBlock(parent *types.Header) error { return err } + baseFee := d.blockchain.CalculateBaseFee(parent) + header.GasLimit = gasLimit + header.BaseFee = baseFee miner, err := d.GetBlockCreator(header) if err != nil { @@ -180,7 +183,7 @@ func (d *Dev) writeNewBlock(parent *types.Header) error { return err } - txns := d.writeTransactions(gasLimit, transition) + txns := d.writeTransactions(baseFee, gasLimit, transition) // Commit the changes _, root := transition.Commit() diff --git a/consensus/ibft/consensus_backend.go b/consensus/ibft/consensus_backend.go index 742405b98c..d06786695d 100644 --- a/consensus/ibft/consensus_backend.go +++ b/consensus/ibft/consensus_backend.go @@ -195,6 +195,9 @@ func (i *backendIBFT) buildBlock(parent *types.Header) (*types.Block, error) { return nil, err } + // calculate base fee + baseFee := i.blockchain.CalculateBaseFee(parent) + header.GasLimit = gasLimit if err := i.currentHooks.ModifyHeader(header, i.currentSigner.Address()); err != nil { @@ -223,6 +226,7 @@ func (i *backendIBFT) buildBlock(parent *types.Header) (*types.Block, error) { txs := i.writeTransactions( writeCtx, + baseFee, gasLimit, header.Number, transition, @@ -304,6 +308,7 @@ type transitionInterface interface { func (i *backendIBFT) writeTransactions( writeCtx context.Context, + baseFee, gasLimit, blockNumber uint64, transition transitionInterface, @@ -330,7 +335,7 @@ func (i *backendIBFT) writeTransactions( ) }() - i.txpool.Prepare() + i.txpool.Prepare(baseFee) write: for { @@ -378,7 +383,7 @@ func (i *backendIBFT) writeTransaction( return nil, false } - if tx.ExceedsBlockGasLimit(gasLimit) { + if tx.Gas > gasLimit { i.txpool.Drop(tx) // continue processing diff --git a/consensus/ibft/fork/hooks_test.go b/consensus/ibft/fork/hooks_test.go index e650b3ee8a..3cf696e00f 100644 --- a/consensus/ibft/fork/hooks_test.go +++ b/consensus/ibft/fork/hooks_test.go @@ -279,6 +279,9 @@ func newTestTransition( ex := state.NewExecutor(&chain.Params{ Forks: chain.AllForksEnabled, + BurnContract: map[uint64]string{ + 0: types.ZeroAddress.String(), + }, }, st, hclog.NewNullLogger()) rootHash, err := ex.WriteGenesis(nil, types.Hash{}) diff --git a/consensus/ibft/ibft.go b/consensus/ibft/ibft.go index 23b48a70ab..08c719ddd0 100644 --- a/consensus/ibft/ibft.go +++ b/consensus/ibft/ibft.go @@ -34,16 +34,15 @@ const ( ) var ( - ErrInvalidHookParam = errors.New("invalid IBFT hook param passed in") - ErrProposerSealByNonValidator = errors.New("proposer seal by non-validator") - ErrInvalidMixHash = errors.New("invalid mixhash") - ErrInvalidSha3Uncles = errors.New("invalid sha3 uncles") - ErrWrongDifficulty = errors.New("wrong difficulty") - ErrParentCommittedSealsNotFound = errors.New("parent committed seals not found") + ErrInvalidHookParam = errors.New("invalid IBFT hook param passed in") + ErrProposerSealByNonValidator = errors.New("proposer seal by non-validator") + ErrInvalidMixHash = errors.New("invalid mixhash") + ErrInvalidSha3Uncles = errors.New("invalid sha3 uncles") + ErrWrongDifficulty = errors.New("wrong difficulty") ) type txPoolInterface interface { - Prepare() + Prepare(uint64) Length() uint64 Peek() *types.Transaction Pop(tx *types.Transaction) diff --git a/consensus/polybft/block_builder.go b/consensus/polybft/block_builder.go index 28cf7079dc..6f56f59924 100644 --- a/consensus/polybft/block_builder.go +++ b/consensus/polybft/block_builder.go @@ -35,6 +35,9 @@ type BlockBuilderParams struct { // txPoolInterface implementation TxPool txPoolInterface + + // BaseFee is the base fee + BaseFee uint64 } func NewBlockBuilder(params *BlockBuilderParams) *BlockBuilder { @@ -82,6 +85,7 @@ func (b *BlockBuilder) Reset() error { ReceiptsRoot: types.EmptyRootHash, // this avoids needing state for now Sha3Uncles: types.EmptyUncleHash, GasLimit: b.params.GasLimit, + BaseFee: b.params.BaseFee, Timestamp: uint64(headerTime.Unix()), } @@ -128,7 +132,7 @@ func (b *BlockBuilder) Build(handler func(h *types.Header)) (*types.FullBlock, e // WriteTx applies given transaction to the state. If transaction apply fails, it reverts the saved snapshot. func (b *BlockBuilder) WriteTx(tx *types.Transaction) error { - if tx.ExceedsBlockGasLimit(b.params.GasLimit) { + if tx.Gas > b.params.GasLimit { b.params.Logger.Info("Transaction gas limit exceedes block gas limit", "hash", tx.Hash, "tx gas limit", tx.Gas, "block gas limt", b.params.GasLimit) @@ -148,7 +152,7 @@ func (b *BlockBuilder) WriteTx(tx *types.Transaction) error { func (b *BlockBuilder) Fill() { blockTimer := time.NewTimer(b.params.BlockTime) - b.params.TxPool.Prepare() + b.params.TxPool.Prepare(b.params.BaseFee) write: for { select { diff --git a/consensus/polybft/blockchain_wrapper.go b/consensus/polybft/blockchain_wrapper.go index 3a8b642108..9475652c9f 100644 --- a/consensus/polybft/blockchain_wrapper.go +++ b/consensus/polybft/blockchain_wrapper.go @@ -95,7 +95,7 @@ func (p *blockchainWrapper) ProcessBlock(parent *types.Header, block *types.Bloc // apply transactions from block for _, tx := range block.Transactions { - if err := transition.Write(tx); err != nil { + if err = transition.Write(tx); err != nil { return nil, fmt.Errorf("process block tx error, tx = %v, err = %w", tx.Hash, err) } } @@ -165,6 +165,7 @@ func (p *blockchainWrapper) NewBlockBuilder( Coinbase: coinbase, Executor: p.executor, GasLimit: gasLimit, + BaseFee: p.blockchain.CalculateBaseFee(parent), TxPool: txPool, Logger: logger, }), nil diff --git a/consensus/polybft/consensus_runtime.go b/consensus/polybft/consensus_runtime.go index b1a30cb70b..13e1e08e37 100644 --- a/consensus/polybft/consensus_runtime.go +++ b/consensus/polybft/consensus_runtime.go @@ -35,7 +35,7 @@ var ( // txPoolInterface is an abstraction of transaction pool type txPoolInterface interface { - Prepare() + Prepare(uint64) Length() uint64 Peek() *types.Transaction Pop(*types.Transaction) diff --git a/consensus/polybft/mocks_test.go b/consensus/polybft/mocks_test.go index 9eff33161d..9035114955 100644 --- a/consensus/polybft/mocks_test.go +++ b/consensus/polybft/mocks_test.go @@ -488,8 +488,8 @@ type txPoolMock struct { mock.Mock } -func (tp *txPoolMock) Prepare() { - tp.Called() +func (tp *txPoolMock) Prepare(baseFee uint64) { + tp.Called(baseFee) } func (tp *txPoolMock) Length() uint64 { diff --git a/consensus/polybft/system_state_test.go b/consensus/polybft/system_state_test.go index 46001d1c48..6c26d60e36 100644 --- a/consensus/polybft/system_state_test.go +++ b/consensus/polybft/system_state_test.go @@ -63,16 +63,16 @@ func TestSystemState_GetValidatorSet(t *testing.T) { }) solcContract, err := cc.Compile() - assert.NoError(t, err) + require.NoError(t, err) bin, err := hex.DecodeString(solcContract.Bin) - assert.NoError(t, err) + require.NoError(t, err) transition := newTestTransition(t, nil) // deploy a contract result := transition.Create2(types.Address{}, bin, big.NewInt(0), 1000000000) - assert.NoError(t, result.Err) + require.NoError(t, result.Err) provider := &stateProvider{ transition: transition, @@ -199,6 +199,9 @@ func newTestTransition(t *testing.T, alloc map[types.Address]*chain.GenesisAccou ex := state.NewExecutor(&chain.Params{ Forks: chain.AllForksEnabled, + BurnContract: map[uint64]string{ + 0: types.ZeroAddress.String(), + }, }, st, hclog.NewNullLogger()) rootHash, err := ex.WriteGenesis(alloc, types.Hash{}) diff --git a/crypto/txsigner.go b/crypto/txsigner.go index c35f0b3581..f0c7e203b4 100644 --- a/crypto/txsigner.go +++ b/crypto/txsigner.go @@ -4,12 +4,16 @@ import ( "crypto/ecdsa" "fmt" "math/big" - "math/bits" "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/helper/keccak" "github.com/0xPolygon/polygon-edge/types" - "github.com/umbracle/fastrlp" +) + +// Magic numbers from Ethereum, used in v calculation +var ( + big27 = big.NewInt(27) + big35 = big.NewInt(35) ) // TxSigner is a utility interface used to recover data from a transaction @@ -22,9 +26,6 @@ type TxSigner interface { // SignTx signs a transaction SignTx(tx *types.Transaction, priv *ecdsa.PrivateKey) (*types.Transaction, error) - - // CalculateV calculates the V value based on the type of signer used - CalculateV(parity byte) []byte } // NewSigner creates a new signer object (EIP155 or FrontierSigner) @@ -32,27 +33,54 @@ func NewSigner(forks chain.ForksInTime, chainID uint64) TxSigner { var signer TxSigner if forks.EIP155 { - signer = &EIP155Signer{chainID: chainID, isHomestead: forks.Homestead} + signer = NewEIP155Signer(chainID, forks.Homestead) } else { - signer = &FrontierSigner{forks.Homestead} + signer = NewFrontierSigner(forks.Homestead) + } + + // London signer requires a fallback signer that is defined above. + // This is the reason why the london signer check is separated. + if forks.London { + return NewLondonSigner(chainID, forks.Homestead, signer) } return signer } -type FrontierSigner struct { - isHomestead bool -} +// encodeSignature generates a signature value based on the R, S and V value +func encodeSignature(R, S, V *big.Int, isHomestead bool) ([]byte, error) { + if !ValidateSignatureValues(V, R, S, isHomestead) { + return nil, fmt.Errorf("invalid txn signature") + } -var signerPool fastrlp.ArenaPool + sig := make([]byte, 65) + copy(sig[32-len(R.Bytes()):32], R.Bytes()) + copy(sig[64-len(S.Bytes()):64], S.Bytes()) + sig[64] = byte(V.Int64()) // here is safe to convert it since ValidateSignatureValues will validate the v value + + return sig, nil +} // calcTxHash calculates the transaction hash (keccak256 hash of the RLP value) func calcTxHash(tx *types.Transaction, chainID uint64) types.Hash { a := signerPool.Get() + isDynamicFeeTx := tx.Type == types.DynamicFeeTx v := a.NewArray() + + if isDynamicFeeTx { + v.Set(a.NewUint(chainID)) + } + v.Set(a.NewUint(tx.Nonce)) - v.Set(a.NewBigInt(tx.GasPrice)) + + if isDynamicFeeTx { + v.Set(a.NewBigInt(tx.GasTipCap)) + v.Set(a.NewBigInt(tx.GasFeeCap)) + } else { + v.Set(a.NewBigInt(tx.GasPrice)) + } + v.Set(a.NewUint(tx.Gas)) if tx.To == nil { @@ -62,183 +90,28 @@ func calcTxHash(tx *types.Transaction, chainID uint64) types.Hash { } v.Set(a.NewBigInt(tx.Value)) + v.Set(a.NewCopyBytes(tx.Input)) - // EIP155 - if chainID != 0 { - v.Set(a.NewUint(chainID)) - v.Set(a.NewUint(0)) - v.Set(a.NewUint(0)) + if isDynamicFeeTx { + v.Set(a.NewArray()) + } else { + // EIP155 + if chainID != 0 { + v.Set(a.NewUint(chainID)) + v.Set(a.NewUint(0)) + v.Set(a.NewUint(0)) + } } - hash := keccak.Keccak256Rlp(nil, v) + var hash []byte + if isDynamicFeeTx { + hash = keccak.PrefixedKeccak256Rlp([]byte{byte(tx.Type)}, nil, v) + } else { + hash = keccak.Keccak256Rlp(nil, v) + } signerPool.Put(a) return types.BytesToHash(hash) } - -// Hash is a wrapper function for the calcTxHash, with chainID 0 -func (f *FrontierSigner) Hash(tx *types.Transaction) types.Hash { - return calcTxHash(tx, 0) -} - -// Magic numbers from Ethereum, used in v calculation -var ( - big27 = big.NewInt(27) - big35 = big.NewInt(35) -) - -// Sender decodes the signature and returns the sender of the transaction -func (f *FrontierSigner) Sender(tx *types.Transaction) (types.Address, error) { - refV := big.NewInt(0) - if tx.V != nil { - refV.SetBytes(tx.V.Bytes()) - } - - refV.Sub(refV, big27) - - sig, err := encodeSignature(tx.R, tx.S, refV, f.isHomestead) - if err != nil { - return types.Address{}, err - } - - pub, err := Ecrecover(f.Hash(tx).Bytes(), sig) - if err != nil { - return types.Address{}, err - } - - buf := Keccak256(pub[1:])[12:] - - return types.BytesToAddress(buf), nil -} - -// SignTx signs the transaction using the passed in private key -func (f *FrontierSigner) SignTx( - tx *types.Transaction, - privateKey *ecdsa.PrivateKey, -) (*types.Transaction, error) { - tx = tx.Copy() - - h := f.Hash(tx) - - sig, err := Sign(privateKey, h[:]) - if err != nil { - return nil, err - } - - tx.R = new(big.Int).SetBytes(sig[:32]) - tx.S = new(big.Int).SetBytes(sig[32:64]) - tx.V = new(big.Int).SetBytes(f.CalculateV(sig[64])) - - return tx, nil -} - -// calculateV returns the V value for transactions pre EIP155 -func (f *FrontierSigner) CalculateV(parity byte) []byte { - reference := big.NewInt(int64(parity)) - reference.Add(reference, big27) - - return reference.Bytes() -} - -// NewEIP155Signer returns a new EIP155Signer object -func NewEIP155Signer(forks chain.ForksInTime, chainID uint64) *EIP155Signer { - return &EIP155Signer{chainID: chainID, isHomestead: forks.Homestead} -} - -type EIP155Signer struct { - chainID uint64 - isHomestead bool -} - -// Hash is a wrapper function that calls calcTxHash with the EIP155Signer's chainID -func (e *EIP155Signer) Hash(tx *types.Transaction) types.Hash { - return calcTxHash(tx, e.chainID) -} - -// Sender returns the transaction sender -func (e *EIP155Signer) Sender(tx *types.Transaction) (types.Address, error) { - protected := true - - // Check if v value conforms to an earlier standard (before EIP155) - bigV := big.NewInt(0) - if tx.V != nil { - bigV.SetBytes(tx.V.Bytes()) - } - - if vv := bigV.Uint64(); bits.Len(uint(vv)) <= 8 { - protected = vv != 27 && vv != 28 - } - - if !protected { - return (&FrontierSigner{}).Sender(tx) - } - - // Reverse the V calculation to find the original V in the range [0, 1] - // v = CHAIN_ID * 2 + 35 + {0, 1} - mulOperand := big.NewInt(0).Mul(big.NewInt(int64(e.chainID)), big.NewInt(2)) - bigV.Sub(bigV, mulOperand) - bigV.Sub(bigV, big35) - - sig, err := encodeSignature(tx.R, tx.S, bigV, e.isHomestead) - if err != nil { - return types.Address{}, err - } - - pub, err := Ecrecover(e.Hash(tx).Bytes(), sig) - if err != nil { - return types.Address{}, err - } - - buf := Keccak256(pub[1:])[12:] - - return types.BytesToAddress(buf), nil -} - -// SignTx signs the transaction using the passed in private key -func (e *EIP155Signer) SignTx( - tx *types.Transaction, - privateKey *ecdsa.PrivateKey, -) (*types.Transaction, error) { - tx = tx.Copy() - - h := e.Hash(tx) - - sig, err := Sign(privateKey, h[:]) - if err != nil { - return nil, err - } - - tx.R = new(big.Int).SetBytes(sig[:32]) - tx.S = new(big.Int).SetBytes(sig[32:64]) - tx.V = new(big.Int).SetBytes(e.CalculateV(sig[64])) - - return tx, nil -} - -// calculateV returns the V value for transaction signatures. Based on EIP155 -func (e *EIP155Signer) CalculateV(parity byte) []byte { - reference := big.NewInt(int64(parity)) - reference.Add(reference, big35) - - mulOperand := big.NewInt(0).Mul(big.NewInt(int64(e.chainID)), big.NewInt(2)) - - reference.Add(reference, mulOperand) - - return reference.Bytes() -} - -// encodeSignature generates a signature value based on the R, S and V value -func encodeSignature(R, S, V *big.Int, isHomestead bool) ([]byte, error) { - if !ValidateSignatureValues(V, R, S, isHomestead) { - return nil, fmt.Errorf("invalid txn signature") - } - - sig := make([]byte, 65) - copy(sig[32-len(R.Bytes()):32], R.Bytes()) - copy(sig[64-len(S.Bytes()):64], S.Bytes()) - sig[64] = byte(V.Int64()) // here is safe to convert it since ValidateSignatureValues will validate the v value - - return sig, nil -} diff --git a/crypto/txsigner_eip155.go b/crypto/txsigner_eip155.go new file mode 100644 index 0000000000..faa394347b --- /dev/null +++ b/crypto/txsigner_eip155.go @@ -0,0 +1,99 @@ +package crypto + +import ( + "crypto/ecdsa" + "math/big" + "math/bits" + + "github.com/0xPolygon/polygon-edge/types" +) + +type EIP155Signer struct { + chainID uint64 + isHomestead bool +} + +// NewEIP155Signer returns a new EIP155Signer object +func NewEIP155Signer(chainID uint64, isHomestead bool) *EIP155Signer { + return &EIP155Signer{ + chainID: chainID, + isHomestead: isHomestead, + } +} + +// Hash is a wrapper function that calls calcTxHash with the EIP155Signer's chainID +func (e *EIP155Signer) Hash(tx *types.Transaction) types.Hash { + return calcTxHash(tx, e.chainID) +} + +// Sender returns the transaction sender +func (e *EIP155Signer) Sender(tx *types.Transaction) (types.Address, error) { + protected := true + + // Check if v value conforms to an earlier standard (before EIP155) + bigV := big.NewInt(0) + if tx.V != nil { + bigV.SetBytes(tx.V.Bytes()) + } + + if vv := bigV.Uint64(); bits.Len(uint(vv)) <= 8 { + protected = vv != 27 && vv != 28 + } + + if !protected { + return (&FrontierSigner{}).Sender(tx) + } + + // Reverse the V calculation to find the original V in the range [0, 1] + // v = CHAIN_ID * 2 + 35 + {0, 1} + mulOperand := big.NewInt(0).Mul(big.NewInt(int64(e.chainID)), big.NewInt(2)) + bigV.Sub(bigV, mulOperand) + bigV.Sub(bigV, big35) + + sig, err := encodeSignature(tx.R, tx.S, bigV, e.isHomestead) + if err != nil { + return types.Address{}, err + } + + pub, err := Ecrecover(e.Hash(tx).Bytes(), sig) + if err != nil { + return types.Address{}, err + } + + buf := Keccak256(pub[1:])[12:] + + return types.BytesToAddress(buf), nil +} + +// SignTx signs the transaction using the passed in private key +func (e *EIP155Signer) SignTx( + tx *types.Transaction, + privateKey *ecdsa.PrivateKey, +) (*types.Transaction, error) { + tx = tx.Copy() + + h := e.Hash(tx) + + sig, err := Sign(privateKey, h[:]) + if err != nil { + return nil, err + } + + tx.R = new(big.Int).SetBytes(sig[:32]) + tx.S = new(big.Int).SetBytes(sig[32:64]) + tx.V = new(big.Int).SetBytes(e.calculateV(sig[64])) + + return tx, nil +} + +// calculateV returns the V value for transaction signatures. Based on EIP155 +func (e *EIP155Signer) calculateV(parity byte) []byte { + reference := big.NewInt(int64(parity)) + reference.Add(reference, big35) + + mulOperand := big.NewInt(0).Mul(big.NewInt(int64(e.chainID)), big.NewInt(2)) + + reference.Add(reference, mulOperand) + + return reference.Bytes() +} diff --git a/crypto/txsigner_test.go b/crypto/txsigner_eip155_test.go similarity index 74% rename from crypto/txsigner_test.go rename to crypto/txsigner_eip155_test.go index f53cb5f694..a312b4023f 100644 --- a/crypto/txsigner_test.go +++ b/crypto/txsigner_eip155_test.go @@ -4,71 +4,59 @@ import ( "math/big" "testing" - "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/types" "github.com/stretchr/testify/assert" ) -func TestFrontierSigner(t *testing.T) { - signer := &FrontierSigner{} - - toAddress := types.StringToAddress("1") - key, err := GenerateECDSAKey() - assert.NoError(t, err) - - txn := &types.Transaction{ - To: &toAddress, - Value: big.NewInt(10), - GasPrice: big.NewInt(0), - } - signedTx, err := signer.SignTx(txn, key) - assert.NoError(t, err) - - from, err := signer.Sender(signedTx) - assert.NoError(t, err) - assert.Equal(t, from, PubKeyToAddress(&key.PublicKey)) -} - func TestEIP155Signer_Sender(t *testing.T) { t.Parallel() toAddress := types.StringToAddress("1") testTable := []struct { - name string - chainID *big.Int + name string + chainID *big.Int + isHomestead bool }{ { "mainnet", big.NewInt(1), + true, }, { "expanse mainnet", big.NewInt(2), + true, }, { "ropsten", big.NewInt(3), + true, }, { "rinkeby", big.NewInt(4), + true, }, { "goerli", big.NewInt(5), + true, }, { "kovan", big.NewInt(42), + true, }, { "geth private", big.NewInt(1337), + true, }, { "mega large", big.NewInt(0).Exp(big.NewInt(2), big.NewInt(20), nil), // 2**20 + true, }, } @@ -88,7 +76,10 @@ func TestEIP155Signer_Sender(t *testing.T) { GasPrice: big.NewInt(0), } - signer := NewEIP155Signer(chain.AllForksEnabled.At(0), testCase.chainID.Uint64()) + signer := NewEIP155Signer( + testCase.chainID.Uint64(), + testCase.isHomestead, + ) signedTx, signErr := signer.SignTx(txn, key) if signErr != nil { @@ -121,7 +112,7 @@ func TestEIP155Signer_ChainIDMismatch(t *testing.T) { GasPrice: big.NewInt(0), } - signer := NewEIP155Signer(chain.AllForksEnabled.At(0), chainIDTop) + signer := NewEIP155Signer(chainIDTop, true) signedTx, signErr := signer.SignTx(txn, key) if signErr != nil { @@ -129,7 +120,7 @@ func TestEIP155Signer_ChainIDMismatch(t *testing.T) { } for _, chainIDBottom := range chainIDS { - signerBottom := NewEIP155Signer(chain.AllForksEnabled.At(0), chainIDBottom) + signerBottom := NewEIP155Signer(chainIDBottom, true) recoveredSender, recoverErr := signerBottom.Sender(signedTx) if chainIDTop == chainIDBottom { diff --git a/crypto/txsigner_frontier.go b/crypto/txsigner_frontier.go new file mode 100644 index 0000000000..f2d2297d75 --- /dev/null +++ b/crypto/txsigner_frontier.go @@ -0,0 +1,82 @@ +package crypto + +import ( + "crypto/ecdsa" + "math/big" + + "github.com/umbracle/fastrlp" + + "github.com/0xPolygon/polygon-edge/types" +) + +var signerPool fastrlp.ArenaPool + +// FrontierSigner implements tx signer interface +type FrontierSigner struct { + isHomestead bool +} + +// NewFrontierSigner is the constructor of FrontierSigner +func NewFrontierSigner(isHomestead bool) *FrontierSigner { + return &FrontierSigner{ + isHomestead: isHomestead, + } +} + +// Hash is a wrapper function for the calcTxHash, with chainID 0 +func (f *FrontierSigner) Hash(tx *types.Transaction) types.Hash { + return calcTxHash(tx, 0) +} + +// Sender decodes the signature and returns the sender of the transaction +func (f *FrontierSigner) Sender(tx *types.Transaction) (types.Address, error) { + refV := big.NewInt(0) + if tx.V != nil { + refV.SetBytes(tx.V.Bytes()) + } + + refV.Sub(refV, big27) + + sig, err := encodeSignature(tx.R, tx.S, refV, f.isHomestead) + if err != nil { + return types.Address{}, err + } + + pub, err := Ecrecover(f.Hash(tx).Bytes(), sig) + if err != nil { + return types.Address{}, err + } + + buf := Keccak256(pub[1:])[12:] + + return types.BytesToAddress(buf), nil +} + +// SignTx signs the transaction using the passed in private key +func (f *FrontierSigner) SignTx( + tx *types.Transaction, + privateKey *ecdsa.PrivateKey, +) (*types.Transaction, error) { + tx = tx.Copy() + + h := f.Hash(tx) + + sig, err := Sign(privateKey, h[:]) + if err != nil { + return nil, err + } + + tx.R = new(big.Int).SetBytes(sig[:32]) + tx.S = new(big.Int).SetBytes(sig[32:64]) + tx.V = new(big.Int).SetBytes(f.calculateV(sig[64])) + + return tx, nil +} + +// calculateV returns the V value for transactions pre EIP155 +func (f *FrontierSigner) calculateV(parity byte) []byte { + reference := big.NewInt(int64(parity)) + reference.Add(reference, big27) + + return reference.Bytes() +} diff --git a/crypto/txsigner_frontier_test.go b/crypto/txsigner_frontier_test.go new file mode 100644 index 0000000000..e0d9c3ddfb --- /dev/null +++ b/crypto/txsigner_frontier_test.go @@ -0,0 +1,29 @@ +package crypto + +import ( + "math/big" + "testing" + + "github.com/0xPolygon/polygon-edge/types" + "github.com/stretchr/testify/assert" +) + +func TestFrontierSigner(t *testing.T) { + signer := &FrontierSigner{} + + toAddress := types.StringToAddress("1") + key, err := GenerateECDSAKey() + assert.NoError(t, err) + + txn := &types.Transaction{ + To: &toAddress, + Value: big.NewInt(10), + GasPrice: big.NewInt(0), + } + signedTx, err := signer.SignTx(txn, key) + assert.NoError(t, err) + + from, err := signer.Sender(signedTx) + assert.NoError(t, err) + assert.Equal(t, from, PubKeyToAddress(&key.PublicKey)) +} diff --git a/crypto/txsigner_london.go b/crypto/txsigner_london.go new file mode 100644 index 0000000000..727368b28b --- /dev/null +++ b/crypto/txsigner_london.go @@ -0,0 +1,79 @@ +package crypto + +import ( + "crypto/ecdsa" + "math/big" + + "github.com/0xPolygon/polygon-edge/types" +) + +// LondonSigner implements signer for EIP-1559 +type LondonSigner struct { + chainID uint64 + isHomestead bool + fallbackSigner TxSigner +} + +// NewLondonSigner returns a new LondonSigner object +func NewLondonSigner(chainID uint64, isHomestead bool, fallbackSigner TxSigner) *LondonSigner { + return &LondonSigner{ + chainID: chainID, + isHomestead: isHomestead, + fallbackSigner: fallbackSigner, + } +} + +// Hash is a wrapper function that calls calcTxHash with the LondonSigner's fields +func (e *LondonSigner) Hash(tx *types.Transaction) types.Hash { + return calcTxHash(tx, e.chainID) +} + +// Sender returns the transaction sender +func (e *LondonSigner) Sender(tx *types.Transaction) (types.Address, error) { + // Apply fallback signer for non-dynamic-fee-txs + if tx.Type != types.DynamicFeeTx { + return e.fallbackSigner.Sender(tx) + } + + sig, err := encodeSignature(tx.R, tx.S, tx.V, e.isHomestead) + if err != nil { + return types.Address{}, err + } + + pub, err := Ecrecover(e.Hash(tx).Bytes(), sig) + if err != nil { + return types.Address{}, err + } + + buf := Keccak256(pub[1:])[12:] + + return types.BytesToAddress(buf), nil +} + +// SignTx signs the transaction using the passed in private key +func (e *LondonSigner) SignTx(tx *types.Transaction, pk *ecdsa.PrivateKey) (*types.Transaction, error) { + // Apply fallback signer for non-dynamic-fee-txs + if tx.Type != types.DynamicFeeTx { + return e.fallbackSigner.SignTx(tx, pk) + } + + tx = tx.Copy() + + h := e.Hash(tx) + + sig, err := Sign(pk, h[:]) + if err != nil { + return nil, err + } + + tx.R = new(big.Int).SetBytes(sig[:32]) + tx.S = new(big.Int).SetBytes(sig[32:64]) + tx.V = new(big.Int).SetBytes(e.calculateV(sig[64])) + + return tx, nil +} + +// calculateV returns the V value for transaction signatures. Based on EIP155 +func (e *LondonSigner) calculateV(parity byte) []byte { + return big.NewInt(int64(parity)).Bytes() +} diff --git a/crypto/txsigner_london_test.go b/crypto/txsigner_london_test.go new file mode 100644 index 0000000000..f27997dd43 --- /dev/null +++ b/crypto/txsigner_london_test.go @@ -0,0 +1,145 @@ +package crypto + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/0xPolygon/polygon-edge/types" +) + +func TestLondonSignerSender(t *testing.T) { + t.Parallel() + + toAddress := types.StringToAddress("1") + + testTable := []struct { + name string + chainID *big.Int + isGomestead bool + }{ + { + "mainnet", + big.NewInt(1), + true, + }, + { + "expanse mainnet", + big.NewInt(2), + true, + }, + { + "ropsten", + big.NewInt(3), + true, + }, + { + "rinkeby", + big.NewInt(4), + true, + }, + { + "goerli", + big.NewInt(5), + true, + }, + { + "kovan", + big.NewInt(42), + true, + }, + { + "geth private", + big.NewInt(1337), + true, + }, + { + "mega large", + big.NewInt(0).Exp(big.NewInt(2), big.NewInt(20), nil), // 2**20 + true, + }, + } + + for _, testCase := range testTable { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + key, keyGenError := GenerateECDSAKey() + if keyGenError != nil { + t.Fatalf("Unable to generate key") + } + + txn := &types.Transaction{ + To: &toAddress, + Value: big.NewInt(1), + GasPrice: big.NewInt(0), + } + + chainID := testCase.chainID.Uint64() + signer := NewLondonSigner(chainID, true, NewEIP155Signer(chainID, true)) + + signedTx, signErr := signer.SignTx(txn, key) + if signErr != nil { + t.Fatalf("Unable to sign transaction") + } + + recoveredSender, recoverErr := signer.Sender(signedTx) + if recoverErr != nil { + t.Fatalf("Unable to recover sender") + } + + assert.Equal(t, recoveredSender.String(), PubKeyToAddress(&key.PublicKey).String()) + }) + } +} + +func Test_LondonSigner_Sender(t *testing.T) { + t.Parallel() + + signer := NewLondonSigner(100, true, NewEIP155Signer(100, true)) + to := types.StringToAddress("0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF") + + r, ok := big.NewInt(0).SetString("102623819621514684481463796449525884981685455700611671612296611353030973716382", 10) + require.True(t, ok) + + s, ok := big.NewInt(0).SetString("52694559292202008915948760944211702951173212957828665318138448463580296965840", 10) + require.True(t, ok) + + testTable := []struct { + name string + tx *types.Transaction + sender types.Address + }{ + { + name: "sender is 0x85dA99c8a7C2C95964c8EfD687E95E632Fc533D6", + tx: &types.Transaction{ + Type: types.DynamicFeeTx, + GasPrice: big.NewInt(1000000402), + GasTipCap: big.NewInt(1000000000), + GasFeeCap: big.NewInt(10000000000), + Gas: 21000, + To: &to, + Value: big.NewInt(100000000000000), + V: big.NewInt(0), + R: r, + S: s, + }, + sender: types.StringToAddress("0x85dA99c8a7C2C95964c8EfD687E95E632Fc533D6"), + }, + } + + for _, tt := range testTable { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + sender, err := signer.Sender(tt.tx) + require.NoError(t, err) + require.Equal(t, tt.sender, sender) + }) + } +} diff --git a/e2e-polybft/e2e/consensus_test.go b/e2e-polybft/e2e/consensus_test.go index 3ac51e5fd7..7ada766089 100644 --- a/e2e-polybft/e2e/consensus_test.go +++ b/e2e-polybft/e2e/consensus_test.go @@ -196,9 +196,11 @@ func TestE2E_Consensus_RegisterValidator(t *testing.T) { // send some tokens from the owner to the second validator so that the second validator can register and stake receipt, err = txRelayer.SendTransaction(ðgo.Transaction{ - From: ownerAcc.Ecdsa.Address(), - To: &secondValidatorAddr, - Value: initialBalance, + From: ownerAcc.Ecdsa.Address(), + To: &secondValidatorAddr, + Value: initialBalance, + MaxFeePerGas: big.NewInt(1000000000), + MaxPriorityFeePerGas: big.NewInt(100000000), }, ownerAcc.Ecdsa) require.NoError(t, err) require.Equal(t, uint64(types.ReceiptSuccess), receipt.Status) diff --git a/e2e-polybft/e2e/helpers_test.go b/e2e-polybft/e2e/helpers_test.go index e68007c4e3..5b531b7972 100644 --- a/e2e-polybft/e2e/helpers_test.go +++ b/e2e-polybft/e2e/helpers_test.go @@ -11,17 +11,19 @@ import ( "testing" "time" + "github.com/umbracle/ethgo" + "github.com/umbracle/ethgo/contract" + "github.com/0xPolygon/polygon-edge/consensus/polybft" "github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi" "github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi/artifact" "github.com/0xPolygon/polygon-edge/e2e-polybft/framework" + "github.com/0xPolygon/polygon-edge/helper/common" "github.com/0xPolygon/polygon-edge/helper/hex" "github.com/0xPolygon/polygon-edge/state/runtime/addresslist" "github.com/0xPolygon/polygon-edge/txrelayer" "github.com/0xPolygon/polygon-edge/types" "github.com/stretchr/testify/require" - "github.com/umbracle/ethgo" - "github.com/umbracle/ethgo/contract" ethgow "github.com/umbracle/ethgo/wallet" ) @@ -71,7 +73,7 @@ func getRootchainValidators(relayer txrelayer.TxRelayer, checkpointManagerAddr e return nil, err } - validatorsCount, err := types.ParseUint64orHex(&validatorsCountRaw) + validatorsCount, err := common.ParseUint64orHex(&validatorsCountRaw) if err != nil { return nil, err } diff --git a/e2e-polybft/e2e/txpool_test.go b/e2e-polybft/e2e/txpool_test.go index 91cb52384a..8dc45fb19b 100644 --- a/e2e-polybft/e2e/txpool_test.go +++ b/e2e-polybft/e2e/txpool_test.go @@ -6,15 +6,16 @@ import ( "testing" "time" - "github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi" - "github.com/0xPolygon/polygon-edge/e2e-polybft/framework" - "github.com/0xPolygon/polygon-edge/txrelayer" - "github.com/0xPolygon/polygon-edge/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/umbracle/ethgo" "github.com/umbracle/ethgo/jsonrpc" "github.com/umbracle/ethgo/wallet" + + "github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi" + "github.com/0xPolygon/polygon-edge/e2e-polybft/framework" + "github.com/0xPolygon/polygon-edge/txrelayer" + "github.com/0xPolygon/polygon-edge/types" ) func TestE2E_TxPool_Transfer(t *testing.T) { @@ -48,17 +49,27 @@ func TestE2E_TxPool_Transfer(t *testing.T) { go func(i int, to ethgo.Address) { defer wg.Done() - gasPrice, err := client.GasPrice() - require.NoError(t, err) - txn := ðgo.Transaction{ - From: sender.Address(), - To: &to, - GasPrice: gasPrice, - Gas: 30000, // enough to send a transfer - Value: big.NewInt(int64(sendAmount)), - Nonce: uint64(i), + From: sender.Address(), + To: &to, + Gas: 30000, // enough to send a transfer + Value: big.NewInt(int64(sendAmount)), + Nonce: uint64(i), + } + + // Send every second transaction as a dynamic fees one + if i%2 == 0 { + txn.Type = ethgo.TransactionDynamicFee + 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 } + sendTransaction(t, client, sender, txn) }(i, receivers[i]) } @@ -112,6 +123,17 @@ func TestE2E_TxPool_Transfer_Linear(t *testing.T) { return err } + populateTxFees := func(txn *ethgo.Transaction, i int) { + if i%2 == 0 { + txn.Type = ethgo.TransactionDynamicFee + txn.MaxFeePerGas = big.NewInt(1000000000) + txn.MaxPriorityFeePerGas = big.NewInt(100000000) + } else { + txn.Type = ethgo.TransactionLegacy + txn.GasPrice = gasPrice + } + } + num := 4 receivers := []*wallet.Key{ premine, @@ -124,11 +146,7 @@ func TestE2E_TxPool_Transfer_Linear(t *testing.T) { receivers = append(receivers, key) } - // Gas cost is always the same since value transfers are deterministic (21k gas). - // Then, the total gas cost required to make a transfer is 21k multiplied by - // the selected gas price. - gasCost := int(21000 * gasPrice) - sendAmount := 3000000 + const sendAmount = 3000000 // We are going to fund the accounts in linear fashion: // A (premined account) -> B -> C -> D -> E @@ -139,15 +157,23 @@ func TestE2E_TxPool_Transfer_Linear(t *testing.T) { // its child i+1 (cover costs + send amounts). // This means that since gasCost and sendAmount are fixed, account C must receive gasCost * 2 // (to cover two more transfers C->D and D->E) + sendAmount * 3 (one bundle for each C,D and E). - amount := gasCost*(num-i-1) + sendAmount*(num-i) recipient := receivers[i].Address() txn := ðgo.Transaction{ - Value: big.NewInt(int64(amount)), - To: &recipient, - GasPrice: gasPrice, - Gas: 21000, - Nonce: 0, + Value: big.NewInt(int64(sendAmount * (num - i))), + To: &recipient, + Gas: 21000, + } + + // Populate fees fields for the current transaction + 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)) } + sendTransaction(t, client, receivers[i-1], txn) err := waitUntilBalancesChanged(receivers[i].Address()) @@ -186,6 +212,85 @@ func TestE2E_TxPool_TransactionWithHeaderInstuctions(t *testing.T) { require.NoError(t, cluster.WaitForBlock(10, 1*time.Minute)) } +// TestE2E_TxPool_BroadcastTransactions sends several transactions (legacy and dynamic fees) to the cluster +// with the 1 amount of eth and checks that all cluster nodes have the recipient balance updated. +func TestE2E_TxPool_BroadcastTransactions(t *testing.T) { + var ( + sendAmount = ethgo.Ether(1) + ) + + const ( + txNum = 10 + ) + + // Create recipient key + key, err := wallet.GenerateKey() + assert.NoError(t, err) + + recipient := key.Address() + + t.Logf("Recipient %s\n", recipient) + + // Create pre-mined balance for sender + sender, err := wallet.GenerateKey() + require.NoError(t, err) + + // First account should have some matics premined + cluster := framework.NewTestCluster(t, 5, + framework.WithPremine(types.Address(sender.Address())), + ) + defer cluster.Stop() + + // Wait until the cluster is up and running + cluster.WaitForReady(t) + + client := cluster.Servers[0].JSONRPC().Eth() + + sentAmount := new(big.Int) + nonce := uint64(0) + + for i := 0; i < txNum; i++ { + txn := ðgo.Transaction{ + Value: sendAmount, + To: &recipient, + Gas: 21000, + Nonce: nonce, + } + + if i%2 == 0 { + txn.Type = ethgo.TransactionDynamicFee + 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 + } + + sendTransaction(t, client, sender, txn) + sentAmount = sentAmount.Add(sentAmount, txn.Value) + nonce++ + } + + // Wait until the balance has changed on all nodes in the cluster + err = cluster.WaitUntil(time.Minute, time.Second*3, func() bool { + for _, srv := range cluster.Servers { + balance, err := srv.WaitForNonZeroBalance(recipient, time.Second*10) + assert.NoError(t, err) + if balance != nil && balance.BitLen() > 0 { + assert.Equal(t, sentAmount, balance) + } else { + return false + } + } + + return true + }) + assert.NoError(t, err) +} + // sendTransaction is a helper function which signs transaction with provided private key and sends it func sendTransaction(t *testing.T, client *jsonrpc.Eth, sender *wallet.Key, txn *ethgo.Transaction) { t.Helper() @@ -193,6 +298,10 @@ func sendTransaction(t *testing.T, client *jsonrpc.Eth, sender *wallet.Key, txn chainID, err := client.ChainID() require.NoError(t, err) + if txn.Type == ethgo.TransactionDynamicFee { + txn.ChainID = chainID + } + signer := wallet.NewEIP155Signer(chainID.Uint64()) signedTxn, err := signer.SignTx(txn, sender) require.NoError(t, err) @@ -203,3 +312,15 @@ func sendTransaction(t *testing.T, client *jsonrpc.Eth, sender *wallet.Key, txn _, err = client.SendRawTransaction(txnRaw) require.NoError(t, err) } + +func txCost(t *ethgo.Transaction) *big.Int { + var factor *big.Int + + if t.Type == ethgo.TransactionDynamicFee { + factor = new(big.Int).Set(t.MaxFeePerGas) + } else { + factor = new(big.Int).SetUint64(t.GasPrice) + } + + return new(big.Int).Mul(factor, new(big.Int).SetUint64(t.Gas)) +} diff --git a/e2e-polybft/framework/test-cluster.go b/e2e-polybft/framework/test-cluster.go index 8795630df9..f990df32d0 100644 --- a/e2e-polybft/framework/test-cluster.go +++ b/e2e-polybft/framework/test-cluster.go @@ -80,6 +80,7 @@ type TestClusterConfig struct { LogsDir string TmpDir string BlockGasLimit uint64 + BurnContracts map[uint64]types.Address ValidatorPrefix string Binary string ValidatorSetSize uint64 @@ -250,6 +251,16 @@ func WithBlockGasLimit(blockGasLimit uint64) ClusterOption { } } +func WithBurnContract(block uint64, address types.Address) ClusterOption { + return func(h *TestClusterConfig) { + if h.BurnContracts == nil { + h.BurnContracts = map[uint64]types.Address{} + } + + h.BurnContracts[block] = address + } +} + func WithNumBlockConfirmations(numBlockConfirmations uint64) ClusterOption { return func(h *TestClusterConfig) { h.NumBlockConfirmations = numBlockConfirmations @@ -429,6 +440,15 @@ func NewTestCluster(t *testing.T, validatorsCount int, opts ...ClusterOption) *T args = append(args, "--mintable-native-token") } + if len(cluster.Config.BurnContracts) != 0 { + for block, addr := range cluster.Config.BurnContracts { + args = append(args, "--burn-contract", fmt.Sprintf("%d:%s", block, addr)) + } + } else { + // London hardfork is enabled by default so there must be a default burn contract + args = append(args, "--burn-contract", "0:0x0000000000000000000000000000000000000000") + } + validators, err := genesis.ReadValidatorsByPrefix( cluster.Config.TmpDir, cluster.Config.ValidatorPrefix) require.NoError(t, err) diff --git a/e2e/framework/config.go b/e2e/framework/config.go index 85ee0eb060..aed0eab16c 100644 --- a/e2e/framework/config.go +++ b/e2e/framework/config.go @@ -50,17 +50,19 @@ type TestServerConfig struct { EpochSize uint64 // The epoch size in blocks for the IBFT layer BlockGasLimit uint64 // Block gas limit BlockGasTarget uint64 // Gas target for new blocks + BaseFee uint64 // Initial base fee ShowsLog bool // Flag specifying if logs are shown Name string // Name of the server SaveLogs bool // Flag specifying if logs are saved LogsDir string // Directory where logs are saved IsPos bool // Specifies the mechanism used for IBFT (PoA / PoS) - Signer *crypto.EIP155Signer // Signer used for transactions + Signer crypto.TxSigner // Signer used for transactions MinValidatorCount uint64 // Min validator count MaxValidatorCount uint64 // Max validator count BlockTime uint64 // Minimum block generation time (in s) IBFTBaseTimeout uint64 // Base Timeout in seconds for IBFT PredeployParams *PredeployParams + BurnContracts map[uint64]types.Address } func (t *TestServerConfig) SetPredeployParams(params *PredeployParams) { @@ -77,7 +79,7 @@ func (t *TestServerConfig) DataDir() string { } } -func (t *TestServerConfig) SetSigner(signer *crypto.EIP155Signer) { +func (t *TestServerConfig) SetSigner(signer crypto.TxSigner) { t.Signer = signer } @@ -118,6 +120,15 @@ func (t *TestServerConfig) SetBlockGasTarget(target uint64) { t.BlockGasTarget = target } +// SetBurnContract sets the given burn contract for the test server +func (t *TestServerConfig) SetBurnContract(block uint64, address types.Address) { + if t.BurnContracts == nil { + t.BurnContracts = map[uint64]types.Address{} + } + + t.BurnContracts[block] = address +} + // SetConsensus callback sets consensus func (t *TestServerConfig) SetConsensus(c ConsensusType) { t.Consensus = c diff --git a/e2e/framework/testserver.go b/e2e/framework/testserver.go index 3d215023f0..b62853ffac 100644 --- a/e2e/framework/testserver.go +++ b/e2e/framework/testserver.go @@ -21,11 +21,19 @@ import ( "testing" "time" - "github.com/0xPolygon/polygon-edge/chain" - "github.com/0xPolygon/polygon-edge/command/genesis/predeploy" + "github.com/hashicorp/go-hclog" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/umbracle/ethgo" + "github.com/umbracle/ethgo/jsonrpc" + "github.com/umbracle/ethgo/wallet" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + empty "google.golang.org/protobuf/types/known/emptypb" + "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/command" "github.com/0xPolygon/polygon-edge/command/genesis" + "github.com/0xPolygon/polygon-edge/command/genesis/predeploy" ibftSwitch "github.com/0xPolygon/polygon-edge/command/ibft/switch" initCmd "github.com/0xPolygon/polygon-edge/command/secrets/init" "github.com/0xPolygon/polygon-edge/command/server" @@ -41,14 +49,6 @@ import ( txpoolProto "github.com/0xPolygon/polygon-edge/txpool/proto" "github.com/0xPolygon/polygon-edge/types" "github.com/0xPolygon/polygon-edge/validators" - "github.com/hashicorp/go-hclog" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/umbracle/ethgo" - "github.com/umbracle/ethgo/jsonrpc" - "github.com/umbracle/ethgo/wallet" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - empty "google.golang.org/protobuf/types/known/emptypb" ) type TestServerConfigCallback func(*TestServerConfig) @@ -83,7 +83,7 @@ func NewTestServer(t *testing.T, rootDir string, callback TestServerConfigCallba LibP2PPort: ports[1].Port(), JSONRPCPort: ports[2].Port(), RootDir: rootDir, - Signer: crypto.NewEIP155Signer(chain.AllForksEnabled.At(0), 100), + Signer: crypto.NewSigner(chain.AllForksEnabled.At(0), 100), ValidatorType: validators.ECDSAValidatorType, } @@ -346,6 +346,21 @@ func (t *TestServer) GenerateGenesis() error { blockGasLimit := strconv.FormatUint(t.Config.BlockGasLimit, 10) args = append(args, "--block-gas-limit", blockGasLimit) + // add base fee + if t.Config.BaseFee != 0 { + args = append(args, "--base-fee", *types.EncodeUint64(t.Config.BaseFee)) + } + + // add burn contracts + if len(t.Config.BurnContracts) != 0 { + for block, addr := range t.Config.BurnContracts { + args = append(args, "--burn-contract", fmt.Sprintf("%d:%s", block, addr)) + } + } else { + // london hardfork is enabled by default so there must be a default burn contract + args = append(args, "--burn-contract", "0:0x0000000000000000000000000000000000000000") + } + cmd := exec.Command(resolveBinary(), args...) //nolint:gosec cmd.Dir = t.Config.RootDir diff --git a/e2e/pos_test.go b/e2e/pos_test.go index c8935a3fea..4d8779a184 100644 --- a/e2e/pos_test.go +++ b/e2e/pos_test.go @@ -9,6 +9,11 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/umbracle/ethgo" + "github.com/umbracle/ethgo/jsonrpc" + "github.com/0xPolygon/polygon-edge/chain" ibftOp "github.com/0xPolygon/polygon-edge/consensus/ibft/proto" "github.com/0xPolygon/polygon-edge/contracts/staking" @@ -19,9 +24,6 @@ import ( txpoolOp "github.com/0xPolygon/polygon-edge/txpool/proto" "github.com/0xPolygon/polygon-edge/types" "github.com/golang/protobuf/ptypes/any" - "github.com/stretchr/testify/assert" - "github.com/umbracle/ethgo" - "github.com/umbracle/ethgo/jsonrpc" ) // foundInValidatorSet is a helper function for searching through the passed in set for a specific @@ -358,28 +360,36 @@ func TestPoS_UnstakeExploit(t *testing.T) { // Required default values numTransactions := 5 - signer := crypto.NewEIP155Signer(chain.AllForksEnabled.At(0), 100) + signer := crypto.NewSigner(chain.AllForksEnabled.At(0), 100) currentNonce := 0 // TxPool client clt := srv.TxnPoolOperator() - generateTx := func() *types.Transaction { - signedTx, signErr := signer.SignTx(&types.Transaction{ - Nonce: uint64(currentNonce), - From: types.ZeroAddress, - To: &stakingContractAddr, - GasPrice: bigGasPrice, - Gas: framework.DefaultGasLimit, - Value: big.NewInt(0), - V: big.NewInt(1), // it is necessary to encode in rlp, - Input: framework.MethodSig("unstake"), - }, senderKey) - - if signErr != nil { - t.Fatalf("Unable to sign transaction, %v", signErr) + generateTx := func(i int) *types.Transaction { + unsignedTx := &types.Transaction{ + Nonce: uint64(currentNonce), + From: types.ZeroAddress, + To: &stakingContractAddr, + Gas: framework.DefaultGasLimit, + Value: big.NewInt(0), + V: big.NewInt(1), // it is necessary to encode in rlp, + Input: framework.MethodSig("unstake"), + } + + // Just make very second transaction with dynamic gas fee + if i%2 == 0 { + unsignedTx.Type = types.DynamicFeeTx + unsignedTx.GasFeeCap = bigGasPrice + unsignedTx.GasTipCap = bigGasPrice + } else { + unsignedTx.Type = types.LegacyTx + unsignedTx.GasPrice = bigGasPrice } + signedTx, err := signer.SignTx(unsignedTx, senderKey) + require.NoError(t, err, "Unable to sign transaction") + currentNonce++ return signedTx @@ -390,7 +400,7 @@ func TestPoS_UnstakeExploit(t *testing.T) { for i := 0; i < numTransactions; i++ { var msg *txpoolOp.AddTxnReq - unstakeTxn := generateTx() + unstakeTxn := generateTx(i) msg = &txpoolOp.AddTxnReq{ Raw: &any.Any{ @@ -500,28 +510,36 @@ func TestPoS_StakeUnstakeExploit(t *testing.T) { // Required default values numTransactions := 6 - signer := crypto.NewEIP155Signer(chain.AllForksEnabled.At(0), 100) + signer := crypto.NewSigner(chain.AllForksEnabled.At(0), 100) currentNonce := 0 // TxPool client txpoolClient := srv.TxnPoolOperator() - generateTx := func(value *big.Int, methodName string) *types.Transaction { - signedTx, signErr := signer.SignTx(&types.Transaction{ - Nonce: uint64(currentNonce), - From: types.ZeroAddress, - To: &stakingContractAddr, - GasPrice: bigGasPrice, - Gas: framework.DefaultGasLimit, - Value: value, - V: big.NewInt(1), // it is necessary to encode in rlp - Input: framework.MethodSig(methodName), - }, senderKey) - - if signErr != nil { - t.Fatalf("Unable to sign transaction, %v", signErr) + generateTx := func(i int, value *big.Int, methodName string) *types.Transaction { + unsignedTx := &types.Transaction{ + Nonce: uint64(currentNonce), + From: types.ZeroAddress, + To: &stakingContractAddr, + Gas: framework.DefaultGasLimit, + Value: value, + V: big.NewInt(1), // it is necessary to encode in rlp + Input: framework.MethodSig(methodName), + } + + // Just make very second transaction with dynamic gas fee + if i%2 == 0 { + unsignedTx.Type = types.DynamicFeeTx + unsignedTx.GasFeeCap = bigGasPrice + unsignedTx.GasTipCap = bigGasPrice + } else { + unsignedTx.Type = types.LegacyTx + unsignedTx.GasPrice = bigGasPrice } + signedTx, err := signer.SignTx(unsignedTx, senderKey) + require.NoError(t, err, "Unable to sign transaction") + currentNonce++ return signedTx @@ -535,7 +553,7 @@ func TestPoS_StakeUnstakeExploit(t *testing.T) { var msg *txpoolOp.AddTxnReq if i%2 == 0 { - unstakeTxn := generateTx(zeroEth, "unstake") + unstakeTxn := generateTx(i, zeroEth, "unstake") msg = &txpoolOp.AddTxnReq{ Raw: &any.Any{ Value: unstakeTxn.MarshalRLP(), @@ -543,7 +561,7 @@ func TestPoS_StakeUnstakeExploit(t *testing.T) { From: types.ZeroAddress.String(), } } else { - stakeTxn := generateTx(oneEth, "stake") + stakeTxn := generateTx(i, oneEth, "stake") msg = &txpoolOp.AddTxnReq{ Raw: &any.Any{ Value: stakeTxn.MarshalRLP(), @@ -631,28 +649,35 @@ func TestPoS_StakeUnstakeWithinSameBlock(t *testing.T) { } // Required default values - signer := crypto.NewEIP155Signer(chain.AllForksEnabled.At(0), 100) + signer := crypto.NewSigner(chain.AllForksEnabled.At(0), 100) currentNonce := 0 // TxPool client txpoolClient := srv.TxnPoolOperator() - generateTx := func(value *big.Int, methodName string) *types.Transaction { - signedTx, signErr := signer.SignTx(&types.Transaction{ - Nonce: uint64(currentNonce), - From: types.ZeroAddress, - To: &stakingContractAddr, - GasPrice: bigGasPrice, - Gas: framework.DefaultGasLimit, - Value: value, - V: big.NewInt(1), // it is necessary to encode in rlp - Input: framework.MethodSig(methodName), - }, senderKey) - - if signErr != nil { - t.Fatalf("Unable to sign transaction, %v", signErr) + generateTx := func(dynamicTx bool, value *big.Int, methodName string) *types.Transaction { + unsignedTx := &types.Transaction{ + Nonce: uint64(currentNonce), + From: types.ZeroAddress, + To: &stakingContractAddr, + Gas: framework.DefaultGasLimit, + Value: value, + V: big.NewInt(1), // it is necessary to encode in rlp + Input: framework.MethodSig(methodName), } + if dynamicTx { + unsignedTx.Type = types.DynamicFeeTx + unsignedTx.GasFeeCap = bigGasPrice + unsignedTx.GasTipCap = bigGasPrice + } else { + unsignedTx.Type = types.LegacyTx + unsignedTx.GasPrice = bigGasPrice + } + + signedTx, err := signer.SignTx(unsignedTx, senderKey) + require.NoError(t, err, "Unable to signatransaction") + currentNonce++ return signedTx @@ -663,8 +688,8 @@ func TestPoS_StakeUnstakeWithinSameBlock(t *testing.T) { // addTxn is a helper method for generating and adding a transaction // through the operator command - addTxn := func(value *big.Int, methodName string) { - txn := generateTx(value, methodName) + addTxn := func(dynamicTx bool, value *big.Int, methodName string) { + txn := generateTx(dynamicTx, value, methodName) txnMsg := &txpoolOp.AddTxnReq{ Raw: &any.Any{ Value: txn.MarshalRLP(), @@ -681,10 +706,10 @@ func TestPoS_StakeUnstakeWithinSameBlock(t *testing.T) { } // Stake transaction - addTxn(oneEth, "stake") + addTxn(false, oneEth, "stake") // Unstake transaction - addTxn(zeroEth, "unstake") + addTxn(true, zeroEth, "unstake") // Wait for the transactions to go through totalGasUsed := srv.GetGasTotal(txHashes) diff --git a/e2e/transaction_test.go b/e2e/transaction_test.go index bb5c811771..b088391467 100644 --- a/e2e/transaction_test.go +++ b/e2e/transaction_test.go @@ -12,7 +12,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/umbracle/ethgo" + "github.com/umbracle/ethgo/jsonrpc" "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/contracts/abis" @@ -21,8 +24,6 @@ import ( "github.com/0xPolygon/polygon-edge/helper/tests" "github.com/0xPolygon/polygon-edge/types" "github.com/0xPolygon/polygon-edge/validators" - "github.com/stretchr/testify/assert" - "github.com/umbracle/ethgo/jsonrpc" ) func TestPreminedBalance(t *testing.T) { @@ -271,6 +272,7 @@ func getCount( // IBFT_Loop and Dev_Loop stress tests func generateStressTestTx( t *testing.T, + txNum int, currentNonce uint64, contractAddr types.Address, senderKey *ecdsa.PrivateKey, @@ -278,7 +280,7 @@ func generateStressTestTx( t.Helper() bigGasPrice := big.NewInt(framework.DefaultGasPrice) - signer := crypto.NewEIP155Signer(chain.AllForksEnabled.At(0), 100) + signer := crypto.NewSigner(chain.AllForksEnabled.At(0), 100) setNameMethod, ok := abis.StressTestABI.Methods["setName"] if !ok { @@ -294,21 +296,28 @@ func generateStressTestTx( t.Fatalf("Unable to encode inputs, %v", encodeErr) } - signedTx, signErr := signer.SignTx(&types.Transaction{ - Nonce: currentNonce, - From: types.ZeroAddress, - To: &contractAddr, - GasPrice: bigGasPrice, - Gas: framework.DefaultGasLimit, - Value: big.NewInt(0), - V: big.NewInt(1), // it is necessary to encode in rlp, - Input: append(setNameMethod.ID(), encodedInput...), - }, senderKey) - - if signErr != nil { - t.Fatalf("Unable to sign transaction, %v", signErr) + unsignedTx := &types.Transaction{ + Nonce: currentNonce, + From: types.ZeroAddress, + To: &contractAddr, + Gas: framework.DefaultGasLimit, + Value: big.NewInt(0), + V: big.NewInt(1), // it is necessary to encode in rlp, + Input: append(setNameMethod.ID(), encodedInput...), } + if txNum%2 == 0 { + unsignedTx.Type = types.DynamicFeeTx + unsignedTx.GasFeeCap = bigGasPrice + unsignedTx.GasTipCap = bigGasPrice + } else { + unsignedTx.Type = types.LegacyTx + unsignedTx.GasPrice = bigGasPrice + } + + signedTx, err := signer.SignTx(unsignedTx, senderKey) + require.NoError(t, err, "Unable to sign transaction") + return signedTx } @@ -331,6 +340,7 @@ func addStressTxnsWithHashes( for i := 0; i < numTransactions; i++ { setNameTxn := generateStressTestTx( t, + i, uint64(currentNonce), contractAddr, senderKey, diff --git a/e2e/txpool_test.go b/e2e/txpool_test.go index e78332e603..5ab6d80f9c 100644 --- a/e2e/txpool_test.go +++ b/e2e/txpool_test.go @@ -9,6 +9,9 @@ import ( "time" "github.com/0xPolygon/polygon-edge/chain" + + "github.com/stretchr/testify/require" + "github.com/0xPolygon/polygon-edge/txpool" "github.com/umbracle/ethgo" @@ -23,7 +26,7 @@ import ( var ( oneEth = framework.EthToWei(1) - signer = crypto.NewEIP155Signer(chain.AllForksEnabled.At(0), 100) + signer = crypto.NewSigner(chain.AllForksEnabled.At(0), 100) ) type generateTxReqParams struct { @@ -32,25 +35,34 @@ type generateTxReqParams struct { referenceKey *ecdsa.PrivateKey toAddress types.Address gasPrice *big.Int + gasFeeCap *big.Int + gasTipCap *big.Int value *big.Int t *testing.T } func generateTx(params generateTxReqParams) *types.Transaction { - signedTx, signErr := signer.SignTx(&types.Transaction{ - Nonce: params.nonce, - From: params.referenceAddr, - To: ¶ms.toAddress, - GasPrice: params.gasPrice, - Gas: 1000000, - Value: params.value, - V: big.NewInt(27), // it is necessary to encode in rlp - }, params.referenceKey) - - if signErr != nil { - params.t.Fatalf("Unable to sign transaction, %v", signErr) + unsignedTx := &types.Transaction{ + Nonce: params.nonce, + From: params.referenceAddr, + To: ¶ms.toAddress, + Gas: 1000000, + Value: params.value, + V: big.NewInt(27), // it is necessary to encode in rlp } + if params.gasPrice != nil { + unsignedTx.Type = types.LegacyTx + unsignedTx.GasPrice = params.gasPrice + } else { + unsignedTx.Type = types.DynamicFeeTx + unsignedTx.GasFeeCap = params.gasFeeCap + unsignedTx.GasTipCap = params.gasTipCap + } + + signedTx, err := signer.SignTx(unsignedTx, params.referenceKey) + require.NoError(params.t, err, "Unable to sign transaction") + return signedTx } @@ -67,33 +79,64 @@ func generateReq(params generateTxReqParams) *txpoolOp.AddTxnReq { func TestTxPool_ErrorCodes(t *testing.T) { gasPrice := big.NewInt(10000) + gasFeeCap := big.NewInt(1000000000) + gasTipCap := big.NewInt(100000000) devInterval := 5 testTable := []struct { name string defaultBalance *big.Int txValue *big.Int + gasPrice *big.Int + gasFeeCap *big.Int + gasTipCap *big.Int expectedError error }{ { // Test scenario: + // Add legacy tx with nonce 0 + // -> Check if tx has been parsed // Add tx with nonce 0 + // -> tx shouldn't be added, since the nonce is too low + name: "ErrNonceTooLow - legacy", + defaultBalance: framework.EthToWei(10), + txValue: oneEth, + gasPrice: gasPrice, + expectedError: txpool.ErrNonceTooLow, + }, + { + // Test scenario: + // Add dynamic fee tx with nonce 0 // -> Check if tx has been parsed // Add tx with nonce 0 // -> tx shouldn't be added, since the nonce is too low - "ErrNonceTooLow", - framework.EthToWei(10), - oneEth, - txpool.ErrNonceTooLow, + name: "ErrNonceTooLow - dynamic fees", + defaultBalance: framework.EthToWei(10), + txValue: oneEth, + gasFeeCap: gasFeeCap, + gasTipCap: gasTipCap, + expectedError: txpool.ErrNonceTooLow, }, { // Test scenario: - // Add tx with insufficient funds + // Add legacy tx with insufficient funds // -> Tx should be discarded because of low funds - "ErrInsufficientFunds", - framework.EthToWei(1), - framework.EthToWei(5), - txpool.ErrInsufficientFunds, + name: "ErrInsufficientFunds - legacy", + defaultBalance: framework.EthToWei(1), + txValue: framework.EthToWei(5), + gasPrice: gasPrice, + expectedError: txpool.ErrInsufficientFunds, + }, + { + // Test scenario: + // Add dynamic fee tx with insufficient funds + // -> Tx should be discarded because of low funds + name: "ErrInsufficientFunds - dynamic fee", + defaultBalance: framework.EthToWei(1), + txValue: framework.EthToWei(5), + gasFeeCap: gasFeeCap, + gasTipCap: gasTipCap, + expectedError: txpool.ErrInsufficientFunds, }, } @@ -119,7 +162,9 @@ func TestTxPool_ErrorCodes(t *testing.T) { referenceAddr: referenceAddr, referenceKey: referenceKey, toAddress: toAddress, - gasPrice: gasPrice, + gasPrice: testCase.gasPrice, + gasFeeCap: testCase.gasFeeCap, + gasTipCap: testCase.gasTipCap, value: testCase.txValue, t: t, }) @@ -188,7 +233,7 @@ func TestTxPool_TransactionCoalescing(t *testing.T) { client := srv.JSONRPC() // Required default values - signer := crypto.NewEIP155Signer(chain.AllForksEnabled.At(0), 100) + signer := crypto.NewEIP155Signer(100, true) // TxPool client clt := srv.TxnPoolOperator() @@ -340,7 +385,7 @@ func TestTxPool_RecoverableError(t *testing.T) { // 1. Send a first valid transaction with gasLimit = block gas limit - 1 // // 2. Send a second transaction with gasLimit = block gas limit / 2. Since there is not enough gas remaining, - // the transaction will be pushed back to the pending queue so that is can be executed in the next block. + // the transaction will be pushed back to the pending queue so that can be executed in the next block. // // 3. Send a third - valid - transaction, both the previous one and this one should be executed. // @@ -367,13 +412,15 @@ func TestTxPool_RecoverableError(t *testing.T) { From: senderAddress, }, { - Nonce: 2, - GasPrice: big.NewInt(framework.DefaultGasPrice), - Gas: 22000, - To: &receiverAddress, - Value: oneEth, - V: big.NewInt(27), - From: senderAddress, + Type: types.DynamicFeeTx, + Nonce: 2, + GasFeeCap: big.NewInt(10000000000), + GasTipCap: big.NewInt(1000000000), + Gas: 22000, + To: &receiverAddress, + Value: oneEth, + V: big.NewInt(27), + From: senderAddress, }, } diff --git a/helper/common/common.go b/helper/common/common.go index a570e41f56..aaacf45a2f 100644 --- a/helper/common/common.go +++ b/helper/common/common.go @@ -13,11 +13,11 @@ import ( "os/user" "path/filepath" "strconv" + "strings" "syscall" "time" "github.com/0xPolygon/polygon-edge/helper/hex" - "github.com/0xPolygon/polygon-edge/types" ) var ( @@ -48,12 +48,21 @@ func Max(a, b uint64) uint64 { return b } +// BigMin returns the smallest of x or y. +func BigMin(x, y *big.Int) *big.Int { + if x.Cmp(y) > 0 { + return y + } + + return x +} + func ConvertUnmarshalledUint(x interface{}) (uint64, error) { switch tx := x.(type) { case float64: return uint64(roundFloat(tx)), nil case string: - v, err := types.ParseUint64orHex(&tx) + v, err := ParseUint64orHex(&tx) if err != nil { return 0, err } @@ -64,14 +73,26 @@ func ConvertUnmarshalledUint(x interface{}) (uint64, error) { } } -func roundFloat(num float64) int64 { - return int64(num + math.Copysign(0.5, num)) -} +// ParseUint64orHex parses the given uint64 hex string into the number. +// It can parse the string with 0x prefix as well. +func ParseUint64orHex(val *string) (uint64, error) { + if val == nil { + return 0, nil + } + + str := *val + base := 10 -func ToFixedFloat(num float64, precision int) float64 { - output := math.Pow(10, float64(precision)) + if strings.HasPrefix(str, "0x") { + str = str[2:] + base = 16 + } - return float64(roundFloat(num*output)) / output + return strconv.ParseUint(str, base, 64) +} + +func roundFloat(num float64) int64 { + return int64(num + math.Copysign(0.5, num)) } // SetupDataDir sets up the data directory and the corresponding sub-directories diff --git a/helper/keccak/pool.go b/helper/keccak/pool.go index cf0bcc64be..d6934e9068 100644 --- a/helper/keccak/pool.go +++ b/helper/keccak/pool.go @@ -53,3 +53,13 @@ func Keccak256Rlp(dst []byte, src *fastrlp.Value) []byte { return dst } + +// PrefixedKeccak256Rlp hashes a fastrlp.Value using keccak-256 with the given prefix +func PrefixedKeccak256Rlp(prefix, dst []byte, src *fastrlp.Value) []byte { + h := DefaultKeccakPool.Get() + _, _ = h.Write(prefix) + dst = h.WriteRlp(dst, src) + DefaultKeccakPool.Put(h) + + return dst +} diff --git a/helper/tests/testing.go b/helper/tests/testing.go index 64971c065a..ce91641857 100644 --- a/helper/tests/testing.go +++ b/helper/tests/testing.go @@ -16,7 +16,6 @@ import ( "github.com/multiformats/go-multiaddr" "github.com/umbracle/ethgo" - "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/crypto" txpoolOp "github.com/0xPolygon/polygon-edge/txpool/proto" "github.com/0xPolygon/polygon-edge/types" @@ -237,7 +236,7 @@ type GenerateTxReqParams struct { } func generateTx(params GenerateTxReqParams) (*types.Transaction, error) { - signer := crypto.NewEIP155Signer(chain.AllForksEnabled.At(0), 100) + signer := crypto.NewEIP155Signer(100, true) signedTx, signErr := signer.SignTx(&types.Transaction{ Nonce: params.Nonce, diff --git a/jsonrpc/debug_endpoint_test.go b/jsonrpc/debug_endpoint_test.go index cd3828a4d9..5fd5aa6441 100644 --- a/jsonrpc/debug_endpoint_test.go +++ b/jsonrpc/debug_endpoint_test.go @@ -562,35 +562,41 @@ func TestTraceCall(t *testing.T) { t.Parallel() var ( - from = types.StringToAddress("1") - to = types.StringToAddress("2") - gas = argUint64(10000) - gasPrice = argBytes(new(big.Int).SetUint64(10).Bytes()) - value = argBytes(new(big.Int).SetUint64(1000).Bytes()) - data = argBytes([]byte("data")) - input = argBytes([]byte("input")) - nonce = argUint64(1) + from = types.StringToAddress("1") + to = types.StringToAddress("2") + gas = argUint64(10000) + gasPrice = argBytes(new(big.Int).SetUint64(10).Bytes()) + gasTipCap = argBytes(new(big.Int).SetUint64(10).Bytes()) + gasFeeCap = argBytes(new(big.Int).SetUint64(10).Bytes()) + value = argBytes(new(big.Int).SetUint64(1000).Bytes()) + data = argBytes([]byte("data")) + input = argBytes([]byte("input")) + nonce = argUint64(1) blockNumber = BlockNumber(testBlock10.Number()) txArg = &txnArgs{ - From: &from, - To: &to, - Gas: &gas, - GasPrice: &gasPrice, - Value: &value, - Data: &data, - Input: &input, - Nonce: &nonce, + From: &from, + To: &to, + Gas: &gas, + GasPrice: &gasPrice, + GasTipCap: &gasTipCap, + GasFeeCap: &gasFeeCap, + Value: &value, + Data: &data, + Input: &input, + Nonce: &nonce, } decodedTx = &types.Transaction{ - Nonce: uint64(nonce), - GasPrice: new(big.Int).SetBytes([]byte(gasPrice)), - Gas: uint64(gas), - To: &to, - Value: new(big.Int).SetBytes([]byte(value)), - Input: data, - From: from, + Nonce: uint64(nonce), + GasPrice: new(big.Int).SetBytes([]byte(gasPrice)), + GasTipCap: new(big.Int).SetBytes([]byte(gasTipCap)), + GasFeeCap: new(big.Int).SetBytes([]byte(gasFeeCap)), + Gas: uint64(gas), + To: &to, + Value: new(big.Int).SetBytes([]byte(value)), + Input: data, + From: from, } ) diff --git a/jsonrpc/eth_endpoint_test.go b/jsonrpc/eth_endpoint_test.go index 696bed3a1d..55402e0414 100644 --- a/jsonrpc/eth_endpoint_test.go +++ b/jsonrpc/eth_endpoint_test.go @@ -35,22 +35,26 @@ func TestEth_DecodeTxn(t *testing.T) { { name: "should be successful", arg: &txnArgs{ - From: &addr1, - To: &addr2, - Gas: toArgUint64Ptr(21000), - GasPrice: toArgBytesPtr(big.NewInt(10000).Bytes()), - Value: toArgBytesPtr(oneEther.Bytes()), - Data: nil, - Nonce: toArgUint64Ptr(0), + From: &addr1, + To: &addr2, + Gas: toArgUint64Ptr(21000), + GasPrice: toArgBytesPtr(big.NewInt(10000).Bytes()), + GasTipCap: toArgBytesPtr(big.NewInt(10000).Bytes()), + GasFeeCap: toArgBytesPtr(big.NewInt(10000).Bytes()), + Value: toArgBytesPtr(oneEther.Bytes()), + Data: nil, + Nonce: toArgUint64Ptr(0), }, res: &types.Transaction{ - From: addr1, - To: &addr2, - Gas: 21000, - GasPrice: big.NewInt(10000), - Value: oneEther, - Input: []byte{}, - Nonce: 0, + From: addr1, + To: &addr2, + Gas: 21000, + GasPrice: big.NewInt(10000), + GasTipCap: big.NewInt(10000), + GasFeeCap: big.NewInt(10000), + Value: oneEther, + Input: []byte{}, + Nonce: 0, }, err: nil, }, @@ -64,13 +68,15 @@ func TestEth_DecodeTxn(t *testing.T) { Data: nil, }, res: &types.Transaction{ - From: types.ZeroAddress, - To: &addr2, - Gas: 21000, - GasPrice: big.NewInt(10000), - Value: oneEther, - Input: []byte{}, - Nonce: 0, + From: types.ZeroAddress, + To: &addr2, + Gas: 21000, + GasPrice: big.NewInt(10000), + GasTipCap: new(big.Int), + GasFeeCap: new(big.Int), + Value: oneEther, + Input: []byte{}, + Nonce: 0, }, err: nil, }, @@ -90,13 +96,15 @@ func TestEth_DecodeTxn(t *testing.T) { Data: nil, }, res: &types.Transaction{ - From: addr1, - To: &addr2, - Gas: 21000, - GasPrice: big.NewInt(10000), - Value: oneEther, - Input: []byte{}, - Nonce: 10, + From: addr1, + To: &addr2, + Gas: 21000, + GasPrice: big.NewInt(10000), + GasTipCap: new(big.Int), + GasFeeCap: new(big.Int), + Value: oneEther, + Input: []byte{}, + Nonce: 10, }, err: nil, }, @@ -111,13 +119,15 @@ func TestEth_DecodeTxn(t *testing.T) { Nonce: toArgUint64Ptr(1), }, res: &types.Transaction{ - From: addr1, - To: &addr2, - Gas: 21000, - GasPrice: big.NewInt(10000), - Value: new(big.Int).SetBytes([]byte{}), - Input: []byte{}, - Nonce: 1, + From: addr1, + To: &addr2, + Gas: 21000, + GasPrice: big.NewInt(10000), + GasTipCap: new(big.Int), + GasFeeCap: new(big.Int), + Value: new(big.Int).SetBytes([]byte{}), + Input: []byte{}, + Nonce: 1, }, err: nil, }, @@ -131,13 +141,15 @@ func TestEth_DecodeTxn(t *testing.T) { Nonce: toArgUint64Ptr(1), }, res: &types.Transaction{ - From: addr1, - To: &addr2, - Gas: 0, - GasPrice: big.NewInt(10000), - Value: new(big.Int).SetBytes([]byte{}), - Input: []byte{}, - Nonce: 1, + From: addr1, + To: &addr2, + Gas: 0, + GasPrice: big.NewInt(10000), + GasTipCap: new(big.Int), + GasFeeCap: new(big.Int), + Value: new(big.Int).SetBytes([]byte{}), + Input: []byte{}, + Nonce: 1, }, err: nil, }, diff --git a/jsonrpc/helper.go b/jsonrpc/helper.go index 2bdd7be6fa..254eb0d022 100644 --- a/jsonrpc/helper.go +++ b/jsonrpc/helper.go @@ -189,6 +189,14 @@ func DecodeTxn(arg *txnArgs, store nonceGetter) (*types.Transaction, error) { arg.GasPrice = argBytesPtr([]byte{}) } + if arg.GasTipCap == nil { + arg.GasTipCap = argBytesPtr([]byte{}) + } + + if arg.GasFeeCap == nil { + arg.GasFeeCap = argBytesPtr([]byte{}) + } + var input []byte if arg.Data != nil { input = *arg.Data @@ -209,13 +217,16 @@ func DecodeTxn(arg *txnArgs, store nonceGetter) (*types.Transaction, error) { } txn := &types.Transaction{ - From: *arg.From, - Gas: uint64(*arg.Gas), - GasPrice: new(big.Int).SetBytes(*arg.GasPrice), - Value: new(big.Int).SetBytes(*arg.Value), - Input: input, - Nonce: uint64(*arg.Nonce), + From: *arg.From, + Gas: uint64(*arg.Gas), + GasPrice: new(big.Int).SetBytes(*arg.GasPrice), + GasTipCap: new(big.Int).SetBytes(*arg.GasTipCap), + GasFeeCap: new(big.Int).SetBytes(*arg.GasFeeCap), + Value: new(big.Int).SetBytes(*arg.Value), + Input: input, + Nonce: uint64(*arg.Nonce), } + if arg.To != nil { txn.To = arg.To } diff --git a/jsonrpc/helper_test.go b/jsonrpc/helper_test.go index 56b67b14c6..2e1cbf1f91 100644 --- a/jsonrpc/helper_test.go +++ b/jsonrpc/helper_test.go @@ -630,6 +630,8 @@ func TestDecodeTxn(t *testing.T) { to = types.StringToAddress("2") gas = argUint64(uint64(1)) gasPrice = argBytes(new(big.Int).SetUint64(2).Bytes()) + gasTipCap = argBytes(new(big.Int).SetUint64(2).Bytes()) + gasFeeCap = argBytes(new(big.Int).SetUint64(2).Bytes()) value = argBytes(new(big.Int).SetUint64(4).Bytes()) data = argBytes(new(big.Int).SetUint64(8).Bytes()) input = argBytes(new(big.Int).SetUint64(16).Bytes()) @@ -649,23 +651,27 @@ func TestDecodeTxn(t *testing.T) { { name: "should return mapped transaction", arg: &txnArgs{ - From: &from, - To: &to, - Gas: &gas, - GasPrice: &gasPrice, - Value: &value, - Input: &input, - Nonce: &nonce, + From: &from, + To: &to, + Gas: &gas, + GasPrice: &gasPrice, + GasTipCap: &gasTipCap, + GasFeeCap: &gasFeeCap, + Value: &value, + Input: &input, + Nonce: &nonce, }, store: &debugEndpointMockStore{}, expected: &types.Transaction{ - From: from, - To: &to, - Gas: uint64(gas), - GasPrice: new(big.Int).SetBytes([]byte(gasPrice)), - Value: new(big.Int).SetBytes([]byte(value)), - Input: input, - Nonce: uint64(nonce), + From: from, + To: &to, + Gas: uint64(gas), + GasPrice: new(big.Int).SetBytes([]byte(gasPrice)), + GasTipCap: new(big.Int).SetBytes([]byte(gasTipCap)), + GasFeeCap: new(big.Int).SetBytes([]byte(gasFeeCap)), + Value: new(big.Int).SetBytes([]byte(value)), + Input: input, + Nonce: uint64(nonce), }, err: false, }, @@ -681,13 +687,15 @@ func TestDecodeTxn(t *testing.T) { }, store: &debugEndpointMockStore{}, expected: &types.Transaction{ - From: types.ZeroAddress, - To: &to, - Gas: uint64(gas), - GasPrice: new(big.Int).SetBytes([]byte(gasPrice)), - Value: new(big.Int).SetBytes([]byte(value)), - Input: input, - Nonce: uint64(0), + From: types.ZeroAddress, + To: &to, + Gas: uint64(gas), + GasPrice: new(big.Int).SetBytes([]byte(gasPrice)), + GasTipCap: new(big.Int), + GasFeeCap: new(big.Int), + Value: new(big.Int).SetBytes([]byte(value)), + Input: input, + Nonce: uint64(0), }, err: false, }, @@ -714,13 +722,15 @@ func TestDecodeTxn(t *testing.T) { }, }, expected: &types.Transaction{ - From: from, - To: &to, - Gas: uint64(gas), - GasPrice: new(big.Int).SetBytes([]byte(gasPrice)), - Value: new(big.Int).SetBytes([]byte(value)), - Input: input, - Nonce: uint64(stateNonce), + From: from, + To: &to, + Gas: uint64(gas), + GasPrice: new(big.Int).SetBytes([]byte(gasPrice)), + GasTipCap: new(big.Int), + GasFeeCap: new(big.Int), + Value: new(big.Int).SetBytes([]byte(value)), + Input: input, + Nonce: uint64(stateNonce), }, err: false, }, @@ -738,13 +748,15 @@ func TestDecodeTxn(t *testing.T) { }, store: &debugEndpointMockStore{}, expected: &types.Transaction{ - From: from, - To: &to, - Gas: uint64(gas), - GasPrice: new(big.Int).SetBytes([]byte(gasPrice)), - Value: new(big.Int).SetBytes([]byte(value)), - Input: data, - Nonce: uint64(nonce), + From: from, + To: &to, + Gas: uint64(gas), + GasPrice: new(big.Int).SetBytes([]byte(gasPrice)), + GasTipCap: new(big.Int), + GasFeeCap: new(big.Int), + Value: new(big.Int).SetBytes([]byte(value)), + Input: data, + Nonce: uint64(nonce), }, err: false, }, @@ -757,13 +769,15 @@ func TestDecodeTxn(t *testing.T) { }, store: &debugEndpointMockStore{}, expected: &types.Transaction{ - From: from, - To: &to, - Gas: uint64(0), - GasPrice: new(big.Int), - Value: new(big.Int), - Input: []byte{}, - Nonce: uint64(nonce), + From: from, + To: &to, + Gas: uint64(0), + GasPrice: new(big.Int), + GasTipCap: new(big.Int), + GasFeeCap: new(big.Int), + Value: new(big.Int), + Input: []byte{}, + Nonce: uint64(nonce), }, err: false, }, diff --git a/jsonrpc/testsuite/block-empty.json b/jsonrpc/testsuite/block-empty.json index 3c77c97506..55408d0795 100644 --- a/jsonrpc/testsuite/block-empty.json +++ b/jsonrpc/testsuite/block-empty.json @@ -18,5 +18,6 @@ "nonce": "0x0a00000000000000", "hash": "0x0800000000000000000000000000000000000000000000000000000000000000", "transactions": null, - "uncles": null -} \ No newline at end of file + "uncles": null, + "baseFee": "0xf" +} diff --git a/jsonrpc/testsuite/block-with-no-basefee.json b/jsonrpc/testsuite/block-with-no-basefee.json new file mode 100644 index 0000000000..293c7cb582 --- /dev/null +++ b/jsonrpc/testsuite/block-with-no-basefee.json @@ -0,0 +1,22 @@ +{ + "parentHash": "0x0100000000000000000000000000000000000000000000000000000000000000", + "sha3Uncles": "0x0200000000000000000000000000000000000000000000000000000000000000", + "miner": "0x0100000000000000000000000000000000000000", + "stateRoot": "0x0400000000000000000000000000000000000000000000000000000000000000", + "transactionsRoot": "0x0500000000000000000000000000000000000000000000000000000000000000", + "receiptsRoot": "0x0600000000000000000000000000000000000000000000000000000000000000", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "difficulty": "0xa", + "totalDifficulty": "0x0", + "size": "0x0", + "number": "0xb", + "gasLimit": "0xc", + "gasUsed": "0xd", + "timestamp": "0xe", + "extraData": "0x616263646566", + "mixHash": "0x0700000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0a00000000000000", + "hash": "0x0800000000000000000000000000000000000000000000000000000000000000", + "transactions": null, + "uncles": null +} diff --git a/jsonrpc/testsuite/block-with-txn-bodies.json b/jsonrpc/testsuite/block-with-txn-bodies.json index e6435a01af..663484d23d 100644 --- a/jsonrpc/testsuite/block-with-txn-bodies.json +++ b/jsonrpc/testsuite/block-with-txn-bodies.json @@ -35,5 +35,6 @@ "transactionIndex": "0x2" } ], - "uncles": null -} \ No newline at end of file + "uncles": null, + "baseFee": "0xf" +} diff --git a/jsonrpc/testsuite/block-with-txn-hashes.json b/jsonrpc/testsuite/block-with-txn-hashes.json index e3cebad1b7..5925537af4 100644 --- a/jsonrpc/testsuite/block-with-txn-hashes.json +++ b/jsonrpc/testsuite/block-with-txn-hashes.json @@ -20,5 +20,6 @@ "transactions": [ "0x0800000000000000000000000000000000000000000000000000000000000000" ], - "uncles": null -} \ No newline at end of file + "uncles": null, + "baseFee": "0xf" +} diff --git a/jsonrpc/testsuite/transaction-eip1559.json b/jsonrpc/testsuite/transaction-eip1559.json new file mode 100644 index 0000000000..7f870d3f11 --- /dev/null +++ b/jsonrpc/testsuite/transaction-eip1559.json @@ -0,0 +1,18 @@ +{ + "nonce": "0x1", + "gasPrice": "0xa", + "gasTipCap": "0xa", + "gasFeeCap": "0xa", + "gas": "0x64", + "to": "0x0000000000000000000000000000000000000000", + "value": "0x3e8", + "input": "0x0102", + "v": "0x1", + "r": "0x2", + "s": "0x3", + "hash": "0x0200000000000000000000000000000000000000000000000000000000000000", + "from": "0x0300000000000000000000000000000000000000", + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "blockNumber": "0x1", + "transactionIndex": "0x2" +} \ No newline at end of file diff --git a/jsonrpc/txpool_endpoint.go b/jsonrpc/txpool_endpoint.go index f786df059f..0d91a06ec7 100644 --- a/jsonrpc/txpool_endpoint.go +++ b/jsonrpc/txpool_endpoint.go @@ -41,6 +41,8 @@ type StatusResponse struct { type txpoolTransaction struct { Nonce argUint64 `json:"nonce"` GasPrice argBig `json:"gasPrice"` + GasFeeCap *argBig `json:"gasFeeCap,omitempty"` + GasTipCap *argBig `json:"gasTipCap,omitempty"` Gas argUint64 `json:"gas"` To *types.Address `json:"to"` Value argBig `json:"value"` @@ -53,9 +55,23 @@ type txpoolTransaction struct { } func toTxPoolTransaction(t *types.Transaction) *txpoolTransaction { + var gasTipCap, gasFeeCap *argBig + + if t.GasTipCap != nil { + gasTipCapVal := argBig(*t.GasTipCap) + gasTipCap = &gasTipCapVal + } + + if t.GasFeeCap != nil { + gasFeeCapVal := argBig(*t.GasFeeCap) + gasFeeCap = &gasFeeCapVal + } + return &txpoolTransaction{ Nonce: argUint64(t.Nonce), GasPrice: argBig(*t.GasPrice), + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, Gas: argUint64(t.Gas), To: t.To, Value: argBig(*t.Value), diff --git a/jsonrpc/types.go b/jsonrpc/types.go index e09bb89327..a418c6a963 100644 --- a/jsonrpc/types.go +++ b/jsonrpc/types.go @@ -17,6 +17,8 @@ type transactionOrHash interface { type transaction struct { Nonce argUint64 `json:"nonce"` GasPrice argBig `json:"gasPrice"` + GasTipCap *argBig `json:"gasTipCap,omitempty"` + GasFeeCap *argBig `json:"gasFeeCap,omitempty"` Gas argUint64 `json:"gas"` To *types.Address `json:"to"` Value argBig `json:"value"` @@ -66,6 +68,16 @@ func toTransaction( From: t.From, } + if t.GasTipCap != nil { + gasTipCap := argBig(*t.GasTipCap) + res.GasTipCap = &gasTipCap + } + + if t.GasFeeCap != nil { + gasFeeCap := argBig(*t.GasFeeCap) + res.GasFeeCap = &gasFeeCap + } + if blockNumber != nil { res.BlockNumber = blockNumber } @@ -102,6 +114,7 @@ type block struct { Hash types.Hash `json:"hash"` Transactions []transactionOrHash `json:"transactions"` Uncles []types.Hash `json:"uncles"` + BaseFee argUint64 `json:"baseFee,omitempty"` } func (b *block) Copy() *block { @@ -140,6 +153,7 @@ func toBlock(b *types.Block, fullTx bool) *block { Hash: h.Hash, Transactions: []transactionOrHash{}, Uncles: []types.Hash{}, + BaseFee: argUint64(h.BaseFee), } for idx, txn := range b.Transactions { @@ -307,14 +321,16 @@ func encodeToHex(b []byte) []byte { // txnArgs is the transaction argument for the rpc endpoints type txnArgs struct { - From *types.Address - To *types.Address - Gas *argUint64 - GasPrice *argBytes - Value *argBytes - Data *argBytes - Input *argBytes - Nonce *argUint64 + From *types.Address + To *types.Address + Gas *argUint64 + GasPrice *argBytes + GasTipCap *argBytes + GasFeeCap *argBytes + Value *argBytes + Data *argBytes + Input *argBytes + Nonce *argUint64 } type progression struct { diff --git a/jsonrpc/types_test.go b/jsonrpc/types_test.go index a3958c9c8e..9258140fa1 100644 --- a/jsonrpc/types_test.go +++ b/jsonrpc/types_test.go @@ -143,64 +143,64 @@ func TestBlock_Copy(t *testing.T) { var testsuite embed.FS func TestBlock_Encoding(t *testing.T) { - b := block{ - ParentHash: types.Hash{0x1}, - Sha3Uncles: types.Hash{0x2}, - Miner: types.Address{0x1}.Bytes(), - StateRoot: types.Hash{0x4}, - TxRoot: types.Hash{0x5}, - ReceiptsRoot: types.Hash{0x6}, - LogsBloom: types.Bloom{0x0}, - Difficulty: 10, - Number: 11, - GasLimit: 12, - GasUsed: 13, - Timestamp: 14, - ExtraData: []byte{97, 98, 99, 100, 101, 102}, - MixHash: types.Hash{0x7}, - Nonce: types.Nonce{10}, - Hash: types.Hash{0x8}, + getBlock := func() block { + return block{ + ParentHash: types.Hash{0x1}, + Sha3Uncles: types.Hash{0x2}, + Miner: types.Address{0x1}.Bytes(), + StateRoot: types.Hash{0x4}, + TxRoot: types.Hash{0x5}, + ReceiptsRoot: types.Hash{0x6}, + LogsBloom: types.Bloom{0x0}, + Difficulty: 10, + Number: 11, + GasLimit: 12, + GasUsed: 13, + Timestamp: 14, + ExtraData: []byte{97, 98, 99, 100, 101, 102}, + MixHash: types.Hash{0x7}, + Nonce: types.Nonce{10}, + Hash: types.Hash{0x8}, + BaseFee: 15, + } } - testBlock := func(name string) { + testBlock := func(name string, b block) { res, err := json.Marshal(b) require.NoError(t, err) data, err := testsuite.ReadFile(name) require.NoError(t, err) - - data = removeWhiteSpace(data) - require.Equal(t, res, data) + require.JSONEq(t, string(data), string(res)) } t.Run("empty block", func(t *testing.T) { - testBlock("testsuite/block-empty.json") + testBlock("testsuite/block-empty.json", getBlock()) + }) + + t.Run("block with no base fee", func(t *testing.T) { + b := getBlock() + b.BaseFee = 0 + testBlock("testsuite/block-with-no-basefee.json", b) }) t.Run("block with transaction hashes", func(t *testing.T) { + b := getBlock() b.Transactions = []transactionOrHash{ transactionHash{0x8}, } - testBlock("testsuite/block-with-txn-hashes.json") + testBlock("testsuite/block-with-txn-hashes.json", b) }) t.Run("block with transaction bodies", func(t *testing.T) { + b := getBlock() b.Transactions = []transactionOrHash{ mockTxn(), } - testBlock("testsuite/block-with-txn-bodies.json") + testBlock("testsuite/block-with-txn-bodies.json", b) }) } -func removeWhiteSpace(d []byte) []byte { - s := string(d) - s = strings.Replace(s, "\n", "", -1) - s = strings.Replace(s, "\t", "", -1) - s = strings.Replace(s, " ", "", -1) - - return []byte(s) -} - func mockTxn() *transaction { to := types.Address{} @@ -225,28 +225,38 @@ func mockTxn() *transaction { } func TestTransaction_Encoding(t *testing.T) { - tt := mockTxn() - - testTransaction := func(name string) { + testTransaction := func(name string, tt *transaction) { res, err := json.Marshal(tt) require.NoError(t, err) data, err := testsuite.ReadFile(name) require.NoError(t, err) - - data = removeWhiteSpace(data) - require.Equal(t, res, data) + require.JSONEq(t, string(data), string(res)) } t.Run("sealed", func(t *testing.T) { - testTransaction("testsuite/transaction-sealed.json") + tt := mockTxn() + + testTransaction("testsuite/transaction-sealed.json", tt) }) t.Run("pending", func(t *testing.T) { + tt := mockTxn() tt.BlockHash = nil tt.BlockNumber = nil tt.TxIndex = nil - testTransaction("testsuite/transaction-pending.json") + testTransaction("testsuite/transaction-pending.json", tt) + }) + + t.Run("eip-1559", func(t *testing.T) { + gasTipCap := argBig(*big.NewInt(10)) + gasFeeCap := argBig(*big.NewInt(10)) + + tt := mockTxn() + tt.GasTipCap = &gasTipCap + tt.GasFeeCap = &gasFeeCap + + testTransaction("testsuite/transaction-eip1559.json", tt) }) } diff --git a/server/server.go b/server/server.go index fb2521aebf..f98f110171 100644 --- a/server/server.go +++ b/server/server.go @@ -268,8 +268,15 @@ func NewServer(config *Config) (*Server, error) { // compute the genesis root state config.Chain.Genesis.StateRoot = genesisRoot - // use the eip155 signer - signer := crypto.NewEIP155Signer(chain.AllForksEnabled.At(0), uint64(m.config.Chain.Params.ChainID)) + // Use the london signer with eip-155 as a fallback one + var signer crypto.TxSigner = crypto.NewLondonSigner( + uint64(m.config.Chain.Params.ChainID), + chain.AllForksEnabled.At(0).Homestead, + crypto.NewEIP155Signer( + uint64(m.config.Chain.Params.ChainID), + chain.AllForksEnabled.At(0).Homestead, + ), + ) // create storage instance for blockchain var db storage.Storage @@ -291,7 +298,14 @@ func NewServer(config *Config) (*Server, error) { } // blockchain object - m.blockchain, err = blockchain.NewBlockchain(logger, db, config.Chain, nil, m.executor, signer) + m.blockchain, err = blockchain.NewBlockchain( + logger, + db, + config.Chain, + nil, + m.executor, + signer, + ) if err != nil { return nil, err } diff --git a/state/executor.go b/state/executor.go index aea0ae1931..4c7880424a 100644 --- a/state/executor.go +++ b/state/executor.go @@ -6,16 +6,18 @@ import ( "math" "math/big" + "github.com/hashicorp/go-hclog" + "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" "github.com/0xPolygon/polygon-edge/state/runtime/precompiled" "github.com/0xPolygon/polygon-edge/state/runtime/tracer" "github.com/0xPolygon/polygon-edge/types" - "github.com/hashicorp/go-hclog" ) const ( @@ -136,11 +138,11 @@ func (e *Executor) ProcessBlock( } for _, t := range block.Transactions { - if t.ExceedsBlockGasLimit(block.Header.GasLimit) { + if t.Gas > block.Header.GasLimit { continue } - if err := txn.Write(t); err != nil { + if err = txn.Write(t); err != nil { return nil, err } } @@ -175,15 +177,25 @@ func (e *Executor) BeginTxn( return nil, err } + burnContract := types.ZeroAddress + if forkConfig.London { + burnContract, err = e.config.CalculateBurnContract(header.Number) + if err != nil { + return nil, err + } + } + newTxn := NewTxn(auxSnap2) txCtx := runtime.TxContext{ - Coinbase: coinbaseReceiver, - Timestamp: int64(header.Timestamp), - Number: int64(header.Number), - Difficulty: types.BytesToHash(new(big.Int).SetUint64(header.Difficulty).Bytes()), - GasLimit: int64(header.GasLimit), - ChainID: e.config.ChainID, + Coinbase: coinbaseReceiver, + Timestamp: int64(header.Timestamp), + Number: int64(header.Number), + Difficulty: types.BytesToHash(new(big.Int).SetUint64(header.Difficulty).Bytes()), + BaseFee: new(big.Int).SetUint64(header.BaseFee), + GasLimit: int64(header.GasLimit), + ChainID: e.config.ChainID, + BurnContract: burnContract, } txn := &Transition{ @@ -309,7 +321,8 @@ var emptyFrom = types.Address{} func (t *Transition) Write(txn *types.Transaction) error { var err error - if txn.From == emptyFrom && txn.Type == types.LegacyTx { + if txn.From == emptyFrom && + (txn.Type == types.LegacyTx || txn.Type == types.DynamicFeeTx) { // Decrypt the from address signer := crypto.NewSigner(t.config, uint64(t.ctx.ChainID)) @@ -411,9 +424,18 @@ func (t *Transition) ContextPtr() *runtime.TxContext { } func (t *Transition) subGasLimitPrice(msg *types.Transaction) error { - // deduct the upfront max gas cost - upfrontGasCost := new(big.Int).Set(msg.GasPrice) - upfrontGasCost.Mul(upfrontGasCost, new(big.Int).SetUint64(msg.Gas)) + 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) if err := t.state.SubBalance(msg.From, upfrontGasCost); err != nil { if errors.Is(err, runtime.ErrNotEnoughFunds) { @@ -436,6 +458,42 @@ func (t *Transition) nonceCheck(msg *types.Transaction) error { return nil } +// checkDynamicFees checks correctness of the EIP-1559 feature-related fields. +// Basically, makes sure gas tip cap and gas fee cap are good. +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 +} + // errors that can originate in the consensus rules checks of the apply method below // surfacing of these errors reject the transaction thus not including it in the block @@ -445,7 +503,22 @@ var ( ErrBlockLimitReached = fmt.Errorf("gas limit reached in the pool") ErrIntrinsicGasOverflow = fmt.Errorf("overflow in intrinsic gas calculation") ErrNotEnoughIntrinsicGas = fmt.Errorf("not enough gas supplied for intrinsic gas costs") - ErrNotEnoughFunds = fmt.Errorf("not enough funds for transfer with given value") + + // ErrTipAboveFeeCap is a sanity error to ensure no one is able to specify a + // transaction with a tip higher than the total fee cap. + ErrTipAboveFeeCap = errors.New("max priority fee per gas higher than max fee per gas") + + // ErrTipVeryHigh is a sanity error to avoid extremely big numbers specified + // in the tip field. + ErrTipVeryHigh = errors.New("max priority fee per gas higher than 2^256-1") + + // ErrFeeCapVeryHigh is a sanity error to avoid extremely big numbers specified + // in the fee cap field. + ErrFeeCapVeryHigh = errors.New("max fee per gas higher than 2^256-1") + + // ErrFeeCapTooLow is returned if the transaction fee cap is less than the + // the base fee of the block. + ErrFeeCapTooLow = errors.New("max fee per gas less than block base fee") ) type TransitionApplicationError struct { @@ -475,18 +548,20 @@ func NewGasLimitReachedTransitionApplicationError(err error) *GasLimitReachedTra } func (t *Transition) apply(msg *types.Transaction) (*runtime.ExecutionResult, error) { + var err error + if msg.Type == types.StateTx { - if err := checkAndProcessStateTx(msg, t); err != nil { - return nil, err - } + err = checkAndProcessStateTx(msg) } else { - if err := checkAndProcessLegacyTx(msg, t); err != nil { - return nil, err - } + err = checkAndProcessTx(msg, t) + } + + if err != nil { + return nil, err } // the amount of gas required is available in the block - if err := t.subGasPool(msg.Gas); err != nil { + if err = t.subGasPool(msg.Gas); err != nil { return nil, NewGasLimitReachedTransitionApplicationError(err) } @@ -507,7 +582,7 @@ func (t *Transition) apply(msg *types.Transaction) (*runtime.ExecutionResult, er return nil, NewTransitionApplicationError(ErrNotEnoughIntrinsicGas, false) } - gasPrice := new(big.Int).Set(msg.GasPrice) + gasPrice := msg.GetGasPrice(t.ctx.BaseFee.Uint64()) value := new(big.Int).Set(msg.Value) // set the specific transaction fields in the context @@ -529,14 +604,33 @@ func (t *Transition) apply(msg *types.Transaction) (*runtime.ExecutionResult, er t.ctx.Tracer.TxEnd(result.GasLeft) } - // refund the sender - remaining := new(big.Int).Mul(new(big.Int).SetUint64(result.GasLeft), msg.GasPrice) + // Refund the sender + remaining := new(big.Int).Mul(new(big.Int).SetUint64(result.GasLeft), gasPrice) t.state.AddBalance(msg.From, remaining) - // pay the coinbase - coinbaseFee := new(big.Int).Mul(new(big.Int).SetUint64(result.GasUsed), msg.GasPrice) + // Spec: https://eips.ethereum.org/EIPS/eip-1559#specification + // 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), + ) + } + + // 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) t.state.AddBalance(t.ctx.Coinbase, coinbaseFee) + // Burn some amount if the london hardfork is applied. + // Basically, burn amount is just transferred to the current burn contract. + if t.config.London && msg.Type != types.StateTx { + burnAmount := new(big.Int).Mul(new(big.Int).SetUint64(result.GasUsed), t.ctx.BaseFee) + t.state.AddBalance(t.ctx.BurnContract, burnAmount) + } + // return gas to the pool t.addGasPool(result.GasLeft) @@ -967,17 +1061,22 @@ func TransactionGasCost(msg *types.Transaction, isHomestead, isIstanbul bool) (u return cost, nil } -// checkAndProcessLegacyTx - first check if this message satisfies all consensus rules before +// checkAndProcessTx - first check if this message satisfies all consensus rules before // applying the message. The rules include these clauses: // 1. the nonce of the message caller is correct -// 2. caller has enough balance to cover transaction fee(gaslimit * gasprice) -func checkAndProcessLegacyTx(msg *types.Transaction, t *Transition) error { +// 2. caller has enough balance to cover transaction fee(gaslimit * gasprice * val) or fee(gasfeecap * gasprice * val) +func checkAndProcessTx(msg *types.Transaction, t *Transition) error { // 1. the nonce of the message caller is correct if err := t.nonceCheck(msg); err != nil { return NewTransitionApplicationError(err, true) } - // 2. caller has enough balance to cover transaction fee(gaslimit * gasprice) + // 2. check dynamic fees of the transaction + if err := t.checkDynamicFees(msg); err != nil { + return NewTransitionApplicationError(err, true) + } + + // 3. caller has enough balance to cover transaction if err := t.subGasLimitPrice(msg); err != nil { return NewTransitionApplicationError(err, true) } @@ -985,7 +1084,7 @@ func checkAndProcessLegacyTx(msg *types.Transaction, t *Transition) error { return nil } -func checkAndProcessStateTx(msg *types.Transaction, t *Transition) error { +func checkAndProcessStateTx(msg *types.Transaction) error { if msg.GasPrice.Cmp(big.NewInt(0)) != 0 { return NewTransitionApplicationError( errors.New("gasPrice of state transaction must be zero"), diff --git a/state/executor_test.go b/state/executor_test.go index 8edb9acaba..79ae3c96e3 100644 --- a/state/executor_test.go +++ b/state/executor_test.go @@ -1,12 +1,16 @@ package state import ( + "fmt" "math/big" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/0xPolygon/polygon-edge/chain" + "github.com/0xPolygon/polygon-edge/state/runtime" "github.com/0xPolygon/polygon-edge/types" - "github.com/stretchr/testify/require" ) func TestOverride(t *testing.T) { @@ -61,3 +65,92 @@ func TestOverride(t *testing.T) { require.Equal(t, types.Hash{0x0}, tt.state.GetState(types.Address{0x1}, types.ZeroHash)) require.Equal(t, types.Hash{0x1}, tt.state.GetState(types.Address{0x1}, types.Hash{0x1})) } + +func Test_Transition_checkDynamicFees(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + baseFee *big.Int + tx *types.Transaction + wantErr assert.ErrorAssertionFunc + }{ + { + name: "happy path", + baseFee: big.NewInt(100), + tx: &types.Transaction{ + Type: types.DynamicFeeTx, + GasFeeCap: big.NewInt(100), + GasTipCap: big.NewInt(100), + }, + wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + assert.NoError(t, err, i) + + return false + }, + }, + { + name: "happy path with empty values", + baseFee: big.NewInt(0), + tx: &types.Transaction{ + Type: types.DynamicFeeTx, + GasFeeCap: big.NewInt(0), + GasTipCap: big.NewInt(0), + }, + wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + assert.NoError(t, err, i) + + return false + }, + }, + { + name: "gas fee cap less than base fee", + baseFee: big.NewInt(20), + tx: &types.Transaction{ + Type: types.DynamicFeeTx, + GasFeeCap: big.NewInt(10), + GasTipCap: big.NewInt(0), + }, + wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + expectedError := fmt.Sprintf("max fee per gas less than block base fee: "+ + "address %s, GasFeeCap: 10, BaseFee: 20", types.ZeroAddress) + assert.EqualError(t, err, expectedError, i) + + return true + }, + }, + { + name: "gas fee cap less than tip cap", + baseFee: big.NewInt(5), + tx: &types.Transaction{ + Type: types.DynamicFeeTx, + GasFeeCap: big.NewInt(10), + GasTipCap: big.NewInt(15), + }, + wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + expectedError := fmt.Sprintf("max priority fee per gas higher than max fee per gas: "+ + "address %s, GasTipCap: 15, GasFeeCap: 10", types.ZeroAddress) + assert.EqualError(t, err, expectedError, i) + + return true + }, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + tr := &Transition{ + ctx: runtime.TxContext{ + BaseFee: tt.baseFee, + }, + } + + err := tr.checkDynamicFees(tt.tx) + tt.wantErr(t, err, fmt.Sprintf("checkDynamicFees(%v)", tt.tx)) + }) + } +} diff --git a/state/runtime/runtime.go b/state/runtime/runtime.go index 3ba74d6d27..0b86f901ef 100644 --- a/state/runtime/runtime.go +++ b/state/runtime/runtime.go @@ -11,15 +11,17 @@ import ( // TxContext is the context of the transaction type TxContext struct { - GasPrice types.Hash - Origin types.Address - Coinbase types.Address - Number int64 - Timestamp int64 - GasLimit int64 - ChainID int64 - Difficulty types.Hash - Tracer tracer.Tracer + GasPrice types.Hash + Origin types.Address + Coinbase types.Address + Number int64 + Timestamp int64 + GasLimit int64 + ChainID int64 + Difficulty types.Hash + Tracer tracer.Tracer + BaseFee *big.Int + BurnContract types.Address } // StorageStatus is the status of the storage access diff --git a/txpool/query.go b/txpool/query.go index 7ffc45e761..8e4c9ba91f 100644 --- a/txpool/query.go +++ b/txpool/query.go @@ -1,6 +1,10 @@ package txpool -import "github.com/0xPolygon/polygon-edge/types" +import ( + "sync/atomic" + + "github.com/0xPolygon/polygon-edge/types" +) /* QUERY methods */ // Used to query the pool for specific state info. @@ -46,3 +50,8 @@ func (p *TxPool) GetTxs(inclQueued bool) ( return } + +// GetBaseFee returns current base fee +func (p *TxPool) GetBaseFee() uint64 { + return atomic.LoadUint64(&p.baseFee) +} diff --git a/txpool/queue.go b/txpool/queue_account.go similarity index 64% rename from txpool/queue.go rename to txpool/queue_account.go index 4cff31222f..75cc020c6e 100644 --- a/txpool/queue.go +++ b/txpool/queue_account.go @@ -152,90 +152,3 @@ func (q *minNonceQueue) Pop() interface{} { return x } - -type pricedQueue struct { - queue maxPriceQueue -} - -func newPricedQueue() *pricedQueue { - q := pricedQueue{ - queue: make(maxPriceQueue, 0), - } - - heap.Init(&q.queue) - - return &q -} - -// clear empties the underlying queue. -func (q *pricedQueue) clear() { - q.queue = q.queue[:0] -} - -// Pushes the given transactions onto the queue. -func (q *pricedQueue) push(tx *types.Transaction) { - heap.Push(&q.queue, tx) -} - -// Pop removes the first transaction from the queue -// or nil if the queue is empty. -func (q *pricedQueue) pop() *types.Transaction { - if q.length() == 0 { - return nil - } - - transaction, ok := heap.Pop(&q.queue).(*types.Transaction) - if !ok { - return nil - } - - return transaction -} - -// length returns the number of transactions in the queue. -func (q *pricedQueue) length() uint64 { - return uint64(q.queue.Len()) -} - -// transactions sorted by gas price (descending) -type maxPriceQueue []*types.Transaction - -/* Queue methods required by the heap interface */ - -func (q *maxPriceQueue) Peek() *types.Transaction { - if q.Len() == 0 { - return nil - } - - return (*q)[0] -} - -func (q *maxPriceQueue) Len() int { - return len(*q) -} - -func (q *maxPriceQueue) Swap(i, j int) { - (*q)[i], (*q)[j] = (*q)[j], (*q)[i] -} - -func (q *maxPriceQueue) Less(i, j int) bool { - return (*q)[i].GasPrice.Uint64() > (*q)[j].GasPrice.Uint64() -} - -func (q *maxPriceQueue) Push(x interface{}) { - transaction, ok := x.(*types.Transaction) - if !ok { - return - } - - *q = append(*q, transaction) -} - -func (q *maxPriceQueue) Pop() interface{} { - old := q - n := len(*old) - x := (*old)[n-1] - *q = (*old)[0 : n-1] - - return x -} diff --git a/txpool/queue_priced.go b/txpool/queue_priced.go new file mode 100644 index 0000000000..88390a11fd --- /dev/null +++ b/txpool/queue_priced.go @@ -0,0 +1,149 @@ +package txpool + +import ( + "container/heap" + "math/big" + "sync/atomic" + + "github.com/0xPolygon/polygon-edge/types" +) + +type pricedQueue struct { + queue *maxPriceQueue +} + +func newPricedQueue() *pricedQueue { + q := pricedQueue{ + queue: &maxPriceQueue{}, + } + + heap.Init(q.queue) + + return &q +} + +// clear empties the underlying queue. +func (q *pricedQueue) clear() { + q.queue.txs = q.queue.txs[:0] +} + +// Pushes the given transactions onto the queue. +func (q *pricedQueue) push(tx *types.Transaction) { + heap.Push(q.queue, tx) +} + +// Pop removes the first transaction from the queue +// or nil if the queue is empty. +func (q *pricedQueue) pop() *types.Transaction { + if q.length() == 0 { + return nil + } + + transaction, ok := heap.Pop(q.queue).(*types.Transaction) + if !ok { + return nil + } + + return transaction +} + +// length returns the number of transactions in the queue. +func (q *pricedQueue) length() uint64 { + return uint64(q.queue.Len()) +} + +// transactions sorted by gas price (descending) +type maxPriceQueue struct { + baseFee uint64 + txs []*types.Transaction +} + +/* Queue methods required by the heap interface */ + +func (q *maxPriceQueue) Peek() *types.Transaction { + if q.Len() == 0 { + return nil + } + + return q.txs[0] +} + +func (q *maxPriceQueue) Len() int { + return len(q.txs) +} + +func (q *maxPriceQueue) Swap(i, j int) { + q.txs[i], q.txs[j] = q.txs[j], q.txs[i] +} + +func (q *maxPriceQueue) Less(i, j int) bool { + switch q.cmp(q.txs[i], q.txs[j]) { + case -1: + return true + case 1: + return false + default: + return q.txs[i].Nonce > q.txs[j].Nonce + } +} + +func (q *maxPriceQueue) Push(x interface{}) { + transaction, ok := x.(*types.Transaction) + if !ok { + return + } + + q.txs = append(q.txs, transaction) +} + +func (q *maxPriceQueue) Pop() interface{} { + old := q.txs + n := len(old) + x := old[n-1] + q.txs = old[0 : n-1] + + return x +} + +// cmp compares the given transactions by their fees and returns: +// - 0 if they have same fees +// - 1 if a has higher fees than b +// - -1 if b has higher fees than a +func (q *maxPriceQueue) cmp(a, b *types.Transaction) int { + baseFee := atomic.LoadUint64(&q.baseFee) + effectiveTipA := a.EffectiveTip(baseFee) + effectiveTipB := b.EffectiveTip(baseFee) + + // Compare effective tips if baseFee is specified + if c := effectiveTipA.Cmp(effectiveTipB); c != 0 { + return c + } + + aGasFeeCap, bGasFeeCap := new(big.Int), new(big.Int) + + if a.GasFeeCap != nil { + aGasFeeCap = aGasFeeCap.Set(a.GasFeeCap) + } + + if b.GasFeeCap != nil { + bGasFeeCap = bGasFeeCap.Set(b.GasFeeCap) + } + + // Compare fee caps if baseFee is not specified or effective tips are equal + if c := aGasFeeCap.Cmp(bGasFeeCap); c != 0 { + return c + } + + aGasTipCap, bGasTipCap := new(big.Int), new(big.Int) + + if a.GasTipCap != nil { + aGasTipCap = aGasTipCap.Set(a.GasTipCap) + } + + if b.GasTipCap != nil { + bGasTipCap = bGasTipCap.Set(b.GasTipCap) + } + + // Compare tips if effective tips and fee caps are equal + return aGasTipCap.Cmp(bGasTipCap) +} diff --git a/txpool/queue_priced_test.go b/txpool/queue_priced_test.go new file mode 100644 index 0000000000..7e8aea3695 --- /dev/null +++ b/txpool/queue_priced_test.go @@ -0,0 +1,326 @@ +package txpool + +import ( + "math/big" + "math/rand" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/0xPolygon/polygon-edge/types" +) + +func Test_maxPriceQueue(t *testing.T) { + t.Parallel() + + testTable := []struct { + name string + baseFee uint64 + unsorted []*types.Transaction + sorted []*types.Transaction + }{ + { + name: "sort txs by tips with base fee", + baseFee: 1000, + unsorted: []*types.Transaction{ + // Highest tx fee + { + Type: types.DynamicFeeTx, + GasPrice: big.NewInt(0), + GasFeeCap: big.NewInt(2000), + GasTipCap: big.NewInt(500), + }, + // Lowest tx fee + { + Type: types.LegacyTx, + GasPrice: big.NewInt(100), + }, + // Middle tx fee + { + Type: types.DynamicFeeTx, + GasPrice: big.NewInt(0), + GasFeeCap: big.NewInt(1500), + GasTipCap: big.NewInt(200), + }, + }, + sorted: []*types.Transaction{ + // Highest tx fee + { + Type: types.DynamicFeeTx, + GasPrice: big.NewInt(0), + GasFeeCap: big.NewInt(2000), + GasTipCap: big.NewInt(500), + }, + // Middle tx fee + { + Type: types.DynamicFeeTx, + GasPrice: big.NewInt(0), + GasFeeCap: big.NewInt(1500), + GasTipCap: big.NewInt(200), + }, + // Lowest tx fee + { + Type: types.LegacyTx, + GasPrice: big.NewInt(100), + }, + }, + }, + { + name: "sort txs by nonce with base fee", + baseFee: 1000, + unsorted: []*types.Transaction{ + // Highest tx fee + { + Type: types.DynamicFeeTx, + GasFeeCap: big.NewInt(2000), + GasTipCap: big.NewInt(500), + Nonce: 3, + }, + // Lowest tx fee + { + Type: types.DynamicFeeTx, + GasFeeCap: big.NewInt(2000), + GasTipCap: big.NewInt(500), + Nonce: 1, + }, + // Middle tx fee + { + Type: types.DynamicFeeTx, + GasFeeCap: big.NewInt(2000), + GasTipCap: big.NewInt(500), + Nonce: 2, + }, + }, + sorted: []*types.Transaction{ + // Highest tx fee + { + Type: types.DynamicFeeTx, + GasFeeCap: big.NewInt(2000), + GasTipCap: big.NewInt(500), + Nonce: 1, + }, + // Middle tx fee + { + Type: types.DynamicFeeTx, + GasFeeCap: big.NewInt(2000), + GasTipCap: big.NewInt(500), + Nonce: 2, + }, + // Lowest tx fee + { + Type: types.DynamicFeeTx, + GasFeeCap: big.NewInt(2000), + GasTipCap: big.NewInt(500), + Nonce: 3, + }, + }, + }, + { + name: "sort txs without base fee by fee cap", + baseFee: 0, + unsorted: []*types.Transaction{ + // Highest tx fee + { + GasFeeCap: big.NewInt(3000), + GasTipCap: big.NewInt(100), + }, + // Lowest tx fee + { + GasFeeCap: big.NewInt(1000), + GasTipCap: big.NewInt(100), + }, + // Middle tx fee + { + GasFeeCap: big.NewInt(2000), + GasTipCap: big.NewInt(100), + }, + }, + sorted: []*types.Transaction{ + // Highest tx fee + { + GasFeeCap: big.NewInt(3000), + GasTipCap: big.NewInt(100), + }, + // Middle tx fee + { + GasFeeCap: big.NewInt(2000), + GasTipCap: big.NewInt(100), + }, + // Lowest tx fee + { + GasFeeCap: big.NewInt(1000), + GasTipCap: big.NewInt(100), + }, + }, + }, + { + name: "sort txs without base fee by tip cap", + baseFee: 0, + unsorted: []*types.Transaction{ + // Highest tx fee + { + GasFeeCap: big.NewInt(1000), + GasTipCap: big.NewInt(300), + }, + // Lowest tx fee + { + GasFeeCap: big.NewInt(1000), + GasTipCap: big.NewInt(100), + }, + // Middle tx fee + { + GasFeeCap: big.NewInt(1000), + GasTipCap: big.NewInt(200), + }, + }, + sorted: []*types.Transaction{ + // Highest tx fee + { + GasFeeCap: big.NewInt(1000), + GasTipCap: big.NewInt(300), + }, + // Middle tx fee + { + GasFeeCap: big.NewInt(1000), + GasTipCap: big.NewInt(200), + }, + // Lowest tx fee + { + GasFeeCap: big.NewInt(1000), + GasTipCap: big.NewInt(100), + }, + }, + }, + { + name: "sort txs without base fee by gas price", + baseFee: 0, + unsorted: []*types.Transaction{ + // Highest tx fee + { + GasPrice: big.NewInt(1000), + }, + // Lowest tx fee + { + GasPrice: big.NewInt(100), + }, + // Middle tx fee + { + GasPrice: big.NewInt(500), + }, + }, + sorted: []*types.Transaction{ + // Highest tx fee + { + GasPrice: big.NewInt(1000), + }, + // Middle tx fee + { + GasPrice: big.NewInt(500), + }, + // Lowest tx fee + { + GasPrice: big.NewInt(100), + }, + }, + }, + } + + for _, tt := range testTable { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + queue := &maxPriceQueue{ + baseFee: tt.baseFee, + txs: tt.unsorted, + } + + sort.Sort(queue) + + for _, tx := range tt.sorted { + actual := queue.Pop() + assert.Equal(t, tx, actual) + } + }) + } +} + +func Benchmark_pricedQueue(t *testing.B) { + testTable := []struct { + name string + unsortedTxs []*types.Transaction + }{ + { + name: "1000 transactions", + unsortedTxs: generateTxs(1000), + }, + { + name: "10000 transactions", + unsortedTxs: generateTxs(10000), + }, + { + name: "100000 transactions", + unsortedTxs: generateTxs(100000), + }, + } + + for _, tt := range testTable { + t.Run(tt.name, func(b *testing.B) { + for i := 0; i < t.N; i++ { + q := newPricedQueue() + q.queue.baseFee = uint64(i) + + for _, tx := range tt.unsortedTxs { + q.push(tx) + } + + for q.length() > 0 { + _ = q.pop() + } + } + }) + } +} + +func generateTxs(num int) []*types.Transaction { + txs := make([]*types.Transaction, num) + + for i := 0; i < num; i++ { + txs[i] = generateTx(i + 1) + } + + return txs +} + +func generateTx(i int) *types.Transaction { + s := rand.NewSource(int64(i)) + r := rand.New(s) + + txTypes := []types.TxType{ + types.LegacyTx, + types.DynamicFeeTx, + } + + tx := &types.Transaction{ + Type: txTypes[r.Intn(len(txTypes))], + } + + switch tx.Type { + case types.LegacyTx: + minGasPrice := 1000 * i + maxGasPrice := 100000 * i + tx.GasPrice = new(big.Int).SetInt64(int64(rand.Intn(maxGasPrice-minGasPrice) + minGasPrice)) + case types.DynamicFeeTx: + minGasFeeCap := 1000 * i + maxGasFeeCap := 100000 * i + tx.GasFeeCap = new(big.Int).SetInt64(int64(rand.Intn(maxGasFeeCap-minGasFeeCap) + minGasFeeCap)) + + minGasTipCap := 100 * i + maxGasTipCap := 10000 * i + tx.GasTipCap = new(big.Int).SetInt64(int64(rand.Intn(maxGasTipCap-minGasTipCap) + minGasTipCap)) + } + + return tx +} diff --git a/txpool/txpool.go b/txpool/txpool.go index d9d3b13102..172cf117d4 100644 --- a/txpool/txpool.go +++ b/txpool/txpool.go @@ -57,6 +57,9 @@ var ( ErrRejectFutureTx = errors.New("rejected future tx due to low slots") ErrSmartContractRestricted = errors.New("smart contract deployment restricted") ErrInvalidTxType = errors.New("invalid tx type") + ErrTipAboveFeeCap = errors.New("max priority fee per gas higher than max fee per gas") + ErrTipVeryHigh = errors.New("max priority fee per gas higher than 2^256-1") + ErrFeeCapVeryHigh = errors.New("max fee per gas higher than 2^256-1") ) // indicates origin of a transaction @@ -172,6 +175,10 @@ type TxPool struct { // and should therefore gossip transactions sealing uint32 + // baseFee is the base fee of the current head. + // This is needed to sort transactions by price + baseFee uint64 + // Event manager for txpool events eventManager *eventManager @@ -376,12 +383,15 @@ func (p *TxPool) AddTx(tx *types.Transaction) error { // Prepare generates all the transactions // ready for execution. (primaries) -func (p *TxPool) Prepare() { +func (p *TxPool) Prepare(baseFee uint64) { // clear from previous round if p.executables.length() != 0 { p.executables.clear() } + // set base fee + p.updateBaseFee(baseFee) + // fetch primary from each account primaries := p.accounts.getPrimaries() @@ -621,9 +631,38 @@ func (p *TxPool) validateTx(tx *types.Transaction) error { } } - // Reject underpriced transactions - if tx.IsUnderpriced(p.priceLimit) { - return ErrUnderpriced + if tx.Type == types.DynamicFeeTx { + // Reject dynamic fee tx if london hardfork is not enabled + if !p.forks.London { + return ErrInvalidTxType + } + + // Check EIP-1559-related fields and make sure they are correct + if tx.GasFeeCap == nil || tx.GasTipCap == nil { + return ErrUnderpriced + } + + if tx.GasFeeCap.BitLen() > 256 { + return ErrFeeCapVeryHigh + } + + if tx.GasTipCap.BitLen() > 256 { + return ErrTipVeryHigh + } + + if tx.GasFeeCap.Cmp(tx.GasTipCap) < 0 { + return ErrTipAboveFeeCap + } + + // Reject underpriced transactions + if tx.GasFeeCap.Cmp(new(big.Int).SetUint64(p.GetBaseFee())) < 0 { + return ErrUnderpriced + } + } else { + // Legacy approach to check if the given tx is not underpriced + if tx.GetGasPrice(p.GetBaseFee()).Cmp(big.NewInt(0).SetUint64(p.priceLimit)) < 0 { + return ErrUnderpriced + } } // Grab the state root for the latest block @@ -958,6 +997,12 @@ func (p *TxPool) Length() uint64 { return p.accounts.promoted() } +// updateBaseFee updates base fee in the tx pool and priced queue +func (p *TxPool) updateBaseFee(baseFee uint64) { + atomic.StoreUint64(&p.baseFee, baseFee) + atomic.StoreUint64(&p.executables.queue.baseFee, baseFee) +} + // toHash returns the hash(es) of given transaction(s) func toHash(txs ...*types.Transaction) (hashes []types.Hash) { for _, tx := range txs { diff --git a/txpool/txpool_test.go b/txpool/txpool_test.go index 2e203621c7..d2663ae977 100644 --- a/txpool/txpool_test.go +++ b/txpool/txpool_test.go @@ -115,7 +115,7 @@ type result struct { func TestAddTxErrors(t *testing.T) { t.Parallel() - poolSigner := crypto.NewEIP155Signer(chain.AllForksEnabled.At(0), 100) + poolSigner := crypto.NewEIP155Signer(100, true) // Generate a private key and address defaultKey, defaultAddr := tests.GenerateKeyAndAddr(t) @@ -546,7 +546,7 @@ func TestAddGossipTx(t *testing.T) { t.Parallel() key, sender := tests.GenerateKeyAndAddr(t) - signer := crypto.NewEIP155Signer(chain.AllForksEnabled.At(0), uint64(100)) + signer := crypto.NewEIP155Signer(100, true) tx := newTx(types.ZeroAddress, 1, 1) t.Run("node is a validator", func(t *testing.T) { @@ -1435,7 +1435,7 @@ func TestPop(t *testing.T) { assert.Equal(t, uint64(1), pool.accounts.get(addr1).promoted.length()) // pop the tx - pool.Prepare() + pool.Prepare(0) tx := pool.Peek() pool.Pop(tx) @@ -1463,7 +1463,7 @@ func TestDrop(t *testing.T) { assert.Equal(t, uint64(1), pool.accounts.get(addr1).promoted.length()) // pop the tx - pool.Prepare() + pool.Prepare(0) tx := pool.Peek() pool.Drop(tx) @@ -1496,7 +1496,7 @@ func TestDemote(t *testing.T) { assert.Equal(t, uint64(0), pool.accounts.get(addr1).Demotions()) // call demote - pool.Prepare() + pool.Prepare(0) tx := pool.Peek() pool.Demote(tx) @@ -1532,7 +1532,7 @@ func TestDemote(t *testing.T) { pool.accounts.get(addr1).demotions = maxAccountDemotions // call demote - pool.Prepare() + pool.Prepare(0) tx := pool.Peek() pool.Demote(tx) @@ -1688,9 +1688,9 @@ func Test_updateAccountSkipsCounts(t *testing.T) { func TestPermissionSmartContractDeployment(t *testing.T) { t.Parallel() - signer := crypto.NewEIP155Signer(chain.AllForksEnabled.At(0), uint64(100)) + signer := crypto.NewEIP155Signer(100, true) - poolSigner := crypto.NewEIP155Signer(chain.AllForksEnabled.At(0), 100) + poolSigner := crypto.NewEIP155Signer(100, true) // Generate a private key and address defaultKey, defaultAddr := tests.GenerateKeyAndAddr(t) @@ -1787,6 +1787,54 @@ func TestPermissionSmartContractDeployment(t *testing.T) { runtime.ErrMaxCodeSizeExceeded, ) }) + + t.Run("transaction with eip-1559 fields can pass", func(t *testing.T) { + t.Parallel() + + pool := setupPool() + pool.baseFee = 1000 + + tx := newTx(defaultAddr, 0, 1) + tx.Type = types.DynamicFeeTx + tx.GasFeeCap = big.NewInt(1100) + tx.GasTipCap = big.NewInt(10) + + assert.NoError(t, pool.validateTx(signTx(tx))) + }) + + t.Run("gas fee cap less than base fee", func(t *testing.T) { + t.Parallel() + + pool := setupPool() + pool.baseFee = 1000 + + tx := newTx(defaultAddr, 0, 1) + tx.Type = types.DynamicFeeTx + tx.GasFeeCap = big.NewInt(100) + tx.GasTipCap = big.NewInt(10) + + assert.ErrorIs(t, + pool.validateTx(signTx(tx)), + ErrUnderpriced, + ) + }) + + t.Run("gas fee cap less than tip cap", func(t *testing.T) { + t.Parallel() + + pool := setupPool() + pool.baseFee = 1000 + + tx := newTx(defaultAddr, 0, 1) + tx.Type = types.DynamicFeeTx + tx.GasFeeCap = big.NewInt(10000) + tx.GasTipCap = big.NewInt(100000) + + assert.ErrorIs(t, + pool.validateTx(signTx(tx)), + ErrTipAboveFeeCap, + ) + }) } /* "Integrated" tests */ @@ -1842,7 +1890,7 @@ func (e *eoa) signTx(t *testing.T, tx *types.Transaction, signer crypto.TxSigner return signedTx } -var signerEIP155 = crypto.NewEIP155Signer(chain.AllForksEnabled.At(0), 100) +var signerEIP155 = crypto.NewEIP155Signer(100, true) func TestResetAccounts_Promoted(t *testing.T) { t.Parallel() @@ -2493,7 +2541,7 @@ func TestRecovery(t *testing.T) { assert.Len(t, waitForEvents(ctx, promoteSubscription, totalTx), totalTx) func() { - pool.Prepare() + pool.Prepare(0) for { tx := pool.Peek() if tx == nil { diff --git a/types/encoding.go b/types/encoding.go index 3f3dce4330..b3d6b33688 100644 --- a/types/encoding.go +++ b/types/encoding.go @@ -9,6 +9,10 @@ import ( "github.com/0xPolygon/polygon-edge/helper/hex" ) +// ParseUint64orHex parses the given string as uint64 in hex +// It should go to the common package from the logical perspective +// as well as avoiding cycle imports. +// DEPRECATED. Use common.ParseUint64orHex. func ParseUint64orHex(val *string) (uint64, error) { if val == nil { return 0, nil diff --git a/types/header.go b/types/header.go index bf2750f05b..923115d366 100644 --- a/types/header.go +++ b/types/header.go @@ -26,6 +26,9 @@ type Header struct { MixHash Hash Nonce Nonce Hash Hash + + // BaseFee was added by EIP-1559 and is ignored in legacy headers. + BaseFee uint64 } func (h *Header) Equal(hh *Header) bool { @@ -75,6 +78,7 @@ func (h *Header) Copy() *Header { GasLimit: h.GasLimit, GasUsed: h.GasUsed, Timestamp: h.Timestamp, + BaseFee: h.BaseFee, } newHeader.Miner = make([]byte, len(h.Miner)) diff --git a/types/rlp_encoding_test.go b/types/rlp_encoding_test.go index 7e2e00c3c9..ee5068928e 100644 --- a/types/rlp_encoding_test.go +++ b/types/rlp_encoding_test.go @@ -1,6 +1,7 @@ package types import ( + "fmt" "math/big" "reflect" "testing" @@ -134,35 +135,40 @@ func TestRLPMarshall_And_Unmarshall_TypedTransaction(t *testing.T) { addrTo := StringToAddress("11") addrFrom := StringToAddress("22") originalTx := &Transaction{ - Nonce: 0, - GasPrice: big.NewInt(11), - Gas: 11, - To: &addrTo, - From: addrFrom, - Value: big.NewInt(1), - Input: []byte{1, 2}, - V: big.NewInt(25), - S: big.NewInt(26), - R: big.NewInt(27), + Nonce: 0, + GasPrice: big.NewInt(11), + GasFeeCap: big.NewInt(12), + GasTipCap: big.NewInt(13), + Gas: 11, + To: &addrTo, + From: addrFrom, + Value: big.NewInt(1), + Input: []byte{1, 2}, + V: big.NewInt(25), + S: big.NewInt(26), + R: big.NewInt(27), } txTypes := []TxType{ StateTx, LegacyTx, + DynamicFeeTx, } for _, v := range txTypes { - originalTx.Type = v - originalTx.ComputeHash() + t.Run(v.String(), func(t *testing.T) { + originalTx.Type = v + originalTx.ComputeHash() - txRLP := originalTx.MarshalRLP() + txRLP := originalTx.MarshalRLP() - unmarshalledTx := new(Transaction) - assert.NoError(t, unmarshalledTx.UnmarshalRLP(txRLP)) + unmarshalledTx := new(Transaction) + assert.NoError(t, unmarshalledTx.UnmarshalRLP(txRLP)) - unmarshalledTx.ComputeHash() - assert.Equal(t, originalTx.Type, unmarshalledTx.Type) - assert.Equal(t, originalTx.Hash, unmarshalledTx.Hash) + unmarshalledTx.ComputeHash() + assert.Equal(t, originalTx.Type, unmarshalledTx.Type) + assert.Equal(t, originalTx.Hash, unmarshalledTx.Hash) + }) } } @@ -172,6 +178,7 @@ func TestRLPMarshall_Unmarshall_Missing_Data(t *testing.T) { txTypes := []TxType{ StateTx, LegacyTx, + DynamicFeeTx, } for _, txType := range txTypes { @@ -179,41 +186,54 @@ func TestRLPMarshall_Unmarshall_Missing_Data(t *testing.T) { testTable := []struct { name string expectedErr bool - ommitedValues map[string]bool + omittedValues map[string]bool fromAddrSet bool }{ { - name: "Insuficient params", + name: fmt.Sprintf("[%s] Insuficient params", txType), expectedErr: true, - ommitedValues: map[string]bool{ + omittedValues: map[string]bool{ "Nonce": true, "GasPrice": true, }, }, { - name: "Missing From", + name: fmt.Sprintf("[%s] Missing From", txType), expectedErr: false, - ommitedValues: map[string]bool{ - "From": true, + omittedValues: map[string]bool{ + "ChainID": txType != DynamicFeeTx, + "GasTipCap": txType != DynamicFeeTx, + "GasFeeCap": txType != DynamicFeeTx, + "GasPrice": txType == DynamicFeeTx, + "AccessList": txType != DynamicFeeTx, + "From": txType != StateTx, }, - fromAddrSet: false, + fromAddrSet: txType == StateTx, }, { - name: "Address set for state tx only", - expectedErr: false, - ommitedValues: map[string]bool{}, - fromAddrSet: txType == StateTx, + name: fmt.Sprintf("[%s] Address set for state tx only", txType), + expectedErr: false, + omittedValues: map[string]bool{ + "ChainID": txType != DynamicFeeTx, + "GasTipCap": txType != DynamicFeeTx, + "GasFeeCap": txType != DynamicFeeTx, + "GasPrice": txType == DynamicFeeTx, + "AccessList": txType != DynamicFeeTx, + "From": txType != StateTx, + }, + fromAddrSet: txType == StateTx, }, } for _, tt := range testTable { tt := tt + t.Run(tt.name, func(t *testing.T) { t.Parallel() arena := fastrlp.DefaultArenaPool.Get() parser := fastrlp.DefaultParserPool.Get() - testData := testRLPData(arena, tt.ommitedValues) + testData := testRLPData(arena, tt.omittedValues) v, err := parser.Parse(testData) assert.Nil(t, err) @@ -247,6 +267,10 @@ func TestRLPMarshall_And_Unmarshall_TxType(t *testing.T) { name: "LegacyTx", txType: LegacyTx, }, + { + name: "DynamicFeeTx", + txType: DynamicFeeTx, + }, { name: "undefined type", txType: TxType(0x09), @@ -269,56 +293,62 @@ func TestRLPMarshall_And_Unmarshall_TxType(t *testing.T) { } } -func testRLPData(arena *fastrlp.Arena, ommitValues map[string]bool) []byte { +func testRLPData(arena *fastrlp.Arena, omitValues map[string]bool) []byte { vv := arena.NewArray() - _, ommit := ommitValues["Nonce"] - if !ommit { + if omit, _ := omitValues["ChainID"]; !omit { + vv.Set(arena.NewBigInt(big.NewInt(0))) + } + + if omit, _ := omitValues["Nonce"]; !omit { vv.Set(arena.NewUint(10)) } - _, ommit = ommitValues["GasPrice"] - if !ommit { + if omit, _ := omitValues["GasTipCap"]; !omit { + vv.Set(arena.NewBigInt(big.NewInt(11))) + } + + if omit, _ := omitValues["GasFeeCap"]; !omit { + vv.Set(arena.NewBigInt(big.NewInt(11))) + } + + if omit, _ := omitValues["GasPrice"]; !omit { vv.Set(arena.NewBigInt(big.NewInt(11))) } - _, ommit = ommitValues["Gas"] - if !ommit { + if omit, _ := omitValues["Gas"]; !omit { vv.Set(arena.NewUint(12)) } - _, ommit = ommitValues["To"] - if !ommit { + if omit, _ := omitValues["To"]; !omit { vv.Set(arena.NewBytes((StringToAddress("13")).Bytes())) } - _, ommit = ommitValues["Value"] - if !ommit { + if omit, _ := omitValues["Value"]; !omit { vv.Set(arena.NewBigInt(big.NewInt(14))) } - _, ommit = ommitValues["Input"] - if !ommit { + if omit, _ := omitValues["Input"]; !omit { vv.Set(arena.NewCopyBytes([]byte{1, 2})) } - _, ommit = ommitValues["V"] - if !ommit { + if omit, _ := omitValues["AccessList"]; !omit { + vv.Set(arena.NewArray()) + } + + if omit, _ := omitValues["V"]; !omit { vv.Set(arena.NewBigInt(big.NewInt(15))) } - _, ommit = ommitValues["R"] - if !ommit { + if omit, _ := omitValues["R"]; !omit { vv.Set(arena.NewBigInt(big.NewInt(16))) } - _, ommit = ommitValues["S"] - if !ommit { + if omit, _ := omitValues["S"]; !omit { vv.Set(arena.NewBigInt(big.NewInt(17))) } - _, ommit = ommitValues["From"] - if !ommit { + if omit, _ := omitValues["From"]; !omit { vv.Set(arena.NewBytes((StringToAddress("18")).Bytes())) } diff --git a/types/rlp_marshal.go b/types/rlp_marshal.go index 7e244b9d27..cc9d3c32ec 100644 --- a/types/rlp_marshal.go +++ b/types/rlp_marshal.go @@ -1,6 +1,8 @@ package types import ( + "math/big" + "github.com/umbracle/fastrlp" ) @@ -91,6 +93,8 @@ func (h *Header) MarshalRLPWith(arena *fastrlp.Arena) *fastrlp.Value { vv.Set(arena.NewBytes(h.MixHash.Bytes())) vv.Set(arena.NewCopyBytes(h.Nonce[:])) + vv.Set(arena.NewUint(h.BaseFee)) + return vv } @@ -188,8 +192,25 @@ func (t *Transaction) MarshalRLPTo(dst []byte) []byte { func (t *Transaction) MarshalRLPWith(arena *fastrlp.Arena) *fastrlp.Value { vv := arena.NewArray() + // Specify zero chain ID as per spec. + // This is needed to have the same format as other EVM chains do. + // There is no chain ID in the TX object, so it is always 0 here just to be compatible. + // Check Transaction1559Payload there https://eips.ethereum.org/EIPS/eip-1559#specification + if t.Type == DynamicFeeTx { + vv.Set(arena.NewBigInt(big.NewInt(0))) + } + vv.Set(arena.NewUint(t.Nonce)) - vv.Set(arena.NewBigInt(t.GasPrice)) + + if t.Type == DynamicFeeTx { + // Add EIP-1559 related fields. + // For non-dynamic-fee-tx gas price is used. + vv.Set(arena.NewBigInt(t.GasTipCap)) + vv.Set(arena.NewBigInt(t.GasFeeCap)) + } else { + vv.Set(arena.NewBigInt(t.GasPrice)) + } + vv.Set(arena.NewUint(t.Gas)) // Address may be empty @@ -202,6 +223,14 @@ func (t *Transaction) MarshalRLPWith(arena *fastrlp.Arena) *fastrlp.Value { vv.Set(arena.NewBigInt(t.Value)) vv.Set(arena.NewCopyBytes(t.Input)) + // Specify access list as per spec. + // This is needed to have the same format as other EVM chains do. + // There is no access list feature here, so it is always empty just to be compatible. + // Check Transaction1559Payload there https://eips.ethereum.org/EIPS/eip-1559#specification + if t.Type == DynamicFeeTx { + vv.Set(arena.NewArray()) + } + // signature values vv.Set(arena.NewBigInt(t.V)) vv.Set(arena.NewBigInt(t.R)) diff --git a/types/rlp_unmarshal.go b/types/rlp_unmarshal.go index 5177507224..b651863bb4 100644 --- a/types/rlp_unmarshal.go +++ b/types/rlp_unmarshal.go @@ -218,6 +218,14 @@ func (h *Header) unmarshalRLPFrom(_ *fastrlp.Parser, v *fastrlp.Value) error { h.SetNonce(nonce) + // basefee + // In order to be backward compatible, the len should be checked before accessing the element + if len(elems) > 15 { + if h.BaseFee, err = elems[15].GetUint64(); err != nil { + return err + } + } + // compute the hash after the decoding h.ComputeHash() @@ -376,30 +384,71 @@ func (t *Transaction) unmarshalRLPFrom(p *fastrlp.Parser, v *fastrlp.Value) erro return err } - if len(elems) < 9 { - return fmt.Errorf("incorrect number of elements to decode transaction, expected 9 but found %d", len(elems)) + getElem := func() *fastrlp.Value { + val := elems[0] + elems = elems[1:] + + return val + } + + var num int + + switch t.Type { + case LegacyTx: + num = 9 + case StateTx: + num = 10 + case DynamicFeeTx: + num = 12 + default: + return fmt.Errorf("transaction type %d not found", t.Type) + } + + if numElems := len(elems); numElems != num { + return fmt.Errorf("incorrect number of transaction elements, expected %d but found %d", num, numElems) } p.Hash(t.Hash[:0], v) + // Skipping Chain ID field since we don't support it (yet) + // This is needed to be compatible with other EVM chains and have the same format. + // Since we don't have a chain ID, just skip it here. + if t.Type == DynamicFeeTx { + _ = getElem() + } + // nonce - if t.Nonce, err = elems[0].GetUint64(); err != nil { + if t.Nonce, err = getElem().GetUint64(); err != nil { return err } - // gasPrice - t.GasPrice = new(big.Int) - if err = elems[1].GetBigInt(t.GasPrice); err != nil { - return err + if t.Type == DynamicFeeTx { + // gasTipCap + t.GasTipCap = new(big.Int) + if err = getElem().GetBigInt(t.GasTipCap); err != nil { + return err + } + + // gasFeeCap + t.GasFeeCap = new(big.Int) + if err = getElem().GetBigInt(t.GasFeeCap); err != nil { + return err + } + } else { + // gasPrice + t.GasPrice = new(big.Int) + if err = getElem().GetBigInt(t.GasPrice); err != nil { + return err + } } // gas - if t.Gas, err = elems[2].GetUint64(); err != nil { + if t.Gas, err = getElem().GetUint64(); err != nil { return err } // to - if vv, _ := v.Get(3).Bytes(); len(vv) == 20 { + if vv, _ := getElem().Bytes(); len(vv) == 20 { // address addr := BytesToAddress(vv) t.To = &addr @@ -410,45 +459,48 @@ func (t *Transaction) unmarshalRLPFrom(p *fastrlp.Parser, v *fastrlp.Value) erro // value t.Value = new(big.Int) - if err = elems[4].GetBigInt(t.Value); err != nil { + if err = getElem().GetBigInt(t.Value); err != nil { return err } // input - if t.Input, err = elems[5].GetBytes(t.Input[:0]); err != nil { + if t.Input, err = getElem().GetBytes(t.Input[:0]); err != nil { return err } + // Skipping Access List field since we don't support it. + // This is needed to be compatible with other EVM chains and have the same format. + // Since we don't have access list, just skip it here. + if t.Type == DynamicFeeTx { + _ = getElem() + } + // V t.V = new(big.Int) - if err = elems[6].GetBigInt(t.V); err != nil { + if err = getElem().GetBigInt(t.V); err != nil { return err } // R t.R = new(big.Int) - if err = elems[7].GetBigInt(t.R); err != nil { + if err = getElem().GetBigInt(t.R); err != nil { return err } // S t.S = new(big.Int) - if err = elems[8].GetBigInt(t.S); err != nil { + if err = getElem().GetBigInt(t.S); err != nil { return err } if t.Type == StateTx { - // set From with default value t.From = ZeroAddress // We need to set From field for state transaction, // because we are using unique, predefined address, for sending such transactions - // From - if len(elems) >= 10 { - if vv, err := v.Get(9).Bytes(); err == nil && len(vv) == AddressLength { - // address - t.From = BytesToAddress(vv) - } + if vv, err := getElem().Bytes(); err == nil && len(vv) == AddressLength { + // address + t.From = BytesToAddress(vv) } } diff --git a/types/state_transaction.go b/types/state_transaction.go new file mode 100644 index 0000000000..bcdc0ec378 --- /dev/null +++ b/types/state_transaction.go @@ -0,0 +1,135 @@ +package types + +import ( + "fmt" + "math/big" + + "github.com/umbracle/ethgo" + "github.com/umbracle/ethgo/abi" +) + +var ( + stateSyncABIType = abi.MustNewType( + "tuple(tuple(uint256 id, address sender, address receiver, bytes data))") + + // this will change to use the generated code, but for now, we leave it as is, because of the circular import + ExecuteStateSyncABIMethod, _ = abi.NewMethod("function execute(" + + "bytes32[] proof, " + + "tuple(uint256 id, address sender, address receiver, bytes data) obj)") +) + +const ( + abiMethodIDLength = 4 +) + +// StateSyncEvent is a bridge event from the rootchain +type StateSyncEvent struct { + // ID is the decoded 'index' field from the event + ID uint64 + // Sender is the decoded 'sender' field from the event + Sender ethgo.Address + // Receiver is the decoded 'receiver' field from the event + Receiver ethgo.Address + // Data is the decoded 'data' field from the event + Data []byte +} + +// ToMap converts StateSyncEvent to map +func (sse *StateSyncEvent) ToMap() map[string]interface{} { + return map[string]interface{}{ + "id": sse.ID, + "sender": sse.Sender, + "receiver": sse.Receiver, + "data": sse.Data, + } +} + +// ToABI converts StateSyncEvent to ABI +func (sse *StateSyncEvent) EncodeAbi() ([]byte, error) { + return stateSyncABIType.Encode([]interface{}{sse.ToMap()}) +} + +func (sse *StateSyncEvent) String() string { + return fmt.Sprintf("Id=%d, Sender=%v, Target=%v", sse.ID, sse.Sender, sse.Receiver) +} + +type StateSyncProof struct { + Proof []Hash + StateSync *StateSyncEvent +} + +// EncodeAbi contains logic for encoding given ABI data +func (ssp *StateSyncProof) EncodeAbi() ([]byte, error) { + return ExecuteStateSyncABIMethod.Encode([2]interface{}{ssp.Proof, ssp.StateSync.ToMap()}) +} + +// DecodeAbi contains logic for decoding given ABI data +func (ssp *StateSyncProof) DecodeAbi(txData []byte) error { + if len(txData) < abiMethodIDLength { + return fmt.Errorf("invalid proof data, len = %d", len(txData)) + } + + rawResult, err := ExecuteStateSyncABIMethod.Inputs.Decode(txData[abiMethodIDLength:]) + if err != nil { + return err + } + + result, isOk := rawResult.(map[string]interface{}) + if !isOk { + return fmt.Errorf("invalid proof data") + } + + stateSyncEventEncoded, isOk := result["obj"].(map[string]interface{}) + if !isOk { + return fmt.Errorf("invalid state sync data") + } + + proofEncoded, isOk := result["proof"].([][32]byte) + if !isOk { + return fmt.Errorf("invalid proof data") + } + + id, isOk := stateSyncEventEncoded["id"].(*big.Int) + if !isOk { + return fmt.Errorf("invalid state sync event id") + } + + senderEthgo, isOk := stateSyncEventEncoded["sender"].(ethgo.Address) + if !isOk { + return fmt.Errorf("invalid state sync sender field") + } + + receiverEthgo, isOk := stateSyncEventEncoded["receiver"].(ethgo.Address) + if !isOk { + return fmt.Errorf("invalid state sync receiver field") + } + + data, isOk := stateSyncEventEncoded["data"].([]byte) + if !isOk { + return fmt.Errorf("invalid state sync data field") + } + + stateSync := &StateSyncEvent{ + ID: id.Uint64(), + Sender: senderEthgo, + Receiver: receiverEthgo, + Data: data, + } + + proof := make([]Hash, len(proofEncoded)) + for i := 0; i < len(proofEncoded); i++ { + proof[i] = Hash(proofEncoded[i]) + } + + *ssp = StateSyncProof{ + Proof: proof, + StateSync: stateSync, + } + + return nil +} + +type ExitProof struct { + Proof []Hash + LeafIndex uint64 +} diff --git a/types/transaction.go b/types/transaction.go index 92d4ef5e2c..b3ae1be5a0 100644 --- a/types/transaction.go +++ b/types/transaction.go @@ -5,52 +5,62 @@ import ( "math/big" "sync/atomic" + "github.com/0xPolygon/polygon-edge/helper/common" "github.com/0xPolygon/polygon-edge/helper/keccak" ) +const ( + // StateTransactionGasLimit is arbitrary default gas limit for state transactions + StateTransactionGasLimit = 1000000 +) + +// TxType is the transaction type. type TxType byte +// List of supported transaction types const ( - LegacyTx TxType = 0x0 - StateTx TxType = 0x7f - - StateTransactionGasLimit = 1000000 // some arbitrary default gas limit for state transactions + LegacyTx TxType = 0x0 + StateTx TxType = 0x7f + DynamicFeeTx TxType = 0x02 ) func txTypeFromByte(b byte) (TxType, error) { tt := TxType(b) switch tt { - case LegacyTx, StateTx: + case LegacyTx, StateTx, DynamicFeeTx: return tt, nil default: return tt, fmt.Errorf("unknown transaction type: %d", b) } } +// String returns string representation of the transaction type. func (t TxType) String() (s string) { switch t { case LegacyTx: return "LegacyTx" case StateTx: return "StateTx" + case DynamicFeeTx: + return "DynamicFeeTx" } return } type Transaction struct { - Nonce uint64 - GasPrice *big.Int - Gas uint64 - To *Address - Value *big.Int - Input []byte - V *big.Int - R *big.Int - S *big.Int - Hash Hash - From Address + Nonce uint64 + GasPrice *big.Int + GasTipCap *big.Int + GasFeeCap *big.Int + Gas uint64 + To *Address + Value *big.Int + Input []byte + V, R, S *big.Int + Hash Hash + From Address Type TxType @@ -86,6 +96,16 @@ func (t *Transaction) Copy() *Transaction { tt.GasPrice.Set(t.GasPrice) } + tt.GasTipCap = new(big.Int) + if t.GasTipCap != nil { + tt.GasTipCap.Set(t.GasTipCap) + } + + tt.GasFeeCap = new(big.Int) + if t.GasFeeCap != nil { + tt.GasFeeCap.Set(t.GasFeeCap) + } + tt.Value = new(big.Int) if t.Value != nil { tt.Value.Set(t.Value) @@ -109,12 +129,57 @@ func (t *Transaction) Copy() *Transaction { // Cost returns gas * gasPrice + value func (t *Transaction) Cost() *big.Int { - total := new(big.Int).Mul(t.GasPrice, new(big.Int).SetUint64(t.Gas)) - total.Add(total, t.Value) + var factor *big.Int + + if t.GasFeeCap != nil && t.GasFeeCap.BitLen() > 0 { + factor = new(big.Int).Set(t.GasFeeCap) + } else { + factor = new(big.Int).Set(t.GasPrice) + } + + total := new(big.Int).Mul(factor, new(big.Int).SetUint64(t.Gas)) + total = total.Add(total, t.Value) return total } +// GetGasPrice returns gas price if not empty, or calculates one based on +// the given EIP-1559 fields if exist +// +// Here is the logic: +// - use existing gas price if exists +// - or calculate a value with formula: min(gasFeeCap, gasTipCap * baseFee); +func (t *Transaction) GetGasPrice(baseFee uint64) *big.Int { + if t.GasPrice != nil && t.GasPrice.BitLen() > 0 { + return new(big.Int).Set(t.GasPrice) + } else if baseFee == 0 { + return new(big.Int) + } + + gasFeeCap := new(big.Int) + if t.GasFeeCap != nil { + gasFeeCap = gasFeeCap.Set(t.GasFeeCap) + } + + gasTipCap := new(big.Int) + if t.GasTipCap != nil { + gasTipCap = gasTipCap.Set(t.GasTipCap) + } + + gasPrice := new(big.Int) + if gasFeeCap.BitLen() > 0 || gasTipCap.BitLen() > 0 { + gasPrice = common.BigMin( + new(big.Int).Add( + gasTipCap, + new(big.Int).SetUint64(baseFee), + ), + new(big.Int).Set(gasFeeCap), + ) + } + + return gasPrice +} + func (t *Transaction) Size() uint64 { if size := t.size.Load(); size != nil { sizeVal, ok := size.(uint64) @@ -131,10 +196,17 @@ func (t *Transaction) Size() uint64 { return size } -func (t *Transaction) ExceedsBlockGasLimit(blockGasLimit uint64) bool { - return t.Gas > blockGasLimit -} +// EffectiveTip defines effective tip based on tx type. +// Spec: https://eips.ethereum.org/EIPS/eip-1559#specification +// We use EIP-1559 fields of the tx if the london hardfork is enabled. +// Effective tip be came to be either gas tip cap or (gas fee cap - current base fee) +func (t *Transaction) EffectiveTip(baseFee uint64) *big.Int { + if t.GasFeeCap != nil && t.GasTipCap != nil { + return common.BigMin( + new(big.Int).Sub(t.GasFeeCap, new(big.Int).SetUint64(baseFee)), + new(big.Int).Set(t.GasTipCap), + ) + } -func (t *Transaction) IsUnderpriced(priceLimit uint64) bool { - return t.GasPrice.Cmp(big.NewInt(0).SetUint64(priceLimit)) < 0 + return t.GetGasPrice(baseFee) } diff --git a/types/types.go b/types/types.go index 4cf4bd1948..d88e8bcc4f 100644 --- a/types/types.go +++ b/types/types.go @@ -10,15 +10,26 @@ import ( "github.com/0xPolygon/polygon-edge/helper/keccak" ) -var ZeroAddress = Address{} -var ZeroHash = Hash{} - const ( HashLength = 32 AddressLength = 20 + + SignatureSize = 4 ) -const SignatureSize = 4 +var ( + // ZeroAddress is the default zero address + ZeroAddress = Address{} + + // ZeroHash is the default zero hash + ZeroHash = Hash{} + + // EmptyRootHash is the root when there are no transactions + EmptyRootHash = StringToHash("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") + + // EmptyUncleHash is the root when there are no uncles + EmptyUncleHash = StringToHash("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347") +) type Hash [HashLength]byte @@ -149,14 +160,6 @@ func (a Address) MarshalText() ([]byte, error) { return []byte(a.String()), nil } -var ( - // EmptyRootHash is the root when there are no transactions - EmptyRootHash = StringToHash("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") - - // EmptyUncleHash is the root when there are no uncles - EmptyUncleHash = StringToHash("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347") -) - type Proof struct { Data []Hash // the proof himself Metadata map[string]interface{} diff --git a/types/types_test.go b/types/types_test.go index 82d6ae3ed6..7dc52446d5 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -68,15 +68,17 @@ func TestEIP55(t *testing.T) { func TestTransactionCopy(t *testing.T) { addrTo := StringToAddress("11") txn := &Transaction{ - Nonce: 0, - GasPrice: big.NewInt(11), - Gas: 11, - To: &addrTo, - Value: big.NewInt(1), - Input: []byte{1, 2}, - V: big.NewInt(25), - S: big.NewInt(26), - R: big.NewInt(27), + Nonce: 0, + GasTipCap: big.NewInt(11), + GasFeeCap: big.NewInt(11), + GasPrice: big.NewInt(11), + Gas: 11, + To: &addrTo, + Value: big.NewInt(1), + Input: []byte{1, 2}, + V: big.NewInt(25), + S: big.NewInt(26), + R: big.NewInt(27), } newTxn := txn.Copy() diff --git a/validators/store/contract/contract_test.go b/validators/store/contract/contract_test.go index e164538126..b91a89633e 100644 --- a/validators/store/contract/contract_test.go +++ b/validators/store/contract/contract_test.go @@ -4,6 +4,10 @@ import ( "errors" "testing" + "github.com/hashicorp/go-hclog" + lru "github.com/hashicorp/golang-lru" + "github.com/stretchr/testify/assert" + "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/contracts/staking" "github.com/0xPolygon/polygon-edge/crypto" @@ -14,9 +18,6 @@ import ( "github.com/0xPolygon/polygon-edge/types" "github.com/0xPolygon/polygon-edge/validators" "github.com/0xPolygon/polygon-edge/validators/store" - "github.com/hashicorp/go-hclog" - lru "github.com/hashicorp/golang-lru" - "github.com/stretchr/testify/assert" ) var ( @@ -82,6 +83,9 @@ func newTestTransition( ex := state.NewExecutor(&chain.Params{ Forks: chain.AllForksEnabled, + BurnContract: map[uint64]string{ + 0: types.ZeroAddress.String(), + }, }, st, hclog.NewNullLogger()) rootHash, err := ex.WriteGenesis(nil, types.Hash{})