diff --git a/command/e2e/params.go b/command/e2e/params.go deleted file mode 100644 index bd0ed46387..0000000000 --- a/command/e2e/params.go +++ /dev/null @@ -1,53 +0,0 @@ -package e2e - -import ( - "errors" - "fmt" - "os" - - "github.com/0xPolygon/polygon-edge/types" -) - -const ( - dataDirFlag = "data-dir" - registratorDataDirFlag = "registrator-data-dir" - balanceFlag = "balance" - stakeFlag = "stake" - chainIDFlag = "chain-id" -) - -type registerParams struct { - newValidatorDataDir string - registratorValidatorDataDir string - jsonRPCAddr string - balance string - stake string - chainID int64 -} - -func (rp *registerParams) validateFlags() error { - if _, err := os.Stat(rp.newValidatorDataDir); errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("provided new validator data directory '%s' doesn't exist", rp.newValidatorDataDir) - } - - if _, err := os.Stat(rp.registratorValidatorDataDir); errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("provided registrator validator data directory '%s' doesn't exist", rp.registratorValidatorDataDir) - } - - balance, err := types.ParseUint256orHex(&rp.balance) - if err != nil { - return fmt.Errorf("provided balance '%s' isn't valid", rp.balance) - } - - stake, err := types.ParseUint256orHex(&rp.stake) - if err != nil { - return fmt.Errorf("provided stake '%s' isn't valid", rp.stake) - } - - if stake.Cmp(balance) > 0 { - return fmt.Errorf("provided stake is greater than funded balance (stake=%s balance=%s)", - stake.String(), balance.String()) - } - - return nil -} diff --git a/command/e2e/register_validator.go b/command/e2e/register_validator.go deleted file mode 100644 index 956cfe0f54..0000000000 --- a/command/e2e/register_validator.go +++ /dev/null @@ -1,530 +0,0 @@ -package e2e - -import ( - "context" - "encoding/hex" - "errors" - "fmt" - "math/big" - "time" - - "github.com/0xPolygon/polygon-edge/chain" - "github.com/0xPolygon/polygon-edge/command" - "github.com/0xPolygon/polygon-edge/command/helper" - "github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi" - bls "github.com/0xPolygon/polygon-edge/consensus/polybft/signer" - "github.com/0xPolygon/polygon-edge/consensus/polybft/wallet" - "github.com/0xPolygon/polygon-edge/contracts" - "github.com/0xPolygon/polygon-edge/crypto" - "github.com/0xPolygon/polygon-edge/secrets" - secretsHelper "github.com/0xPolygon/polygon-edge/secrets/helper" - "github.com/0xPolygon/polygon-edge/types" - "github.com/mitchellh/go-glint" - gc "github.com/mitchellh/go-glint/components" - "github.com/spf13/cobra" - "github.com/umbracle/ethgo" - "github.com/umbracle/ethgo/jsonrpc" -) - -const ( - defaultBalance = "0xD3C21BCECCEDA1000000" // 1e24 - defaultStake = "0x56BC75E2D63100000" // 1e20 -) - -var params registerParams - -func GetCommand() *cobra.Command { - registerCmd := &cobra.Command{ - Use: "register-validator", - Short: "Registers a new validator", - PreRunE: runPreRun, - RunE: runCommand, - } - - setFlags(registerCmd) - - return registerCmd -} - -func setFlags(cmd *cobra.Command) { - cmd.Flags().StringVar( - ¶ms.newValidatorDataDir, - dataDirFlag, - "", - "the directory path where new validator key is stored", - ) - cmd.Flags().StringVar( - ¶ms.registratorValidatorDataDir, - registratorDataDirFlag, - "", - "the directory path where registrator validator key is stored", - ) - - cmd.Flags().StringVar( - ¶ms.balance, - balanceFlag, - defaultBalance, - "balance which is going to be funded to the new validator account", - ) - - cmd.Flags().StringVar( - ¶ms.stake, - stakeFlag, - defaultStake, - "stake represents amount which is going to be staked by the new validator account", - ) - - cmd.Flags().Int64Var( - ¶ms.chainID, - chainIDFlag, - command.DefaultChainID, - "the ID of the chain", - ) - - helper.RegisterJSONRPCFlag(cmd) -} - -func runPreRun(cmd *cobra.Command, _ []string) error { - params.jsonRPCAddr = helper.GetJSONRPCAddress(cmd) - - return params.validateFlags() -} - -func runCommand(cmd *cobra.Command, _ []string) error { - secretsManager, err := secretsHelper.SetupLocalSecretsManager(params.registratorValidatorDataDir) - if err != nil { - return err - } - - existingValidatorAccount, err := wallet.NewAccountFromSecret(secretsManager) - if err != nil { - return err - } - - existingValidatorSender, err := newTxnSender(existingValidatorAccount) - if err != nil { - return err - } - - secretsManager, err = secretsHelper.SetupLocalSecretsManager(params.newValidatorDataDir) - if err != nil { - return err - } - - newValidatorAccount, err := wallet.NewAccountFromSecret(secretsManager) - if err != nil { - return err - } - - newValidatorSender, err := newTxnSender(newValidatorAccount) - if err != nil { - return err - } - - sRaw, err := secretsManager.GetSecret(secrets.ValidatorBLSSignature) - if err != nil { - return err - } - - sb, err := hex.DecodeString(string(sRaw)) - if err != nil { - return err - } - - blsSignature, err := bls.UnmarshalSignature(sb) - if err != nil { - return err - } - - var validator *NewValidator - - steps := []*txnStep{ - { - name: "whitelist", - action: func() asyncTxn { - return whitelist(existingValidatorSender, types.Address(newValidatorAccount.Ecdsa.Address())) - }, - }, - { - name: "fund", - action: func() asyncTxn { - return fund(existingValidatorSender, types.Address(newValidatorAccount.Ecdsa.Address())) - }, - }, - { - name: "register", - action: func() asyncTxn { - return registerValidator(newValidatorSender, newValidatorAccount, blsSignature) - }, - postHook: func(receipt *ethgo.Receipt) error { - if receipt.Status != uint64(types.ReceiptSuccess) { - return errors.New("register validator transaction failed") - } - - for _, log := range receipt.Logs { - if newValidatorEvent.Match(log) { - event, err := newValidatorEvent.ParseLog(log) - if err != nil { - return err - } - - validatorAddr, ok := event["validator"].(ethgo.Address) - if !ok { - return errors.New("type assertions failed for parameter validator") - } - - validator = &NewValidator{ - Validator: validatorAddr, - } - - return nil - } - } - - return errors.New("NewValidator event was not emitted") - }, - }, - { - name: "stake", - action: func() asyncTxn { - return stake(newValidatorSender) - }, - }, - } - - d := glint.New() - go d.Render(context.Background()) - - printStatus := func(done bool) { - comps := []glint.Component{} - - for _, step := range steps { - var status glint.Component - - var opts []glint.StyleOption - - switch step.status { - case txnStepQueued: - status = glint.Text("-") - - case txnStepPending: - status = gc.Spinner() - - opts = append(opts, glint.Color("yellow")) - - case txnStepCompleted: - status = glint.Text("✓") - opts = append(opts, glint.Color("green")) - - case txnStepFailed: - status = glint.Text("✗") - opts = append(opts, glint.Color("red")) - } - - comps = append(comps, glint.Style( - glint.Layout( - status, - glint.Layout(glint.Text(step.name+"...")).MarginLeft(1), - ).Row(), - opts..., - )) - if step.err != nil { - comps = append(comps, glint.Style( - glint.Layout( - status, - glint.Layout(glint.Text("error: "+step.err.Error())).MarginLeft(5), - ).Row(), - opts..., - )) - } - } - - if done { - if validator != nil { - comps = append(comps, glint.Text("\nDone: "+validator.Validator.String()+"\n")) - } else { - comps = append(comps, glint.Text("\nDone\n")) - } - } else { - comps = append(comps, glint.Text("\nWaiting...")) - } - - d.Set(comps...) - } - - for _, step := range steps { - step.status = txnStepPending - - printStatus(false) - - txn := step.action() - receipt, err := txn.Wait() - - if err != nil { - step.status = txnStepFailed - step.err = err - } else { - if receipt.Status == uint64(types.ReceiptFailed) { - step.status = txnStepFailed - } else { - step.status = txnStepCompleted - } - } - - if step.postHook != nil { - err := step.postHook(receipt) - if err != nil { - step.status = txnStepFailed - step.err = err - } - } - - if step.status == txnStepFailed { - break - } - } - - printStatus(true) - - d.RenderFrame() - d.Pause() - - return nil -} - -const ( - defaultGasPrice = 1879048192 // 0x70000000 - defaultGasLimit = 5242880 // 0x500000 -) - -var ( - stakeManager = contracts.ValidatorSetContract - stakeFn = contractsapi.ChildValidatorSet.Abi.Methods["stake"] - whitelistFn = contractsapi.ChildValidatorSet.Abi.Methods["addToWhitelist"] - registerFn = contractsapi.ChildValidatorSet.Abi.Methods["register"] - newValidatorEvent = contractsapi.ChildValidatorSet.Abi.Events["NewValidator"] -) - -type asyncTxn interface { - Wait() (*ethgo.Receipt, error) -} - -type asyncTxnImpl struct { - t *txnSender - hash ethgo.Hash - err error -} - -func (a *asyncTxnImpl) Wait() (*ethgo.Receipt, error) { - // propagate error if there were any - if a.err != nil { - return nil, a.err - } - - return a.t.waitForReceipt(a.hash) -} - -type txnStepStatus int - -const ( - txnStepQueued txnStepStatus = iota - txnStepPending - txnStepCompleted - txnStepFailed -) - -type txnStep struct { - name string - action func() asyncTxn - postHook func(receipt *ethgo.Receipt) error - status txnStepStatus - err error -} - -type txnSender struct { - client *jsonrpc.Client - account *wallet.Account -} - -func (t *txnSender) sendTransaction(txn *types.Transaction) asyncTxn { - if txn.GasPrice == nil { - txn.GasPrice = big.NewInt(defaultGasPrice) - } - - if txn.Gas == 0 { - txn.Gas = defaultGasLimit - } - - if txn.Nonce == 0 { - nonce, err := t.client.Eth().GetNonce(t.account.Ecdsa.Address(), ethgo.Latest) - if err != nil { - return &asyncTxnImpl{err: err} - } - - txn.Nonce = nonce - } - - chainID, err := t.client.Eth().ChainID() - if err != nil { - return &asyncTxnImpl{err: err} - } - - privateKey, err := t.account.GetEcdsaPrivateKey() - if err != nil { - return &asyncTxnImpl{err: err} - } - - signer := crypto.NewEIP155Signer( - chain.AllForksEnabled.At(0), - chainID.Uint64(), - ) - - signedTxn, err := signer.SignTx(txn, privateKey) - - if err != nil { - return &asyncTxnImpl{err: err} - } - - txnRaw := signedTxn.MarshalRLP() - hash, err := t.client.Eth().SendRawTransaction(txnRaw) - - if err != nil { - return &asyncTxnImpl{err: err} - } - - return &asyncTxnImpl{hash: hash, t: t} -} - -func (t *txnSender) waitForReceipt(hash ethgo.Hash) (*ethgo.Receipt, error) { - var count uint64 - - for { - receipt, err := t.client.Eth().GetTransactionReceipt(hash) - if err != nil { - if err.Error() != "not found" { - return nil, err - } - } - - if receipt != nil { - return receipt, nil - } - - if count > 1200 { - break - } - - time.Sleep(1000 * time.Millisecond) - count++ - } - - return nil, fmt.Errorf("timeout") -} - -func newDemoClient() (*jsonrpc.Client, error) { - client, err := jsonrpc.NewClient(params.jsonRPCAddr) - if err != nil { - return nil, fmt.Errorf("cannot connect with jsonrpc: %w", err) - } - - return client, err -} - -func newTxnSender(sender *wallet.Account) (*txnSender, error) { - client, err := newDemoClient() - if err != nil { - return nil, err - } - - return &txnSender{ - account: sender, - client: client, - }, nil -} - -func stake(sender *txnSender) asyncTxn { - if stakeFn == nil { - return &asyncTxnImpl{err: errors.New("failed to create stake ABI function")} - } - - input, err := stakeFn.Encode([]interface{}{}) - if err != nil { - return &asyncTxnImpl{err: err} - } - - stake, err := types.ParseUint256orHex(¶ms.stake) - if err != nil { - return &asyncTxnImpl{err: err} - } - - receipt := sender.sendTransaction(&types.Transaction{ - To: &stakeManager, - Input: input, - Value: stake, - }) - - return receipt -} - -func whitelist(sender *txnSender, addr types.Address) asyncTxn { - if whitelistFn == nil { - return &asyncTxnImpl{err: errors.New("failed to create whitelist ABI function")} - } - - input, err := whitelistFn.Encode([]interface{}{ - []types.Address{addr}, - }) - if err != nil { - return &asyncTxnImpl{err: err} - } - - receipt := sender.sendTransaction(&types.Transaction{ - To: &stakeManager, - Input: input, - }) - - return receipt -} - -func fund(sender *txnSender, addr types.Address) asyncTxn { - balance, err := types.ParseUint256orHex(¶ms.balance) - if err != nil { - return &asyncTxnImpl{err: err} - } - - txn := &types.Transaction{ - To: &addr, - Value: balance, - } - - return sender.sendTransaction(txn) -} - -func registerValidator(sender *txnSender, account *wallet.Account, signature *bls.Signature) asyncTxn { - if registerFn == nil { - return &asyncTxnImpl{err: errors.New("failed to create register ABI function")} - } - - sigMarshal, err := signature.ToBigInt() - if err != nil { - return &asyncTxnImpl{err: err} - } - - input, err := registerFn.Encode([]interface{}{ - sigMarshal, - account.Bls.PublicKey().ToBigInt(), - }) - if err != nil { - return &asyncTxnImpl{err: err} - } - - return sender.sendTransaction(&types.Transaction{ - To: &stakeManager, - Input: input, - }) -} - -// NewValidator represents validator which is being registered to the chain -type NewValidator struct { - Validator ethgo.Address -} diff --git a/command/polybft/polybft_command.go b/command/polybft/polybft_command.go index 328c6641a8..22b0c2f6fa 100644 --- a/command/polybft/polybft_command.go +++ b/command/polybft/polybft_command.go @@ -1,10 +1,12 @@ package polybft import ( - "github.com/0xPolygon/polygon-edge/command/e2e" + "github.com/0xPolygon/polygon-edge/command/sidechain/registration" "github.com/0xPolygon/polygon-edge/command/sidechain/staking" "github.com/0xPolygon/polygon-edge/command/sidechain/unstaking" "github.com/0xPolygon/polygon-edge/command/sidechain/validators" + + "github.com/0xPolygon/polygon-edge/command/sidechain/whitelist" "github.com/0xPolygon/polygon-edge/command/sidechain/withdraw" "github.com/spf13/cobra" ) @@ -20,7 +22,8 @@ func GetCommand() *cobra.Command { unstaking.GetCommand(), withdraw.GetCommand(), validators.GetCommand(), - e2e.GetCommand(), + whitelist.GetCommand(), + registration.GetCommand(), ) return polybftCmd diff --git a/command/sidechain/registration/params.go b/command/sidechain/registration/params.go new file mode 100644 index 0000000000..9c3f51815c --- /dev/null +++ b/command/sidechain/registration/params.go @@ -0,0 +1,62 @@ +package registration + +import ( + "bytes" + "fmt" + + "github.com/0xPolygon/polygon-edge/command/helper" + sidechainHelper "github.com/0xPolygon/polygon-edge/command/sidechain" + "github.com/0xPolygon/polygon-edge/types" +) + +const ( + stakeFlag = "stake" + chainIDFlag = "chain-id" +) + +type registerParams struct { + accountDir string + configPath string + jsonRPC string + stake string + chainID int64 +} + +func (rp *registerParams) validateFlags() error { + if err := sidechainHelper.ValidateSecretFlags(rp.accountDir, rp.configPath); err != nil { + return err + } + + if rp.stake != "" { + _, err := types.ParseUint256orHex(&rp.stake) + if err != nil { + return fmt.Errorf("provided stake '%s' isn't valid", rp.stake) + } + } + + return nil +} + +type registerResult struct { + validatorAddress string + stakeResult string + amount uint64 +} + +func (rr registerResult) GetOutput() string { + var buffer bytes.Buffer + + var vals []string + + buffer.WriteString("\n[REGISTRATION]\n") + + vals = make([]string, 0, 3) + vals = append(vals, fmt.Sprintf("Validator Address|%s", rr.validatorAddress)) + vals = append(vals, fmt.Sprintf("Staking Result|%s", rr.stakeResult)) + vals = append(vals, fmt.Sprintf("Amount Staked|%v", rr.amount)) + + buffer.WriteString(helper.FormatKV(vals)) + buffer.WriteString("\n") + + return buffer.String() +} diff --git a/command/sidechain/registration/register_validator.go b/command/sidechain/registration/register_validator.go new file mode 100644 index 0000000000..8e630c1b50 --- /dev/null +++ b/command/sidechain/registration/register_validator.go @@ -0,0 +1,238 @@ +package registration + +import ( + "encoding/hex" + "errors" + "fmt" + "math/big" + + "github.com/0xPolygon/polygon-edge/command" + "github.com/0xPolygon/polygon-edge/command/helper" + "github.com/0xPolygon/polygon-edge/command/polybftsecrets" + "github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi" + bls "github.com/0xPolygon/polygon-edge/consensus/polybft/signer" + "github.com/0xPolygon/polygon-edge/consensus/polybft/wallet" + "github.com/0xPolygon/polygon-edge/contracts" + "github.com/0xPolygon/polygon-edge/secrets" + "github.com/0xPolygon/polygon-edge/txrelayer" + "github.com/0xPolygon/polygon-edge/types" + "github.com/spf13/cobra" + "github.com/umbracle/ethgo" +) + +var ( + stakeManager = contracts.ValidatorSetContract + stakeFn = contractsapi.ChildValidatorSet.Abi.Methods["stake"] + newValidatorEventABI = contractsapi.ChildValidatorSet.Abi.Events["NewValidator"] + stakeEventABI = contractsapi.ChildValidatorSet.Abi.Events["Staked"] +) + +var params registerParams + +func GetCommand() *cobra.Command { + registerCmd := &cobra.Command{ + Use: "register-validator", + Short: "Registers and stake an enlisted validator", + PreRunE: runPreRun, + RunE: runCommand, + } + + setFlags(registerCmd) + + return registerCmd +} + +func setFlags(cmd *cobra.Command) { + cmd.Flags().StringVar( + ¶ms.accountDir, + polybftsecrets.DataPathFlag, + "", + polybftsecrets.DataPathFlagDesc, + ) + + cmd.Flags().StringVar( + ¶ms.configPath, + polybftsecrets.ConfigFlag, + "", + polybftsecrets.ConfigFlagDesc, + ) + + cmd.Flags().StringVar( + ¶ms.stake, + stakeFlag, + "", + "stake represents amount which is going to be staked by the new validator account", + ) + + cmd.Flags().Int64Var( + ¶ms.chainID, + chainIDFlag, + command.DefaultChainID, + "the ID of the chain", + ) + + helper.RegisterJSONRPCFlag(cmd) + cmd.MarkFlagsMutuallyExclusive(polybftsecrets.ConfigFlag, polybftsecrets.DataPathFlag) +} + +func runPreRun(cmd *cobra.Command, _ []string) error { + params.jsonRPC = helper.GetJSONRPCAddress(cmd) + + return params.validateFlags() +} + +func runCommand(cmd *cobra.Command, _ []string) error { + outputter := command.InitializeOutputter(cmd) + defer outputter.WriteOutput() + + secretsManager, err := polybftsecrets.GetSecretsManager(params.accountDir, params.configPath, true) + if err != nil { + return err + } + + txRelayer, err := txrelayer.NewTxRelayer(txrelayer.WithIPAddress(params.jsonRPC)) + if err != nil { + return err + } + + newValidatorAccount, err := wallet.NewAccountFromSecret(secretsManager) + if err != nil { + return err + } + + sRaw, err := secretsManager.GetSecret(secrets.ValidatorBLSSignature) + if err != nil { + return err + } + + sb, err := hex.DecodeString(string(sRaw)) + if err != nil { + return err + } + + blsSignature, err := bls.UnmarshalSignature(sb) + if err != nil { + return err + } + + receipt, err := registerValidator(txRelayer, newValidatorAccount, blsSignature) + if err != nil { + return err + } + + if receipt.Status != uint64(types.ReceiptSuccess) { + return errors.New("register validator transaction failed") + } + + result := ®isterResult{} + foundLog := false + + for _, log := range receipt.Logs { + if newValidatorEventABI.Match(log) { + event, err := newValidatorEventABI.ParseLog(log) + if err != nil { + return err + } + + result.validatorAddress = event["validator"].(ethgo.Address).String() //nolint:forcetypeassert + result.stakeResult = "No stake parameters have been submitted" + result.amount = 0 + foundLog = true + + break + } + } + + if !foundLog { + return fmt.Errorf("could not find an appropriate log in receipt that registration happened") + } + + if params.stake != "" { + receipt, err = stake(txRelayer, newValidatorAccount) + if err != nil { + result.stakeResult = fmt.Sprintf("Failed to execute stake transaction: %s", err.Error()) + } else { + populateStakeResults(receipt, result) + } + } + + outputter.WriteCommandResult(result) + + return nil +} + +func stake(sender txrelayer.TxRelayer, account *wallet.Account) (*ethgo.Receipt, error) { + if stakeFn == nil { + return nil, errors.New("failed to create stake ABI function") + } + + input, err := stakeFn.Encode([]interface{}{}) + if err != nil { + return nil, err + } + + stake, err := types.ParseUint256orHex(¶ms.stake) + if err != nil { + return nil, err + } + + txn := ðgo.Transaction{ + Input: input, + To: (*ethgo.Address)(&stakeManager), + Value: stake, + } + + return sender.SendTransaction(txn, account.Ecdsa) +} + +func populateStakeResults(receipt *ethgo.Receipt, result *registerResult) { + if receipt.Status != uint64(types.ReceiptSuccess) { + result.stakeResult = "Stake transaction failed" + + return + } + + // check the logs to verify stake + for _, log := range receipt.Logs { + if stakeEventABI.Match(log) { + event, err := stakeEventABI.ParseLog(log) + if err != nil { + result.stakeResult = "Failed to parse stake log" + + return + } + + result.amount = event["amount"].(*big.Int).Uint64() //nolint:forcetypeassert + result.stakeResult = "Stake succeeded" + + return + } + } + + result.stakeResult = "Could not find an appropriate log in receipt that stake happened" +} + +func registerValidator(sender txrelayer.TxRelayer, account *wallet.Account, + signature *bls.Signature) (*ethgo.Receipt, error) { + sigMarshal, err := signature.ToBigInt() + if err != nil { + return nil, fmt.Errorf("register validator failed: %w", err) + } + + registerFn := &contractsapi.RegisterFunction{ + Signature: sigMarshal, + Pubkey: account.Bls.PublicKey().ToBigInt(), + } + + input, err := registerFn.EncodeAbi() + if err != nil { + return nil, fmt.Errorf("register validator failed: %w", err) + } + + txn := ðgo.Transaction{ + Input: input, + To: (*ethgo.Address)(&stakeManager), + } + + return sender.SendTransaction(txn, account.Ecdsa) +} diff --git a/command/sidechain/staking/stake.go b/command/sidechain/staking/stake.go index f570f3dac7..b43e9d99a5 100644 --- a/command/sidechain/staking/stake.go +++ b/command/sidechain/staking/stake.go @@ -134,7 +134,7 @@ func runCommand(cmd *cobra.Command, _ []string) error { validatorAddress: validatorAccount.Ecdsa.Address().String(), } - var foundLog bool + foundLog := false // check the logs to check for the result for _, log := range receipt.Logs { diff --git a/command/sidechain/unstaking/unstake.go b/command/sidechain/unstaking/unstake.go index 9e08aac451..66c85741ca 100644 --- a/command/sidechain/unstaking/unstake.go +++ b/command/sidechain/unstaking/unstake.go @@ -132,7 +132,7 @@ func runCommand(cmd *cobra.Command, _ []string) error { validatorAddress: validatorAccount.Ecdsa.Address().String(), } - var foundLog bool + foundLog := false // check the logs to check for the result for _, log := range receipt.Logs { diff --git a/command/sidechain/whitelist/params.go b/command/sidechain/whitelist/params.go new file mode 100644 index 0000000000..c57cf04b58 --- /dev/null +++ b/command/sidechain/whitelist/params.go @@ -0,0 +1,43 @@ +package whitelist + +import ( + "bytes" + "fmt" + + "github.com/0xPolygon/polygon-edge/command/helper" + sidechainHelper "github.com/0xPolygon/polygon-edge/command/sidechain" +) + +var ( + newValidatorAddressFlag = "address" +) + +type whitelistParams struct { + accountDir string + configPath string + jsonRPC string + newValidatorAddress string +} + +func (ep *whitelistParams) validateFlags() error { + return sidechainHelper.ValidateSecretFlags(ep.accountDir, ep.configPath) +} + +type enlistResult struct { + newValidatorAddress string +} + +func (er enlistResult) GetOutput() string { + var buffer bytes.Buffer + + var vals []string + + buffer.WriteString("\n[ENLIST VALIDATOR]\n") + + vals = append(vals, fmt.Sprintf("Validator Address|%s", er.newValidatorAddress)) + + buffer.WriteString(helper.FormatKV(vals)) + buffer.WriteString("\n") + + return buffer.String() +} diff --git a/command/sidechain/whitelist/whitelist_validator.go b/command/sidechain/whitelist/whitelist_validator.go new file mode 100644 index 0000000000..420c04d82d --- /dev/null +++ b/command/sidechain/whitelist/whitelist_validator.go @@ -0,0 +1,130 @@ +package whitelist + +import ( + "fmt" + "time" + + "github.com/0xPolygon/polygon-edge/command" + "github.com/0xPolygon/polygon-edge/command/helper" + "github.com/0xPolygon/polygon-edge/command/polybftsecrets" + sidechainHelper "github.com/0xPolygon/polygon-edge/command/sidechain" + "github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi" + "github.com/0xPolygon/polygon-edge/contracts" + "github.com/0xPolygon/polygon-edge/txrelayer" + "github.com/0xPolygon/polygon-edge/types" + "github.com/spf13/cobra" + "github.com/umbracle/ethgo" +) + +var ( + whitelistFn = contractsapi.ChildValidatorSet.Abi.Methods["addToWhitelist"] + whitelistEventABI = contractsapi.ChildValidatorSet.Abi.Events["AddedToWhitelist"] +) + +var params whitelistParams + +func GetCommand() *cobra.Command { + registerCmd := &cobra.Command{ + Use: "whitelist-validator", + Short: "whitelist a new validator", + PreRunE: runPreRun, + RunE: runCommand, + } + + setFlags(registerCmd) + + return registerCmd +} + +func setFlags(cmd *cobra.Command) { + cmd.Flags().StringVar( + ¶ms.accountDir, + polybftsecrets.DataPathFlag, + "", + polybftsecrets.DataPathFlagDesc, + ) + + cmd.Flags().StringVar( + ¶ms.configPath, + polybftsecrets.ConfigFlag, + "", + polybftsecrets.ConfigFlagDesc, + ) + + cmd.Flags().StringVar( + ¶ms.newValidatorAddress, + newValidatorAddressFlag, + "", + "account address of a possible validator", + ) + + cmd.MarkFlagsMutuallyExclusive(polybftsecrets.DataPathFlag, polybftsecrets.ConfigFlag) + helper.RegisterJSONRPCFlag(cmd) +} + +func runPreRun(cmd *cobra.Command, _ []string) error { + params.jsonRPC = helper.GetJSONRPCAddress(cmd) + + return params.validateFlags() +} + +func runCommand(cmd *cobra.Command, _ []string) error { + outputter := command.InitializeOutputter(cmd) + defer outputter.WriteOutput() + + ownerAccount, err := sidechainHelper.GetAccount(params.accountDir, params.configPath) + if err != nil { + return fmt.Errorf("enlist validator failed: %w", err) + } + + txRelayer, err := txrelayer.NewTxRelayer(txrelayer.WithIPAddress(params.jsonRPC), + txrelayer.WithReceiptTimeout(150*time.Millisecond)) + if err != nil { + return fmt.Errorf("enlist validator failed: %w", err) + } + + encoded, err := whitelistFn.Encode([]interface{}{ + []types.Address{types.StringToAddress(params.newValidatorAddress)}, + }) + + txn := ðgo.Transaction{ + From: ownerAccount.Ecdsa.Address(), + Input: encoded, + To: (*ethgo.Address)(&contracts.ValidatorSetContract), + GasPrice: sidechainHelper.DefaultGasPrice, + } + + receipt, err := txRelayer.SendTransaction(txn, ownerAccount.Ecdsa) + if err != nil { + return fmt.Errorf("enlist validator failed %w", err) + } + + if receipt.Status == uint64(types.ReceiptFailed) { + return fmt.Errorf("enlist validator transaction failed on block %d", receipt.BlockNumber) + } + + result := &enlistResult{} + foundLog := false + + for _, log := range receipt.Logs { + if whitelistEventABI.Match(log) { + event, err := whitelistEventABI.ParseLog(log) + if err != nil { + return err + } + + result.newValidatorAddress = event["validator"].(ethgo.Address).String() //nolint:forcetypeassert + foundLog = true + + break + } + } + + if !foundLog { + return fmt.Errorf("could not find an appropriate log in receipt that enlistment happened") + } + + outputter.WriteCommandResult(result) + + return nil +} diff --git a/command/sidechain/withdraw/withdraw.go b/command/sidechain/withdraw/withdraw.go index 98d89993c2..82cd13d7be 100644 --- a/command/sidechain/withdraw/withdraw.go +++ b/command/sidechain/withdraw/withdraw.go @@ -108,7 +108,7 @@ func runCommand(cmd *cobra.Command, _ []string) error { validatorAddress: validatorAccount.Ecdsa.Address().String(), } - var foundLog bool + foundLog := false for _, log := range receipt.Logs { if withdrawEventABI.Match(log) { diff --git a/consensus/polybft/contractsapi/bindings-gen/main.go b/consensus/polybft/contractsapi/bindings-gen/main.go index ab84bffcca..cebbce3d94 100644 --- a/consensus/polybft/contractsapi/bindings-gen/main.go +++ b/consensus/polybft/contractsapi/bindings-gen/main.go @@ -50,6 +50,8 @@ func main() { []string{ "commitEpoch", "initialize", + "addToWhitelist", + "register", }, []string{}, }, diff --git a/consensus/polybft/contractsapi/contractsapi.go b/consensus/polybft/contractsapi/contractsapi.go index d3e53c9431..ad3216eb9c 100644 --- a/consensus/polybft/contractsapi/contractsapi.go +++ b/consensus/polybft/contractsapi/contractsapi.go @@ -199,6 +199,31 @@ func (i *InitializeChildValidatorSetFunction) DecodeAbi(buf []byte) error { return decodeMethod(ChildValidatorSet.Abi.Methods["initialize"], buf, i) } +type AddToWhitelistFunction struct { + WhitelistAddreses []ethgo.Address `abi:"whitelistAddreses"` +} + +func (a *AddToWhitelistFunction) EncodeAbi() ([]byte, error) { + return ChildValidatorSet.Abi.Methods["addToWhitelist"].Encode(a) +} + +func (a *AddToWhitelistFunction) DecodeAbi(buf []byte) error { + return decodeMethod(ChildValidatorSet.Abi.Methods["addToWhitelist"], buf, a) +} + +type RegisterFunction struct { + Signature [2]*big.Int `abi:"signature"` + Pubkey [4]*big.Int `abi:"pubkey"` +} + +func (r *RegisterFunction) EncodeAbi() ([]byte, error) { + return ChildValidatorSet.Abi.Methods["register"].Encode(r) +} + +func (r *RegisterFunction) DecodeAbi(buf []byte) error { + return decodeMethod(ChildValidatorSet.Abi.Methods["register"], buf, r) +} + type SyncStateFunction struct { Receiver types.Address `abi:"receiver"` Data []byte `abi:"data"` diff --git a/e2e-polybft/consensus_test.go b/e2e-polybft/consensus_test.go index 2d7291383d..58f8583bf4 100644 --- a/e2e-polybft/consensus_test.go +++ b/e2e-polybft/consensus_test.go @@ -1,6 +1,7 @@ package e2e import ( + "fmt" "math/big" "path" "testing" @@ -100,24 +101,30 @@ func TestE2E_Consensus_Bulk_Drop(t *testing.T) { func TestE2E_Consensus_RegisterValidator(t *testing.T) { const ( - validatorSize = 5 - epochSize = 5 - newValidatorSecrets = "test-chain-6" - premineBalance = "0x1A784379D99DB42000000" // 2M native tokens (so that we have enough balance to fund new validator) + validatorSize = 5 + epochSize = 5 + epochReward = 1000000000 + premineBalance = "0x1A784379D99DB42000000" // 2M native tokens (so that we have enough balance to fund new validator) ) - newValidatorStakeRaw := "0x152D02C7E14AF6800000" // 100k native tokens - newValidatorBalanceRaw := "0xD3C21BCECCEDA1000000" // 1M native tokens + // new validator data + firstValidatorDataDir := fmt.Sprintf("test-chain-%d", validatorSize+1) // directory where the first validator secrets will be stored + secondValidatorDataDir := fmt.Sprintf("test-chain-%d", validatorSize+2) // directory where the second validator secrets will be stored + newValidatorInitBalance := "500000000000000000000000" // 500k - balance which will be transferred to the new validator + newValidatorStakeRaw := "0x8AC7230489E80000" // 10 native tokens - amout which will be staked by the new validator newValidatorStake, err := types.ParseUint256orHex(&newValidatorStakeRaw) require.NoError(t, err) + // start cluster with 'validatorSize' validators cluster := framework.NewTestCluster(t, validatorSize, framework.WithEpochSize(epochSize), - framework.WithEpochReward(1000), + framework.WithEpochReward(epochReward), framework.WithPremineValidators(premineBalance)) defer cluster.Stop() - srv := cluster.Servers[0] - txRelayer, err := txrelayer.NewTxRelayer(txrelayer.WithIPAddress(srv.JSONRPCAddr())) + + // first validator is the owner of ChildValidator set smart contract + owner := cluster.Servers[0] + txRelayer, err := txrelayer.NewTxRelayer(txrelayer.WithIPAddress(owner.JSONRPCAddr())) require.NoError(t, err) systemState := polybft.NewSystemState( @@ -126,41 +133,98 @@ func TestE2E_Consensus_RegisterValidator(t *testing.T) { ValidatorSetAddr: contracts.ValidatorSetContract}, &e2eStateProvider{txRelayer: txRelayer}) - // create new account - addrs, err := cluster.InitSecrets(newValidatorSecrets, 1) + // create the first account and extract the address + addrs, err := cluster.InitSecrets(firstValidatorDataDir, 1) require.NoError(t, err) - // extract new validator address - newValidatorAddr := ethgo.Address(addrs[0]) + firstValidatorAddr := ethgo.Address(addrs[0]) + + // create the second account and extract the address + addrs, err = cluster.InitSecrets(secondValidatorDataDir, 1) + require.NoError(t, err) - // assert that account is created + secondValidatorAddr := ethgo.Address(addrs[0]) + + // assert that accounts are created validatorSecrets, err := genesis.GetValidatorKeyFiles(cluster.Config.TmpDir, cluster.Config.ValidatorPrefix) require.NoError(t, err) - require.Equal(t, validatorSize+1, len(validatorSecrets)) + require.Equal(t, validatorSize+2, len(validatorSecrets)) + + // collect owners validator secrets + ownerSecrets := validatorSecrets[0] // wait for consensus to start require.NoError(t, cluster.WaitForBlock(1, 10*time.Second)) - // register new validator - require.NoError(t, srv.RegisterValidator(newValidatorSecrets, newValidatorBalanceRaw, newValidatorStakeRaw)) + // owner enlists both new validators + require.NoError(t, owner.WhitelistValidator(firstValidatorAddr.String(), ownerSecrets)) + require.NoError(t, owner.WhitelistValidator(secondValidatorAddr.String(), ownerSecrets)) - go func() { - // start new validator - cluster.InitTestServer(t, 6, true, false) - }() + // start the first and the second validator + cluster.InitTestServer(t, validatorSize+1, true, false) + cluster.InitTestServer(t, validatorSize+2, true, false) + + ownerAcc, err := sidechain.GetAccountFromDir(path.Join(cluster.Config.TmpDir, ownerSecrets)) + require.NoError(t, err) + + // get the initial balance of the new validator + initialBalance, ok := new(big.Int).SetString(newValidatorInitBalance, 10) + require.True(t, ok) + + // send some tokens from the owner to the first validator so that the first validator can register and stake + receipt1, err := txRelayer.SendTransaction(ðgo.Transaction{ + From: ownerAcc.Ecdsa.Address(), + To: &firstValidatorAddr, + Value: initialBalance, + }, ownerAcc.Ecdsa) + require.NoError(t, err) + require.Equal(t, uint64(types.ReceiptSuccess), receipt1.Status) + + // send some tokens from the owner to the second validator so that the second validator can register and stake + receipt2, err := txRelayer.SendTransaction(ðgo.Transaction{ + From: ownerAcc.Ecdsa.Address(), + To: &secondValidatorAddr, + Value: initialBalance, + }, ownerAcc.Ecdsa) + require.NoError(t, err) + require.Equal(t, uint64(types.ReceiptSuccess), receipt2.Status) + + // collect the first and the second validator from the cluster + firstValidator := cluster.Servers[validatorSize] + secondValidator := cluster.Servers[validatorSize+1] + + // wait for the first validator's balance to be received + firstBalance, err := firstValidator.WaitForNonZeroBalance(firstValidatorAddr, 5*time.Second) + require.NoError(t, err) + t.Logf("First validator balance=%d\n", firstBalance) + + // wait for the first validator's balance to be received + secondBalance, err := secondValidator.WaitForNonZeroBalance(secondValidatorAddr, 5*time.Second) + require.NoError(t, err) + t.Logf("Second validator balance=%d\n", secondBalance) + + // register the first validator with stake + require.NoError(t, firstValidator.RegisterValidator(firstValidatorDataDir, newValidatorStakeRaw)) + + // register the second validator without stake + require.NoError(t, secondValidator.RegisterValidator(secondValidatorDataDir, "")) + + // stake manually for the second validator + require.NoError(t, secondValidator.Stake(newValidatorStake.Uint64())) validators := polybft.AccountSet{} + // assert that new validator is among validator set require.NoError(t, cluster.WaitUntil(20*time.Second, func() bool { // query validators validators, err = systemState.GetValidatorSet() require.NoError(t, err) - return validators.ContainsAddress((types.Address(newValidatorAddr))) + return validators.ContainsAddress((types.Address(firstValidatorAddr))) && validators.ContainsAddress((types.Address(secondValidatorAddr))) })) // assert that validators hash is correct - block, err := srv.JSONRPC().Eth().GetBlockByNumber(ethgo.Latest, false) + block, err := owner.JSONRPC().Eth().GetBlockByNumber(ethgo.Latest, false) require.NoError(t, err) t.Logf("Block Number=%d\n", block.Number) @@ -174,28 +238,54 @@ func TestE2E_Consensus_RegisterValidator(t *testing.T) { require.NoError(t, err) require.Equal(t, extra.Checkpoint.NextValidatorsHash, validatorsHash) - // query registered validator - newValidatorInfo, err := sidechain.GetValidatorInfo(newValidatorAddr, txRelayer) + // query the first validator + firstValidatorInfo, err := sidechain.GetValidatorInfo(firstValidatorAddr, txRelayer) + require.NoError(t, err) + + // assert the first validator's stake + t.Logf("First validator stake=%d\n", firstValidatorInfo.TotalStake) + require.Equal(t, newValidatorStake, firstValidatorInfo.TotalStake) + + // query the second validatorr + secondValidatorInfo, err := sidechain.GetValidatorInfo(secondValidatorAddr, txRelayer) require.NoError(t, err) - // assert registered validator's stake - t.Logf("New validator stake=%d\n", newValidatorInfo.TotalStake) - require.Equal(t, newValidatorStake, newValidatorInfo.TotalStake) + // assert the second validator's stake + t.Logf("Second validator stake=%d\n", secondValidatorInfo.TotalStake) + require.Equal(t, newValidatorStake, secondValidatorInfo.TotalStake) // wait 3 more epochs, so that rewards get accumulated to the registered validator account - currentBlock, err := srv.JSONRPC().Eth().BlockNumber() + currentBlock, err := owner.JSONRPC().Eth().BlockNumber() require.NoError(t, err) cluster.WaitForBlock(currentBlock+3*epochSize, 2*time.Minute) - // query registered validator - newValidatorInfo, err = sidechain.GetValidatorInfo(newValidatorAddr, txRelayer) + // query the first validator info again + firstValidatorInfo, err = sidechain.GetValidatorInfo(firstValidatorAddr, txRelayer) + require.NoError(t, err) + + // check if the first validator has signed any prposals + firstSealed, err := firstValidator.HasValidatorSealed(currentBlock, currentBlock+3*epochSize, validators, firstValidatorAddr) require.NoError(t, err) - // assert registered validator's rewards - currentBlock, err = srv.JSONRPC().Eth().BlockNumber() + if firstSealed { + // assert registered validator's rewards) + t.Logf("First validator rewards (block %d)=%d\n", currentBlock, firstValidatorInfo.WithdrawableRewards) + require.True(t, firstValidatorInfo.WithdrawableRewards.Cmp(big.NewInt(0)) > 0) + } + + // query the second validator info again + secondValidatorInfo, err = sidechain.GetValidatorInfo(secondValidatorAddr, txRelayer) require.NoError(t, err) - t.Logf("New validator rewards (block %d)=%d\n", currentBlock, newValidatorInfo.WithdrawableRewards) - require.True(t, newValidatorInfo.WithdrawableRewards.Cmp(big.NewInt(0)) > 0) + + // check if the second validator has signed any prposals + secondSealed, err := secondValidator.HasValidatorSealed(currentBlock, currentBlock+3*epochSize, validators, secondValidatorAddr) + require.NoError(t, err) + + if secondSealed { + // assert registered validator's rewards + t.Logf("Second validator rewards (block %d)=%d\n", currentBlock, secondValidatorInfo.WithdrawableRewards) + require.True(t, secondValidatorInfo.WithdrawableRewards.Cmp(big.NewInt(0)) > 0) + } } func TestE2E_Consensus_Delegation_Undelegation(t *testing.T) { diff --git a/e2e-polybft/framework/test-bridge.go b/e2e-polybft/framework/test-bridge.go index f8f57d3561..4659f67762 100644 --- a/e2e-polybft/framework/test-bridge.go +++ b/e2e-polybft/framework/test-bridge.go @@ -40,6 +40,7 @@ func (t *TestBridge) Start() error { "rootchain", "server", "--data-dir", t.clusterConfig.Dir("test-rootchain"), + "--no-console", } stdout := t.clusterConfig.GetStdout("bridge") diff --git a/e2e-polybft/framework/test-server.go b/e2e-polybft/framework/test-server.go index 8f87eece83..97d575aea5 100644 --- a/e2e-polybft/framework/test-server.go +++ b/e2e-polybft/framework/test-server.go @@ -3,13 +3,17 @@ package framework import ( "fmt" "io/ioutil" + "math/big" "path" "strconv" "sync/atomic" "testing" + "time" "github.com/0xPolygon/polygon-edge/command/polybftsecrets" + "github.com/0xPolygon/polygon-edge/consensus/polybft" "github.com/0xPolygon/polygon-edge/server/proto" + "github.com/0xPolygon/polygon-edge/types" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/umbracle/ethgo" @@ -193,20 +197,35 @@ func (t *TestServer) Unstake(amount uint64) error { } // RegisterValidator is a wrapper function which registers new validator with given balance and stake -func (t *TestServer) RegisterValidator(secrets string, balance string, stake string) error { +func (t *TestServer) RegisterValidator(secrets string, stake string) error { args := []string{ "polybft", "register-validator", "--" + polybftsecrets.DataPathFlag, path.Join(t.clusterConfig.TmpDir, secrets), - "--registrator-data-dir", path.Join(t.clusterConfig.TmpDir, "test-chain-1"), "--jsonrpc", t.JSONRPCAddr(), - "--balance", balance, - "--stake", stake, + } + + if stake != "" { + args = append(args, "--stake", stake) } return runCommand(t.clusterConfig.Binary, args, t.clusterConfig.GetStdout("register-validator")) } +// WhitelistValidator invokes whitelist-validator helper CLI command, +// which sends whitelist transaction to ChildValidatorSet +func (t *TestServer) WhitelistValidator(address, secrets string) error { + args := []string{ + "polybft", + "whitelist-validator", + "--" + polybftsecrets.DataPathFlag, path.Join(t.clusterConfig.TmpDir, secrets), + "--address", address, + "--jsonrpc", t.JSONRPCAddr(), + } + + return runCommand(t.clusterConfig.Binary, args, t.clusterConfig.GetStdout("whitelist-validator")) +} + // Delegate delegates given amount by the account in secrets to validatorAddr validator func (t *TestServer) Delegate(amount uint64, secrets string, validatorAddr ethgo.Address) error { args := []string{ @@ -247,3 +266,57 @@ func (t *TestServer) Withdraw(secrets string, recipient ethgo.Address) error { return runCommand(t.clusterConfig.Binary, args, t.clusterConfig.GetStdout("withdrawal")) } + +// HasValidatorSealed checks whether given validator has signed at least single block for the given range of blocks +func (t *TestServer) HasValidatorSealed(firstBlock, lastBlock uint64, validators polybft.AccountSet, + validatorAddr ethgo.Address) (bool, error) { + rpcClient := t.JSONRPC() + for i := firstBlock + 1; i <= lastBlock; i++ { + block, err := rpcClient.Eth().GetBlockByNumber(ethgo.BlockNumber(i), false) + if err != nil { + return false, err + } + + extra, err := polybft.GetIbftExtra(block.ExtraData) + if err != nil { + return false, err + } + + signers, err := validators.GetFilteredValidators(extra.Parent.Bitmap) + if err != nil { + return false, err + } + + if signers.ContainsAddress(types.Address(validatorAddr)) { + return true, nil + } + } + + return false, nil +} + +func (t *TestServer) WaitForNonZeroBalance(address ethgo.Address, dur time.Duration) (*big.Int, error) { + timer := time.NewTimer(dur) + defer timer.Stop() + + ticker := time.NewTicker(150 * time.Millisecond) + defer ticker.Stop() + + rpcClient := t.JSONRPC() + + for { + select { + case <-timer.C: + return nil, fmt.Errorf("timeout occurred while waiting for balance ") + case <-ticker.C: + balance, err := rpcClient.Eth().GetBalance(address, ethgo.Latest) + if err != nil { + return nil, fmt.Errorf("error getting balance") + } + + if balance.Cmp(big.NewInt(0)) == 1 { + return balance, nil + } + } + } +} diff --git a/txrelayer/txrelayer.go b/txrelayer/txrelayer.go index bc66d0cc6b..bc7a6695a0 100644 --- a/txrelayer/txrelayer.go +++ b/txrelayer/txrelayer.go @@ -15,6 +15,7 @@ const ( defaultGasPrice = 1879048192 // 0x70000000 defaultGasLimit = 5242880 // 0x500000 DefaultRPCAddress = "http://127.0.0.1:8545" + numRetries = 1000 ) var ( @@ -159,7 +160,7 @@ func (t *TxRelayerImpl) waitForReceipt(hash ethgo.Hash) (*ethgo.Receipt, error) return receipt, nil } - if count > 100 { + if count > numRetries { return nil, fmt.Errorf("timeout while waiting for transaction %s to be processed", hash) }