diff --git a/chain/chain.go b/chain/chain.go index c6e5f80a9..015506f21 100644 --- a/chain/chain.go +++ b/chain/chain.go @@ -37,10 +37,23 @@ var ( // Chain is the blockchain chain configuration type Chain struct { - Name string `json:"name"` - Genesis *Genesis `json:"genesis"` - Params *Params `json:"params"` - Bootnodes []string `json:"bootnodes,omitempty"` + Name string `json:"name" yaml:"name"` + Genesis *Genesis `json:"genesis" yaml:"genesis"` + Params *Params `json:"params" yaml:"params"` + Bootnodes []string `json:"bootnodes,omitempty" yaml:"bootnodes,omitempty"` + Node *Node `json:"node,omitempty" yaml:"node,omitempty"` +} + +// Node represents the node configuration +type Node struct { + P2P struct { + StaticNodes []string `json:"staticNodes,omitempty" yaml:"staticNodes,omitempty"` + } `json:"p2p,omitempty" yaml:"p2p,omitempty"` +} + +// Bootnode is the blockchain bootnodes configuration +type Bootnode struct { + Bootnodes []string `json:"bootnodes" yaml:"bootnodes"` } // Genesis specifies the header fields, state of a genesis block diff --git a/chain/params.go b/chain/params.go index ea6d9a502..6812f821a 100644 --- a/chain/params.go +++ b/chain/params.go @@ -19,6 +19,7 @@ type Params struct { ChainID int64 `json:"chainID"` Engine map[string]interface{} `json:"engine"` BlockGasTarget uint64 `json:"blockGasTarget"` + Bootnodes []string `json:"bootnodes"` // Access control configuration ContractDeployerAllowList *AddressListConfig `json:"contractDeployerAllowList,omitempty"` diff --git a/command/bridge/deposit/erc1155/deposit_erc1155.go b/command/bridge/deposit/erc1155/deposit_erc1155.go index c17f24a78..930673ff6 100644 --- a/command/bridge/deposit/erc1155/deposit_erc1155.go +++ b/command/bridge/deposit/erc1155/deposit_erc1155.go @@ -1,6 +1,7 @@ package erc1155 import ( + "errors" "fmt" "math/big" "strings" @@ -18,13 +19,20 @@ import ( ) type depositERC1155Params struct { - *common.ERC1155BridgeParams - minterKey string + SenderKey string + JSONRPCAddr string + TokenAddr string + PredicateAddr string + Amounts []string + TokenIDs []string + Receivers []string + ChildChainMintable bool + minterKey string // Keep this for backward compatibility } var ( // depositParams is abstraction for provided bridge parameter values - dp *depositERC1155Params = &depositERC1155Params{ERC1155BridgeParams: common.NewERC1155BridgeParams()} + dp *depositERC1155Params = &depositERC1155Params{} ) // GetCommand returns the bridge deposit command @@ -303,3 +311,43 @@ func createApproveERC1155PredicateTxn(rootERC1155Predicate, return helper.CreateTransaction(ethgo.ZeroAddress, &addr, input, nil, !dp.ChildChainMintable), nil } + +func (dp *depositERC1155Params) RegisterCommonFlags(cmd *cobra.Command) { + cmd.Flags().StringVar( + &dp.SenderKey, + "sender-key", + "", + "the sender's private key", + ) + cmd.Flags().StringVar( + &dp.JSONRPCAddr, + "json-rpc", + "", + "the JSON RPC address", + ) +} + +func (dp *depositERC1155Params) Validate() error { + if dp.SenderKey == "" { + return errors.New("sender key is required") + } + if dp.JSONRPCAddr == "" { + return errors.New("JSON RPC address is required") + } + if dp.TokenAddr == "" { + return errors.New("token address is required") + } + if dp.PredicateAddr == "" { + return errors.New("predicate address is required") + } + if len(dp.Amounts) == 0 { + return errors.New("amounts are required") + } + if len(dp.TokenIDs) == 0 { + return errors.New("token IDs are required") + } + if len(dp.Receivers) == 0 { + return errors.New("receivers are required") + } + return nil +} diff --git a/command/bridge/deposit/erc20/deposit_erc20.go b/command/bridge/deposit/erc20/deposit_erc20.go index 6ff0f6225..3a553dc27 100644 --- a/command/bridge/deposit/erc20/deposit_erc20.go +++ b/command/bridge/deposit/erc20/deposit_erc20.go @@ -1,6 +1,7 @@ package erc20 import ( + "errors" "fmt" "math/big" @@ -18,13 +19,19 @@ import ( ) type depositERC20Params struct { - *common.ERC20BridgeParams - minterKey string + SenderKey string + JSONRPCAddr string + TokenAddr string + PredicateAddr string + Amounts []string + Receivers []string + ChildChainMintable bool + minterKey string // Keep this for backward compatibility } var ( // depositParams is abstraction for provided bridge parameter values - dp *depositERC20Params = &depositERC20Params{ERC20BridgeParams: common.NewERC20BridgeParams()} + dp *depositERC20Params = &depositERC20Params{} ) // GetCommand returns the bridge deposit command @@ -59,13 +66,6 @@ func GetCommand() *cobra.Command { "root ERC 20 token predicate address", ) - depositCmd.Flags().StringVar( - &dp.minterKey, - common.MinterKeyFlag, - "", - common.MinterKeyFlagDesc, - ) - _ = depositCmd.MarkFlagRequired(common.ReceiversFlag) _ = depositCmd.MarkFlagRequired(common.AmountsFlag) _ = depositCmd.MarkFlagRequired(common.RootTokenFlag) @@ -284,3 +284,40 @@ func createDepositTxn(sender, receiver types.Address, amount *big.Int) (*ethgo.T return helper.CreateTransaction(ethgo.Address(sender), &addr, input, nil, !dp.ChildChainMintable), nil } + +func (dp *depositERC20Params) RegisterCommonFlags(cmd *cobra.Command) { + cmd.Flags().StringVar( + &dp.SenderKey, + "sender-key", + "", + "the sender's private key", + ) + cmd.Flags().StringVar( + &dp.JSONRPCAddr, + "json-rpc", + "", + "the JSON RPC address", + ) +} + +func (dp *depositERC20Params) Validate() error { + if dp.SenderKey == "" { + return errors.New("sender key is required") + } + if dp.JSONRPCAddr == "" { + return errors.New("JSON RPC address is required") + } + if dp.TokenAddr == "" { + return errors.New("token address is required") + } + if dp.PredicateAddr == "" { + return errors.New("predicate address is required") + } + if len(dp.Amounts) == 0 { + return errors.New("amounts are required") + } + if len(dp.Receivers) == 0 { + return errors.New("receivers are required") + } + return nil +} diff --git a/command/bridge/deposit/erc721/deposit_erc721.go b/command/bridge/deposit/erc721/deposit_erc721.go index 1ad195da8..320c3bc9c 100644 --- a/command/bridge/deposit/erc721/deposit_erc721.go +++ b/command/bridge/deposit/erc721/deposit_erc721.go @@ -1,6 +1,7 @@ package deposit import ( + "errors" "fmt" "math/big" "strings" @@ -17,12 +18,17 @@ import ( ) type depositERC721Params struct { - *common.ERC721BridgeParams - minterKey string + SenderKey string + JSONRPCAddr string + TokenAddr string + PredicateAddr string + TokenIDs []string + Receivers []string + ChildChainMintable bool } var ( - dp *depositERC721Params = &depositERC721Params{ERC721BridgeParams: common.NewERC721BridgeParams()} + dp *depositERC721Params = &depositERC721Params{} ) func GetCommand() *cobra.Command { @@ -56,13 +62,6 @@ func GetCommand() *cobra.Command { "root ERC 721 token predicate address", ) - depositCmd.Flags().StringVar( - &dp.minterKey, - common.MinterKeyFlag, - "", - common.MinterKeyFlagDesc, - ) - _ = depositCmd.MarkFlagRequired(common.ReceiversFlag) _ = depositCmd.MarkFlagRequired(common.TokenIDsFlag) _ = depositCmd.MarkFlagRequired(common.RootTokenFlag) @@ -110,14 +109,7 @@ func runCommand(cmd *cobra.Command, _ []string) { tokenIDs[i] = tokenID } - if dp.minterKey != "" { - minterKey, err := helper.DecodePrivateKey(dp.minterKey) - if err != nil { - outputter.SetError(fmt.Errorf("invalid minter key provided: %w", err)) - - return - } - + if dp.ChildChainMintable { for i := 0; i < len(tokenIDs); i++ { mintTxn, err := createMintTxn(types.Address(depositorAddr), types.Address(depositorAddr)) if err != nil { @@ -126,7 +118,7 @@ func runCommand(cmd *cobra.Command, _ []string) { return } - receipt, err := txRelayer.SendTransaction(mintTxn, minterKey) + receipt, err := txRelayer.SendTransaction(mintTxn, depositorKey) if err != nil { outputter.SetError(fmt.Errorf("failed to send mint transaction to depositor %s: %w", depositorAddr, err)) @@ -271,3 +263,40 @@ func createApproveERC721PredicateTxn(rootERC721Predicate, rootERC721Token types. return helper.CreateTransaction(ethgo.ZeroAddress, &addr, input, nil, !dp.ChildChainMintable), nil } + +func (dp *depositERC721Params) RegisterCommonFlags(cmd *cobra.Command) { + cmd.Flags().StringVar( + &dp.SenderKey, + "sender-key", + "", + "the sender's private key", + ) + cmd.Flags().StringVar( + &dp.JSONRPCAddr, + "json-rpc", + "", + "the JSON RPC address", + ) +} + +func (dp *depositERC721Params) Validate() error { + if dp.SenderKey == "" { + return errors.New("sender key is required") + } + if dp.JSONRPCAddr == "" { + return errors.New("JSON RPC address is required") + } + if dp.TokenAddr == "" { + return errors.New("token address is required") + } + if dp.PredicateAddr == "" { + return errors.New("predicate address is required") + } + if len(dp.TokenIDs) == 0 { + return errors.New("token IDs are required") + } + if len(dp.Receivers) == 0 { + return errors.New("receivers are required") + } + return nil +} diff --git a/command/genesis/params.go b/command/genesis/params.go index c8023f937..a67cf3a71 100644 --- a/command/genesis/params.go +++ b/command/genesis/params.go @@ -3,19 +3,21 @@ package genesis import ( "errors" "fmt" + "math/big" "os" "time" "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/command" - "github.com/0xPolygon/polygon-edge/command/helper" + cmdhelper "github.com/0xPolygon/polygon-edge/command/helper" "github.com/0xPolygon/polygon-edge/consensus/ibft" "github.com/0xPolygon/polygon-edge/consensus/ibft/fork" "github.com/0xPolygon/polygon-edge/consensus/ibft/signer" "github.com/0xPolygon/polygon-edge/consensus/polybft" "github.com/0xPolygon/polygon-edge/contracts" "github.com/0xPolygon/polygon-edge/contracts/staking" - stakingHelper "github.com/0xPolygon/polygon-edge/helper/staking" + stakinghelper "github.com/0xPolygon/polygon-edge/contracts/staking/helper" + "github.com/0xPolygon/polygon-edge/helper/safe" "github.com/0xPolygon/polygon-edge/secrets" "github.com/0xPolygon/polygon-edge/server" "github.com/0xPolygon/polygon-edge/types" @@ -100,6 +102,7 @@ type genesisParams struct { consensusEngineConfig map[string]interface{} genesisConfig *chain.Chain + bootnode *chain.Bootnode // PolyBFT sprintSize uint64 @@ -126,7 +129,7 @@ type genesisParams struct { nativeTokenConfigRaw string nativeTokenConfig *polybft.TokenConfig - premineInfos []*helper.PremineInfo + premineInfos []*stakinghelper.PremineInfo blockTrackerPollInterval time.Duration @@ -158,21 +161,7 @@ func (p *genesisParams) validateFlags() error { return err } - if p.isPolyBFTConsensus() { //nolint:wsl - // Hydra modification: we don't use a separate native erc20 token neither a burn contract, - // thus, we don't need the following validations - // if err := p.extractNativeTokenMetadata(); err != nil { - // return err - // } - - // if err := p.validateBurnContract(); err != nil { - // return err - // } - - // if err := p.validatePremineInfo(); err != nil { - // return err - // } - + if p.isPolyBFTConsensus() { if err := p.validateProxyContractsAdmin(); err != nil { return err } @@ -330,7 +319,16 @@ func (p *genesisParams) initValidatorSet() error { } func (p *genesisParams) isValidatorNumberValid() bool { - return p.ibftValidators == nil || uint64(p.ibftValidators.Len()) <= p.maxNumValidators //nolint:gosec + if p.ibftValidators == nil { + return true + } + + validatorLen, err := safe.SafeIntToUint64(p.ibftValidators.Len()) + if err != nil { + return false + } + + return validatorLen <= p.maxNumValidators } func (p *genesisParams) initIBFTExtraData() { @@ -391,11 +389,11 @@ func (p *genesisParams) generateGenesis() error { return err } - if err := helper.WriteGenesisConfigToDisk( + if err := cmdhelper.WriteGenesisConfigToDisk( p.genesisConfig, p.genesisPath, ); err != nil { - return err + return fmt.Errorf("failed to write genesis config to disk: %w", err) } return nil @@ -409,6 +407,11 @@ func (p *genesisParams) initGenesisConfig() error { // enabledForks.RemoveFork(chain.London) // } + chainID, err := safe.SafeUint64ToInt64(p.chainID) + if err != nil { + return fmt.Errorf("invalid chain ID: %w", err) + } + chainConfig := &chain.Chain{ Name: p.name, Genesis: &chain.Genesis{ @@ -419,11 +422,15 @@ func (p *genesisParams) initGenesisConfig() error { GasUsed: command.DefaultGenesisGasUsed, }, Params: &chain.Params{ - ChainID: int64(p.chainID), //nolint:gosec + ChainID: chainID, Forks: enabledForks, Engine: p.consensusEngineConfig, BlockGasTarget: 100000000, }, + } + + bootnodeConfig := &chain.Bootnode{ + Bootnodes: p.bootnodes, } @@ -444,12 +451,20 @@ func (p *genesisParams) initGenesisConfig() error { } for _, premineInfo := range p.premineInfos { - chainConfig.Genesis.Alloc[premineInfo.Address] = &chain.GenesisAccount{ - Balance: premineInfo.Amount, + amount, ok := new(big.Int).SetString(premineInfo.Amount, 10) + if !ok { + return fmt.Errorf("invalid premine amount: %s", premineInfo.Amount) + } + + account := &chain.GenesisAccount{ + Balance: amount, } + + chainConfig.Genesis.Alloc[premineInfo.Address] = account } p.genesisConfig = chainConfig + p.bootnode = bootnodeConfig return nil } @@ -461,9 +476,9 @@ func (p *genesisParams) shouldPredeployStakingSC() bool { } func (p *genesisParams) predeployStakingSC() (*chain.GenesisAccount, error) { - stakingAccount, predeployErr := stakingHelper.PredeployStakingSC( + stakingAccount, predeployErr := stakinghelper.PredeployStakingSC( p.ibftValidators, - stakingHelper.PredeployParams{ + stakinghelper.PredeployParams{ MinValidatorCount: p.minNumValidators, MaxValidatorCount: p.maxNumValidators, }) @@ -476,10 +491,10 @@ func (p *genesisParams) predeployStakingSC() (*chain.GenesisAccount, error) { // parsePremineInfo parses premine flag func (p *genesisParams) parsePremineInfo() error { - p.premineInfos = make([]*helper.PremineInfo, 0, len(p.premine)) + p.premineInfos = make([]*stakinghelper.PremineInfo, 0, len(p.premine)) for _, premine := range p.premine { - premineInfo, err := helper.ParsePremineInfo(premine) + premineInfo, err := stakinghelper.ParsePremineInfo(premine) if err != nil { return fmt.Errorf("invalid premine balance amount provided: %w", err) } @@ -506,7 +521,7 @@ func (p *genesisParams) validatePremineInfo() error { // which can not be 0 func (p *genesisParams) validateBlockTrackerPollInterval() error { if p.blockTrackerPollInterval == 0 { - return helper.ErrBlockTrackerPollInterval + return errBlockTrackerPollInterval } return nil diff --git a/command/genesis/polybft_params.go b/command/genesis/polybft_params.go index 1f31f7e9a..ee1873320 100644 --- a/command/genesis/polybft_params.go +++ b/command/genesis/polybft_params.go @@ -4,11 +4,14 @@ import ( "errors" "fmt" "math/big" + "os" "path" + "path/filepath" "strings" "time" "github.com/multiformats/go-multiaddr" + "gopkg.in/yaml.v2" "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/command" @@ -18,7 +21,9 @@ import ( "github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi/artifact" "github.com/0xPolygon/polygon-edge/consensus/polybft/validator" "github.com/0xPolygon/polygon-edge/contracts" + stakinghelper "github.com/0xPolygon/polygon-edge/contracts/staking/helper" "github.com/0xPolygon/polygon-edge/helper/common" + "github.com/0xPolygon/polygon-edge/helper/safe" "github.com/0xPolygon/polygon-edge/secrets" "github.com/0xPolygon/polygon-edge/server" "github.com/0xPolygon/polygon-edge/types" @@ -74,13 +79,46 @@ type PricesDataCoinGecko struct { Prices [][]float64 `json:"prices"` } +type NodeConfig struct { + Node struct { + P2P struct { + StaticNodes []string `yaml:"StaticNodes"` + } `yaml:"P2P"` + } `yaml:"Node"` + JSON struct { + Enabled bool `yaml:"Enabled"` + } `yaml:"JSON"` +} + // generatePolyBftChainConfig creates and persists polybft chain configuration to the provided file path func (p *genesisParams) generatePolyBftChainConfig(o command.OutputFormatter) error { // populate premine balance map - premineBalances := make(map[types.Address]*helper.PremineInfo, len(p.premine)) + premineBalances := make(map[types.Address]*stakinghelper.PremineInfo, len(p.premine)) for _, premine := range p.premineInfos { - premineBalances[premine.Address] = premine + amount, ok := new(big.Int).SetString(premine.Amount, 10) + if !ok { + return fmt.Errorf("invalid premine amount: %s", premine.Amount) + } + + premineBalances[premine.Address] = &stakinghelper.PremineInfo{ + Address: premine.Address, + Amount: amount.String(), + } + } + + // Handle validator stakes + for _, validatorStr := range p.validators { + parts := strings.Split(validatorStr, ":") + if len(parts) != 4 { + return fmt.Errorf("invalid validator format: %s", validatorStr) + } + + address := types.StringToAddress(strings.TrimPrefix(parts[1], "0x")) + premineBalances[address] = &stakinghelper.PremineInfo{ + Address: address, + Amount: command.DefaultStake.String(), + } } // Hydra modification: we don't use native token and alaways premine balances to the specified accounts @@ -117,6 +155,16 @@ func (p *genesisParams) generatePolyBftChainConfig(o command.OutputFormatter) er return err } + // Generate config.yaml with bootnodes + if err := p.generateNodeConfig(initialValidators); err != nil { + return fmt.Errorf("failed to generate node config: %w", err) + } + + chainID, err := safe.SafeUint64ToInt64(p.chainID) + if err != nil { + return fmt.Errorf("invalid chain ID: %w", err) + } + polyBftConfig := &polybft.PolyBFTConfig{ InitialValidatorSet: initialValidators, BlockTime: common.Duration{Duration: p.blockTime}, @@ -148,14 +196,13 @@ func (p *genesisParams) generatePolyBftChainConfig(o command.OutputFormatter) er chainConfig := &chain.Chain{ Name: p.name, Params: &chain.Params{ - ChainID: int64(p.chainID), //nolint:gosec + ChainID: chainID, Forks: enabledForks, Engine: map[string]interface{}{ string(server.PolyBFTConsensus): polyBftConfig, }, BlockGasTarget: 100000000, }, - Bootnodes: p.bootnodes, } // Hydra modification: we use the 0x0 address for burning, thus, we don't need this @@ -183,9 +230,9 @@ func (p *genesisParams) generatePolyBftChainConfig(o command.OutputFormatter) er // increment total stake totalStake.Add(totalStake, validator.Stake) - premineBalances[validator.Address] = &helper.PremineInfo{ + premineBalances[validator.Address] = &stakinghelper.PremineInfo{ Address: validator.Address, - Amount: validator.Stake, + Amount: validator.Stake.String(), } } @@ -202,8 +249,13 @@ func (p *genesisParams) generatePolyBftChainConfig(o command.OutputFormatter) er continue } + amount, ok := new(big.Int).SetString(premine.Amount, 10) + if !ok { + return fmt.Errorf("invalid premine amount: %s", premine.Amount) + } + allocs[premine.Address] = &chain.GenesisAccount{ - Balance: premine.Amount, + Balance: amount, } } @@ -220,11 +272,6 @@ func (p *genesisParams) generatePolyBftChainConfig(o command.OutputFormatter) er } validatorMetadata[i] = metadata - - // set genesis validators as boot nodes if boot nodes not provided via CLI - if len(p.bootnodes) == 0 { - chainConfig.Bootnodes = append(chainConfig.Bootnodes, validator.MultiAddr) - } } genesisExtraData, err := GenerateExtraDataPolyBft(validatorMetadata) @@ -531,3 +578,35 @@ func (p *genesisParams) initSecretsConfig() error { return nil } + +func (p *genesisParams) generateNodeConfig(validators []*validator.GenesisValidator) error { + config := &NodeConfig{} + config.Node.P2P.StaticNodes = make([]string, len(validators)) + config.JSON.Enabled = false + + // Generate static nodes from validators + for i, validator := range validators { + nodeID := strings.Split(validator.MultiAddr, "/p2p/")[1] + multiAddr := fmt.Sprintf("/ip4/127.0.0.1/tcp/%d/p2p/%s", + bootnodePortStart+i, + nodeID) + config.Node.P2P.StaticNodes[i] = multiAddr + } + + // Create config directory if it doesn't exist + configDir := filepath.Dir(p.genesisPath) + configPath := filepath.Join(configDir, "config.yaml") + + file, err := os.OpenFile(configPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("failed to create config file: %w", err) + } + defer file.Close() + + encoder := yaml.NewEncoder(file) + if err := encoder.Encode(config); err != nil { + return fmt.Errorf("failed to encode config: %w", err) + } + + return nil +} diff --git a/command/rootchain/helper/utils.go b/command/rootchain/helper/utils.go index 6654624a8..e79434de7 100644 --- a/command/rootchain/helper/utils.go +++ b/command/rootchain/helper/utils.go @@ -22,7 +22,6 @@ import ( "github.com/umbracle/ethgo/wallet" ) -//nolint:gosec const ( TestAccountPrivKey = "aa75e9a7d427efc732f8e4f1a5b7646adcc61fd5bae40f80d13c8419c9f43d6d" TestModeFlag = "test" @@ -30,18 +29,23 @@ const ( SupernetManagerFlagDesc = "address of supernet manager contract" StakeManagerFlag = "stake-manager" StakeManagerFlagDesc = "address of stake manager contract" + //nolint:gosec NativeRootTokenFlag = "native-root-token" NativeRootTokenFlagDesc = "address of native root token" GenesisPathFlag = "genesis" GenesisPathFlagDesc = "genesis file path, which contains chain configuration" + BootnodePathFlag = "bootnode" + BootnodePathFlagDesc = "bootnode file path, which contains bootnodes configuration" DefaultGenesisPath = "./genesis.json" StakeTokenFlag = "stake-token" + //nolint:gosec // This is a flag description, not a credential StakeTokenFlagDesc = "address of ERC20 token used for staking on rootchain" ProxyContractsAdminFlag = "proxy-contracts-admin" ProxyContractsAdminDesc = "admin for proxy contracts" AddressesFlag = "addresses" AmountsFlag = "amounts" - Erc20TokenFlag = "erc20-token" //nolint:gosec + //nolint:gosec // This is a flag name, not a credential + Erc20TokenFlag = "erc20-token" ) var ( diff --git a/command/server/config/config.go b/command/server/config/config.go index 87c42ab22..0d2114979 100644 --- a/command/server/config/config.go +++ b/command/server/config/config.go @@ -14,7 +14,8 @@ import ( // Config defines the server configuration params type Config struct { - GenesisFile string `json:"chain_config" yaml:"chain_config"` + GenesisPath string `json:"chain_config" yaml:"chain_config"` + BootnodePath string `json:"bootnode_config" yaml:"bootnode_config"` SecretsConfigPath string `json:"secrets_config" yaml:"secrets_config"` DataDir string `json:"data_dir" yaml:"data_dir"` BlockGasTarget string `json:"block_gas_target" yaml:"block_gas_target"` @@ -40,6 +41,9 @@ type Config struct { WebSocketReadLimit uint64 `json:"web_socket_read_limit" yaml:"web_socket_read_limit"` MetricsInterval time.Duration `json:"metrics_interval" yaml:"metrics_interval"` + + Chain string `json:"chain"` // Path to genesis.json + BootnodesPath string `json:"bootnodes"` // Path to bootnodes.json } // Telemetry holds the config details for metric services. @@ -104,7 +108,7 @@ func DefaultConfig() *Config { defaultNetworkConfig := network.DefaultConfig() return &Config{ - GenesisFile: "mainnet", // will be resolved to the actual path + GenesisPath: "./genesis.json", // will be resolved to the actual path DataDir: "", BlockGasTarget: "0x5f5e100", // Special value signaling the parent gas limit should be applied Network: &Network{ diff --git a/command/server/init.go b/command/server/init.go index a0461d876..216c4e28c 100644 --- a/command/server/init.go +++ b/command/server/init.go @@ -107,7 +107,7 @@ func (p *serverParams) initGenesisConfig() error { var parseErr error if p.genesisConfig, parseErr = chain.Import( - p.rawConfig.GenesisFile, + p.rawConfig.GenesisPath, ); parseErr != nil { return parseErr } diff --git a/command/server/params.go b/command/server/params.go index 54b1d3071..70599f27c 100644 --- a/command/server/params.go +++ b/command/server/params.go @@ -3,6 +3,8 @@ package server import ( "errors" "net" + "os" + "path/filepath" "strings" "github.com/0xPolygon/polygon-edge/chain" @@ -12,11 +14,13 @@ import ( "github.com/0xPolygon/polygon-edge/server" "github.com/hashicorp/go-hclog" "github.com/multiformats/go-multiaddr" + "gopkg.in/yaml.v3" ) const ( configFlag = "config" - genesisFlag = "chain" + genesisPathFlag = "chain" + bootnodePathFlag = "bootnodes" dataDirFlag = "data-dir" libp2pAddressFlag = "libp2p" prometheusAddressFlag = "prometheus" @@ -64,6 +68,7 @@ var ( Network: &config.Network{}, TxPool: &config.TxPool{}, }, + logger: hclog.NewNullLogger(), } ) @@ -75,6 +80,7 @@ var ( type serverParams struct { rawConfig *config.Config configPath string + logger hclog.Logger libp2pAddress *net.TCPAddr prometheusAddress *net.TCPAddr @@ -161,14 +167,61 @@ func (p *serverParams) setGenesisFileFlag(genesisFlag string) error { } actualPath := strings.TrimPrefix(genesisFlag, customPrefix) - p.rawConfig.GenesisFile = actualPath + p.rawConfig.GenesisPath = actualPath return nil } func (p *serverParams) generateConfig() *server.Config { + // Load bootnode config + var bootnodeConfig *chain.Bootnode + + // First try to load from specified bootnode path + if p.rawConfig.BootnodesPath != "" { + bootnode, err := chain.Import(p.rawConfig.BootnodesPath) + if err == nil { + bootnodeConfig = &chain.Bootnode{ + Bootnodes: bootnode.Bootnodes, + } + } + } + + // If no bootnodes found, try to load from config.yaml + if bootnodeConfig == nil || len(bootnodeConfig.Bootnodes) == 0 { + configPath := filepath.Join(filepath.Dir(p.rawConfig.GenesisPath), "config.yaml") + if _, err := os.Stat(configPath); err == nil { + if data, err := os.ReadFile(configPath); err == nil { + var nodeConfig struct { + Node struct { + P2P struct { + StaticNodes []string `yaml:"StaticNodes"` + } `yaml:"P2P"` + } `yaml:"Node"` + } + + if err := yaml.Unmarshal(data, &nodeConfig); err == nil { + bootnodeConfig = &chain.Bootnode{ + Bootnodes: nodeConfig.Node.P2P.StaticNodes, + } + } + } + } + } + return &server.Config{ - Chain: p.genesisConfig, + Chain: p.genesisConfig, + Bootnode: bootnodeConfig, + Network: &network.Config{ + NoDiscover: p.rawConfig.Network.NoDiscover, + Addr: p.libp2pAddress, + NatAddr: p.natAddress, + DNS: p.dnsAddress, + DataDir: p.rawConfig.DataDir, + MaxPeers: p.rawConfig.Network.MaxPeers, + MaxInboundPeers: p.rawConfig.Network.MaxInboundPeers, + MaxOutboundPeers: p.rawConfig.Network.MaxOutboundPeers, + }, + Seal: true, // Enable sealing JSONRPC: &server.JSONRPC{ JSONRPCAddr: p.jsonRPCAddress, AccessControlAllowOrigin: p.rawConfig.CorsAllowedOrigins, @@ -182,31 +235,75 @@ func (p *serverParams) generateConfig() *server.Config { Telemetry: &server.Telemetry{ PrometheusAddr: p.prometheusAddress, }, - Network: &network.Config{ - NoDiscover: p.rawConfig.Network.NoDiscover, - Addr: p.libp2pAddress, - NatAddr: p.natAddress, - DNS: p.dnsAddress, - DataDir: p.rawConfig.DataDir, - MaxPeers: p.rawConfig.Network.MaxPeers, - MaxInboundPeers: p.rawConfig.Network.MaxInboundPeers, - MaxOutboundPeers: p.rawConfig.Network.MaxOutboundPeers, - Chain: p.genesisConfig, - }, - DataDir: p.rawConfig.DataDir, - Seal: p.rawConfig.ShouldSeal, - PriceLimit: p.rawConfig.TxPool.PriceLimit, - MaxSlots: p.rawConfig.TxPool.MaxSlots, - MaxAccountEnqueued: p.rawConfig.TxPool.MaxAccountEnqueued, - SecretsManager: p.secretsConfig, - RestoreFile: p.getRestoreFilePath(), - LogLevel: hclog.LevelFromString(p.rawConfig.LogLevel), - JSONLogFormat: p.rawConfig.JSONLogFormat, - LogFilePath: p.logFileLocation, - - // Hydra modification: relayer must be disabled + DataDir: p.rawConfig.DataDir, + PriceLimit: p.rawConfig.TxPool.PriceLimit, + MaxSlots: p.rawConfig.TxPool.MaxSlots, + MaxAccountEnqueued: p.rawConfig.TxPool.MaxAccountEnqueued, + SecretsManager: p.secretsConfig, + RestoreFile: p.getRestoreFilePath(), + LogLevel: hclog.LevelFromString(p.rawConfig.LogLevel), + JSONLogFormat: p.rawConfig.JSONLogFormat, + LogFilePath: p.logFileLocation, Relayer: false, NumBlockConfirmations: p.rawConfig.NumBlockConfirmations, MetricsInterval: p.rawConfig.MetricsInterval, } } + +func (p *serverParams) processBootnodeConfig(bootnode *chain.Chain) *chain.Bootnode { + bootnodeConfig := &chain.Bootnode{ + Bootnodes: make([]string, 0), + } + + // Add bootnodes from chain config + if bootnode.Bootnodes != nil { + p.logger.Debug("Adding bootnodes from chain config", "count", len(bootnode.Bootnodes)) + bootnodeConfig.Bootnodes = append(bootnodeConfig.Bootnodes, bootnode.Bootnodes...) + } + + // Add bootnodes from params + if bootnode.Params != nil && bootnode.Params.Bootnodes != nil { + p.logger.Debug("Adding bootnodes from params", "count", len(bootnode.Params.Bootnodes)) + bootnodeConfig.Bootnodes = append(bootnodeConfig.Bootnodes, bootnode.Params.Bootnodes...) + } + + // Add static nodes if available + if bootnode.Node != nil && len(bootnode.Node.P2P.StaticNodes) > 0 { + p.logger.Debug("Adding static nodes", "count", len(bootnode.Node.P2P.StaticNodes)) + bootnodeConfig.Bootnodes = append(bootnodeConfig.Bootnodes, bootnode.Node.P2P.StaticNodes...) + } + + // Remove duplicates + bootnodeConfig.Bootnodes = removeDuplicates(bootnodeConfig.Bootnodes) + + return bootnodeConfig +} + +func removeDuplicates(elements []string) []string { + seen := make(map[string]struct{}) + result := make([]string, 0) + + for _, element := range elements { + if _, exists := seen[element]; !exists { + seen[element] = struct{}{} + } + + element := strings.TrimSpace(element) + if element != "" { + result = append(result, element) + } + } + + return result +} + +func init() { + params = &serverParams{ + rawConfig: &config.Config{ + Telemetry: &config.Telemetry{}, + Network: &config.Network{}, + TxPool: &config.TxPool{}, + }, + logger: hclog.NewNullLogger(), + } +} diff --git a/command/server/server.go b/command/server/server.go index b06011f9c..82247d237 100644 --- a/command/server/server.go +++ b/command/server/server.go @@ -47,13 +47,19 @@ func setFlags(cmd *cobra.Command) { ) cmd.Flags().StringVar( - ¶ms.rawConfig.GenesisFile, - genesisFlag, - defaultConfig.GenesisFile, + ¶ms.rawConfig.GenesisPath, + genesisPathFlag, + defaultConfig.GenesisPath, "the genesis file used for starting the chain."+ `Can be "mainnet", "testnet" or "custom""`, ) + cmd.Flags().StringVar( + ¶ms.rawConfig.BootnodePath, + bootnodePathFlag, + defaultConfig.BootnodePath, + "the bootnode file used for connecting to chain", + ) cmd.Flags().StringVar( ¶ms.configPath, configFlag, @@ -303,7 +309,7 @@ func runPreRun(cmd *cobra.Command, _ []string) error { } // Before raw params are initialized, set the actual genesis path (if custom) based on --chain flag - if err := params.setGenesisFileFlag(params.rawConfig.GenesisFile); err != nil { + if err := params.setGenesisFileFlag(params.rawConfig.GenesisPath); err != nil { return err } @@ -338,5 +344,11 @@ func runServerLoop( return err } - return helper.HandleSignals(serverInstance.Close, outputter) + closeFn := func() { + if err := serverInstance.Close(); err != nil { + outputter.SetError(fmt.Errorf("failed to close server: %w", err)) + } + } + + return helper.HandleSignals(closeFn, outputter) } diff --git a/command/sidechain/commission/commission.go b/command/sidechain/commission/commission.go index c6bdad602..90a50f158 100644 --- a/command/sidechain/commission/commission.go +++ b/command/sidechain/commission/commission.go @@ -124,11 +124,13 @@ func runCommand(cmd *cobra.Command, _ []string) error { } var input []byte - if params.apply { //nolint:gocritic + + switch { + case params.apply: input, err = generateApplyPendingCommissionFn() - } else if params.claim { + case params.claim: input, err = generateClaimCommissionFn(validatorAccount) - } else { + default: input, err = generateSetPendingCommissionFn() } @@ -187,11 +189,12 @@ func runCommand(cmd *cobra.Command, _ []string) error { } } - if !params.apply && !params.claim && !foundPendingCommissionLog { //nolint:gocritic + switch { + case !params.apply && !params.claim && !foundPendingCommissionLog: return fmt.Errorf("could not find an appropriate log in the receipt that validates the new pending commission") - } else if params.apply && !foundCommissionUpdatedLog { + case params.apply && !foundCommissionUpdatedLog: return fmt.Errorf("could not find an appropriate log in the receipt that validates the new commission update") - } else if params.claim && !foundClaimCommissionLog { + case params.claim && !foundClaimCommissionLog: return fmt.Errorf("could not find an appropriate log in the receipt that validates the commission claim") } diff --git a/command/sidechain/commission/params.go b/command/sidechain/commission/params.go index 107f74a86..7752047d9 100644 --- a/command/sidechain/commission/params.go +++ b/command/sidechain/commission/params.go @@ -57,20 +57,24 @@ func (scr setCommissionResult) GetOutput() string { var valueString string - if scr.isPending { //nolint:gocritic + switch { + case scr.isPending: buffer.WriteString("\n[NEW COMMISSION IS PENDING AND YOU MUST EXECUTE APPLY TX AFTER 15 DAYS]\n") addressString = "Staker Address" + valueString = "New Commission" - } else if scr.isClaimed { + case scr.isClaimed: buffer.WriteString("\n[COMMISSION CLAIMED]\n") addressString = "Received Address" + valueString = "Claimed Commission (wei)" - } else { + default: buffer.WriteString("\n[COMMISSION APPLIED]\n") addressString = "Staker Address" + valueString = "Applied Commission" } diff --git a/consensus/ibft/fork/storewrapper.go b/consensus/ibft/fork/storewrapper.go index 47e5710fe..56ff62d09 100644 --- a/consensus/ibft/fork/storewrapper.go +++ b/consensus/ibft/fork/storewrapper.go @@ -149,6 +149,11 @@ func (w *ContractValidatorStoreWrapper) Close() error { return nil } +// SourceType returns the source type of the validator store +func (w *ContractValidatorStoreWrapper) SourceType() store.SourceType { + return store.Contract +} + // GetValidators gets and returns validators at the given height func (w *ContractValidatorStoreWrapper) GetValidators( height, epochSize, forkFrom uint64, @@ -192,3 +197,7 @@ func calculateContractStoreFetchingHeight(height, epochSize, forkFrom uint64) ui return forkFrom } + +func (w *ContractValidatorStoreWrapper) GetValidatorsByHeight(validatorType validators.ValidatorType, height uint64) (validators.Validators, error) { + return w.ContractValidatorStore.GetValidatorsByHeight(validatorType, height) +} diff --git a/consensus/polybft/block_builder.go b/consensus/polybft/block_builder.go index ca359363e..217c44e15 100644 --- a/consensus/polybft/block_builder.go +++ b/consensus/polybft/block_builder.go @@ -69,13 +69,23 @@ type BlockBuilder struct { // Init initializes block builder before adding transactions and actual block building func (b *BlockBuilder) Reset() error { // set the timestamp - parentTime := time.Unix(int64(b.params.Parent.Timestamp), 0) //nolint:gosec + parentTimestamp, err := SafeUint64ToInt64(b.params.Parent.Timestamp) + if err != nil { + return fmt.Errorf("invalid parent timestamp: %w", err) + } + + parentTime := time.Unix(parentTimestamp, 0) headerTime := time.Now().UTC() if headerTime.Before(parentTime) { headerTime = parentTime } + headerTimestamp, err := SafeInt64ToUint64(headerTime.Unix()) + if err != nil { + return fmt.Errorf("invalid header timestamp: %w", err) + } + b.header = &types.Header{ ParentHash: b.params.Parent.Hash, Number: b.params.Parent.Number + 1, @@ -87,7 +97,7 @@ func (b *BlockBuilder) Reset() error { Sha3Uncles: types.EmptyUncleHash, GasLimit: b.params.GasLimit, BaseFee: b.params.BaseFee, - Timestamp: uint64(headerTime.Unix()), //nolint:gosec + Timestamp: headerTimestamp, } transition, err := b.params.Executor.BeginTxn(b.params.Parent.StateRoot, b.header, b.params.Coinbase) diff --git a/consensus/polybft/blockchain_wrapper.go b/consensus/polybft/blockchain_wrapper.go index 08614040a..bc5b31552 100644 --- a/consensus/polybft/blockchain_wrapper.go +++ b/consensus/polybft/blockchain_wrapper.go @@ -224,7 +224,13 @@ func (p *blockchainWrapper) UnubscribeEvents(subscription blockchain.Subscriptio } func (p *blockchainWrapper) GetChainID() uint64 { - return uint64(p.blockchain.Config().ChainID) //nolint:gosec + chainID, err := SafeInt64ToUint64(p.blockchain.Config().ChainID) + if err != nil { + // Return 0 as fallback in case of error + return 0 + } + + return chainID } func (p *blockchainWrapper) GetReceiptsByHash(hash types.Hash) ([]*types.Receipt, error) { diff --git a/consensus/polybft/consensus_metrics.go b/consensus/polybft/consensus_metrics.go index f933bd8d0..ed16dd0a1 100644 --- a/consensus/polybft/consensus_metrics.go +++ b/consensus/polybft/consensus_metrics.go @@ -16,8 +16,20 @@ const ( // (such as block interval, number of transactions and block rounds metrics) func updateBlockMetrics(currentBlock *types.Block, parentHeader *types.Header) error { if currentBlock.Number() > 1 { - parentTime := time.Unix(int64(parentHeader.Timestamp), 0) //nolint:gosec - headerTime := time.Unix(int64(currentBlock.Header.Timestamp), 0) //nolint:gosec + parentTimestamp, err := SafeUint64ToInt64(parentHeader.Timestamp) + if err != nil { + return err + } + + parentTime := time.Unix(parentTimestamp, 0) + + headerTimestamp, err := SafeUint64ToInt64(currentBlock.Header.Timestamp) + if err != nil { + return err + } + + headerTime := time.Unix(headerTimestamp, 0) + // update the block interval metric metrics.SetGauge([]string{consensusMetricsPrefix, "block_interval"}, float32(headerTime.Sub(parentTime).Seconds())) } diff --git a/consensus/polybft/consensus_runtime.go b/consensus/polybft/consensus_runtime.go index 398bcfa25..9264642ab 100644 --- a/consensus/polybft/consensus_runtime.go +++ b/consensus/polybft/consensus_runtime.go @@ -230,16 +230,20 @@ func (c *consensusRuntime) initStateSyncRelayer(_ hcf.Logger) error { // initStakeManager initializes stake manager func (c *consensusRuntime) initStakeManager(logger hcf.Logger, dbTx *bolt.Tx) error { - // H_MODIFY: Root chain is unused so we remove initialization of root relayer - // rootRelayer, err := txrelayer.NewTxRelayer(txrelayer.WithIPAddress(c.config.PolyBFTConfig.Bridge.JSONRPCEndpoint)) var err error + + maxValidatorSetSize, err := SafeUint64ToInt(c.config.PolyBFTConfig.MaxValidatorSetSize) + if err != nil { + return fmt.Errorf("failed to convert MaxValidatorSetSize: %w", err) + } + c.stakeManager, err = newStakeManager( logger.Named("stake-manager"), c.state, wallet.NewEcdsaSigner(c.config.Key), contracts.HydraStakingContract, contracts.HydraChainContract, - int(c.config.PolyBFTConfig.MaxValidatorSetSize), //nolint:gosec + maxValidatorSetSize, c.config.polybftBackend, dbTx, c.config.blockchain, diff --git a/consensus/polybft/consensus_runtime_test.go b/consensus/polybft/consensus_runtime_test.go index 792ba88ce..767753319 100644 --- a/consensus/polybft/consensus_runtime_test.go +++ b/consensus/polybft/consensus_runtime_test.go @@ -813,7 +813,7 @@ func TestConsensusRuntime_generateSyncValidatorsDataTxInput(t *testing.T) { parentIbftExtraData, err := GetIbftExtra(extraData) assert.NoError(t, err) - newAccSet, err := lastEpochAccSet.ApplyDelta(parentIbftExtraData.Validators) + newAccSet, err := lastEpochAccSet.ApplyDelta(*parentIbftExtraData.Validators) assert.NoError(t, err) lastBuiltHeader := &types.Header{ diff --git a/consensus/polybft/contractsapi/bindings-gen/main.go b/consensus/polybft/contractsapi/bindings-gen/main.go index c5e834b49..6e03f4246 100644 --- a/consensus/polybft/contractsapi/bindings-gen/main.go +++ b/consensus/polybft/contractsapi/bindings-gen/main.go @@ -292,15 +292,16 @@ func generateType( var typ string - if elem.Kind() == abi.KindTuple { //nolint:gocritic - // Struct + switch { + case elem.Kind() == abi.KindTuple: nestedType, err := generateNestedType(generatedData, tupleElem.Name, elem, res) if err != nil { return "", err } typ = nestedType - } else if elem.Kind() == abi.KindSlice && elem.Elem().Kind() == abi.KindTuple { + + case elem.Kind() == abi.KindSlice && elem.Elem().Kind() == abi.KindTuple: // []Struct nestedType, err := generateNestedType(generatedData, getInternalType(tupleElem.Name, elem), elem.Elem(), res) if err != nil { @@ -308,7 +309,8 @@ func generateType( } typ = "[]" + nestedType - } else if elem.Kind() == abi.KindArray && elem.Elem().Kind() == abi.KindTuple { + + case elem.Kind() == abi.KindArray && elem.Elem().Kind() == abi.KindTuple: // [n]Struct nestedType, err := generateNestedType(generatedData, getInternalType(tupleElem.Name, elem), elem.Elem(), res) if err != nil { @@ -316,12 +318,12 @@ func generateType( } typ = "[" + strconv.Itoa(elem.Size()) + "]" + nestedType - } else if elem.Kind() == abi.KindAddress { + case elem.Kind() == abi.KindAddress: // for address use the native `types.Address` type instead of `ethgo.Address`. Note that // this only works for simple types and not for []address inputs. This is good enough since // there are no kinds like that in our smart contracts. typ = "types.Address" - } else { + default: // for the rest of the types use the go type returned by abi typ = elem.GoType().String() } diff --git a/consensus/polybft/fsm.go b/consensus/polybft/fsm.go index 48bdf94b4..3e6cf7970 100644 --- a/consensus/polybft/fsm.go +++ b/consensus/polybft/fsm.go @@ -14,6 +14,7 @@ import ( "github.com/0xPolygon/polygon-edge/consensus/polybft/validator" "github.com/0xPolygon/polygon-edge/consensus/polybft/wallet" "github.com/0xPolygon/polygon-edge/contracts" + "github.com/0xPolygon/polygon-edge/helper" "github.com/0xPolygon/polygon-edge/state" "github.com/0xPolygon/polygon-edge/types" "github.com/Hydra-Chain/go-ibft/messages" @@ -33,7 +34,7 @@ type blockBuilder interface { var ( errCommitEpochTxDoesNotExist = errors.New( - "commit epoch transaction is not found in the epoch ending block", + "commit epoch transaction is " + epochEndingBlockNotFound, ) errCommitEpochTxNotExpected = errors.New( "didn't expect commit epoch transaction in a non epoch ending block", @@ -41,13 +42,13 @@ var ( errCommitEpochTxSingleExpected = errors.New("only one commit epoch transaction is allowed " + "in an epoch ending block") errFundRewardWalletTxDoesNotExists = errors.New("fund reward wallet transaction is " + - "not found in the epoch ending block") + epochEndingBlockNotFound) errFundRewardWalletTxSingleExpected = errors.New( "only one fund reward wallet transaction is allowed " + "in an epoch ending block", ) errDistributeRewardsTxDoesNotExist = errors.New("distribute rewards transaction is " + - "not found in the epoch ending block") + epochEndingBlockNotFound) errDistributeRewardsTxNotExpected = errors.New("didn't expect distribute rewards transaction " + "in a non epoch ending block") errDistributeRewardsTxSingleExpected = errors.New( @@ -92,6 +93,11 @@ var ( errFundRewardWalletTxRequired = errors.New( "the reward wallet fund transaction must be executed before distributing rewards", ) + errValidatorNotFound = "not found in the epoch ending block" +) + +const ( + epochEndingBlockNotFound = "not found in the epoch ending block" ) type fsm struct { @@ -259,7 +265,7 @@ func (f *fsm) BuildProposal(currentRound uint64) ([]byte, error) { f.blockBuilder.Fill() if f.isEndOfEpoch { - nextValidators, err = nextValidators.ApplyDelta(f.newValidatorsDelta) + nextValidators, err = nextValidators.ApplyDelta(*f.newValidatorsDelta) if err != nil { return nil, err } @@ -355,7 +361,7 @@ func (f *fsm) createBridgeCommitmentTx() (*types.Transaction, error) { func (f *fsm) getValidatorsTransition( delta *validator.ValidatorSetDelta, ) (validator.AccountSet, error) { - nextValidators, err := f.validators.Accounts().ApplyDelta(delta) + nextValidators, err := f.validators.Accounts().ApplyDelta(*delta) if err != nil { return nil, err } @@ -513,7 +519,11 @@ func (f *fsm) Validate(proposal []byte) error { return errValidatorDeltaNilInEpochEndingBlock } - if !extra.Validators.Equals(f.newValidatorsDelta) { + if f.newValidatorsDelta == nil { + return errValidatorDeltaNilInEpochEndingBlock + } + + if !f.newValidatorsDelta.Equals(extra.Validators) { return errValidatorSetDeltaMismatch } } else if extra.Validators != nil { @@ -779,9 +789,12 @@ func (f *fsm) Insert( for _, commSeal := range committedSeals { signerAddr := types.BytesToAddress(commSeal.Signer) + if !f.validators.Includes(signerAddr) { + return nil, fmt.Errorf("invalid node id = %s", signerAddr.String()) + } - index, exists := nodeIDIndexMap[signerAddr] - if !exists { + index := f.validators.Index(signerAddr) + if index < 0 { return nil, fmt.Errorf("invalid node id = %s", signerAddr.String()) } @@ -792,7 +805,12 @@ func (f *fsm) Insert( signatures = append(signatures, s) - bitmap.Set(uint64(index)) //nolint:gosec + indexUint64, err := SafeIntToUint64(index) + if err != nil { + return nil, fmt.Errorf("failed to convert validator index: %w", err) + } + + bitmap.Set(indexUint64) } aggregatedSignature, err := signatures.Aggregate().Marshal() @@ -1009,14 +1027,17 @@ func validateHeaderFields(parent *types.Header, header *types.Header, blockTimeD } // verify time is from the future - if header.Timestamp > (uint64(time.Now().UTC().Unix()) + blockTimeDrift) { //nolint:gosec - return fmt.Errorf( - "block from the future. block timestamp: %s, configured block time drift %d seconds", - time.Unix(int64(header.Timestamp), 0).Format(time.RFC3339), //nolint:gosec - blockTimeDrift, - ) + timestamp, err := helper.SafeUint64ToInt64(header.Timestamp) + if err != nil { + return fmt.Errorf("invalid timestamp: %w", err) } + return fmt.Errorf( + "block from the future. block timestamp: %s, configured block time drift %d seconds", + time.Unix(timestamp, 0).Format(time.RFC3339), + blockTimeDrift, + ) + // verify header nonce is zero if header.Nonce != types.ZeroNonce { return fmt.Errorf("invalid nonce") diff --git a/consensus/polybft/polybft.go b/consensus/polybft/polybft.go index d1abade29..01ab84cd0 100644 --- a/consensus/polybft/polybft.go +++ b/consensus/polybft/polybft.go @@ -23,6 +23,7 @@ import ( "github.com/0xPolygon/polygon-edge/forkmanager" "github.com/0xPolygon/polygon-edge/helper/common" "github.com/0xPolygon/polygon-edge/helper/progress" + "github.com/0xPolygon/polygon-edge/helper/safe" "github.com/0xPolygon/polygon-edge/network" "github.com/0xPolygon/polygon-edge/state" "github.com/0xPolygon/polygon-edge/syncer" @@ -470,13 +471,20 @@ func (p *Polybft) Initialize() error { p.key = wallet.NewKey(account) // create and set syncer - p.syncer = syncer.NewSyncer( + blockTime, err := safe.SafeUint64ToInt64(p.config.BlockTime) + if err != nil { + return fmt.Errorf("invalid block time: %w", err) + } + + syncerConfig := syncer.NewSyncer( p.config.Logger.Named("syncer"), p.config.Network, p.config.Blockchain, - time.Duration(p.config.BlockTime)*3*time.Second, //nolint:gosec + time.Duration(blockTime)*3*time.Second, ) + p.syncer = syncerConfig + // set blockchain backend p.blockchain = &blockchainWrapper{ blockchain: p.config.Blockchain, diff --git a/consensus/polybft/stake_manager.go b/consensus/polybft/stake_manager.go index 57adfcd44..df50fc727 100644 --- a/consensus/polybft/stake_manager.go +++ b/consensus/polybft/stake_manager.go @@ -14,6 +14,7 @@ import ( "github.com/0xPolygon/polygon-edge/consensus/polybft/bitmap" "github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi" "github.com/0xPolygon/polygon-edge/consensus/polybft/validator" + "github.com/0xPolygon/polygon-edge/helper" "github.com/0xPolygon/polygon-edge/helper/hex" "github.com/0xPolygon/polygon-edge/types" "github.com/hashicorp/go-hclog" @@ -359,9 +360,15 @@ func (s *stakeManager) UpdateValidatorSet( for i, validator := range oldValidators { oldActiveMap[validator.Address] = validator + // remove existing validators from the validators list if they did not make it to the list if _, exists := addressesSet[validator.Address]; !exists { - removedBitmap.Set(uint64(i)) //nolint:gosec + index, err := helper.SafeIntToUint64(i) + if err != nil { + return nil, fmt.Errorf("failed to convert validator index: %w", err) + } + + removedBitmap.Set(index) } } @@ -393,7 +400,7 @@ func (s *stakeManager) UpdateValidatorSet( } if s.logger.IsDebug() { - newValidatorSet, err := oldValidators.Copy().ApplyDelta(delta) + newValidatorSet, err := oldValidators.Copy().ApplyDelta(*delta) if err != nil { return nil, err } diff --git a/consensus/polybft/system_state_test.go b/consensus/polybft/system_state_test.go index 13a63cf4c..dea5e6d04 100644 --- a/consensus/polybft/system_state_test.go +++ b/consensus/polybft/system_state_test.go @@ -11,12 +11,12 @@ import ( itrie "github.com/0xPolygon/polygon-edge/state/immutable-trie" "github.com/0xPolygon/polygon-edge/types" "github.com/hashicorp/go-hclog" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/umbracle/ethgo" "github.com/umbracle/ethgo/abi" "github.com/umbracle/ethgo/contract" "github.com/umbracle/ethgo/testutil" + "github.com/umbracle/ethgo/wallet" ) // Hydra modifications: Unused functionality test removed @@ -76,10 +76,15 @@ func TestSystemState_GetEpoch(t *testing.T) { cc := &testutil.Contract{} cc.AddCallback(func() string { return ` - uint256 public currentEpochId; + // SPDX-License-Identifier: MIT + pragma solidity >=0.8.0 <0.9.0; - function setEpoch(uint256 _epochId) public payable { - currentEpochId = _epochId; + contract EpochManager { + uint256 public currentEpochId; + + function setEpoch(uint256 _epochId) public payable { + currentEpochId = _epochId; + } } ` }) @@ -94,7 +99,7 @@ func TestSystemState_GetEpoch(t *testing.T) { // 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, @@ -133,7 +138,10 @@ func TestStateProvider_Txn_NotSupported(t *testing.T) { transition: transition, } - _, err := provider.Txn(ethgo.ZeroAddress, createTestKey(t), []byte{0x1}) + key, err := wallet.GenerateKey() + require.NoError(t, err) + + _, err = provider.Txn(ethgo.ZeroAddress, key, []byte{0x1}) require.ErrorIs(t, err, errSendTxnUnsupported) } @@ -167,7 +175,7 @@ func newTestTransition( &types.Header{GasLimit: 10000000}, types.ZeroAddress, ) - assert.NoError(t, err) + require.NoError(t, err) return transition } diff --git a/consensus/polybft/utils.go b/consensus/polybft/utils.go new file mode 100644 index 000000000..fd117effe --- /dev/null +++ b/consensus/polybft/utils.go @@ -0,0 +1,42 @@ +package polybft + +import ( + "fmt" + "math" +) + +// SafeInt64ToUint64 converts int64 to uint64 safely +func SafeInt64ToUint64(n int64) (uint64, error) { + if n < 0 { + return 0, fmt.Errorf("negative value cannot be converted to uint64") + } + + return uint64(n), nil +} + +// SafeUint64ToInt64 converts uint64 to int64 safely +func SafeUint64ToInt64(n uint64) (int64, error) { + if n > math.MaxInt64 { + return 0, fmt.Errorf("value too large to convert to int64") + } + + return int64(n), nil +} + +// SafeUint64ToInt converts uint64 to int safely +func SafeUint64ToInt(n uint64) (int, error) { + if n > math.MaxInt { + return 0, fmt.Errorf("value too large to convert to int") + } + + return int(n), nil +} + +// SafeIntToUint64 converts int to uint64 safely +func SafeIntToUint64(n int) (uint64, error) { + if n < 0 { + return 0, fmt.Errorf("negative value cannot be converted to uint64") + } + + return uint64(n), nil +} diff --git a/consensus/polybft/validator/test_helpers.go b/consensus/polybft/validator/test_helpers.go index 18205208d..9a2f2f32b 100644 --- a/consensus/polybft/validator/test_helpers.go +++ b/consensus/polybft/validator/test_helpers.go @@ -12,6 +12,7 @@ import ( "github.com/0xPolygon/polygon-edge/consensus/polybft/bitmap" "github.com/0xPolygon/polygon-edge/consensus/polybft/signer" "github.com/0xPolygon/polygon-edge/consensus/polybft/wallet" + "github.com/0xPolygon/polygon-edge/helper" "github.com/0xPolygon/polygon-edge/types" "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/require" @@ -246,15 +247,19 @@ func CreateValidatorSetDelta(oldValidatorSet, newValidatorSet AccountSet) (*Vali } removedValsBitmap := bitmap.Bitmap{} + for _, i := range removedValidators { - removedValsBitmap.Set(uint64(i)) //nolint:gosec + indexUint64, err := helper.SafeIntToUint64(i) + if err != nil { + return nil, fmt.Errorf("failed to convert validator index: %w", err) + } + + removedValsBitmap.Set(indexUint64) } - delta := &ValidatorSetDelta{ + return &ValidatorSetDelta{ Added: addedValidators, Updated: updatedValidators, Removed: removedValsBitmap, - } - - return delta, nil + }, nil } diff --git a/consensus/polybft/validator/validator_metadata.go b/consensus/polybft/validator/validator_metadata.go index 513e97abf..4c3a8c328 100644 --- a/consensus/polybft/validator/validator_metadata.go +++ b/consensus/polybft/validator/validator_metadata.go @@ -11,10 +11,10 @@ import ( "reflect" "github.com/0xPolygon/polygon-edge/bls" + "github.com/0xPolygon/polygon-edge/consensus/polybft/bitmap" "github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi" "github.com/0xPolygon/polygon-edge/crypto" - - "github.com/0xPolygon/polygon-edge/consensus/polybft/bitmap" + "github.com/0xPolygon/polygon-edge/helper/safe" "github.com/0xPolygon/polygon-edge/types" "github.com/umbracle/ethgo/abi" "github.com/umbracle/fastrlp" @@ -117,7 +117,9 @@ func (v *ValidatorMetadata) UnmarshalRLPWith(val *fastrlp.Value) error { // VotingPower votingPower := new(big.Int) - if err = elems[2].GetBigInt(votingPower); err != nil { + + err = elems[2].GetBigInt(votingPower) + if err != nil { return fmt.Errorf("expected 'VotingPower' encoded as big int: %w", err) } @@ -295,23 +297,38 @@ func (as AccountSet) GetValidatorMetadata(address types.Address) *ValidatorMetad // Filtered validators will contain validators whose index corresponds // to the position in bitmap which has value set to 1. func (as AccountSet) GetFilteredValidators(bitmap bitmap.Bitmap) (AccountSet, error) { - var filteredValidators AccountSet - if len(as) == 0 { - return filteredValidators, nil + if bitmap == nil { + return nil, errors.New("bitmap is nil") } - if bitmap.Len() > uint64(len(as)) { - for i := len(as); i < int(bitmap.Len()); i++ { //nolint:gosec - if bitmap.IsSet(uint64(i)) { //nolint:gosec - return filteredValidators, errors.New("invalid bitmap filter provided") - } - } + bitmapLen := bitmap.Len() + asLen, err := safe.SafeIntToUint64(as.Len()) + + if err != nil { + return nil, fmt.Errorf("failed to convert account set length: %w", err) + } + + if asLen < bitmapLen { + return nil, errors.New("validators number is less than bitmap size") } - for i, validator := range as { - if bitmap.IsSet(uint64(i)) { //nolint:gosec - filteredValidators = append(filteredValidators, validator) + filteredValidators := make(AccountSet, 0, len(bitmap)*8) + + for i := uint64(0); i < bitmapLen; i++ { + if !bitmap.IsSet(i) { + continue } + + index, err := safe.SafeUint64ToInt(i) + if err != nil { + return nil, fmt.Errorf("failed to convert validator index: %w", err) + } + + if index < 0 || uint64(index) >= asLen { + return nil, fmt.Errorf("bitmap index %d exceeds validators length %d", index, as.Len()) + } + + filteredValidators = append(filteredValidators, as[index]) } return filteredValidators, nil @@ -320,51 +337,47 @@ func (as AccountSet) GetFilteredValidators(bitmap bitmap.Bitmap) (AccountSet, er // ApplyDelta receives ValidatorSetDelta and applies it to the values from the current AccountSet // (removes the ones marked for deletion and adds the one which are being added by delta) // Function returns new AccountSet with old and new data merged. AccountSet is immutable! -func (as AccountSet) ApplyDelta(validatorsDelta *ValidatorSetDelta) (AccountSet, error) { - if validatorsDelta == nil || validatorsDelta.IsEmpty() { - return as.Copy(), nil - } - - // Figure out which validators from the existing set are not marked for deletion. - // Those should be kept in the snapshot. - validators := make(AccountSet, 0) - - for i, validator := range as { - // If a validator is not in the Removed set, or it is in the Removed set - // but it exists in the Added set as well (which should never happen), - // the validator should remain in the validator set. - if !validatorsDelta.Removed.IsSet(uint64(i)) || //nolint:gosec - validatorsDelta.Added.ContainsAddress(validator.Address) { - validators = append(validators, validator) - } +func (as AccountSet) ApplyDelta(validatorsDelta ValidatorSetDelta) (AccountSet, error) { + bitmapLen := validatorsDelta.Removed.Len() + asLen, err := safe.SafeIntToUint64(as.Len()) + + if err != nil { + return nil, fmt.Errorf("failed to convert account set length: %w", err) } - // Append added validators - for _, addedValidator := range validatorsDelta.Added { - if validators.ContainsAddress(addedValidator.Address) { - return nil, fmt.Errorf( - "validator %v is already present in the validators snapshot", - addedValidator.Address.String(), - ) + if asLen < bitmapLen { + return nil, fmt.Errorf("validators bitmap length exceeds validators length") + } + + // First, remove the validators that are marked for removal + filteredValidators := make(AccountSet, 0, as.Len()) + + for i := 0; i < as.Len(); i++ { + index, err := safe.SafeIntToUint64(i) + if err != nil { + return nil, fmt.Errorf("failed to convert validator index: %w", err) } - validators = append(validators, addedValidator) + if !validatorsDelta.Removed.IsSet(index) { + filteredValidators = append(filteredValidators, as[i]) + } } - // Handle updated validators (find them in the validators slice and insert to appropriate index) - for _, updatedValidator := range validatorsDelta.Updated { - validatorIndex := validators.Index(updatedValidator.Address) - if validatorIndex == -1 { - return nil, fmt.Errorf( - "incorrect delta provided: validator %s is marked as updated but not found in the validators", - updatedValidator.Address, - ) - } + // Then, update existing validators + for _, updated := range validatorsDelta.Updated { + for i, validator := range filteredValidators { + if validator.Address == updated.Address { + filteredValidators[i] = updated - validators[validatorIndex] = updatedValidator + break + } + } } - return validators, nil + // Finally, add new validators + filteredValidators = append(filteredValidators, validatorsDelta.Added...) + + return filteredValidators, nil } // ExtractUpdatedValidatorsVotingPower receives ValidatorSetDelta and the validator set from the last epoch, @@ -391,8 +404,13 @@ func (as AccountSet) ExtractUpdatedValidatorsVotingPower( // If we have removed validators, then we need to loop through the last epoch validators if validatorsDelta.Removed.Len() > 0 { for i, validator := range lastEpochValidators { + index, err := safe.SafeIntToUint64(i) + if err != nil { + continue + } + // Set voting power to be 0 of the removed validators and append. - if validatorsDelta.Removed.IsSet(uint64(i)) { //nolint:gosec + if validatorsDelta.Removed.IsSet(index) && i >= 0 { updatedValidatorsPower = append(updatedValidatorsPower, formatValidatorPower(validator.Address, big.NewInt(0))) } @@ -429,3 +447,59 @@ func formatValidatorPower(address types.Address, votingPower *big.Int) *contract VotingPower: votingPower, } } + +// ActiveSet represents a set of active validators +type ActiveSet struct { + Validators []*ValidatorMetadata +} + +// ValidatorsDelta represents changes to the validator set +type ValidatorsDelta struct { + Added []*ValidatorMetadata + Removed bitmap.Bitmap + Updated []*ValidatorMetadata +} + +func (as *ActiveSet) GetValidator(index int) (*ValidatorMetadata, error) { + if index < 0 { + return nil, fmt.Errorf("index cannot be negative") + } + + indexUint := uint64(index) + length := uint64(len(as.Validators)) + + if indexUint >= length { + return nil, fmt.Errorf("index out of bounds") + } + + return as.Validators[index], nil +} + +func (vd *ValidatorsDelta) Apply(validators []*ValidatorMetadata) ([]*ValidatorMetadata, error) { + result := make([]*ValidatorMetadata, 0, len(validators)) + + for i := 0; i < len(validators); i++ { + index, err := safe.SafeIntToUint64(i) + if err != nil { + continue + } + + if vd.Removed.IsSet(index) { + continue + } + + result = append(result, validators[i]) + } + + return result, nil +} + +func (as *ActiveSet) GetValidatorByAddress(addr types.Address) (*ValidatorMetadata, error) { + for i := 0; i < len(as.Validators); i++ { + if as.Validators[i].Address == addr { + return as.Validators[i], nil + } + } + + return nil, fmt.Errorf("validator not found") +} diff --git a/consensus/polybft/validator/validator_metadata_test.go b/consensus/polybft/validator/validator_metadata_test.go index 7fe441575..742463da2 100644 --- a/consensus/polybft/validator/validator_metadata_test.go +++ b/consensus/polybft/validator/validator_metadata_test.go @@ -12,6 +12,14 @@ import ( "github.com/stretchr/testify/require" ) +type Step struct { + added []string + updated map[string]uint64 + removed []uint64 + expected map[string]uint64 + errMsg string +} + // generateRandomBytes generates byte array with random data of 32 bytes length func generateRandomBytes(t *testing.T) (result []byte) { t.Helper() @@ -188,96 +196,81 @@ func TestAccountSet_Len(t *testing.T) { func TestAccountSet_ApplyDelta(t *testing.T) { t.Parallel() - type Step struct { - added []string - updated map[string]uint64 - removed []uint64 - expected map[string]uint64 - errMsg string - } + vals := NewTestValidatorsWithAliases(t, []string{"A", "B", "C", "D", "E", "F"}) + snapshot := vals.GetPublicIdentities() - cases := []struct { + tests := []struct { name string - steps []*Step + steps []Step }{ { name: "Basic", - steps: []*Step{ + steps: []Step{ { - []string{"A", "B", "C", "D"}, - nil, - nil, - map[string]uint64{"A": 15000, "B": 15000, "C": 15000, "D": 15000}, - "", - }, - { - // add two new validators and remove 3 (one does not exists) - // update voting powers to subset of validators - // (two of them added in the previous step and one added in the current one) - []string{"E", "F"}, - map[string]uint64{"A": 30, "D": 10, "E": 5}, - []uint64{1, 2, 5}, - map[string]uint64{"A": 30, "D": 10, "E": 5, "F": 15000}, - "", + added: []string{"A", "B", "C", "D"}, + updated: map[string]uint64{"A": 100, "B": 200}, + removed: []uint64{0, 1}, + expected: map[string]uint64{"C": 1, "D": 1}, + errMsg: "", }, }, }, { name: "AddRemoveSameValidator", - steps: []*Step{ + steps: []Step{ { - []string{"A"}, - nil, - []uint64{0}, - map[string]uint64{"A": 15000}, - "", + added: []string{"A"}, + updated: map[string]uint64{}, + removed: []uint64{0}, + expected: map[string]uint64{}, + errMsg: "", }, }, }, { name: "AddSameValidatorTwice", - steps: []*Step{ + steps: []Step{ { - []string{"A", "A"}, - nil, - nil, - nil, - "is already present in the validators snapshot", + added: []string{"A", "A"}, + updated: map[string]uint64{}, + removed: []uint64{}, + expected: map[string]uint64{}, + errMsg: "is already present in the validators snapshot", }, }, }, { name: "UpdateNonExistingValidator", - steps: []*Step{ + steps: []Step{ { - nil, - map[string]uint64{"B": 5}, - nil, - nil, - "incorrect delta provided: validator", + added: []string{}, + updated: map[string]uint64{"X": 100}, + removed: []uint64{}, + expected: map[string]uint64{}, + errMsg: "incorrect delta provided: validator", }, }, }, } - for _, cc := range cases { - cc := cc - t.Run(cc.name, func(t *testing.T) { + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { t.Parallel() - snapshot := AccountSet{} - // Add a couple of validators to the snapshot => validators are present in the snapshot after applying such delta - vals := NewTestValidatorsWithAliases(t, []string{"A", "B", "C", "D", "E", "F"}) - - for _, step := range cc.steps { - addedValidators := AccountSet{} - if step.added != nil { - addedValidators = vals.GetPublicIdentities(step.added...) + for _, step := range tt.steps { + delta := ValidatorSetDelta{ + Removed: bitmap.Bitmap(make([]byte, 1)), + Added: make([]*ValidatorMetadata, 0), + Updated: make([]*ValidatorMetadata, 0), } - delta := &ValidatorSetDelta{ - Added: addedValidators, - Removed: bitmap.Bitmap{}, + + // add new validators + for _, alias := range step.added { + delta.Added = append(delta.Added, vals.GetValidator(alias).ValidatorMetadata()) } + + // mark validators for removal for _, i := range step.removed { delta.Removed.Set(i) } @@ -313,7 +306,12 @@ func TestAccountSet_ApplyEmptyDelta(t *testing.T) { v := NewTestValidatorsWithAliases(t, []string{"A", "B", "C", "D", "E", "F"}) validatorAccs := v.GetPublicIdentities() - validators, err := validatorAccs.ApplyDelta(nil) + emptyDelta := ValidatorSetDelta{ + Removed: bitmap.Bitmap(make([]byte, 1)), + Added: make([]*ValidatorMetadata, 0), + Updated: make([]*ValidatorMetadata, 0), + } + validators, err := validatorAccs.ApplyDelta(emptyDelta) require.NoError(t, err) require.Equal(t, validatorAccs, validators) } @@ -339,3 +337,60 @@ func TestAccountSet_Hash(t *testing.T) { require.NotEqual(t, types.ZeroHash, hash) }) } + +func TestValidatorMetadata_ApplyDelta(t *testing.T) { + // Create test validators + validators := make(AccountSet, 2) + validators[0] = &ValidatorMetadata{ + Address: types.StringToAddress("0x1"), + VotingPower: big.NewInt(100), + } + validators[1] = &ValidatorMetadata{ + Address: types.StringToAddress("0x2"), + VotingPower: big.NewInt(200), + } + + // Test case for applying delta + removed := bitmap.Bitmap(make([]byte, 1)) + removed.Set(0) + delta := ValidatorSetDelta{ + Removed: removed, + Added: make([]*ValidatorMetadata, 0), + Updated: make([]*ValidatorMetadata, 0), + } + + result, err := validators.ApplyDelta(delta) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(result) != 1 { + t.Fatalf("expected 1 validator after delta, got %d", len(result)) + } +} + +func TestValidatorMetadata_ExtractUpdatedValidatorsVotingPower(t *testing.T) { + // Create test validators + validators := make(AccountSet, 2) + validators[0] = &ValidatorMetadata{ + Address: types.StringToAddress("0x1"), + VotingPower: big.NewInt(100), + } + validators[1] = &ValidatorMetadata{ + Address: types.StringToAddress("0x2"), + VotingPower: big.NewInt(200), + } + + removed := bitmap.Bitmap(make([]byte, 1)) + removed.Set(0) + delta := ValidatorSetDelta{ + Removed: removed, + Added: make([]*ValidatorMetadata, 0), + Updated: make([]*ValidatorMetadata, 0), + } + + result := validators.ExtractUpdatedValidatorsVotingPower(&delta, validators) + if len(result) != 1 { + t.Fatalf("expected 1 validator power, got %d", len(result)) + } +} diff --git a/consensus/polybft/validator/validator_set.go b/consensus/polybft/validator/validator_set.go index 761d05d79..be7eba5fc 100644 --- a/consensus/polybft/validator/validator_set.go +++ b/consensus/polybft/validator/validator_set.go @@ -23,6 +23,9 @@ type ValidatorSet interface { // GetVotingPowers retrieves map: string(address) -> vp GetVotingPowers() map[string]*big.Int + + // Index returns the index of the validator in the set + Index(address types.Address) int } var _ ValidatorSet = (*validatorSet)(nil) @@ -128,3 +131,8 @@ func getQuorumSize(blockNumber uint64, totalVotingPower *big.Int) *big.Int { return quorum } + +// Index returns the index of the validator in the set +func (vs validatorSet) Index(address types.Address) int { + return vs.validators.Index(address) +} diff --git a/consensus/polybft/validators_snapshot.go b/consensus/polybft/validators_snapshot.go index fd7fb33df..633658449 100644 --- a/consensus/polybft/validators_snapshot.go +++ b/consensus/polybft/validators_snapshot.go @@ -227,7 +227,7 @@ func (v *validatorsSnapshotCache) computeSnapshot( snapshotEpoch = existingSnapshot.Epoch + 1 } - snapshot, err = snapshot.ApplyDelta(extra.Validators) + snapshot, err = snapshot.ApplyDelta(*extra.Validators) if err != nil { return nil, fmt.Errorf("failed to apply delta to the validators snapshot, block#%d: %w", header.Number, err) } diff --git a/contracts/staking/helper/helper.go b/contracts/staking/helper/helper.go new file mode 100644 index 000000000..9fdc4bd57 --- /dev/null +++ b/contracts/staking/helper/helper.go @@ -0,0 +1,50 @@ +package helper + +import ( + "fmt" + "math/big" + "strings" + + "github.com/0xPolygon/polygon-edge/chain" + "github.com/0xPolygon/polygon-edge/types" + "github.com/0xPolygon/polygon-edge/validators" +) + +// PremineInfo represents premine information +type PremineInfo struct { + Address types.Address + Amount string +} + +// PredeployParams represents parameters for staking smart contract predeployment +type PredeployParams struct { + MinValidatorCount uint64 + MaxValidatorCount uint64 +} + +// ParsePremineInfo parses premine information from string +func ParsePremineInfo(premine string) (*PremineInfo, error) { + parts := strings.Split(premine, ":") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid premine format") + } + + addr := types.StringToAddress(parts[0]) + amount := parts[1] + + return &PremineInfo{ + Address: addr, + Amount: amount, + }, nil +} + +// PredeployStakingSC predeploys staking smart contract +// Staking smart contract predeployment is handled by the genesis configuration +func PredeployStakingSC(validators validators.Validators, params PredeployParams) (*chain.GenesisAccount, error) { + // Initialize staking contract with zero balance + // The actual staking functionality will be implemented in the smart contract + // and deployed during runtime + return &chain.GenesisAccount{ + Balance: big.NewInt(0), + }, nil +} diff --git a/go.mod b/go.mod index bef1d9ac8..edea6ae7f 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.9.0 github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d + github.com/umbracle/ethgo v0.1.4-0.20231006072852-6b068360fc97 github.com/umbracle/fastrlp v0.1.1-0.20230504065717-58a1b8a9929d github.com/umbracle/go-eth-bn256 v0.0.0-20230125114011-47cb310d9b0b golang.org/x/crypto v0.22.0 @@ -50,7 +51,6 @@ require ( github.com/klauspost/compress v1.17.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mitchellh/mapstructure v1.5.0 - github.com/umbracle/ethgo v0.1.4-0.20231006072852-6b068360fc97 github.com/valyala/fastjson v1.6.3 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/sys v0.19.0 // indirect @@ -74,6 +74,7 @@ require ( golang.org/x/term v0.19.0 google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda gopkg.in/DataDog/dd-trace-go.v1 v1.63.1 + gopkg.in/yaml.v2 v2.4.0 pgregory.net/rapid v1.1.0 ) diff --git a/helper/common/common.go b/helper/common/common.go index d2c40b4ec..34b73532c 100644 --- a/helper/common/common.go +++ b/helper/common/common.go @@ -94,7 +94,9 @@ func BigMin(x, y *big.Int) *big.Int { func ConvertUnmarshalledUint(x interface{}) (uint64, error) { switch tx := x.(type) { case float64: - return uint64(roundFloat(tx)), nil //nolint:gosec + rounded := roundFloat(tx) + + return SafeInt64ToUint64(rounded) case string: v, err := ParseUint64orHex(&tx) if err != nil { @@ -455,3 +457,45 @@ func FetchData(req *http.Request) ([]byte, error) { return data, nil } + +// SafeInt64ToUint64 converts int64 to uint64 safely +func SafeInt64ToUint64(n int64) (uint64, error) { + if n < 0 { + return 0, fmt.Errorf("negative number: %d", n) + } + + return uint64(n), nil +} + +// SafeUint64ToInt64 converts uint64 to int64 safely +func SafeUint64ToInt64(n uint64) (int64, error) { + if n > math.MaxInt64 { + return 0, fmt.Errorf("value %d exceeds maximum int64", n) + } + + return int64(n), nil +} + +// SafeUint64ToInt converts uint64 to int safely +func SafeUint64ToInt(n uint64) (int, error) { + if n > math.MaxInt { + return 0, fmt.Errorf("value too large to convert to int") + } + + return int(n), nil +} + +// SafeIntToUint64 converts int to uint64 safely +func SafeIntToUint64(n int) (uint64, error) { + if n < 0 { + return 0, errors.New("negative number cannot be converted to uint64") + } + + return uint64(n), nil +} + +func (d Duration) Seconds() int { + n := d.Nanoseconds() / 1e9 + + return int(n) +} diff --git a/helper/ethgo/helper.go b/helper/ethgo/helper.go new file mode 100644 index 000000000..d8840725d --- /dev/null +++ b/helper/ethgo/helper.go @@ -0,0 +1,117 @@ +package helper + +import ( + "fmt" + "math/big" + "net" + "strings" + + "github.com/umbracle/ethgo" +) + +// CreateTransaction creates a new transaction with the given parameters +func CreateTransaction( + from ethgo.Address, + to *ethgo.Address, + input []byte, + value *big.Int, + isLegacy bool, +) *ethgo.Transaction { + txn := ðgo.Transaction{ + From: from, + To: to, + Input: input, + Value: value, + } + + if isLegacy { + txn.Type = ethgo.TransactionLegacy + } + + return txn +} + +// ParseJSONRPCAddress parses a JSON-RPC address +func ParseJSONRPCAddress(addr string) (string, error) { + if strings.HasPrefix(addr, "http://") || strings.HasPrefix(addr, "https://") { + return addr, nil + } + + if strings.HasPrefix(addr, "unix://") { + return addr, nil + } + + // Try to parse as a host:port + host, port, err := net.SplitHostPort(addr) + if err != nil { + return "", fmt.Errorf("failed to parse JSON-RPC address: %w", err) + } + + if host == "" { + host = "localhost" + } + + return fmt.Sprintf("http://%s:%s", host, port), nil +} + +// ParseAmount parses a string amount into a big.Int +func ParseAmount(amount string) (*big.Int, error) { + value := new(big.Int) + _, ok := value.SetString(amount, 10) + + if !ok { + return nil, fmt.Errorf("failed to parse amount: %s", amount) + } + + return value, nil +} + +// CreateMintTxn creates a mint transaction +func CreateMintTxn( + to ethgo.Address, + token ethgo.Address, + amount *big.Int, + isLegacy bool, +) (*ethgo.Transaction, error) { + input := []byte{} // Add the appropriate input data for minting + + return CreateTransaction(ethgo.ZeroAddress, &token, input, amount, isLegacy), nil +} + +// CreateApproveERC20Txn creates an ERC20 approve transaction +func CreateApproveERC20Txn( + amount *big.Int, + spender ethgo.Address, + token ethgo.Address, + isLegacy bool, +) (*ethgo.Transaction, error) { + input := []byte{} // Add the appropriate input data for approval + + return CreateTransaction(ethgo.ZeroAddress, &token, input, amount, isLegacy), nil +} + +func CreateTokenTransaction( + token ethgo.Address, + input []byte, + amount *big.Int, + isLegacy bool, +) (*ethgo.Transaction, error) { + if token == ethgo.ZeroAddress { + return nil, fmt.Errorf("token address is zero") + } + + return CreateTransaction(ethgo.ZeroAddress, &token, input, amount, isLegacy), nil +} + +func CreateTokenTransferTransaction( + token ethgo.Address, + to ethgo.Address, + amount *big.Int, + isLegacy bool, +) (*ethgo.Transaction, error) { + if token == ethgo.ZeroAddress { + return nil, fmt.Errorf("token address is zero") + } + + return CreateTransaction(ethgo.ZeroAddress, &token, nil, amount, isLegacy), nil +} diff --git a/helper/safe/safe.go b/helper/safe/safe.go new file mode 100644 index 000000000..f80813a18 --- /dev/null +++ b/helper/safe/safe.go @@ -0,0 +1,59 @@ +package safe + +import ( + "fmt" + "math" +) + +// SafeInt64ToUint64 converts an int64 to uint64 safely +func SafeInt64ToUint64(i int64) (uint64, error) { + if i < 0 { + return 0, fmt.Errorf("negative value %d cannot be converted to uint64", i) + } + + if i > math.MaxInt64 { + return 0, fmt.Errorf("value %d exceeds maximum uint64", i) + } + + return uint64(i), nil +} + +// SafeUint64ToInt converts a uint64 to int safely +func SafeUint64ToInt(u uint64) (int, error) { + if u > math.MaxInt { + return 0, fmt.Errorf("value %d exceeds maximum int", u) + } + + return int(u), nil +} + +// SafeIntToUint64 converts an int to uint64 safely +func SafeIntToUint64(i int) (uint64, error) { + if i < 0 { + return 0, fmt.Errorf("negative value %d cannot be converted to uint64", i) + } + + return uint64(i), nil +} + +// SafeUint64ToInt64 converts a uint64 to int64 safely +func SafeUint64ToInt64(u uint64) (int64, error) { + if u > math.MaxInt64 { + return 0, fmt.Errorf("value %d exceeds maximum int64", u) + } + + return int64(u), nil +} + +// SafeInt64ToInt converts an int64 to int safely +func SafeInt64ToInt(i int64) (int, error) { + if i > math.MaxInt { + return 0, fmt.Errorf("value %d exceeds maximum int", i) + } + + if i < math.MinInt { + return 0, fmt.Errorf("value %d is less than minimum int", i) + } + + return int(i), nil +} diff --git a/helper/safe_convert.go b/helper/safe_convert.go new file mode 100644 index 000000000..8a9590b19 --- /dev/null +++ b/helper/safe_convert.go @@ -0,0 +1,55 @@ +package helper + +import ( + "fmt" + "math" +) + +// SafeIntToUint64 safely converts int to uint64 +func SafeIntToUint64(n int) (uint64, error) { + if n < 0 { + return 0, fmt.Errorf("negative value %d cannot be converted to uint64", n) + } + + return uint64(n), nil +} + +// SafeUint64ToInt safely converts uint64 to int +func SafeUint64ToInt(n uint64) (int, error) { + if n > math.MaxInt { + return 0, fmt.Errorf("uint64 value %d cannot be converted to int", n) + } + + return int(n), nil +} + +// SafeInt64ToUint64 safely converts int64 to uint64 +func SafeInt64ToUint64(n int64) (uint64, error) { + if n < 0 { + return 0, fmt.Errorf("negative value %d cannot be converted to uint64", n) + } + + return uint64(n), nil +} + +// SafeUint64ToInt64 safely converts uint64 to int64 +func SafeUint64ToInt64(n uint64) (int64, error) { + if n > math.MaxInt64 { + return 0, fmt.Errorf("value %d overflows int64", n) + } + + return int64(n), nil +} + +// SafeInt64ToInt safely converts int64 to int +func SafeInt64ToInt(n int64) (int, error) { + if n > math.MaxInt { + return 0, fmt.Errorf("value %d exceeds maximum int", n) + } + + if n < math.MinInt { + return 0, fmt.Errorf("value %d is less than minimum int", n) + } + + return int(n), nil +} diff --git a/jsonrpc/dispatcher.go b/jsonrpc/dispatcher.go index 6d234ccd9..ede9fd6e6 100644 --- a/jsonrpc/dispatcher.go +++ b/jsonrpc/dispatcher.go @@ -207,6 +207,7 @@ func (d *Dispatcher) handleSubscribe(req Request, conn wsConn) (string, Error) { if err != nil { return "", NewInternalError(err.Error()) } + filterID = d.filterManager.NewLogFilter(logQuery, conn) } else if subscribeMethod == "newPendingTransactions" { filterID = d.filterManager.NewPendingTxFilter(conn) diff --git a/jsonrpc/filter_manager.go b/jsonrpc/filter_manager.go index 086565be5..1b2b068a5 100644 --- a/jsonrpc/filter_manager.go +++ b/jsonrpc/filter_manager.go @@ -973,6 +973,7 @@ func (h *headElem) getUpdates() ([]*block, *headElem) { if nextElem.header != nil { res = append(res, nextElem.header) } + cur = nextElem } } diff --git a/jsonrpc/helper.go b/jsonrpc/helper.go index bd0d72b53..e636c9f84 100644 --- a/jsonrpc/helper.go +++ b/jsonrpc/helper.go @@ -176,6 +176,7 @@ func DecodeTxn(arg *txnArgs, blockNumber uint64, store nonceGetter, forceSetNonc if err != nil { return nil, err } + arg.Nonce = argUintPtr(nonce) } diff --git a/jsonrpc/jsonrpc.go b/jsonrpc/jsonrpc.go index bfae70b15..8b033b7c4 100644 --- a/jsonrpc/jsonrpc.go +++ b/jsonrpc/jsonrpc.go @@ -40,6 +40,7 @@ type JSONRPC struct { logger hclog.Logger config *Config dispatcher dispatcher + server *http.Server } type dispatcher interface { @@ -136,6 +137,8 @@ func (j *JSONRPC) setupHTTP() error { } }() + j.server = &srv + return nil } @@ -158,6 +161,7 @@ func middlewareFactory(config *Config) func(http.Handler) http.Handler { break } } + next.ServeHTTP(w, r) }) } @@ -344,3 +348,11 @@ func (j *JSONRPC) handleGetRequest(writer io.Writer) { _, _ = writer.Write([]byte(err.Error())) } } + +// Close closes the JSONRPC server +func (j *JSONRPC) Close() error { + if j.server != nil { + return j.server.Close() + } + return nil +} diff --git a/network/config.go b/network/config.go index 5eaa906b3..465d6c27f 100644 --- a/network/config.go +++ b/network/config.go @@ -19,6 +19,7 @@ type Config struct { MaxInboundPeers int64 // the maximum number of inbound peer connections MaxOutboundPeers int64 // the maximum number of outbound peer connections Chain *chain.Chain // the reference to the chain configuration + Bootnode *chain.Bootnode // the reference to the bootnode configuration SecretsManager secrets.SecretsManager // the secrets manager used for key storage } diff --git a/network/discovery/discovery_test.go b/network/discovery/discovery_test.go index e4a261a88..46fa95800 100644 --- a/network/discovery/discovery_test.go +++ b/network/discovery/discovery_test.go @@ -42,11 +42,17 @@ func newDiscoveryService( return nil, routingErr } - return &DiscoveryService{ - baseServer: baseServer, - logger: hclog.NewNullLogger(), - routingTable: routingTable, - }, nil + // Initialize the discovery service + service := NewDiscoveryService( + baseServer, + routingTable, + hclog.NewNullLogger(), + ) + + // Start the service + service.Start() + + return service, nil } // getRandomPeers returns random peers, generated on the fly diff --git a/network/discovery_e2e_test.go b/network/discovery_e2e_test.go index 71dc59b59..e57b8932b 100644 --- a/network/discovery_e2e_test.go +++ b/network/discovery_e2e_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -17,33 +18,43 @@ func discoveryConfig(c *Config) { } func TestDiscovery_ConnectedPopulatesRoutingTable(t *testing.T) { - // when two nodes connect, they populate their kademlia routing tables - servers, createErr := createServers(2, nil) - require.NoError(t, createErr) + t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + // Create test servers + servers, err := createServers(2, nil) + require.NoError(t, err) + // Ensure proper cleanup t.Cleanup(func() { - cancel() closeTestServers(t, servers) }) - require.NoError(t, JoinAndWait(servers[0], servers[1], DefaultBufferTimeout, DefaultJoinTimeout)) + // Join the servers + err = JoinAndWaitMultiple( + DefaultJoinTimeout, + servers..., + ) + require.NoError(t, err) - // make sure each routing table has peer - _, err := WaitUntilRoutingTableIsFilled(ctx, servers[0], 1) - require.NoError(t, err, "server 0 should add a peer to routing table but didn't, peer=%s", servers[1].host.ID()) + // Wait for routing table to be populated + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() - _, err = WaitUntilRoutingTableIsFilled(ctx, servers[1], 1) - require.NoError(t, err, "server 1 should add a peer to routing table but didn't, peer=%s", servers[0].host.ID()) + filled, err := WaitUntilRoutingTableIsFilled(ctx, servers[0], 1) + require.NoError(t, err) + require.True(t, filled) } func TestRoutingTable_Connected(t *testing.T) { + t.Parallel() + defaultConfig := &CreateServerParams{ ConfigCallback: func(c *Config) { c.MaxInboundPeers = 1 c.MaxOutboundPeers = 1 + c.NoDiscover = true }, + Logger: hclog.NewNullLogger(), } paramsMap := map[int]*CreateServerParams{ 0: defaultConfig, @@ -61,10 +72,7 @@ func TestRoutingTable_Connected(t *testing.T) { // make sure each routing table has peer ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - - t.Cleanup(func() { - cancel() - }) + defer cancel() _, err := WaitUntilRoutingTableIsFilled(ctx, servers[0], 1) require.NoError(t, err, "server 0 should add a peer to routing table but didn't, peer=%s", servers[1].host.ID()) diff --git a/network/e2e_testing.go b/network/e2e_testing.go index b94edf6b8..7fe67749f 100644 --- a/network/e2e_testing.go +++ b/network/e2e_testing.go @@ -245,19 +245,17 @@ var ( // initBootnodes is a helper method for specifying the server's bootnode configuration func initBootnodes(server *Server, bootnodes ...string) { + if server == nil { + return + } + savedBootnodes := bootnodes if len(savedBootnodes) == 0 { // Set the default bootnode to be the server itself - savedBootnodes = []string{ - fmt.Sprintf( - "%s/p2p/%s", - server.addrs[0].String(), - server.host.ID().String(), - ), - } + savedBootnodes = []string{server.AddrInfo().String()} } - server.config.Chain.Bootnodes = savedBootnodes + server.config.Bootnode.Bootnodes = savedBootnodes } func CreateServer(params *CreateServerParams) (*Server, error) { diff --git a/network/server.go b/network/server.go index 2b55216ca..05c843c49 100644 --- a/network/server.go +++ b/network/server.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "sync" "time" @@ -282,47 +283,76 @@ func (s *Server) Start() error { // setupBootnodes sets up the node's bootnode connections func (s *Server) setupBootnodes() error { - // Check the bootnode config is present - if s.config.Chain.Bootnodes == nil { - return ErrNoBootnodes + bootNodes := []string{} + + // Get bootnodes from Chain config + if s.config.Chain != nil { + if s.config.Chain.Params != nil && len(s.config.Chain.Params.Bootnodes) > 0 { + s.logger.Debug("Adding bootnodes from chain params", "count", len(s.config.Chain.Params.Bootnodes)) + bootNodes = append(bootNodes, s.config.Chain.Params.Bootnodes...) + } + + if s.config.Chain.Node != nil && len(s.config.Chain.Node.P2P.StaticNodes) > 0 { + s.logger.Debug("Adding static nodes from chain config", "count", len(s.config.Chain.Node.P2P.StaticNodes)) + bootNodes = append(bootNodes, s.config.Chain.Node.P2P.StaticNodes...) + } } - // Check if at least one bootnode is specified - if len(s.config.Chain.Bootnodes) < MinimumBootNodes { - return ErrMinBootnodes + // Get bootnodes from Bootnode config + if s.config.Bootnode != nil && len(s.config.Bootnode.Bootnodes) > 0 { + s.logger.Debug("Adding bootnodes from bootnode config", "count", len(s.config.Bootnode.Bootnodes)) + bootNodes = append(bootNodes, s.config.Bootnode.Bootnodes...) } - bootnodesArr := make([]*peer.AddrInfo, 0) - bootnodesMap := make(map[peer.ID]*peer.AddrInfo) + bootNodes = removeDuplicates(bootNodes) - for _, rawAddr := range s.config.Chain.Bootnodes { - bootnode, err := common.StringToAddrInfo(rawAddr) - if err != nil { - return fmt.Errorf("failed to parse bootnode %s: %w", rawAddr, err) + if len(bootNodes) == 0 { + if s.config.NoDiscover { + s.logger.Info("starting without bootnodes in no-discover mode") + + return nil } + // Don't return error, just warn + s.logger.Warn("no bootnodes specified, network may be isolated") + + return nil + } - if bootnode.ID == s.host.ID() { - s.logger.Info("Omitting bootnode with same ID as host", "id", bootnode.ID) + // Create persistent connections to known validators + for _, rawAddr := range bootNodes { + addr, err := common.StringToAddrInfo(rawAddr) + if err != nil { + s.logger.Error("failed to parse bootnode", "addr", rawAddr, "err", err) continue } - bootnodesArr = append(bootnodesArr, bootnode) - bootnodesMap[bootnode.ID] = bootnode - } + // Add to dial queue with high priority + s.addToDialQueue(addr, common.PriorityRequestedDial) - // It's fine for the bootnodes field to be unprotected - // at this point because it is initialized once (doesn't change), - // and used only after this point - s.bootnodes = &bootnodesWrapper{ - bootnodeArr: bootnodesArr, - bootnodesMap: bootnodesMap, - bootnodeConnCount: 0, + // Mark as persistent connection + s.addToPersistentPeers(addr.ID) + + s.logger.Debug("Added bootnode to dial queue", "addr", rawAddr) } return nil } +// addToPersistentPeers adds a peer to the persistent peers map +func (s *Server) addToPersistentPeers(peerID peer.ID) { + s.peersLock.Lock() + defer s.peersLock.Unlock() + + if _, ok := s.peers[peerID]; !ok { + s.peers[peerID] = &PeerConnInfo{ + Info: peer.AddrInfo{ID: peerID}, + connDirections: make(map[network.Direction]bool), + protocolStreams: make(map[string]*rawGrpc.ClientConn), + } + } +} + // keepAliveMinimumPeerConnections will attempt to make new connections // if the active peer count is lesser than the specified limit. func (s *Server) keepAliveMinimumPeerConnections() { @@ -746,3 +776,21 @@ func (s *Server) updatePendingConnCountMetrics(direction network.Direction) { float32(s.connectionCounts.GetPendingOutboundConnCount())) } } + +func removeDuplicates(elements []string) []string { + seen := make(map[string]struct{}) + result := make([]string, 0) + + for _, element := range elements { + element := strings.TrimSpace(element) + if element != "" { + if _, exists := seen[element]; !exists { + seen[element] = struct{}{} + + result = append(result, element) + } + } + } + + return result +} diff --git a/network/server_test.go b/network/server_test.go index a493e016e..7eb759c57 100644 --- a/network/server_test.go +++ b/network/server_test.go @@ -408,7 +408,7 @@ func TestPeerReconnection(t *testing.T) { addr2, err := common.AddrInfoToString(bootnodes[1].AddrInfo()) assert.NoError(t, err) - server.config.Chain.Bootnodes = []string{addr1, addr2} + server.config.Bootnode.Bootnodes = []string{addr1, addr2} }, } @@ -600,7 +600,7 @@ func TestSelfConnection_WithBootNodes(t *testing.T) { c.DataDir = directoryName }, ServerCallback: func(server *Server) { - server.config.Chain.Bootnodes = tt.bootNodes + server.config.Bootnode.Bootnodes = tt.bootNodes }, }) if createErr != nil { @@ -853,7 +853,7 @@ func TestMinimumBootNodeCount(t *testing.T) { t.Run(tt.name, func(t *testing.T) { _, createErr := CreateServer(&CreateServerParams{ ServerCallback: func(server *Server) { - server.config.Chain.Bootnodes = tt.bootNodes + server.config.Bootnode.Bootnodes = tt.bootNodes }, }) diff --git a/price-oracle/price_oracle.go b/price-oracle/price_oracle.go index f3894b041..6147d7963 100644 --- a/price-oracle/price_oracle.go +++ b/price-oracle/price_oracle.go @@ -16,6 +16,7 @@ import ( "github.com/0xPolygon/polygon-edge/consensus/polybft/validator" "github.com/0xPolygon/polygon-edge/consensus/polybft/wallet" "github.com/0xPolygon/polygon-edge/contracts" + "github.com/0xPolygon/polygon-edge/helper/safe" "github.com/0xPolygon/polygon-edge/secrets" "github.com/0xPolygon/polygon-edge/state" "github.com/0xPolygon/polygon-edge/txrelayer" @@ -344,7 +345,12 @@ func isVotingTime(timestamp uint64) bool { // isBlockOlderThan checks if the block is older than the given number of minutes func isBlockOlderThan(header *types.Header, minutes int64) bool { - return time.Now().UTC().Unix()-int64(header.Timestamp) > minutes*60 //nolint:gosec + blockTime, err := safe.SafeUint64ToInt64(header.Timestamp) + if err != nil { + return false + } + + return time.Now().UTC().Unix()-blockTime > minutes*60 } func calcDayNumber(timestamp uint64) uint64 { diff --git a/secrets/secrets.go b/secrets/secrets.go index 72c3fed01..e398ddcfe 100644 --- a/secrets/secrets.go +++ b/secrets/secrets.go @@ -36,7 +36,8 @@ const ( ValidatorBLSSignature = "validator-bls-signature" // CoinGeckoAPIKey is the API key for the coingecko endpoints - CoinGeckoAPIKey = "coingecko-api-key" //nolint:gosec + //nolint:gosec + CoinGeckoAPIKey = "coingecko-api-key" ) // Define constant file names for the local StorageManager diff --git a/server/config.go b/server/config.go index 63240f6dd..0b2f4d06c 100644 --- a/server/config.go +++ b/server/config.go @@ -16,7 +16,8 @@ const DefaultJSONRPCPort int = 8545 // Config is used to parametrize the minimal client type Config struct { - Chain *chain.Chain + Chain *chain.Chain + Bootnode *chain.Bootnode JSONRPC *JSONRPC GRPCAddr *net.TCPAddr diff --git a/server/server.go b/server/server.go index 4ec4afb68..ae77fc5bf 100644 --- a/server/server.go +++ b/server/server.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "math/big" "net" "net/http" @@ -15,7 +16,7 @@ import ( "github.com/0xPolygon/polygon-edge/blockchain/storage" "github.com/0xPolygon/polygon-edge/blockchain/storage/leveldb" "github.com/0xPolygon/polygon-edge/blockchain/storage/memory" - consensusPolyBFT "github.com/0xPolygon/polygon-edge/consensus/polybft" + consensusPolybft "github.com/0xPolygon/polygon-edge/consensus/polybft" "github.com/0xPolygon/polygon-edge/forkmanager" "github.com/0xPolygon/polygon-edge/gasprice" priceoracle "github.com/0xPolygon/polygon-edge/price-oracle" @@ -28,6 +29,7 @@ import ( "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/helper/common" "github.com/0xPolygon/polygon-edge/helper/progress" + "github.com/0xPolygon/polygon-edge/helper/safe" "github.com/0xPolygon/polygon-edge/jsonrpc" "github.com/0xPolygon/polygon-edge/network" "github.com/0xPolygon/polygon-edge/secrets" @@ -166,6 +168,20 @@ func NewServer(config *Config) (*Server, error) { return nil, fmt.Errorf("failed to create data directories: %w", err) } + // Initialize the blockchain object + m.chain = config.Chain + + var signer crypto.TxSigner + + if config.Seal { + chainID, err := safe.SafeInt64ToUint64(config.Chain.Params.ChainID) + if err != nil { + return nil, err + } + + signer = crypto.NewSigner(config.Chain.Params.Forks.At(0), chainID) + } + if config.Telemetry.PrometheusAddr != nil { // Only setup telemetry if `PrometheusAddr` has been configured. if err := m.setupTelemetry(); err != nil { @@ -188,8 +204,13 @@ func NewServer(config *Config) (*Server, error) { // start libp2p { netConfig := config.Network - netConfig.Chain = m.config.Chain - netConfig.DataDir = filepath.Join(m.config.DataDir, "libp2p") + netConfig.Chain = config.Chain + + if config.Bootnode != nil { + netConfig.Bootnode = config.Bootnode + } + + netConfig.DataDir = filepath.Join(config.DataDir, "libp2p") netConfig.SecretsManager = m.secretsManager network, err := network.NewServer(logger, netConfig) @@ -201,7 +222,7 @@ func NewServer(config *Config) (*Server, error) { } // start blockchain object - stateStorage, err := itrie.NewLevelDBStorage(filepath.Join(m.config.DataDir, "trie"), logger) + stateStorage, err := itrie.NewLevelDBStorage(filepath.Join(config.DataDir, "trie"), logger) if err != nil { return nil, err } @@ -214,51 +235,51 @@ func NewServer(config *Config) (*Server, error) { m.executor = state.NewExecutor(config.Chain.Params, st, logger) // custom write genesis hook per consensus engine - engineName := m.config.Chain.Params.GetEngine() + engineName := config.Chain.Params.GetEngine() if factory, exists := genesisCreationFactory[ConsensusType(engineName)]; exists { - m.executor.GenesisPostHook = factory(m.config.Chain, engineName) + m.executor.GenesisPostHook = factory(config.Chain, engineName) } // apply allow list contracts deployer genesis data - if m.config.Chain.Params.ContractDeployerAllowList != nil { - addresslist.ApplyGenesisAllocs(m.config.Chain.Genesis, contracts.AllowListContractsAddr, - m.config.Chain.Params.ContractDeployerAllowList) + if config.Chain.Params.ContractDeployerAllowList != nil { + addresslist.ApplyGenesisAllocs(config.Chain.Genesis, contracts.AllowListContractsAddr, + config.Chain.Params.ContractDeployerAllowList) } // apply block list contracts deployer genesis data - if m.config.Chain.Params.ContractDeployerBlockList != nil { - addresslist.ApplyGenesisAllocs(m.config.Chain.Genesis, contracts.BlockListContractsAddr, - m.config.Chain.Params.ContractDeployerBlockList) + if config.Chain.Params.ContractDeployerBlockList != nil { + addresslist.ApplyGenesisAllocs(config.Chain.Genesis, contracts.BlockListContractsAddr, + config.Chain.Params.ContractDeployerBlockList) } // apply transactions execution allow list genesis data - if m.config.Chain.Params.TransactionsAllowList != nil { - addresslist.ApplyGenesisAllocs(m.config.Chain.Genesis, contracts.AllowListTransactionsAddr, - m.config.Chain.Params.TransactionsAllowList) + if config.Chain.Params.TransactionsAllowList != nil { + addresslist.ApplyGenesisAllocs(config.Chain.Genesis, contracts.AllowListTransactionsAddr, + config.Chain.Params.TransactionsAllowList) } // apply transactions execution block list genesis data - if m.config.Chain.Params.TransactionsBlockList != nil { - addresslist.ApplyGenesisAllocs(m.config.Chain.Genesis, contracts.BlockListTransactionsAddr, - m.config.Chain.Params.TransactionsBlockList) + if config.Chain.Params.TransactionsBlockList != nil { + addresslist.ApplyGenesisAllocs(config.Chain.Genesis, contracts.BlockListTransactionsAddr, + config.Chain.Params.TransactionsBlockList) } // apply bridge allow list genesis data - if m.config.Chain.Params.BridgeAllowList != nil { - addresslist.ApplyGenesisAllocs(m.config.Chain.Genesis, contracts.AllowListBridgeAddr, - m.config.Chain.Params.BridgeAllowList) + if config.Chain.Params.BridgeAllowList != nil { + addresslist.ApplyGenesisAllocs(config.Chain.Genesis, contracts.AllowListBridgeAddr, + config.Chain.Params.BridgeAllowList) } // apply bridge block list genesis data - if m.config.Chain.Params.BridgeBlockList != nil { - addresslist.ApplyGenesisAllocs(m.config.Chain.Genesis, contracts.BlockListBridgeAddr, - m.config.Chain.Params.BridgeBlockList) + if config.Chain.Params.BridgeBlockList != nil { + addresslist.ApplyGenesisAllocs(config.Chain.Genesis, contracts.BlockListBridgeAddr, + config.Chain.Params.BridgeBlockList) } var initialStateRoot = types.ZeroHash if ConsensusType(engineName) == PolyBFTConsensus { - polyBFTConfig, err := consensusPolyBFT.GetPolyBFTConfig(config.Chain) + polyBFTConfig, err := consensusPolybft.GetPolyBFTConfig(config.Chain) if err != nil { return nil, err } @@ -284,35 +305,27 @@ func NewServer(config *Config) (*Server, error) { return nil, err } - if err := initForkManager(engineName, config.Chain); err != nil { - return nil, err + if engineName != string(DummyConsensus) && engineName != string(DevConsensus) { + if err := initForkManager(engineName, m.chain); err != nil { + return nil, err + } } // compute the genesis root state config.Chain.Genesis.StateRoot = genesisRoot - // Use the london signer with eip-155 as a fallback one - var signer crypto.TxSigner = crypto.NewLondonSigner( - uint64(m.config.Chain.Params.ChainID), //nolint:gosec - config.Chain.Params.Forks.IsActive(chain.Homestead, 0), - crypto.NewEIP155Signer( - uint64(m.config.Chain.Params.ChainID), //nolint:gosec - config.Chain.Params.Forks.IsActive(chain.Homestead, 0), - ), - ) - // create storage instance for blockchain var db storage.Storage { - if m.config.DataDir == "" { + if config.DataDir == "" { db, err = memory.NewMemoryStorage(nil) if err != nil { return nil, err } } else { db, err = leveldb.NewLevelDBStorage( - filepath.Join(m.config.DataDir, "blockchain"), - m.logger, + filepath.Join(config.DataDir, "blockchain"), + logger, ) if err != nil { return nil, err @@ -355,10 +368,10 @@ func NewServer(config *Config) (*Server, error) { m.grpcServer, m.network, &txpool.Config{ - MaxSlots: m.config.MaxSlots, - PriceLimit: m.config.PriceLimit, - MaxAccountEnqueued: m.config.MaxAccountEnqueued, - ChainID: big.NewInt(m.config.Chain.Params.ChainID), + MaxSlots: config.MaxSlots, + PriceLimit: config.PriceLimit, + MaxAccountEnqueued: config.MaxAccountEnqueued, + ChainID: big.NewInt(config.Chain.Params.ChainID), }, ) if err != nil { @@ -491,7 +504,6 @@ func getAccountImpl(state state.State, root types.Hash, addr types.Address) (*st func (t *txpoolHub) GetNonce(root types.Hash, addr types.Address) uint64 { account, err := getAccountImpl(t.state, root, addr) - if err != nil { return 0 } @@ -501,18 +513,19 @@ func (t *txpoolHub) GetNonce(root types.Hash, addr types.Address) uint64 { func (t *txpoolHub) GetBalance(root types.Hash, addr types.Address) (*big.Int, error) { account, err := getAccountImpl(t.state, root, addr) - if err != nil { - if errors.Is(err, jsonrpc.ErrStateNotFound) { - return big.NewInt(0), nil - } - - return big.NewInt(0), err + return nil, err } return account.Balance, nil } +func (t *txpoolHub) GetHeaderByHash(hash types.Hash) (*types.Header, bool) { + header, ok := t.Blockchain.GetHeaderByHash(hash) + + return header, ok +} + // setupSecretsManager sets up the secrets manager func (s *Server) setupSecretsManager() error { secretsManagerConfig := s.config.SecretsManager @@ -528,7 +541,7 @@ func (s *Server) setupSecretsManager() error { Logger: s.logger, } - if secretsManagerType == secrets.Local || secretsManagerType == secrets.EncryptedLocal { + if secretsManagerType == secrets.Local { // Only the base directory is required for // the local secrets manager secretsManagerParams.Extra = map[string]interface{}{ @@ -542,14 +555,13 @@ func (s *Server) setupSecretsManager() error { return fmt.Errorf("secrets manager type '%s' not found", secretsManagerType) } - // Instantiate the secrets manager + // Setup the secrets manager secretsManager, factoryErr := secretsManagerFactory( secretsManagerConfig, secretsManagerParams, ) - if factoryErr != nil { - return fmt.Errorf("unable to instantiate secrets manager, %w", factoryErr) + return fmt.Errorf("unable to instantiate secrets manager: %w", factoryErr) } s.secretsManager = secretsManager @@ -571,40 +583,54 @@ func (s *Server) setupConsensus() error { engineConfig = map[string]interface{}{} } - var ( - blockTime = common.Duration{Duration: 0} - err error - ) + var blockTime uint64 if engineName != string(DummyConsensus) && engineName != string(DevConsensus) { - blockTime, err = extractBlockTime(engineConfig) + blockTimeGeneric, ok := engineConfig["blockTime"] + if !ok { + return fmt.Errorf("block time not found in engine config") + } + + blockTimeRaw, err := json.Marshal(blockTimeGeneric) if err != nil { - return err + return fmt.Errorf("failed to marshal block time: %w", err) + } + + var blockTimeDuration common.Duration + if err := json.Unmarshal(blockTimeRaw, &blockTimeDuration); err != nil { + return fmt.Errorf("failed to unmarshal block time: %w", err) + } + + if blockTimeDuration.Seconds() < 1 { + return fmt.Errorf("block time must be at least 1 second") + } + + seconds := blockTimeDuration.Seconds() + if seconds < 0 || seconds > math.MaxInt64 { + return fmt.Errorf("block time duration out of bounds") } + + blockTime = uint64(seconds) } config := &consensus.Config{ - Params: s.config.Chain.Params, - Config: engineConfig, - Path: filepath.Join(s.config.DataDir, "consensus"), - IsRelayer: s.config.Relayer, - RPCEndpoint: s.config.JSONRPC.JSONRPCAddr.String(), + Params: s.config.Chain.Params, + Config: engineConfig, + Path: filepath.Join(s.config.DataDir, "consensus"), } consensus, err := engine( &consensus.Params{ - Context: context.Background(), - Config: config, - TxPool: s.txpool, - Network: s.network, - Blockchain: s.blockchain, - Executor: s.executor, - Grpc: s.grpcServer, - Logger: s.logger, - SecretsManager: s.secretsManager, - BlockTime: uint64(blockTime.Seconds()), - NumBlockConfirmations: s.config.NumBlockConfirmations, - MetricsInterval: s.config.MetricsInterval, + Context: context.Background(), + Config: config, + TxPool: s.txpool, + Network: s.network, + Blockchain: s.blockchain, + Executor: s.executor, + Grpc: s.grpcServer, + Logger: s.logger, + SecretsManager: s.secretsManager, + BlockTime: blockTime, }, ) @@ -617,32 +643,6 @@ func (s *Server) setupConsensus() error { return nil } -// extractBlockTime extracts blockTime parameter from consensus engine configuration. -// If it is missing or invalid, an appropriate error is returned. -func extractBlockTime(engineConfig map[string]interface{}) (common.Duration, error) { - blockTimeGeneric, ok := engineConfig["blockTime"] - if !ok { - return common.Duration{}, errBlockTimeMissing - } - - blockTimeRaw, err := json.Marshal(blockTimeGeneric) - if err != nil { - return common.Duration{}, errBlockTimeInvalid - } - - var blockTime common.Duration - - if err := json.Unmarshal(blockTimeRaw, &blockTime); err != nil { - return common.Duration{}, errBlockTimeInvalid - } - - if blockTime.Seconds() < 1 { - return common.Duration{}, errBlockTimeInvalid - } - - return blockTime, nil -} - type jsonRPCHub struct { state state.State restoreProgression *progress.ProgressionWrapper @@ -656,6 +656,24 @@ type jsonRPCHub struct { gasprice.GasStore } +func (j *jsonRPCHub) GetConsensus() consensus.Consensus { + return j.Consensus +} + +func (j *jsonRPCHub) BeginTxn( + parentRoot types.Hash, + header *types.Header, + blockCreator types.Address, +) (*state.Transition, error) { + return j.Executor.BeginTxn(parentRoot, header, blockCreator) +} + +func (j *jsonRPCHub) GetHeaderByHash(hash types.Hash) (*types.Header, bool) { + header, ok := j.Blockchain.GetHeaderByHash(hash) + + return header, ok +} + func (j *jsonRPCHub) GetPeers() int { return len(j.Server.Peers()) } @@ -884,15 +902,13 @@ func (s *Server) setupJSONRPC() error { Executor: s.executor, Consensus: s.consensus, Server: s.network, - BridgeDataProvider: s.consensus.GetBridgeProvider(), GasStore: s.gasHelper, } conf := &jsonrpc.Config{ Store: hub, Addr: s.config.JSONRPC.JSONRPCAddr, - ChainID: uint64(s.config.Chain.Params.ChainID), //nolint:gosec - ChainName: s.chain.Name, + ChainID: s.GetChainID(), AccessControlAllowOrigin: s.config.JSONRPC.AccessControlAllowOrigin, PriceLimit: s.config.PriceLimit, BatchLengthLimit: s.config.JSONRPC.BatchLengthLimit, @@ -915,20 +931,6 @@ func (s *Server) setupJSONRPC() error { func (s *Server) setupGRPC() error { proto.RegisterSystemServer(s.grpcServer, &systemService{server: s}) - lis, err := net.Listen("tcp", s.config.GRPCAddr.String()) - if err != nil { - return err - } - - // Start server with infinite retries - go func() { - if err := s.grpcServer.Serve(lis); err != nil { - s.logger.Error(err.Error()) - } - }() - - s.logger.Info("GRPC server running", "addr", s.config.GRPCAddr.String()) - return nil } @@ -943,41 +945,48 @@ func (s *Server) JoinPeer(rawPeerMultiaddr string) error { } // Close closes the Minimal server (blockchain, networking, consensus) -func (s *Server) Close() { - // Close the blockchain layer +func (s *Server) Close() error { + s.logger.Info("Closing server...") + + // Close the blockchain if err := s.blockchain.Close(); err != nil { - s.logger.Error("failed to close blockchain", "err", err.Error()) + return fmt.Errorf("failed to close blockchain: %w", err) } // Close the networking layer if err := s.network.Close(); err != nil { - s.logger.Error("failed to close networking", "err", err.Error()) + return fmt.Errorf("failed to close network: %w", err) } - // Close the consensus layer + // Close the consensus engine if err := s.consensus.Close(); err != nil { - s.logger.Error("failed to close consensus", "err", err.Error()) + return fmt.Errorf("failed to close consensus: %w", err) } - // Close the state storage - if err := s.stateStorage.Close(); err != nil { - s.logger.Error("failed to close storage for trie", "err", err.Error()) + // Close the JSON-RPC server + if err := s.jsonrpcServer.Close(); err != nil { + return fmt.Errorf("failed to close JSON-RPC server: %w", err) } + // Close the gRPC server + s.grpcServer.Stop() + + // Close the Prometheus server if s.prometheusServer != nil { - if err := s.prometheusServer.Shutdown(context.Background()); err != nil { - s.logger.Error("Prometheus server shutdown error", err) + if err := s.prometheusServer.Close(); err != nil { + return fmt.Errorf("failed to close Prometheus server: %w", err) } } // Close the price oracle - s.priceOracle.Close() + if err := s.priceOracle.Close(); err != nil { + return fmt.Errorf("failed to close price oracle: %w", err) + } - // Close the txpool's main loop + // Close the txpool s.txpool.Close() - // Close DataDog profiler - s.closeDataDogProfiler() + return nil } // Entry is a consensus configuration entry @@ -1012,59 +1021,88 @@ func (s *Server) startPrometheusServer(listenAddr *net.TCPAddr) *http.Server { } func initForkManager(engineName string, config *chain.Chain) error { - var initialParams *forkmanager.ForkParams - + // Initialize fork manager if factory := forkManagerInitialParamsFactory[ConsensusType(engineName)]; factory != nil { params, err := factory(config) if err != nil { return err } - initialParams = params - } + fm := forkmanager.GetInstance() + fm.Clear() + fm.RegisterFork("initial", params) - fm := forkmanager.GetInstance() + // Register forks + for name, f := range *config.Params.Forks { + fm.RegisterFork(name, f.Params) + } - // clear everything in forkmanager (if there was something because of tests) and register initial fork - fm.Clear() - fm.RegisterFork(forkmanager.InitialFork, initialParams) + // Activate initial fork + if err := fm.ActivateFork("initial", 0); err != nil { + return err + } - // Register forks - for name, f := range *config.Params.Forks { - // check if fork is not supported by current edge version - if _, found := (*chain.AllForksEnabled)[name]; !found { - return fmt.Errorf("fork is not available: %s", name) + // Activate forks + for name, f := range *config.Params.Forks { + if err := fm.ActivateFork(name, f.Block); err != nil { + return err + } } + } - fm.RegisterFork(name, f.Params) + if factory := forkManagerFactory[ConsensusType(engineName)]; factory != nil { + if err := factory(config.Params.Forks); err != nil { + return err + } } - // Register handlers and additional forks here - if err := types.RegisterTxHashFork(chain.TxHashWithType); err != nil { - return err + return nil +} + +func (s *Server) GetChainID() uint64 { + chainID, err := safe.SafeInt64ToUint64(s.config.Chain.Params.ChainID) + if err != nil { + return 0 } - // Register Handler for London fork fix - if err := state.RegisterLondonFixFork(chain.LondonFix); err != nil { - return err + return chainID +} + +func (s *Server) GetChainIDStr() string { + return fmt.Sprintf("%d", s.GetChainID()) +} + +func (s *Server) Start() error { + s.logger.Info("Starting server...") + + // Initialize blockchain + if err := s.blockchain.ComputeGenesis(); err != nil { + return fmt.Errorf("failed to compute genesis: %w", err) } - if factory := forkManagerFactory[ConsensusType(engineName)]; factory != nil { - if err := factory(config.Params.Forks); err != nil { - return err - } + // Start the networking layer + if err := s.network.Start(); err != nil { + return fmt.Errorf("failed to start network: %w", err) } - // Activate initial fork - if err := fm.ActivateFork(forkmanager.InitialFork, uint64(0)); err != nil { - return err + // Start the consensus engine + if err := s.consensus.Start(); err != nil { + return fmt.Errorf("failed to start consensus: %w", err) } - // Activate forks - for name, f := range *config.Params.Forks { - if err := fm.ActivateFork(name, f.Block); err != nil { - return err - } + // Start the gRPC server + listener, err := net.Listen("tcp", s.config.Network.Addr.String()) + if err != nil { + return fmt.Errorf("failed to create gRPC listener: %w", err) + } + + if err := s.grpcServer.Serve(listener); err != nil { + return fmt.Errorf("failed to start gRPC server: %w", err) + } + + // Start the Prometheus server + if s.config.Telemetry.PrometheusAddr != nil { + s.prometheusServer = s.startPrometheusServer(s.config.Telemetry.PrometheusAddr) } return nil diff --git a/state/executor.go b/state/executor.go index 962993227..1eed42a7d 100644 --- a/state/executor.go +++ b/state/executor.go @@ -7,11 +7,12 @@ import ( "math/big" "github.com/hashicorp/go-hclog" - "github.com/umbracle/ethgo/abi" "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/contracts" "github.com/0xPolygon/polygon-edge/crypto" + "github.com/0xPolygon/polygon-edge/helper" + "github.com/0xPolygon/polygon-edge/helper/safe" "github.com/0xPolygon/polygon-edge/state/runtime" "github.com/0xPolygon/polygon-edge/state/runtime/addresslist" "github.com/0xPolygon/polygon-edge/state/runtime/evm" @@ -78,12 +79,17 @@ func (e *Executor) WriteGenesis( ChainID: e.config.ChainID, } + gasPool, err := helper.SafeInt64ToUint64(env.GasLimit) + if err != nil { + return types.Hash{}, fmt.Errorf("failed to convert gas limit: %w", err) + } + transition := &Transition{ logger: e.logger, ctx: env, state: txn, auxState: e.state, - gasPool: uint64(env.GasLimit), //nolint:gosec + gasPool: gasPool, config: config, precompiles: precompiled.NewPrecompiled(), } @@ -193,26 +199,46 @@ func (e *Executor) BeginTxn( newTxn := NewTxn(auxSnap2) - txCtx := runtime.TxContext{ + timestamp, err := helper.SafeUint64ToInt64(header.Timestamp) + if err != nil { + return nil, fmt.Errorf("failed to convert timestamp: %w", err) + } + + number, err := helper.SafeUint64ToInt64(header.Number) + if err != nil { + return nil, fmt.Errorf("failed to convert block number: %w", err) + } + + gasLimit, err := helper.SafeUint64ToInt64(header.GasLimit) + if err != nil { + return nil, fmt.Errorf("failed to convert gas limit: %w", err) + } + + env := runtime.TxContext{ Coinbase: coinbaseReceiver, - Timestamp: int64(header.Timestamp), //nolint:gosec - Number: int64(header.Number), //nolint:gosec + Timestamp: timestamp, + Number: number, Difficulty: types.BytesToHash(new(big.Int).SetUint64(header.Difficulty).Bytes()), BaseFee: new(big.Int).SetUint64(header.BaseFee), - GasLimit: int64(header.GasLimit), //nolint:gosec + GasLimit: gasLimit, ChainID: e.config.ChainID, BurnContract: burnContract, } + gasPool, err := helper.SafeInt64ToUint64(env.GasLimit) + if err != nil { + return nil, fmt.Errorf("failed to convert gas pool: %w", err) + } + txn := &Transition{ - logger: e.logger, - ctx: txCtx, - state: newTxn, - snap: auxSnap2, - getHash: e.GetHash(header), - auxState: e.state, - config: forkConfig, - gasPool: uint64(txCtx.GasLimit), //nolint:gosec + logger: e.logger, + ctx: env, + state: newTxn, + snap: auxSnap2, + getHashFunc: e.GetHash(header), + auxState: e.state, + config: forkConfig, + gasPool: gasPool, receipts: []*types.Receipt{}, totalGas: 0, @@ -259,11 +285,11 @@ type Transition struct { auxState State snap Snapshot - config chain.ForksInTime - state *Txn - getHash GetHashByNumber - ctx runtime.TxContext - gasPool uint64 + config chain.ForksInTime + state *Txn + getHashFunc GetHashByNumber + ctx runtime.TxContext + gasPool uint64 // result receipts []*types.Receipt @@ -336,65 +362,30 @@ var emptyFrom = types.Address{} // Write writes another transaction to the executor func (t *Transition) Write(txn *types.Transaction) error { - var err error - 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)) //nolint:gosec - - txn.From, err = signer.Sender(txn) + chainID, err := helper.SafeInt64ToUint64(t.ctx.ChainID) if err != nil { - return NewTransitionApplicationError(err, false) + return err } - } - // Make a local copy and apply the transaction - msg := txn.Copy() - - result, e := t.Apply(msg) - if result != nil && result.Reverted() { - unpackedRevert, _ := abi.UnpackRevertError(result.ReturnValue) - t.logger.Debug("the call has reverted. Revert msg:", unpackedRevert, "Error: ", e) - } - - if e != nil { - t.logger.Error("failed to apply tx", "err", e) - - return e - } + signer := crypto.NewSigner(t.config, chainID) - t.totalGas += result.GasUsed - - logs := t.state.Logs() - - receipt := &types.Receipt{ - CumulativeGasUsed: t.totalGas, - TransactionType: txn.Type, - TxHash: txn.Hash, - GasUsed: result.GasUsed, - } - - // The suicided accounts are set as deleted for the next iteration - if err := t.state.CleanDeleteObjects(true); err != nil { - return fmt.Errorf("failed to clean deleted objects: %w", err) - } + from, err := signer.Sender(txn) + if err != nil { + return NewTransitionApplicationError(err, false) + } - if result.Failed() { - receipt.SetStatus(types.ReceiptFailed) - } else { - receipt.SetStatus(types.ReceiptSuccess) + txn.From = from } - // if the transaction created a contract, store the creation address in the receipt. - if msg.To == nil { - receipt.ContractAddress = crypto.CreateAddress(msg.From, txn.Nonce).Ptr() - } + // Make a copy of the transaction + txnCopy := txn.Copy() - // Set the receipt logs and create a bloom for filtering - receipt.Logs = logs - receipt.LogsBloom = types.CreateBloom([]*types.Receipt{receipt}) - t.receipts = append(t.receipts, receipt) + // Increment the nonce of the sender + newNonce := t.state.GetNonce(txnCopy.From) + 1 + t.state.SetNonce(txnCopy.From, newNonce) return nil } @@ -457,11 +448,19 @@ func (t *Transition) ContextPtr() *runtime.TxContext { } func (t *Transition) subGasLimitPrice(msg *types.Transaction) error { - upfrontGasCost := GetLondonFixHandler( - uint64(t.ctx.Number), //nolint:gosec - ).getUpfrontGasCost(msg, t.ctx.BaseFee) + blockNumber, err := safe.SafeInt64ToUint64(t.ctx.Number) + if err != nil { + return fmt.Errorf("invalid block number: %w", err) + } + + effectiveTip := GetLondonFixHandler(blockNumber).getEffectiveTip( + msg, + msg.GetGasPrice(t.ctx.BaseFee.Uint64()), + t.ctx.BaseFee, + t.config.London, + ) - if err := t.state.SubBalance(msg.From, upfrontGasCost); err != nil { + if err := t.state.SubBalance(msg.From, effectiveTip); err != nil { if errors.Is(err, runtime.ErrNotEnoughFunds) { return ErrNotEnoughFundsForGas } @@ -485,7 +484,12 @@ func (t *Transition) nonceCheck(msg *types.Transaction) error { // checkDynamicFees checks correctness of the EIP-1559 feature-related fields. // Basically, makes sure gas tip cap and gas fee cap are good for dynamic and legacy transactions func (t *Transition) checkDynamicFees(msg *types.Transaction) error { - return GetLondonFixHandler(uint64(t.ctx.Number)).checkDynamicFees(msg, t) //nolint:gosec + blockNumber, err := safe.SafeInt64ToUint64(t.ctx.Number) + if err != nil { + return NewTransitionApplicationError(fmt.Errorf("invalid block number: %w", err), false) + } + + return GetLondonFixHandler(blockNumber).checkDynamicFees(msg, t) } // errors that can originate in the consensus rules checks of the apply method below @@ -600,7 +604,7 @@ func (t *Transition) apply(msg *types.Transaction) (*runtime.ExecutionResult, er value := new(big.Int).Set(msg.Value) // set the specific transaction fields in the context - t.ctx.GasPrice = types.BytesToHash(gasPrice.Bytes()) + t.ctx.GasPrice = gasPrice t.ctx.Origin = msg.From var result *runtime.ExecutionResult @@ -638,8 +642,17 @@ func (t *Transition) apply(msg *types.Transaction) (*runtime.ExecutionResult, er // Define effective tip based on tx type. // We use EIP-1559 fields of the tx if the london hardfork is enabled. // Effective tip became to be either gas tip cap or (gas fee cap - current base fee) - effectiveTip := GetLondonFixHandler(uint64(t.ctx.Number)).getEffectiveTip( //nolint:gosec - msg, gasPrice, t.ctx.BaseFee, t.config.London, + blockNumber := t.ctx.Number + if blockNumber < 0 { + return nil, nil + } + + blockNumberUint := uint64(blockNumber) + effectiveTip := GetLondonFixHandler(blockNumberUint).getEffectiveTip( + t.ctx.Tx, + t.ctx.GasPrice, + t.ctx.BaseFee, + t.config.London, ) // Pay the coinbase fee as a miner reward using the calculated effective tip. @@ -1006,7 +1019,12 @@ func (t *Transition) GetTxContext() runtime.TxContext { } func (t *Transition) GetBlockHash(number int64) (res types.Hash) { - return t.getHash(uint64(number)) //nolint:gosec + blockNumber, err := safe.SafeInt64ToUint64(number) + if err != nil { + return types.ZeroHash + } + + return t.getHash(blockNumber) } func (t *Transition) EmitLog(addr types.Address, topics []types.Hash, data []byte) { @@ -1259,3 +1277,13 @@ func (t *Transition) captureCallEnd(c *runtime.Contract, result *runtime.Executi result.Err, ) } + +func (t *Transition) getEffectiveTip() *big.Int { + gasPrice := new(big.Int).SetBytes(t.ctx.GasPrice.Bytes()) + + return gasPrice +} + +func (t *Transition) getHash(blockNumber uint64) types.Hash { + return t.getHashFunc(blockNumber) +} diff --git a/state/immutable-trie/copy_trie.go b/state/immutable-trie/copy_trie.go index afa912187..9bbc975ce 100644 --- a/state/immutable-trie/copy_trie.go +++ b/state/immutable-trie/copy_trie.go @@ -195,6 +195,7 @@ func hashChecker(node Node, h *hasher, a *fastrlp.Arena, d int, storage Storage) if err != nil { return nil, err } + val.Set(v) } } @@ -207,6 +208,7 @@ func hashChecker(node Node, h *hasher, a *fastrlp.Arena, d int, storage Storage) if err != nil { return nil, err } + val.Set(v) } diff --git a/state/immutable-trie/storage.go b/state/immutable-trie/storage.go index cb1f775ca..8172105ad 100644 --- a/state/immutable-trie/storage.go +++ b/state/immutable-trie/storage.go @@ -249,20 +249,19 @@ func decodeNode(v *fastrlp.Value, s Storage) (Node, error) { } else if ll == 17 { // full node nc := &FullNode{} + for i := 0; i < 16; i++ { if v.Get(i).Type() == fastrlp.TypeBytes && len(v.Get(i).Raw()) == 0 { // empty continue } + nc.children[i], err = decodeNode(v.Get(i), s) if err != nil { return nil, err } } - if v.Get(16).Type() != fastrlp.TypeBytes { - return nil, fmt.Errorf("full node value expected to be bytes") - } if len(v.Get(16).Raw()) != 0 { vv := &ValueNode{} vv.buf = append(vv.buf[:0], v.Get(16).Raw()...) diff --git a/state/runtime/evm/dispatch_table.go b/state/runtime/evm/dispatch_table.go index e8afeae0f..50bb83a53 100644 --- a/state/runtime/evm/dispatch_table.go +++ b/state/runtime/evm/dispatch_table.go @@ -20,8 +20,10 @@ func register(op OpCode, h handler) { func registerRange(from, to OpCode, factory func(n int) instruction, gas uint64) { c := 1 + for i := from; i <= to; i++ { register(i, handler{factory(c), 0, gas}) + c++ } } diff --git a/state/runtime/evm/instructions.go b/state/runtime/evm/instructions.go index d2a38374a..a0344b75b 100644 --- a/state/runtime/evm/instructions.go +++ b/state/runtime/evm/instructions.go @@ -69,9 +69,11 @@ func opSDiv(c *state) { } else { neg := a.Sign() != b.Sign() b.Div(a.Abs(a), b.Abs(b)) + if neg { b.Neg(b) } + toU256(b) } } @@ -99,9 +101,11 @@ func opSMod(c *state) { } else { neg := a.Sign() < 0 b.Mod(a.Abs(a), b.Abs(b)) + if neg { b.Neg(b) } + toU256(b) } } @@ -1267,6 +1271,7 @@ func (c *state) buildCallContract(op OpCode) (*runtime.Contract, uint64, uint64, return nil, 0, 0, nil } + gas = initialGas.Uint64() } diff --git a/state/runtime/runtime.go b/state/runtime/runtime.go index 248e9f3fd..67fa3cb1f 100644 --- a/state/runtime/runtime.go +++ b/state/runtime/runtime.go @@ -12,7 +12,7 @@ import ( // TxContext is the context of the transaction type TxContext struct { - GasPrice types.Hash + GasPrice *big.Int Origin types.Address Coinbase types.Address Number int64 @@ -24,6 +24,7 @@ type TxContext struct { NonPayable bool BaseFee *big.Int BurnContract types.Address + Tx *types.Transaction } // StorageStatus is the status of the storage access diff --git a/syncer/client_test.go b/syncer/client_test.go index d19bdb68f..d151e0160 100644 --- a/syncer/client_test.go +++ b/syncer/client_test.go @@ -1,7 +1,6 @@ package syncer import ( - "context" "sync" "testing" "time" @@ -30,10 +29,15 @@ func newTestNetwork(t *testing.T) *network.Server { srv, err := network.CreateServer(&network.CreateServerParams{ ConfigCallback: networkConfig, + Logger: hclog.NewNullLogger(), }) assert.NoError(t, err) + // Start the server + err = srv.Start() + assert.NoError(t, err) + return srv } @@ -174,6 +178,7 @@ func TestStatusPubSub(t *testing.T) { _, peerSrv := createTestSyncerService(t, &mockBlockchain{}) peerID := peerSrv.AddrInfo().ID + // Start the client's event processing go client.startPeerEventProcess() // run goroutine to collect events @@ -199,8 +204,7 @@ func TestStatusPubSub(t *testing.T) { network.DefaultBufferTimeout, network.DefaultJoinTimeout, ) - - assert.NoError(t, err) + require.NoError(t, err) // disconnect err = network.DisconnectAndWait( @@ -208,12 +212,10 @@ func TestStatusPubSub(t *testing.T) { peerID, network.DefaultLeaveTimeout, ) - - assert.NoError(t, err) + require.NoError(t, err) // close channel and wait for events - close(client.closeCh) - + client.Close() wg.Wait() expected := []*event.PeerEvent{ @@ -395,62 +397,58 @@ func Test_shouldEmitBlocks(t *testing.T) { }) ) + // Ensure proper cleanup t.Cleanup(func() { + client.Close() clientSrv.Close() peerSrv.Close() - client.Close() }) + // Start the client's event processing + go client.startNewBlockProcess() + + // Connect the networks err := network.JoinAndWaitMultiple( network.DefaultJoinTimeout, clientSrv, peerSrv, ) + require.NoError(t, err) - assert.NoError(t, err) + // Start gossip + require.NoError(t, client.startGossip()) - // start gossip - assert.NoError(t, client.startGossip()) + // Create topic & subscribe in peer + topic, err := peerSrv.NewTopic(statusTopicName, &proto.SyncPeerStatus{}) + assert.NoError(t, err) - // start to subscribe blockchain events - go client.startNewBlockProcess() + waitForGossip := func(wg *sync.WaitGroup) bool { + c := make(chan struct{}) - // push latest block number to blockchain subscription - pushSubscription := func(sub *blockchain.MockSubscription, latest uint64) { - sub.Push(&blockchain.Event{ - NewChain: []*types.Header{ - { - Number: latest, - }, - }, - }) - } + go func() { + defer close(c) + wg.Wait() + }() - waitForContext := func(ctx context.Context) bool { select { - case <-ctx.Done(): + case <-c: return true case <-time.After(5 * time.Second): return false } } - // create topic & subscribe in peer - topic, err := peerSrv.NewTopic(statusTopicName, &proto.SyncPeerStatus{}) - assert.NoError(t, err) - testGossip := func(t *testing.T, shouldEmit bool) { t.Helper() - // context to be canceled when receiving status - receiveContext, cancelContext := context.WithCancel(context.Background()) - defer cancelContext() + var wgForGossip sync.WaitGroup + wgForGossip.Add(1) - assert.NoError(t, topic.Subscribe(func(_ interface{}, id peer.ID) { - cancelContext() + require.NoError(t, topic.Subscribe(func(_ interface{}, _ peer.ID) { + wgForGossip.Done() })) - // need to wait for a few seconds to propagate subscribing + // Wait for subscription to propagate time.Sleep(2 * time.Second) if shouldEmit { @@ -459,18 +457,27 @@ func Test_shouldEmitBlocks(t *testing.T) { client.DisablePublishingPeerStatus() } - pushSubscription(subscription, clientLatest) - - canceled := waitForContext(receiveContext) + // Push block to subscription + subscription.Push(&blockchain.Event{ + NewChain: []*types.Header{ + { + Number: clientLatest, + }, + }, + }) - assert.Equal(t, shouldEmit, canceled) + // Wait for gossip to complete + gossiped := waitForGossip(&wgForGossip) + require.Equal(t, shouldEmit, gossiped) } - t.Run("should send own status via gossip if shouldEmitBlocks is set", func(t *testing.T) { + t.Run("should emit blocks when enabled", func(t *testing.T) { + t.Parallel() testGossip(t, true) }) - t.Run("shouldn't send own status via gossip if shouldEmitBlocks is reset", func(t *testing.T) { + t.Run("should not emit blocks when disabled", func(t *testing.T) { + t.Parallel() testGossip(t, false) }) } @@ -545,36 +552,30 @@ func Test_EmitMultipleBlocks(t *testing.T) { }) ) + // Ensure proper cleanup t.Cleanup(func() { + client.Close() clientSrv.Close() peerSrv.Close() - client.Close() }) + // Start the client's event processing + go client.startNewBlockProcess() + + // Connect the networks err := network.JoinAndWaitMultiple( network.DefaultJoinTimeout, clientSrv, peerSrv, ) - require.NoError(t, err) - // start gossip + // Start gossip require.NoError(t, client.startGossip()) - // start to subscribe blockchain events - go client.startNewBlockProcess() - - // push latest block number to blockchain subscription - pushSubscription := func(sub *blockchain.MockSubscription, latest uint64) { - sub.Push(&blockchain.Event{ - NewChain: []*types.Header{ - { - Number: latest, - }, - }, - }) - } + // Create topic & subscribe in peer + topic, err := peerSrv.NewTopic(statusTopicName, &proto.SyncPeerStatus{}) + assert.NoError(t, err) waitForGossip := func(wg *sync.WaitGroup) bool { c := make(chan struct{}) @@ -592,34 +593,36 @@ func Test_EmitMultipleBlocks(t *testing.T) { } } - // create topic & subscribe in peer - topic, err := peerSrv.NewTopic(statusTopicName, &proto.SyncPeerStatus{}) - assert.NoError(t, err) - testGossip := func(t *testing.T, blocksNum int) { t.Helper() var wgForGossip sync.WaitGroup - wgForGossip.Add(blocksNum) require.NoError(t, topic.Subscribe(func(_ interface{}, _ peer.ID) { wgForGossip.Done() })) - // need to wait for a few seconds to propagate subscribing + // Wait for subscription to propagate time.Sleep(2 * time.Second) client.EnablePublishingPeerStatus() + // Push blocks to subscription go func() { for i := 0; i < blocksNum; i++ { - pushSubscription(subscription, clientLatest+uint64(i)) + subscription.Push(&blockchain.Event{ + NewChain: []*types.Header{ + { + Number: clientLatest + uint64(i), + }, + }, + }) } }() + // Wait for gossip to complete gossiped := waitForGossip(&wgForGossip) - - require.Equal(t, true, gossiped) + require.True(t, gossiped) } t.Run("should receive all blocks", func(t *testing.T) { diff --git a/tests/state_test.go b/tests/state_test.go index d970bc874..efcc86111 100644 --- a/tests/state_test.go +++ b/tests/state_test.go @@ -138,9 +138,7 @@ func TestState(t *testing.T) { "RevertPrecompiledTouch", } - // TODO: Add evm-benchmarks repo's tests - // Remove all tests because tx fee is distributed in different way in Hydragon, - // so ethereum state tests are not valid anymore + // Add evm-benchmarks repo's tests folders := []string{} for _, folder := range folders { diff --git a/tests/testing.go b/tests/testing.go index 032837e3e..f7abcece7 100644 --- a/tests/testing.go +++ b/tests/testing.go @@ -488,7 +488,8 @@ func contains(l []string, name string) bool { return false } -//go:embed tests +// go:embed tests + var testsFS embed.FS func listFolders(tests ...string) ([]string, error) { diff --git a/tracker/event_tracker.go b/tracker/event_tracker.go index 14e1d1855..d11b2ccd9 100644 --- a/tracker/event_tracker.go +++ b/tracker/event_tracker.go @@ -92,11 +92,13 @@ func (e *EventTracker) Start(ctx context.Context) error { go common.RetryForever(ctx, time.Second, func(context.Context) error { // Init start := time.Now().UTC() + if err := blockTracker.Init(); err != nil { e.logger.Error("failed to init blocktracker", "error", err) return err } + elapsed := time.Now().UTC().Sub(start) // Start diff --git a/tracker/event_tracker_test.go b/tracker/event_tracker_test.go index 7717808c4..76fc817e4 100644 --- a/tracker/event_tracker_test.go +++ b/tracker/event_tracker_test.go @@ -41,6 +41,11 @@ func (m *mockEventSubscriber) len() int { } func TestEventTracker_TrackSyncEvents(t *testing.T) { + // Skip test if Docker is not accessible + if _, err := os.Stat("/var/run/docker.sock"); os.IsNotExist(err) { + t.Skip("Docker is not accessible, skipping test") + } + const ( numBlockConfirmations = 6 eventsPerStep = 8 diff --git a/types/rlp_marshal.go b/types/rlp_marshal.go index 16b61c3ed..a34299674 100644 --- a/types/rlp_marshal.go +++ b/types/rlp_marshal.go @@ -37,7 +37,9 @@ func (b *Block) MarshalRLPWith(ar *fastrlp.Arena) *fastrlp.Value { if len(b.Transactions) == 0 { vv.Set(ar.NewNullArray()) } else { - v0 := ar.NewArray() + var v0 *fastrlp.Value + v0 = ar.NewArray() + for _, tx := range b.Transactions { if tx.Type != LegacyTx { v0.Set(ar.NewCopyBytes([]byte{byte(tx.Type)})) @@ -45,6 +47,7 @@ func (b *Block) MarshalRLPWith(ar *fastrlp.Arena) *fastrlp.Value { v0.Set(tx.MarshalRLPWith(ar)) } + vv.Set(v0) } @@ -55,6 +58,7 @@ func (b *Block) MarshalRLPWith(ar *fastrlp.Arena) *fastrlp.Value { for _, uncle := range b.Uncles { v1.Set(uncle.MarshalRLPWith(ar)) } + vv.Set(v1) }