From 91b5c10c48abab026e549816acbd5c58a75b63f5 Mon Sep 17 00:00:00 2001 From: Leeren Date: Fri, 4 Oct 2024 14:55:07 -0700 Subject: [PATCH] feat(cli): add unjail validator subcommand (#170) * feat(cli): add unjail validator subcommand * fix: use static predeploy ca in validator logic --- client/cmd/abi/IPTokenSlashing.abi.json | 369 ++++++++++++++++++++++++ client/cmd/flags.go | 12 + client/cmd/transaction.go | 19 ++ client/cmd/validator.go | 130 ++++++++- 4 files changed, 518 insertions(+), 12 deletions(-) create mode 100644 client/cmd/abi/IPTokenSlashing.abi.json diff --git a/client/cmd/abi/IPTokenSlashing.abi.json b/client/cmd/abi/IPTokenSlashing.abi.json new file mode 100644 index 00000000..53b19a05 --- /dev/null +++ b/client/cmd/abi/IPTokenSlashing.abi.json @@ -0,0 +1,369 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "ipTokenStaking", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "IP_TOKEN_STAKING", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IPTokenStaking" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "UPGRADE_INTERFACE_VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "accessManager", + "type": "address", + "internalType": "address" + }, + { + "name": "newUnjailFee", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pendingOwner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "proxiableUUID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setUnjailFee", + "inputs": [ + { + "name": "newUnjailFee", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unjail", + "inputs": [ + { + "name": "validatorUncmpPubkey", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "unjailFee", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "unjailOnBehalf", + "inputs": [ + { + "name": "validatorCmpPubkey", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "upgradeToAndCall", + "inputs": [ + { + "name": "newImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferStarted", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Unjail", + "inputs": [ + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "validatorCmpPubkey", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "UnjailFeeSet", + "inputs": [ + { + "name": "newUnjailFee", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Upgraded", + "inputs": [ + { + "name": "implementation", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967InvalidImplementation", + "inputs": [ + { + "name": "implementation", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967NonPayable", + "inputs": [] + }, + { + "type": "error", + "name": "FailedInnerCall", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidInitialization", + "inputs": [] + }, + { + "type": "error", + "name": "NotInitializing", + "inputs": [] + }, + { + "type": "error", + "name": "OwnableInvalidOwner", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "UUPSUnauthorizedCallContext", + "inputs": [] + }, + { + "type": "error", + "name": "UUPSUnsupportedProxiableUUID", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + } +] diff --git a/client/cmd/flags.go b/client/cmd/flags.go index b0de2471..2e539014 100644 --- a/client/cmd/flags.go +++ b/client/cmd/flags.go @@ -124,6 +124,11 @@ func bindRollbackFlags(cmd *cobra.Command, cfg *config.Config) { cmd.Flags().BoolVar(&cfg.RemoveBlock, "hard", false, "remove last block as well as state") } +func bindValidatorUnjailFlags(cmd *cobra.Command, cfg *unjailConfig) { + bindValidatorBaseFlags(cmd, &cfg.baseConfig) + cmd.Flags().StringVar(&cfg.ValidatorPubKey, "validator-pubkey", "", "Validator's base64-encoded compressed 33-byte secp256k1 public key") +} + // Flag Validation func validateFlags(flags map[string]string) error { @@ -189,3 +194,10 @@ func validateValidatorUnstakeOnBehalfFlags(cfg stakeConfig) error { "unstake": cfg.StakeAmount, }) } + +func validateValidatorUnjailFlags(cfg unjailConfig) error { + return validateFlags(map[string]string{ + "rpc": cfg.RPC, + "validator-pubkey": cfg.ValidatorPubKey, + }) +} diff --git a/client/cmd/transaction.go b/client/cmd/transaction.go index b996350e..7d16f73b 100644 --- a/client/cmd/transaction.go +++ b/client/cmd/transaction.go @@ -16,6 +16,25 @@ import ( "github.com/piplabs/story/lib/errors" ) +func readContract(ctx context.Context, cfg baseConfig, contractAddress common.Address, data []byte) ([]byte, error) { + client, err := ethclient.Dial(cfg.RPC) + if err != nil { + return nil, errors.Wrap(err, "failed to connect to Ethereum client") + } + + callMsg := ethereum.CallMsg{ + To: &contractAddress, + Data: data, + } + + result, err := client.CallContract(ctx, callMsg, nil) + if err != nil { + return nil, errors.Wrap(err, "contract call failed") + } + + return result, nil +} + func prepareAndSendTransaction(ctx context.Context, cfg baseConfig, contractAddress common.Address, value *big.Int, data []byte) error { client, err := ethclient.Dial(cfg.RPC) if err != nil { diff --git a/client/cmd/validator.go b/client/cmd/validator.go index 18e16763..10e47c49 100644 --- a/client/cmd/validator.go +++ b/client/cmd/validator.go @@ -11,24 +11,37 @@ import ( "os" "strings" + "github.com/decred/dcrd/dcrec/secp256k1" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/joho/godotenv" "github.com/spf13/cobra" + "github.com/piplabs/story/client/genutil/evm/predeploys" "github.com/piplabs/story/lib/errors" _ "embed" ) +type ContractType int + const ( - contractAddressHex = "0xCCcCcC0000000000000000000000000000000001" + STAKING ContractType = iota + SLASHING ) +type ContractInfo struct { + AddressHex string + ABI []byte +} + //go:embed abi/IPTokenStaking.abi.json var ipTokenStakingABI []byte +//go:embed abi/IPTokenSlashing.abi.json +var ipTokenSlashingABI []byte + type baseConfig struct { RPC string PrivateKey string @@ -43,6 +56,11 @@ type stakeConfig struct { StakeAmount string } +type unjailConfig struct { + baseConfig + ValidatorPubKey string +} + type operatorConfig struct { baseConfig Operator string @@ -65,6 +83,17 @@ type exportKeyConfig struct { ExportEVMKey bool } +var contracts = map[ContractType]ContractInfo{ + STAKING: { + AddressHex: predeploys.IPTokenStaking, + ABI: ipTokenStakingABI, + }, + SLASHING: { + AddressHex: predeploys.IPTokenSlashing, + ABI: ipTokenSlashingABI, + }, +} + func loadEnv() { err := godotenv.Load() if err != nil { @@ -89,6 +118,7 @@ func newValidatorCmds() *cobra.Command { newValidatorAddOperatorCmd(), newValidatorRemoveOperatorCmd(), newValidatorSetWithdrawalAddressCmd(), + newValidatorUnjailCmd(), ) return cmd @@ -283,6 +313,27 @@ func newValidatorKeyExportCmd() *cobra.Command { return cmd } +func newValidatorUnjailCmd() *cobra.Command { + var cfg unjailConfig + + cmd := &cobra.Command{ + Use: "unjail", + Short: "Unjail the validator", + Args: cobra.NoArgs, + PreRunE: func(_ *cobra.Command, _ []string) error { + return loadAndValidatePrivateKey(&cfg.baseConfig) + }, + RunE: runValidatorCommand( + func() error { return validateValidatorUnjailFlags(cfg) }, + func(ctx context.Context) error { return unjail(ctx, cfg) }, + ), + } + + bindValidatorUnjailFlags(cmd, &cfg) + + return cmd +} + func runValidatorCommand( validate func() error, execute func(ctx context.Context) error, @@ -381,7 +432,7 @@ func createValidator(ctx context.Context, cfg createValidatorConfig) error { return errors.New("invalid stake amount", "amount", cfg.StakeAmount) } - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "createValidatorOnBehalf", stakeAmount, uncompressedPubKeyBytes) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "createValidatorOnBehalf", stakeAmount, uncompressedPubKeyBytes) if err != nil { return err } @@ -399,7 +450,7 @@ func setWithdrawalAddress(ctx context.Context, cfg withdrawalConfig) error { withdrawalAddress := common.HexToAddress(cfg.WithdrawalAddress) - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "setWithdrawalAddress", big.NewInt(0), uncompressedPubKey, withdrawalAddress) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "setWithdrawalAddress", big.NewInt(0), uncompressedPubKey, withdrawalAddress) if err != nil { return err } @@ -417,7 +468,7 @@ func addOperator(ctx context.Context, cfg operatorConfig) error { operatorAddress := common.HexToAddress(cfg.Operator) - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "addOperator", big.NewInt(0), uncompressedPubKey, operatorAddress) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "addOperator", big.NewInt(0), uncompressedPubKey, operatorAddress) if err != nil { return err } @@ -435,7 +486,7 @@ func removeOperator(ctx context.Context, cfg operatorConfig) error { operatorAddress := common.HexToAddress(cfg.Operator) - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "removeOperator", big.NewInt(0), uncompressedPubKey, operatorAddress) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "removeOperator", big.NewInt(0), uncompressedPubKey, operatorAddress) if err != nil { return err } @@ -461,7 +512,7 @@ func stake(ctx context.Context, cfg stakeConfig) error { return errors.New("invalid stake amount", "amount", cfg.StakeAmount) } - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "stakeOnBehalf", stakeAmount, uncompressedPubKey, validatorPubKeyBytes) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "stakeOnBehalf", stakeAmount, uncompressedPubKey, validatorPubKeyBytes) if err != nil { return err } @@ -491,7 +542,7 @@ func stakeOnBehalf(ctx context.Context, cfg stakeConfig) error { return errors.New("invalid stake amount", "amount", cfg.StakeAmount) } - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "stakeOnBehalf", stakeAmount, uncompressedDelegatorPubKeyBytes, validatorPubKeyBytes) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "stakeOnBehalf", stakeAmount, uncompressedDelegatorPubKeyBytes, validatorPubKeyBytes) if err != nil { return err } @@ -517,7 +568,7 @@ func unstake(ctx context.Context, cfg stakeConfig) error { return errors.New("invalid unstake amount", "amount", cfg.StakeAmount) } - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "unstake", big.NewInt(0), uncompressedPubKey, validatorPubKeyBytes, unstakeAmount) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "unstake", big.NewInt(0), uncompressedPubKey, validatorPubKeyBytes, unstakeAmount) if err != nil { return err } @@ -543,7 +594,7 @@ func unstakeOnBehalf(ctx context.Context, cfg stakeConfig) error { return errors.New("invalid unstake amount", "amount", cfg.StakeAmount) } - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "unstakeOnBehalf", big.NewInt(0), delegatorPubKeyBytes, validatorPubKeyBytes, unstakeAmount) + err = prepareAndExecuteTransaction(ctx, STAKING, &cfg.baseConfig, "unstakeOnBehalf", big.NewInt(0), delegatorPubKeyBytes, validatorPubKeyBytes, unstakeAmount) if err != nil { return err } @@ -553,9 +604,64 @@ func unstakeOnBehalf(ctx context.Context, cfg stakeConfig) error { return nil } -func prepareAndExecuteTransaction(ctx context.Context, cfg *baseConfig, methodName string, value *big.Int, args ...any) error { - contractAddress := common.HexToAddress(contractAddressHex) - contractABI, err := abi.JSON(strings.NewReader(string(ipTokenStakingABI))) +func unjail(ctx context.Context, cfg unjailConfig) error { + validatorPubKeyBytes, err := base64.StdEncoding.DecodeString(cfg.ValidatorPubKey) + if err != nil { + return errors.Wrap(err, "failed to decode base64 validator public key") + } + + if len(validatorPubKeyBytes) != secp256k1.PubKeyBytesLenCompressed { + return fmt.Errorf("invalid compressed public key length: %d", len(validatorPubKeyBytes)) + } + + contractABI, err := abi.JSON(strings.NewReader(string(contracts[SLASHING].ABI))) + if err != nil { + return err + } + + result, err := prepareAndReadContract(ctx, SLASHING, &cfg.baseConfig, "unjailFee") + if err != nil { + return err + } + + var unjailFee *big.Int + err = contractABI.UnpackIntoInterface(&unjailFee, "unjailFee", result) + if err != nil { + return errors.Wrap(err, "failed to unpack unjailFee") + } + + fmt.Printf("Unjail fee: %s\n", unjailFee.String()) + + err = prepareAndExecuteTransaction(ctx, SLASHING, &cfg.baseConfig, "unjailOnBehalf", unjailFee, validatorPubKeyBytes) + if err != nil { + return err + } + + fmt.Println("Validator successfully unjailed!") + + return nil +} + +func prepareAndReadContract(ctx context.Context, contractType ContractType, cfg *baseConfig, methodName string, args ...any) ([]byte, error) { + contractInfo := contracts[contractType] + contractAddress := common.HexToAddress(contractInfo.AddressHex) + contractABI, err := abi.JSON(strings.NewReader(string(contractInfo.ABI))) + if err != nil { + return nil, errors.Wrap(err, "failed to parse ABI") + } + + data, err := contractABI.Pack(methodName, args...) + if err != nil { + return nil, errors.Wrap(err, "failed to pack data") + } + + return readContract(ctx, *cfg, contractAddress, data) +} + +func prepareAndExecuteTransaction(ctx context.Context, contractType ContractType, cfg *baseConfig, methodName string, value *big.Int, args ...any) error { + contractInfo := contracts[contractType] + contractAddress := common.HexToAddress(contractInfo.AddressHex) + contractABI, err := abi.JSON(strings.NewReader(string(contractInfo.ABI))) if err != nil { return errors.Wrap(err, "failed to parse ABI") }