From 4adc015ac7e38cec6caf187ae5b1dfbde41db866 Mon Sep 17 00:00:00 2001 From: Leeren Date: Sat, 24 Aug 2024 00:30:29 -0700 Subject: [PATCH 1/9] chore(release): begin next unstable release (#42) --- lib/buildinfo/buildinfo.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/buildinfo/buildinfo.go b/lib/buildinfo/buildinfo.go index 2963b620..1a5601bc 100644 --- a/lib/buildinfo/buildinfo.go +++ b/lib/buildinfo/buildinfo.go @@ -13,10 +13,10 @@ import ( ) const ( - VersionMajor = 0 // Major version component of the current release - VersionMinor = 9 // Minor version component of the current release - VersionPatch = 9 // Patch version component of the current release - VersionMeta = "stable" // Version metadata to append to the version string + VersionMajor = 0 // Major version component of the current release + VersionMinor = 9 // Minor version component of the current release + VersionPatch = 10 // Patch version component of the current release + VersionMeta = "unstable" // Version metadata to append to the version string ) // Version returns the version of the whole story-monorepo and all binaries built from this git commit. From a73a5e730efbf14fae4a872facf05cd4caa0ddb6 Mon Sep 17 00:00:00 2001 From: Leeren Date: Sat, 24 Aug 2024 05:18:57 -0700 Subject: [PATCH 2/9] feat(client): update validator cli (#44) --- README.md | 4 +- client/cmd/abi/IPTokenStaking.abi.json | 665 ++++++++++++++++++++- client/cmd/key_utils.go | 106 ++++ client/cmd/transaction.go | 122 ++++ client/cmd/validator.go | 798 +++++-------------------- 5 files changed, 1023 insertions(+), 672 deletions(-) create mode 100644 client/cmd/key_utils.go create mode 100644 client/cmd/transaction.go diff --git a/README.md b/README.md index ac376e1f..1dc17ea4 100644 --- a/README.md +++ b/README.md @@ -53,10 +53,10 @@ To connect to Iliad, initialize `story` with the `--network iliad` flag, which w ./story init --network iliad ``` -Afterwards, run the [`story-geth`](https://github.com/piplabs/story-geth) execution client with `iliad` network flag: +Afterwards, run the [`story-geth`](https://github.com/piplabs/story-geth) execution client in `full` sync mode with the `iliad` network flag: ```bash -./geth --iliad +./geth --iliad --syncmode full ``` Now you should be able to sync to the Iliad network with the following: diff --git a/client/cmd/abi/IPTokenStaking.abi.json b/client/cmd/abi/IPTokenStaking.abi.json index ce489f2d..d6c21b70 100644 --- a/client/cmd/abi/IPTokenStaking.abi.json +++ b/client/cmd/abi/IPTokenStaking.abi.json @@ -1,4 +1,102 @@ [ + { + "type": "constructor", + "inputs": [ + { + "name": "stakingRounding", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "defaultCommissionRate", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "defaultMaxCommissionRate", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "defaultMaxCommissionChangeRate", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "DEFAULT_COMMISSION_RATE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "DEFAULT_MAX_COMMISSION_CHANGE_RATE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "DEFAULT_MAX_COMMISSION_RATE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "STAKE_ROUNDING", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "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": "addOperator", @@ -55,7 +153,7 @@ "name": "createValidatorOnBehalf", "inputs": [ { - "name": "validatorPubkey", + "name": "validatorUncmpPubkey", "type": "bytes", "internalType": "bytes" } @@ -63,6 +161,49 @@ "outputs": [], "stateMutability": "payable" }, + { + "type": "function", + "name": "delegatorTotalStakes", + "inputs": [ + { + "name": "delegatorCmpPubkey", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "stakedAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "delegatorValidatorStakes", + "inputs": [ + { + "name": "delegatorCmpPubkey", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "validatorCmpPubkey", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "stakedAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "getOperators", @@ -82,6 +223,117 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "accessManager", + "type": "address", + "internalType": "address" + }, + { + "name": "_minStakeAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_minUnstakeAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_minRedelegateAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_withdrawalAddressChangeInterval", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "minRedelegateAmount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "minStakeAmount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "minUnstakeAmount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "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": "redelegate", @@ -97,12 +349,12 @@ "internalType": "bytes" }, { - "name": "validatorSrcPubkey", + "name": "validatorCmpSrcPubkey", "type": "bytes", "internalType": "bytes" }, { - "name": "validatorDstPubkey", + "name": "validatorCmpDstPubkey", "type": "bytes", "internalType": "bytes" }, @@ -132,12 +384,12 @@ "internalType": "bytes" }, { - "name": "validatorSrcPubkey", + "name": "validatorCmpSrcPubkey", "type": "bytes", "internalType": "bytes" }, { - "name": "validatorDstPubkey", + "name": "validatorCmpDstPubkey", "type": "bytes", "internalType": "bytes" }, @@ -170,6 +422,13 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "roundedStakeAmount", @@ -194,6 +453,45 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "setMinRedelegateAmount", + "inputs": [ + { + "name": "newMinRedelegateAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setMinStakeAmount", + "inputs": [ + { + "name": "newMinStakeAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setMinUnstakeAmount", + "inputs": [ + { + "name": "newMinUnstakeAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "setWithdrawalAddress", @@ -212,6 +510,19 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "setWithdrawalAddressChangeInterval", + "inputs": [ + { + "name": "newWithdrawalAddressChangeInterval", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "stake", @@ -222,7 +533,7 @@ "internalType": "bytes" }, { - "name": "validatorPubkey", + "name": "validatorCmpPubkey", "type": "bytes", "internalType": "bytes" } @@ -235,12 +546,12 @@ "name": "stakeOnBehalf", "inputs": [ { - "name": "delegatorPubkey", + "name": "delegatorUncmpPubkey", "type": "bytes", "internalType": "bytes" }, { - "name": "validatorPubkey", + "name": "validatorCmpPubkey", "type": "bytes", "internalType": "bytes" } @@ -248,6 +559,19 @@ "outputs": [], "stateMutability": "payable" }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "unstake", @@ -258,7 +582,7 @@ "internalType": "bytes" }, { - "name": "validatorPubkey", + "name": "validatorCmpPubkey", "type": "bytes", "internalType": "bytes" }, @@ -281,7 +605,7 @@ "internalType": "bytes" }, { - "name": "validatorPubkey", + "name": "validatorCmpPubkey", "type": "bytes", "internalType": "bytes" }, @@ -294,12 +618,112 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "upgradeToAndCall", + "inputs": [ + { + "name": "newImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "validatorMetadata", + "inputs": [ + { + "name": "validatorCmpPubkey", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "exists", + "type": "bool", + "internalType": "bool" + }, + { + "name": "moniker", + "type": "string", + "internalType": "string" + }, + { + "name": "totalStake", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "commissionRate", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "maxCommissionRate", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "maxCommissionChangeRate", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "withdrawalAddressChange", + "inputs": [ + { + "name": "delegatorCmpPubkey", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "lastChange", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "withdrawalAddressChangeInterval", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "event", "name": "CreateValidator", "inputs": [ { - "name": "validatorPubkey", + "name": "validatorUncmpPubkey", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "validatorCmpPubkey", "type": "bytes", "indexed": false, "internalType": "bytes" @@ -342,13 +766,19 @@ "name": "Deposit", "inputs": [ { - "name": "depositorPubkey", + "name": "delegatorUncmpPubkey", "type": "bytes", "indexed": false, "internalType": "bytes" }, { - "name": "validatorPubkey", + "name": "delegatorCmpPubkey", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "validatorCmpPubkey", "type": "bytes", "indexed": false, "internalType": "bytes" @@ -362,12 +792,102 @@ ], "anonymous": false }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "MinRedelegateAmountSet", + "inputs": [ + { + "name": "minRedelegateAmount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "MinStakeAmountSet", + "inputs": [ + { + "name": "minStakeAmount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "MinUnstakeAmountSet", + "inputs": [ + { + "name": "minUnstakeAmount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "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": "Redelegate", "inputs": [ { - "name": "depositorPubkey", + "name": "delegatorCmpPubkey", "type": "bytes", "indexed": false, "internalType": "bytes" @@ -398,7 +918,7 @@ "name": "SetWithdrawalAddress", "inputs": [ { - "name": "depositorPubkey", + "name": "delegatorCmpPubkey", "type": "bytes", "indexed": false, "internalType": "bytes" @@ -412,18 +932,31 @@ ], "anonymous": false }, + { + "type": "event", + "name": "Upgraded", + "inputs": [ + { + "name": "implementation", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, { "type": "event", "name": "Withdraw", "inputs": [ { - "name": "depositorPubkey", + "name": "delegatorCmpPubkey", "type": "bytes", "indexed": false, "internalType": "bytes" }, { - "name": "validatorPubkey", + "name": "validatorCmpPubkey", "type": "bytes", "indexed": false, "internalType": "bytes" @@ -436,5 +969,103 @@ } ], "anonymous": false + }, + { + "type": "event", + "name": "WithdrawalAddressChangeIntervalSet", + "inputs": [ + { + "name": "newInterval", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "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": "ReentrancyGuardReentrantCall", + "inputs": [] + }, + { + "type": "error", + "name": "UUPSUnauthorizedCallContext", + "inputs": [] + }, + { + "type": "error", + "name": "UUPSUnsupportedProxiableUUID", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + } + ] } ] diff --git a/client/cmd/key_utils.go b/client/cmd/key_utils.go new file mode 100644 index 00000000..5921ff8a --- /dev/null +++ b/client/cmd/key_utils.go @@ -0,0 +1,106 @@ +package cmd + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "os" + + "github.com/ethereum/go-ethereum/crypto" + + "github.com/piplabs/story/lib/errors" +) + +type ValidatorKey struct { + Address string `json:"address"` + PubKey KeyInfo `json:"pub_key"` + PrivKey KeyInfo `json:"priv_key"` +} + +type KeyInfo struct { + Type string `json:"type"` + Value string `json:"value"` +} + +func decodeAndUncompressPubKey(compressedPubKeyBase64 string) (string, error) { + compressedPubKeyBytes, err := base64.StdEncoding.DecodeString(compressedPubKeyBase64) + if err != nil { + return "", errors.Wrap(err, "failed to decode base64 public key") + } + if len(compressedPubKeyBytes) != 33 { + return "", fmt.Errorf("invalid compressed public key length: %d", len(compressedPubKeyBytes)) + } + + curve := elliptic.P256() + x, y := elliptic.UnmarshalCompressed(curve, compressedPubKeyBytes) + if x == nil || y == nil { + return "", errors.New("failed to unmarshal compressed public key") + } + + uncompressedPubKeyBytes := elliptic.Marshal(curve, x, y) + uncompressedPubKeyHex := hex.EncodeToString(uncompressedPubKeyBytes) + + return uncompressedPubKeyHex, nil +} + +func deriveUncompressedPublicKeyFromPrivateKey(evmPrivKey *ecdsa.PrivateKey) ([]byte, error) { + pubKey := evmPrivKey.PublicKey + uncompressedPubKey := elliptic.Marshal(pubKey.Curve, pubKey.X, pubKey.Y) + if len(uncompressedPubKey) != 65 { + return nil, fmt.Errorf("invalid uncompressed public key length: %d", len(uncompressedPubKey)) + } + + return uncompressedPubKey, nil +} + +func validatorKeyExport(keyFilePath string) error { + keyFileBytes, err := os.ReadFile(keyFilePath) + if err != nil { + return errors.Wrap(err, "failed to read key file") + } + + var keyData ValidatorKey + if err := json.Unmarshal(keyFileBytes, &keyData); err != nil { + return errors.Wrap(err, "failed to unmarshal key file") + } + + privKeyBytes, err := base64.StdEncoding.DecodeString(keyData.PrivKey.Value) + if err != nil { + return errors.Wrap(err, "failed to decode private key") + } + + privateKey, err := crypto.ToECDSA(privKeyBytes) + if err != nil { + return errors.Wrap(err, "invalid private key") + } + + publicKey, ok := privateKey.Public().(*ecdsa.PublicKey) + if !ok { + return errors.New("failed to cast public key to ecdsa.PublicKey") + } + evmPublicKey := crypto.PubkeyToAddress(*publicKey).Hex() + + // Handle the compressed public key + compressedPubKeyBytes, err := base64.StdEncoding.DecodeString(keyData.PubKey.Value) + if err != nil { + return errors.Wrap(err, "failed to decode base64 public key") + } + compressedPubKeyHex := hex.EncodeToString(compressedPubKeyBytes) + + // Get the uncompressed public key using the refactored function + uncompressedPubKeyHex, err := decodeAndUncompressPubKey(keyData.PubKey.Value) + if err != nil { + return err + } + + fmt.Println("------------------------------------------------------") + fmt.Println("EVM Public Key:", evmPublicKey) + fmt.Println("Compressed Public Key:", compressedPubKeyHex) + fmt.Println("Uncompressed Public Key:", uncompressedPubKeyHex) + fmt.Println("------------------------------------------------------") + + return nil +} diff --git a/client/cmd/transaction.go b/client/cmd/transaction.go new file mode 100644 index 00000000..f54f4801 --- /dev/null +++ b/client/cmd/transaction.go @@ -0,0 +1,122 @@ +package cmd + +import ( + "context" + "crypto/ecdsa" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/piplabs/story/lib/errors" +) + +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 { + return errors.Wrap(err, "failed to connect to Ethereum client") + } + + evmPrivKey, err := crypto.HexToECDSA(cfg.PrivateKey) + if err != nil { + return errors.Wrap(err, "invalid EVM private key") + } + + publicKey, ok := evmPrivKey.Public().(*ecdsa.PublicKey) + if !ok { + return errors.New("failed to assert type to *ecdsa.PublicKey") + } + fromAddress := crypto.PubkeyToAddress(*publicKey) + + nonce, err := client.PendingNonceAt(ctx, fromAddress) + if err != nil { + return errors.Wrap(err, "failed to get nonce") + } + + gasPrice, err := client.SuggestGasPrice(ctx) + if err != nil { + return errors.Wrap(err, "failed to suggest gas price") + } + gasPrice = new(big.Int).Mul(gasPrice, big.NewInt(120)) + gasPrice = new(big.Int).Div(gasPrice, big.NewInt(100)) + + gasLimit, err := estimateGas(ctx, client, fromAddress, contractAddress, gasPrice, value, data) + if err != nil { + return err + } + + gasTipCap := gasPrice + gasFeeCap := new(big.Int).Mul(gasPrice, big.NewInt(2)) + + gasCost := new(big.Int).Mul(big.NewInt(int64(gasLimit)), gasFeeCap) + totalTxCost := new(big.Int).Add(gasCost, value) + + balance, err := client.BalanceAt(ctx, fromAddress, nil) + if err != nil { + return errors.Wrap(err, "failed to fetch balance") + } + + if balance.Cmp(totalTxCost) < 0 { + return errors.New("insufficient funds for gas * price + value", "balance", balance.String(), "totalTxCost", totalTxCost.String()) + } + + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: big.NewInt(cfg.ChainID), + Nonce: nonce, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + Gas: gasLimit, + To: &contractAddress, + Value: value, + Data: data, + }) + + signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(big.NewInt(cfg.ChainID)), evmPrivKey) + if err != nil { + return errors.Wrap(err, "failed to sign transaction") + } + + txHash := signedTx.Hash().Hex() + fmt.Printf("Transaction hash: %s\n", txHash) + fmt.Printf("Explorer URL: %s/tx/%s\n", cfg.Explorer, txHash) + + if err = client.SendTransaction(ctx, signedTx); err != nil { + return errors.Wrap(err, "failed to send transaction") + } + + fmt.Println("Transaction sent, waiting for confirmation...") + + receipt, err := bind.WaitMined(ctx, client, signedTx) + if err != nil { + return errors.Wrap(err, "transaction failed") + } + + if receipt.Status == types.ReceiptStatusFailed { + return errors.New("transaction failed", "status", receipt.Status) + } + + fmt.Println("Transaction confirmed successfully!") + + return nil +} + +func estimateGas(ctx context.Context, client *ethclient.Client, fromAddress, contractAddress common.Address, gasPrice, value *big.Int, data []byte) (uint64, error) { + msg := ethereum.CallMsg{ + From: fromAddress, + To: &contractAddress, + GasPrice: gasPrice, + Value: value, + Data: data, + } + gasLimit, err := client.EstimateGas(ctx, msg) + if err != nil { + return 0, errors.Wrap(err, "failed to estimate gas") + } + + return gasLimit, nil +} diff --git a/client/cmd/validator.go b/client/cmd/validator.go index 6184e36a..8a62755f 100644 --- a/client/cmd/validator.go +++ b/client/cmd/validator.go @@ -3,8 +3,6 @@ package cmd import ( "context" "crypto/ecdsa" - "crypto/elliptic" - "encoding/base64" "encoding/hex" "encoding/json" "fmt" @@ -13,13 +11,9 @@ import ( "path/filepath" "strings" - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethclient" "github.com/joho/godotenv" "github.com/spf13/cobra" @@ -29,50 +23,36 @@ import ( _ "embed" ) +const ( + contractAddressHex = "0xCCcCcC0000000000000000000000000000000001" +) + //go:embed abi/IPTokenStaking.abi.json var ipTokenStakingABI []byte -type ValidatorKey struct { - Address string `json:"address"` - PubKey KeyInfo `json:"pub_key"` - PrivKey KeyInfo `json:"priv_key"` +type baseConfig struct { + RPC string + PrivateKey string + Explorer string + ChainID int64 } -type KeyInfo struct { - Type string `json:"type"` - Value string `json:"value"` -} type stakeConfig struct { - RPC string + baseConfig ValidatorPubKey string StakeAmount string - PrivateKey string - Explorer string - ChainID int64 } type unstakeConfig struct { - RPC string - ValidatorPubKey string - UnstakeAmount string - ExecutionAddress string - PrivateKey string - Explorer string - ChainID int64 + baseConfig + ValidatorPubKey string + UnstakeAmount string } type createValidatorConfig struct { - RPC string + baseConfig ValidatorKeyFile string StakeAmount string - PrivateKey string - Explorer string - ChainID int64 -} - -func addKeyFileFlag(cmd *cobra.Command, keyFilePath *string) { - defaultKeyFilePath := filepath.Join(config.DefaultHomeDir(), "config", "priv_validator_key.json") - cmd.Flags().StringVar(keyFilePath, "keyfile", defaultKeyFilePath, "Path to the Tendermint key file") } func loadEnv() { @@ -99,278 +79,6 @@ func newValidatorCmds() *cobra.Command { return cmd } -func newValidatorKeyExportCmd() *cobra.Command { - var keyFilePath string - - cmd := &cobra.Command{ - Use: "export", - Short: "Export the EVM private key from the Tendermint key file", - RunE: func(_ *cobra.Command, _ []string) error { - loadEnv() - return validatorKeyExport(keyFilePath) - }, - } - - addKeyFileFlag(cmd, &keyFilePath) - - return cmd -} - -func createValidator(ctx context.Context, cfg createValidatorConfig) error { - // Read the priv_validator_key.json file - keyFileBytes, err := os.ReadFile(cfg.ValidatorKeyFile) - if err != nil { - return errors.Wrap(err, "invalid key file") - } - - var keyFileData ValidatorKey - if err := json.Unmarshal(keyFileBytes, &keyFileData); err != nil { - return errors.Wrap(err, "failed to unmarshal priv_validator_key.json") - } - - // Decode and uncompress the public key - compressedPubKeyBytes, err := base64.StdEncoding.DecodeString(keyFileData.PubKey.Value) - if err != nil { - return errors.Wrap(err, "failed to decode base64 key") - } - if len(compressedPubKeyBytes) != 33 { - return errors.New("invalid compressed public key length", "length", len(compressedPubKeyBytes)) - } - - // Load the private key for funding the EVM transaction from the .env file if not provided as a flag - if cfg.PrivateKey == "" { - cfg.PrivateKey = os.Getenv("PRIVATE_KEY") - if cfg.PrivateKey == "" { - return errors.New("missing required flag", "private-key", "EVM private key") - } - } - - evmPrivKey, err := crypto.HexToECDSA(cfg.PrivateKey) - if err != nil { - return errors.Wrap(err, "invalid EVM private key") - } - - // Connect to the Ethereum client - client, err := ethclient.Dial(cfg.RPC) - if err != nil { - return errors.Wrap(err, "failed to connect to Ethereum client") - } - - // Get the balance of the account - publicKey := evmPrivKey.Public() - publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) - if !ok { - return errors.New("error casting public key to ECDSA") - } - fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) - - // Fetch balance - balance, err := client.BalanceAt(ctx, fromAddress, nil) - if err != nil { - return errors.Wrap(err, "failed to fetch balance") - } - - fmt.Printf("Balance: %s wei\n", balance.String()) - - // Convert the stake amount to a big.Int - stakeAmount, ok := new(big.Int).SetString(cfg.StakeAmount, 10) - if !ok { - return errors.New("invalid stake amount", "amount", cfg.StakeAmount) - } - - // Suggest gas price - gasPrice, err := client.SuggestGasPrice(ctx) - if err != nil { - return errors.Wrap(err, "failed to suggest gas price") - } - - // Increase gas price by 20% to ensure faster confirmation - gasPrice = new(big.Int).Mul(gasPrice, big.NewInt(120)) - gasPrice = new(big.Int).Div(gasPrice, big.NewInt(100)) - - // Read the ABI file from embedded content - contractABI, err := abi.JSON(strings.NewReader(string(ipTokenStakingABI))) - if err != nil { - return errors.Wrap(err, "failed to parse ABI") - } - - contractAddress := common.HexToAddress("0xCCcCcC0000000000000000000000000000000001") - - // Get the nonce for the transaction - nonce, err := client.PendingNonceAt(ctx, fromAddress) - if err != nil { - return errors.Wrap(err, "failed to get nonce") - } - - fmt.Printf("Using Nonce: %d\n", nonce) - - // Prepare transaction data - data, err := contractABI.Pack( - "createValidatorOnBehalf", - compressedPubKeyBytes, - ) - if err != nil { - return errors.Wrap(err, "failed to pack data") - } - - fmt.Printf("Packed Data: %x\n", data) - - chainID := big.NewInt(cfg.ChainID) - - // Estimate gas limit - msg := ethereum.CallMsg{ - From: fromAddress, - To: &contractAddress, - GasPrice: gasPrice, - Value: stakeAmount, - Data: data, - } - gasLimit, err := client.EstimateGas(ctx, msg) - if err != nil { - return errors.Wrap(err, "failed to estimate gas") - } - - // Define gas fee cap and gas tip cap dynamically - gasTipCap := gasPrice - gasFeeCap := new(big.Int).Mul(gasPrice, big.NewInt(2)) - - gasCost := new(big.Int).Mul(big.NewInt(int64(gasLimit)), gasFeeCap) - totalTxCost := new(big.Int).Add(gasCost, stakeAmount) - - fmt.Printf("Stake Amount: %s wei\n", stakeAmount.String()) - fmt.Printf("Gas Limit: %d\n", gasLimit) - fmt.Printf("Gas Price: %s wei\n", gasPrice.String()) - fmt.Printf("Gas Tip Cap: %s wei\n", gasTipCap.String()) - fmt.Printf("Gas Fee Cap: %s wei\n", gasFeeCap.String()) - fmt.Printf("Gas Cost: %s wei\n", gasCost.String()) - fmt.Printf("Total Transaction Cost: %s wei\n", totalTxCost.String()) - - if balance.Cmp(totalTxCost) < 0 { - return errors.New("insufficient funds for gas * price + value", "balance", balance.String(), "totalTxCost", totalTxCost.String()) - } - - tx := types.NewTx(&types.DynamicFeeTx{ - ChainID: chainID, - Nonce: nonce, - GasFeeCap: gasFeeCap, - GasTipCap: gasTipCap, - Gas: gasLimit, - To: &contractAddress, - Value: stakeAmount, - Data: data, - }) - - // Sign the transaction - signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), evmPrivKey) - if err != nil { - return errors.Wrap(err, "failed to sign transaction") - } - - txHash := signedTx.Hash().Hex() - fmt.Printf("Transaction hash: %s\n", txHash) - fmt.Printf("Explorer URL: %s/tx/%s\n", cfg.Explorer, txHash) - - // Send the transaction - err = client.SendTransaction(ctx, signedTx) - if err != nil { - return errors.Wrap(err, "failed to send transaction") - } - - fmt.Println("Transaction sent, waiting for confirmation...") - - // Use bind.WaitMined to wait for the transaction receipt - receipt, err := bind.WaitMined(ctx, client, signedTx) - if err != nil { - return errors.Wrap(err, "transaction failed") - } - - if receipt.Status == types.ReceiptStatusFailed { - return errors.New("transaction failed", "status", receipt.Status) - } - - fmt.Println("Transaction confirmed successfully!") - fmt.Println("Validator created successfully!") - - return nil -} - -func validatorKeyExport(keyFilePath string) error { - // Read the key file - keyFileBytes, err := os.ReadFile(keyFilePath) - if err != nil { - return errors.Wrap(err, "failed to read key file") - } - - // Unmarshal the key file - var keyData ValidatorKey - if err := json.Unmarshal(keyFileBytes, &keyData); err != nil { - return errors.Wrap(err, "failed to unmarshal key file") - } - - // Decode the base64 encoded private key - privKeyBytes, err := base64.StdEncoding.DecodeString(keyData.PrivKey.Value) - if err != nil { - return errors.Wrap(err, "failed to decode private key") - } - - // Convert to EVM private key - privateKey, err := crypto.ToECDSA(privKeyBytes) - if err != nil { - return errors.Wrap(err, "invalid private key") - } - - // Derive EVM public key - publicKeyInterface := privateKey.Public() - publicKey, ok := publicKeyInterface.(*ecdsa.PublicKey) - if !ok { - return errors.New("failed to cast public key to ecdsa.PublicKey") - } - evmPublicKey := crypto.PubkeyToAddress(*publicKey).Hex() - - // Print the EVM private key and the uncompressed public key - evmPrivateKey := hex.EncodeToString(crypto.FromECDSA(privateKey)) - - // Decode the base64 encoded compressed public key - compressedPubKeyBytes, err := base64.StdEncoding.DecodeString(keyData.PubKey.Value) - if err != nil { - return errors.Wrap(err, "failed to decode base64 public key") - } - if len(compressedPubKeyBytes) != 33 { - return fmt.Errorf("invalid compressed public key length: %d", len(compressedPubKeyBytes)) - } - - curve := elliptic.P256() - x, y := elliptic.UnmarshalCompressed(curve, compressedPubKeyBytes) - if x == nil || y == nil { - return errors.New("failed to unmarshal compressed public key") - } - - // lint:ignore SA1019 ignoring deprecation warning for now - uncompressedPubKeyBytes := elliptic.Marshal(curve, x, y) - uncompressedPubKeyHex := hex.EncodeToString(uncompressedPubKeyBytes) - compressedPubKeyHex := hex.EncodeToString(compressedPubKeyBytes) - - fmt.Println("------------------------------------------------------") - fmt.Println("EVM Public Key") - fmt.Println("------------------------------------------------------") - fmt.Println(evmPublicKey) - fmt.Println("------------------------------------------------------") - fmt.Println("EVM Private Key:") - fmt.Println("------------------------------------------------------") - fmt.Println(evmPrivateKey) - fmt.Println("------------------------------------------------------") - fmt.Println("Compressed Public Key:") - fmt.Println("------------------------------------------------------") - fmt.Println(compressedPubKeyHex) - fmt.Println("------------------------------------------------------") - fmt.Println("Uncompressed Public Key:") - fmt.Println("------------------------------------------------------") - fmt.Println(uncompressedPubKeyHex) - fmt.Println("------------------------------------------------------") - - return nil -} - func newValidatorCreateCmd() *cobra.Command { var cfg createValidatorConfig @@ -380,9 +88,9 @@ func newValidatorCreateCmd() *cobra.Command { Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { loadEnv() - if err := validateFlags(cfg); err != nil { + if err := validateCreateFlags(cfg); err != nil { fmt.Println("Debug: Entering cmd.Help()") - _ = cmd.Help() // Print the help message + _ = cmd.Help() return err } @@ -396,35 +104,6 @@ func newValidatorCreateCmd() *cobra.Command { return cmd } -func validateFlags(cfg createValidatorConfig) error { - var missingFlags []string - - if cfg.RPC == "" { - missingFlags = append(missingFlags, "rpc") - } - if cfg.ValidatorKeyFile == "" { - missingFlags = append(missingFlags, "keyfile") - } - if cfg.StakeAmount == "" { - missingFlags = append(missingFlags, "stake") - } - - if len(missingFlags) > 0 { - return fmt.Errorf("missing required flag(s): %s", strings.Join(missingFlags, ", ")) - } - - return nil -} - -func bindCreateValidatorConfig(cmd *cobra.Command, cfg *createValidatorConfig) { - cmd.Flags().StringVar(&cfg.RPC, "rpc", "https://rpc.partner.testnet.storyprotocol.net", "RPC URL to connect to the testnet") - addKeyFileFlag(cmd, &cfg.ValidatorKeyFile) - cmd.Flags().StringVar(&cfg.StakeAmount, "stake", "", "Amount for the validator to self-delegate in wei") - cmd.Flags().StringVar(&cfg.PrivateKey, "private-key", "", "Private key used to issue the validator creation transaction") - cmd.Flags().StringVar(&cfg.Explorer, "explorer", "https://explorer.testnet.storyprotocol.net", "URL of the blockchain explorer") - cmd.Flags().Int64Var(&cfg.ChainID, "chain-id", 1513, "Chain ID to use for the transaction (default 1513)") -} - func newValidatorStakeCmd() *cobra.Command { var cfg stakeConfig @@ -434,6 +113,7 @@ func newValidatorStakeCmd() *cobra.Command { Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { loadEnv() + fmt.Printf("Debug: RPC value before validation: '%s'\n", cfg.RPC) if err := validateStakeFlags(cfg); err != nil { fmt.Println("Debug: Entering cmd.Help()") _ = cmd.Help() // Print the help message @@ -450,35 +130,6 @@ func newValidatorStakeCmd() *cobra.Command { return cmd } -func validateStakeFlags(cfg stakeConfig) error { - var missingFlags []string - - if cfg.RPC == "" { - missingFlags = append(missingFlags, "rpc") - } - if cfg.ValidatorPubKey == "" { - missingFlags = append(missingFlags, "validator-pubkey") - } - if cfg.StakeAmount == "" { - missingFlags = append(missingFlags, "stake") - } - - if len(missingFlags) > 0 { - return fmt.Errorf("missing required flag(s): %s", strings.Join(missingFlags, ", ")) - } - - return nil -} - -func bindStakeConfig(cmd *cobra.Command, cfg *stakeConfig) { - cmd.Flags().StringVar(&cfg.RPC, "rpc", "https://rpc.partner.testnet.storyprotocol.net", "RPC URL to connect to the testnet") - cmd.Flags().StringVar(&cfg.ValidatorPubKey, "validator-pubkey", "", "Validator's 33 bytes compressed secp256k1 public key") - cmd.Flags().StringVar(&cfg.StakeAmount, "stake", "", "Amount to stake on behalf of the delegator in wei") - cmd.Flags().StringVar(&cfg.PrivateKey, "private-key", "", "Private key used to derive the delegator's public key") - cmd.Flags().StringVar(&cfg.Explorer, "explorer", "https://explorer.testnet.storyprotocol.net", "URL of the blockchain explorer") - cmd.Flags().Int64Var(&cfg.ChainID, "chain-id", 1513, "Chain ID to use for the transaction (default 1513)") -} - func newValidatorUnstakeCmd() *cobra.Command { var cfg unstakeConfig @@ -488,6 +139,8 @@ func newValidatorUnstakeCmd() *cobra.Command { Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { loadEnv() + fmt.Printf("Debug: RPC value before validation: '%s'\n", cfg.RPC) + if err := validateUnstakeFlags(cfg); err != nil { fmt.Println("Debug: Entering cmd.Help()") _ = cmd.Help() // Print the help message @@ -504,17 +157,30 @@ func newValidatorUnstakeCmd() *cobra.Command { return cmd } -func validateUnstakeFlags(cfg unstakeConfig) error { - var missingFlags []string +func newValidatorKeyExportCmd() *cobra.Command { + var keyFilePath string - if cfg.RPC == "" { - missingFlags = append(missingFlags, "rpc") - } - if cfg.ValidatorPubKey == "" { - missingFlags = append(missingFlags, "validator-pubkey") + cmd := &cobra.Command{ + Use: "export", + Short: "Export the EVM private key from the Tendermint key file", + RunE: func(_ *cobra.Command, _ []string) error { + loadEnv() + return validatorKeyExport(keyFilePath) + }, } - if cfg.UnstakeAmount == "" { - missingFlags = append(missingFlags, "unstake") + + bindKeyConfig(cmd, &keyFilePath) + + return cmd +} + +func validateFlags(flags map[string]string) error { + var missingFlags []string + + for flag, value := range flags { + if value == "" { + missingFlags = append(missingFlags, flag) + } } if len(missingFlags) > 0 { @@ -524,358 +190,184 @@ func validateUnstakeFlags(cfg unstakeConfig) error { return nil } +func validateCreateFlags(cfg createValidatorConfig) error { + return validateFlags(map[string]string{ + "rpc": cfg.RPC, + "keyfile": cfg.ValidatorKeyFile, + "stake": cfg.StakeAmount, + }) +} + +func validateStakeFlags(cfg stakeConfig) error { + return validateFlags(map[string]string{ + "rpc": cfg.RPC, + "validator-pubkey": cfg.ValidatorPubKey, + "stake": cfg.StakeAmount, + }) +} + +func validateUnstakeFlags(cfg unstakeConfig) error { + return validateFlags(map[string]string{ + "rpc": cfg.RPC, + "validator-pubkey": cfg.ValidatorPubKey, + "unstake": cfg.UnstakeAmount, + }) +} + +func bindBaseConfig(cmd *cobra.Command, cfg *baseConfig) { + cmd.Flags().StringVar(&cfg.RPC, "rpc", "https://testnet.storyrpc.io", "RPC URL to connect to the testnet") + cmd.Flags().StringVar(&cfg.PrivateKey, "private-key", "", "Private key used for the transaction") + cmd.Flags().StringVar(&cfg.Explorer, "explorer", "https://testnet.storyscan.xyz", "URL of the blockchain explorer") + cmd.Flags().Int64Var(&cfg.ChainID, "chain-id", 1513, "Chain ID to use for the transaction (default 1513)") +} + +func bindCreateValidatorConfig(cmd *cobra.Command, cfg *createValidatorConfig) { + bindBaseConfig(cmd, &cfg.baseConfig) + bindKeyConfig(cmd, &cfg.ValidatorKeyFile) + cmd.Flags().StringVar(&cfg.StakeAmount, "stake", "", "Amount for the validator to self-delegate in wei") +} + +func bindStakeConfig(cmd *cobra.Command, cfg *stakeConfig) { + bindBaseConfig(cmd, &cfg.baseConfig) + cmd.Flags().StringVar(&cfg.ValidatorPubKey, "validator-pubkey", "", "Validator's 33 bytes compressed secp256k1 public key") + cmd.Flags().StringVar(&cfg.StakeAmount, "stake", "", "Amount to stake on behalf of the delegator in wei") +} + func bindUnstakeConfig(cmd *cobra.Command, cfg *unstakeConfig) { - cmd.Flags().StringVar(&cfg.RPC, "rpc", "https://rpc.partner.testnet.storyprotocol.net", "RPC URL to connect to the testnet") + bindBaseConfig(cmd, &cfg.baseConfig) cmd.Flags().StringVar(&cfg.ValidatorPubKey, "validator-pubkey", "", "Validator's 33 bytes compressed secp256k1 public key") cmd.Flags().StringVar(&cfg.UnstakeAmount, "unstake", "", "Amount to unstake on behalf of the delegator in wei") - cmd.Flags().StringVar(&cfg.PrivateKey, "private-key", "", "Private key used to derive the delegator's public key") - cmd.Flags().StringVar(&cfg.Explorer, "explorer", "https://explorer.testnet.storyprotocol.net", "URL of the blockchain explorer") - cmd.Flags().Int64Var(&cfg.ChainID, "chain-id", 1513, "Chain ID to use for the transaction (default 1513)") } -func stakeTokens(ctx context.Context, cfg stakeConfig) error { - // Decode the validator public key - validatorPubKeyBytes := common.Hex2Bytes(cfg.ValidatorPubKey) - if len(validatorPubKeyBytes) != 33 { - return fmt.Errorf("invalid validator public key length: %d", len(validatorPubKeyBytes)) - } +func bindKeyConfig(cmd *cobra.Command, keyFilePath *string) { + defaultKeyFilePath := filepath.Join(config.DefaultHomeDir(), "config", "priv_validator_key.json") + cmd.Flags().StringVar(keyFilePath, "keyfile", defaultKeyFilePath, "Path to the Tendermint key file") +} - // Load the private key from the .env file if not provided as a flag - if cfg.PrivateKey == "" { - cfg.PrivateKey = os.Getenv("PRIVATE_KEY") - if cfg.PrivateKey == "" { - return errors.New("missing required flag", "private-key", "EVM private key") - } +func createValidator(ctx context.Context, cfg createValidatorConfig) error { + _, err := loadPrivateKey(&cfg.baseConfig) + if err != nil { + return err } - evmPrivKey, err := crypto.HexToECDSA(cfg.PrivateKey) + keyFileBytes, err := os.ReadFile(cfg.ValidatorKeyFile) if err != nil { - return errors.New("invalid EVM private key", err) + return errors.Wrap(err, "invalid key file") } - // Derive the delegator's uncompressed public key - pubKey := evmPrivKey.PublicKey - // lint:ignore SA1019 ignoring deprecation warning for now - uncompressedPubKey := elliptic.Marshal(pubKey.Curve, pubKey.X, pubKey.Y) - if len(uncompressedPubKey) != 65 { - return fmt.Errorf("invalid uncompressed public key length: %d", len(uncompressedPubKey)) + var keyFileData ValidatorKey + if err := json.Unmarshal(keyFileBytes, &keyFileData); err != nil { + return errors.Wrap(err, "failed to unmarshal priv_validator_key.json") } - fmt.Printf("Uncompressed Delegator PubKey: %x\n", uncompressedPubKey) - // Connect to the Ethereum client - client, err := ethclient.Dial(cfg.RPC) + uncompressedPubKeyHex, err := decodeAndUncompressPubKey(keyFileData.PubKey.Value) if err != nil { - return errors.Wrap(err, "failed to connect to Ethereum client") + return err } - - // Get the balance of the account - publicKey := evmPrivKey.Public() - publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) - if !ok { - return errors.New("error casting public key to ECDSA") - } - fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) - - // Fetch balance - balance, err := client.BalanceAt(ctx, fromAddress, nil) + uncompressedPubKeyBytes, err := hex.DecodeString(uncompressedPubKeyHex) if err != nil { - return errors.Wrap(err, "failed to fetch balance") + return errors.Wrap(err, "failed to decode uncompressed public key hex") } - fmt.Printf("Balance: %s wei\n", balance.String()) - - // Convert the stake amount to a big.Int stakeAmount, ok := new(big.Int).SetString(cfg.StakeAmount, 10) if !ok { return errors.New("invalid stake amount", "amount", cfg.StakeAmount) } - // Suggest gas price - gasPrice, err := client.SuggestGasPrice(ctx) - if err != nil { - return errors.Wrap(err, "failed to suggest gas price") - } - - // Increase gas price by 20% to ensure faster confirmation - gasPrice = new(big.Int).Mul(gasPrice, big.NewInt(120)) - gasPrice = new(big.Int).Div(gasPrice, big.NewInt(100)) - - // Read the ABI file from embedded content - contractABI, err := abi.JSON(strings.NewReader(string(ipTokenStakingABI))) - if err != nil { - return errors.Wrap(err, "failed to parse ABI") - } - - contractAddress := common.HexToAddress("0xCCcCcC0000000000000000000000000000000001") - - // Get the nonce for the transaction - nonce, err := client.PendingNonceAt(ctx, fromAddress) - if err != nil { - return errors.Wrap(err, "failed to get nonce") - } - - fmt.Printf("Using Nonce: %d\n", nonce) - - // Prepare transaction data - data, err := contractABI.Pack( - "stake", - uncompressedPubKey, - validatorPubKeyBytes, - ) - if err != nil { - return errors.Wrap(err, "failed to pack data") - } - - fmt.Printf("Packed Data: %x\n", data) - - chainID := big.NewInt(cfg.ChainID) - - // Estimate gas limit - msg := ethereum.CallMsg{ - From: fromAddress, - To: &contractAddress, - GasPrice: gasPrice, - Value: stakeAmount, - Data: data, - } - gasLimit, err := client.EstimateGas(ctx, msg) + err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "createValidatorOnBehalf", stakeAmount, uncompressedPubKeyBytes) if err != nil { - return errors.Wrap(err, "failed to estimate gas") + return err } - // Define gas fee cap and gas tip cap dynamically - gasTipCap := gasPrice - gasFeeCap := new(big.Int).Mul(gasPrice, big.NewInt(2)) - - gasCost := new(big.Int).Mul(big.NewInt(int64(gasLimit)), gasFeeCap) - totalTxCost := new(big.Int).Add(gasCost, stakeAmount) - - fmt.Printf("Stake Amount: %s wei\n", stakeAmount.String()) - fmt.Printf("Gas Limit: %d\n", gasLimit) - fmt.Printf("Gas Price: %s wei\n", gasPrice.String()) - fmt.Printf("Gas Tip Cap: %s wei\n", gasTipCap.String()) - fmt.Printf("Gas Fee Cap: %s wei\n", gasFeeCap.String()) - fmt.Printf("Gas Cost: %s wei\n", gasCost.String()) - fmt.Printf("Total Transaction Cost: %s wei\n", totalTxCost.String()) - - if balance.Cmp(totalTxCost) < 0 { - return errors.New("insufficient funds for gas * price + value", "balance", balance.String(), "totalTxCost", totalTxCost.String()) - } + fmt.Println("Validator created successfully!") - tx := types.NewTx(&types.DynamicFeeTx{ - ChainID: chainID, - Nonce: nonce, - GasFeeCap: gasFeeCap, - GasTipCap: gasTipCap, - Gas: gasLimit, - To: &contractAddress, - Value: stakeAmount, - Data: data, - }) + return nil +} - // Sign the transaction - signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), evmPrivKey) +func stakeTokens(ctx context.Context, cfg stakeConfig) error { + uncompressedPubKey, err := deriveUncompressedPublicKeyFromConfig(&cfg.baseConfig) if err != nil { - return errors.Wrap(err, "failed to sign transaction") + return err } - txHash := signedTx.Hash().Hex() - fmt.Printf("Transaction hash: %s\n", txHash) - fmt.Printf("Explorer URL: %s/tx/%s\n", cfg.Explorer, txHash) - - // Send the transaction - err = client.SendTransaction(ctx, signedTx) - if err != nil { - return errors.Wrap(err, "failed to send transaction") + stakeAmount, ok := new(big.Int).SetString(cfg.StakeAmount, 10) + if !ok { + return errors.New("invalid stake amount", "amount", cfg.StakeAmount) } - fmt.Println("Transaction sent, waiting for confirmation...") - - // Use bind.WaitMined to wait for the transaction receipt - receipt, err := bind.WaitMined(ctx, client, signedTx) + err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "stake", stakeAmount, uncompressedPubKey, common.Hex2Bytes(cfg.ValidatorPubKey)) if err != nil { - return errors.Wrap(err, "transaction failed") + return err } - if receipt.Status == types.ReceiptStatusFailed { - return errors.New("transaction failed", "status", receipt.Status) - } - - fmt.Println("Transaction confirmed successfully!") fmt.Println("Tokens staked successfully!") return nil } func unstakeTokens(ctx context.Context, cfg unstakeConfig) error { - // Decode the validator public key - validatorPubKeyBytes := common.Hex2Bytes(cfg.ValidatorPubKey) - if len(validatorPubKeyBytes) != 33 { - return fmt.Errorf("invalid validator public key length: %d", len(validatorPubKeyBytes)) - } - - // Load the private key from the .env file if not provided as a flag - if cfg.PrivateKey == "" { - cfg.PrivateKey = os.Getenv("PRIVATE_KEY") - if cfg.PrivateKey == "" { - return errors.New("missing required flag", "private-key", "EVM private key") - } - } - - evmPrivKey, err := crypto.HexToECDSA(cfg.PrivateKey) + uncompressedPubKey, err := deriveUncompressedPublicKeyFromConfig(&cfg.baseConfig) if err != nil { - return errors.Wrap(err, "invalid EVM private key") + return err } - // Derive the delegator's uncompressed public key - pubKey := evmPrivKey.PublicKey - // lint:ignore SA1019 ignoring deprecation warning for now - uncompressedPubKey := elliptic.Marshal(pubKey.Curve, pubKey.X, pubKey.Y) - if len(uncompressedPubKey) != 65 { - return fmt.Errorf("invalid uncompressed public key length: %d", len(uncompressedPubKey)) - } - fmt.Printf("Uncompressed Delegator PubKey: %x\n", uncompressedPubKey) - - // Convert the unstake amount to a big.Int unstakeAmount, ok := new(big.Int).SetString(cfg.UnstakeAmount, 10) if !ok { return errors.New("invalid unstake amount", "amount", cfg.UnstakeAmount) } - // Connect to the Ethereum client - client, err := ethclient.Dial(cfg.RPC) + err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "unstake", big.NewInt(0), uncompressedPubKey, common.Hex2Bytes(cfg.ValidatorPubKey), unstakeAmount) if err != nil { - return errors.Wrap(err, "failed to connect to Ethereum client") + return err } - // Get the balance of the account - publicKey := evmPrivKey.Public() - publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) - if !ok { - return errors.New("error casting public key to ECDSA") - } - - fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) - balance, err := client.BalanceAt(ctx, fromAddress, nil) - if err != nil { - return errors.Wrap(err, "failed to fetch balance") - } - - fmt.Printf("Balance: %s wei\n", balance.String()) - - // Suggest gas price - gasPrice, err := client.SuggestGasPrice(ctx) - if err != nil { - return errors.Wrap(err, "failed to suggest gas price") - } + fmt.Println("Tokens unstaked successfully!") - // Increase gas price by 20% to ensure faster confirmation - gasPrice = new(big.Int).Mul(gasPrice, big.NewInt(120)) - gasPrice = new(big.Int).Div(gasPrice, big.NewInt(100)) + return nil +} - // Read the ABI file from embedded content +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))) if err != nil { return errors.Wrap(err, "failed to parse ABI") } - - contractAddress := common.HexToAddress("0xCCcCcC0000000000000000000000000000000001") - - // Get the nonce for the transaction - nonce, err := client.PendingNonceAt(ctx, fromAddress) - if err != nil { - return errors.Wrap(err, "failed to get nonce") - } - - fmt.Printf("Using Nonce: %d\n", nonce) - - // Prepare transaction data - data, err := contractABI.Pack( - "unstake", - uncompressedPubKey, - validatorPubKeyBytes, - unstakeAmount, - ) + data, err := contractABI.Pack(methodName, args...) if err != nil { return errors.Wrap(err, "failed to pack data") } - fmt.Printf("Packed Data: %x\n", data) - - chainID := big.NewInt(cfg.ChainID) + return prepareAndSendTransaction(ctx, *cfg, contractAddress, value, data) +} - // Estimate gas limit - msg := ethereum.CallMsg{ - From: fromAddress, - To: &contractAddress, - GasPrice: gasPrice, - Data: data, - } - gasLimit, err := client.EstimateGas(ctx, msg) +func deriveUncompressedPublicKeyFromConfig(cfg *baseConfig) ([]byte, error) { + evmPrivKey, err := loadPrivateKey(cfg) if err != nil { - return errors.Wrap(err, "failed to estimate gas") - } - - // Define gas fee cap and gas tip cap dynamically - gasTipCap := gasPrice - gasFeeCap := new(big.Int).Mul(gasPrice, big.NewInt(2)) - - gasCost := new(big.Int).Mul(big.NewInt(int64(gasLimit)), gasFeeCap) - totalTxCost := gasCost // Only gas cost is considered - - fmt.Printf("Unstake Amount: %s wei\n", unstakeAmount.String()) - fmt.Printf("Gas Limit: %d\n", gasLimit) - fmt.Printf("Gas Price: %s wei\n", gasPrice.String()) - fmt.Printf("Gas Tip Cap: %s wei\n", gasTipCap.String()) - fmt.Printf("Gas Fee Cap: %s wei\n", gasFeeCap.String()) - fmt.Printf("Gas Cost: %s wei\n", gasCost.String()) - fmt.Printf("Total Transaction Cost: %s wei\n", totalTxCost.String()) - - if balance.Cmp(totalTxCost) < 0 { - return errors.New("insufficient funds for gas * price + value", "balance", balance.String(), "totalTxCost", totalTxCost.String()) + return nil, err } - tx := types.NewTx(&types.DynamicFeeTx{ - ChainID: chainID, - Nonce: nonce, - GasFeeCap: gasFeeCap, - GasTipCap: gasTipCap, - Gas: gasLimit, - To: &contractAddress, - Value: big.NewInt(0), // No value for unstake - Data: data, - }) - - // Sign the transaction - signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), evmPrivKey) + uncompressedPubKey, err := deriveUncompressedPublicKeyFromPrivateKey(evmPrivKey) if err != nil { - return errors.Wrap(err, "failed to sign transaction") + return nil, errors.Wrap(err, "failed to derive uncompressed public key") } - txHash := signedTx.Hash().Hex() - fmt.Printf("Transaction hash: %s\n", txHash) - fmt.Printf("Explorer URL: %s/tx/%s\n", cfg.Explorer, txHash) - - // Send the transaction - err = client.SendTransaction(ctx, signedTx) - if err != nil { - return errors.Wrap(err, "failed to send transaction") - } + fmt.Printf("Uncompressed Delegator PubKey: %x\n", uncompressedPubKey) - fmt.Println("Transaction sent, waiting for confirmation...") + return uncompressedPubKey, nil +} - // Use bind.WaitMined to wait for the transaction receipt - receipt, err := bind.WaitMined(ctx, client, signedTx) - if err != nil { - return errors.Wrap(err, "transaction failed") +func loadPrivateKey(cfg *baseConfig) (*ecdsa.PrivateKey, error) { + if cfg.PrivateKey == "" { + cfg.PrivateKey = os.Getenv("PRIVATE_KEY") + if cfg.PrivateKey == "" { + return nil, errors.New("missing required flag", "private-key", "EVM private key") + } } - if receipt.Status == types.ReceiptStatusFailed { - return errors.New("transaction failed", "status", receipt.Status) + evmPrivKey, err := crypto.HexToECDSA(cfg.PrivateKey) + if err != nil { + return nil, errors.Wrap(err, "invalid EVM private key") } - fmt.Println("Transaction confirmed successfully!") - fmt.Println("Tokens unstaked successfully!") - - return nil + return evmPrivKey, nil } From e12ac6fe9a7bdde27dd5088a2120ef4e65204f62 Mon Sep 17 00:00:00 2001 From: Leeren Date: Sat, 24 Aug 2024 05:26:38 -0700 Subject: [PATCH 3/9] chore(release): bump story to new stable release (#47) --- lib/buildinfo/buildinfo.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/buildinfo/buildinfo.go b/lib/buildinfo/buildinfo.go index 1a5601bc..c7edc55f 100644 --- a/lib/buildinfo/buildinfo.go +++ b/lib/buildinfo/buildinfo.go @@ -13,10 +13,10 @@ import ( ) const ( - VersionMajor = 0 // Major version component of the current release - VersionMinor = 9 // Minor version component of the current release - VersionPatch = 10 // Patch version component of the current release - VersionMeta = "unstable" // Version metadata to append to the version string + VersionMajor = 0 // Major version component of the current release + VersionMinor = 9 // Minor version component of the current release + VersionPatch = 10 // Patch version component of the current release + VersionMeta = "stable" // Version metadata to append to the version string ) // Version returns the version of the whole story-monorepo and all binaries built from this git commit. From 398936ed4f7b024a9548e2e28d04ed8c3f8e0e9e Mon Sep 17 00:00:00 2001 From: Leeren Date: Sat, 24 Aug 2024 11:12:52 -0700 Subject: [PATCH 4/9] chore(release): start new story unstable release (#48) --- lib/buildinfo/buildinfo.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/buildinfo/buildinfo.go b/lib/buildinfo/buildinfo.go index c7edc55f..c980047d 100644 --- a/lib/buildinfo/buildinfo.go +++ b/lib/buildinfo/buildinfo.go @@ -13,10 +13,10 @@ import ( ) const ( - VersionMajor = 0 // Major version component of the current release - VersionMinor = 9 // Minor version component of the current release - VersionPatch = 10 // Patch version component of the current release - VersionMeta = "stable" // Version metadata to append to the version string + VersionMajor = 0 // Major version component of the current release + VersionMinor = 9 // Minor version component of the current release + VersionPatch = 11 // Patch version component of the current release + VersionMeta = "unstable" // Version metadata to append to the version string ) // Version returns the version of the whole story-monorepo and all binaries built from this git commit. From 098343567a0313bc9b87ec7253fe839e54d93700 Mon Sep 17 00:00:00 2001 From: Leeren Date: Sat, 24 Aug 2024 23:42:27 -0700 Subject: [PATCH 5/9] fix(client/validator): use correct secp256k1 key uncompression (#51) --- client/cmd/key_utils.go | 12 ++++++------ go.mod | 2 ++ go.sum | 7 +++++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/client/cmd/key_utils.go b/client/cmd/key_utils.go index 5921ff8a..989b1e34 100644 --- a/client/cmd/key_utils.go +++ b/client/cmd/key_utils.go @@ -9,6 +9,7 @@ import ( "fmt" "os" + "github.com/decred/dcrd/dcrec/secp256k1" "github.com/ethereum/go-ethereum/crypto" "github.com/piplabs/story/lib/errors" @@ -30,17 +31,16 @@ func decodeAndUncompressPubKey(compressedPubKeyBase64 string) (string, error) { if err != nil { return "", errors.Wrap(err, "failed to decode base64 public key") } - if len(compressedPubKeyBytes) != 33 { + if len(compressedPubKeyBytes) != secp256k1.PubKeyBytesLenCompressed { return "", fmt.Errorf("invalid compressed public key length: %d", len(compressedPubKeyBytes)) } - curve := elliptic.P256() - x, y := elliptic.UnmarshalCompressed(curve, compressedPubKeyBytes) - if x == nil || y == nil { - return "", errors.New("failed to unmarshal compressed public key") + pubKey, err := secp256k1.ParsePubKey(compressedPubKeyBytes) + if err != nil { + return "", errors.Wrap(err, "failed to parse compressed public key") } - uncompressedPubKeyBytes := elliptic.Marshal(curve, x, y) + uncompressedPubKeyBytes := pubKey.SerializeUncompressed() uncompressedPubKeyHex := hex.EncodeToString(uncompressedPubKeyBytes) return uncompressedPubKeyHex, nil diff --git a/go.mod b/go.mod index 22d6dbdf..14816d3b 100644 --- a/go.mod +++ b/go.mod @@ -252,6 +252,7 @@ require ( require ( cosmossdk.io/x/upgrade v0.1.4 + github.com/decred/dcrd/dcrec/secp256k1 v1.0.4 github.com/go-playground/validator/v10 v10.11.1 github.com/joho/godotenv v1.5.1 ) @@ -264,6 +265,7 @@ require ( github.com/aws/aws-sdk-go v1.44.224 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/chzyer/readline v1.5.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0 // indirect github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect diff --git a/go.sum b/go.sum index efb0c824..b13d3bb4 100644 --- a/go.sum +++ b/go.sum @@ -425,8 +425,15 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2 h1:rt5Vlq/jM3ZawwiacWjPa+smINyLRN07EO0cNBV6DGU= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1 v1.0.4 h1:0XErmfJBiVbl0NvyclGn4jr+1hIylDf5beFi9W0o7Fc= +github.com/decred/dcrd/dcrec/secp256k1 v1.0.4/go.mod h1:00z7mJdugt+GBAzPN1QrDRGCXxyKUiexEHu6ukxEw3k= +github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0 h1:3GIJYXQDAKpLEFriGFN8SbSffak10UXHGdIcFaMPykY= +github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0/go.mod h1:3s92l0paYkZoIHuj4X93Teg/HB7eGM9x/zokGw+u4mY= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= From 23a658200ca439dc779470ab13dd5d86ec78dc05 Mon Sep 17 00:00:00 2001 From: Leeren Date: Sun, 25 Aug 2024 00:05:32 -0700 Subject: [PATCH 6/9] chore(client): update local genesis with convenient ip allocatios (#53) --- lib/netconf/local/genesis.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/netconf/local/genesis.json b/lib/netconf/local/genesis.json index b2afa19a..17e9ef01 100644 --- a/lib/netconf/local/genesis.json +++ b/lib/netconf/local/genesis.json @@ -88,7 +88,7 @@ }, "evmengine": { "params": { - "execution_block_hash": "TIMxsVqnHpuE5Xv8Jzd0ojukKn2E2BFVNhmIj+DXUnk=" + "execution_block_hash": "jz/Faw3DoAnmdx2pn/c3kA2oS5rq4V0briJcyLTbvcM=" } }, "genutil": { From 1a41f480c9097fc891a33ea5fe45835897bbc077 Mon Sep 17 00:00:00 2001 From: Leeren Date: Mon, 26 Aug 2024 01:12:02 -0700 Subject: [PATCH 7/9] feat(client): improve validator CLI operations (#58) --- client/cmd/flags.go | 143 +++++++++++- client/cmd/key_utils.go | 61 +---- client/cmd/transaction.go | 4 +- client/cmd/validator.go | 476 +++++++++++++++++++++++++++----------- 4 files changed, 494 insertions(+), 190 deletions(-) diff --git a/client/cmd/flags.go b/client/cmd/flags.go index 647a79b0..d4384376 100644 --- a/client/cmd/flags.go +++ b/client/cmd/flags.go @@ -1,16 +1,23 @@ package cmd import ( + "fmt" + "path/filepath" + "strings" + "github.com/spf13/cobra" "github.com/spf13/pflag" - storycfg "github.com/piplabs/story/client/config" + "github.com/piplabs/story/client/config" libcmd "github.com/piplabs/story/lib/cmd" "github.com/piplabs/story/lib/netconf" "github.com/piplabs/story/lib/tracer" + + // Used for ABI embedding of the staking contract. + _ "embed" ) -func bindRunFlags(cmd *cobra.Command, cfg *storycfg.Config) { +func bindRunFlags(cmd *cobra.Command, cfg *config.Config) { flags := cmd.Flags() libcmd.BindHomeFlag(flags, &cfg.HomeDir) @@ -42,3 +49,135 @@ func bindInitFlags(flags *pflag.FlagSet, cfg *InitConfig) { flags.BoolVar(&cfg.SeedMode, "seed-mode", false, "Enable seed mode") flags.StringVar(&cfg.Moniker, "moniker", cfg.Moniker, "Custom moniker name for this node") } + +func bindValidatorBaseFlags(cmd *cobra.Command, cfg *baseConfig) { + cmd.Flags().StringVar(&cfg.RPC, "rpc", "https://testnet.storyrpc.io", "RPC URL to connect to the testnet") + cmd.Flags().StringVar(&cfg.PrivateKey, "private-key", "", "Private key used for the transaction") + cmd.Flags().StringVar(&cfg.Explorer, "explorer", "https://testnet.storyscan.xyz", "URL of the blockchain explorer") + cmd.Flags().Int64Var(&cfg.ChainID, "chain-id", 1513, "Chain ID to use for the transaction (default 1513)") +} + +func bindValidatorCreateFlags(cmd *cobra.Command, cfg *createValidatorConfig) { + bindValidatorBaseFlags(cmd, &cfg.baseConfig) + bindValidatorKeyFlags(cmd, &cfg.ValidatorKeyFile) + cmd.Flags().StringVar(&cfg.StakeAmount, "stake", "", "Amount for the validator to self-delegate in wei") +} + +func bindAddOperatorFlags(cmd *cobra.Command, cfg *operatorConfig) { + bindValidatorBaseFlags(cmd, &cfg.baseConfig) + cmd.Flags().StringVar(&cfg.Operator, "operator", "", "Adds an operator to your delegator") +} + +func bindRemoveOperatorFlags(cmd *cobra.Command, cfg *operatorConfig) { + bindValidatorBaseFlags(cmd, &cfg.baseConfig) + cmd.Flags().StringVar(&cfg.Operator, "operator", "", "Removes an operator from your delegator") +} + +func bindSetWithdrawalAddressFlags(cmd *cobra.Command, cfg *withdrawalConfig) { + bindValidatorBaseFlags(cmd, &cfg.baseConfig) + cmd.Flags().StringVar(&cfg.WithdrawalAddress, "withdrawal-address", "", "Address to receive staking and reward withdrawals") +} + +func bindValidatorStakeFlags(cmd *cobra.Command, cfg *stakeConfig) { + bindValidatorBaseFlags(cmd, &cfg.baseConfig) + cmd.Flags().StringVar(&cfg.ValidatorPubKey, "validator-pubkey", "", "Validator's base64-encoded compressed 33-byte secp256k1 public key") + cmd.Flags().StringVar(&cfg.StakeAmount, "stake", "", "Amount to stake on behalf of the delegator in wei") +} + +func bindValidatorStakeOnBehalfFlags(cmd *cobra.Command, cfg *stakeConfig) { + bindValidatorBaseFlags(cmd, &cfg.baseConfig) + cmd.Flags().StringVar(&cfg.ValidatorPubKey, "validator-pubkey", "", "Validator's base64-encoded compressed 33-byte secp256k1 public key") + cmd.Flags().StringVar(&cfg.DelegatorPubKey, "delegator-pubkey", "", "Delegator's base64-encoded compressed 33-byte secp256k1 public key") + cmd.Flags().StringVar(&cfg.StakeAmount, "stake", "", "Amount to stake on behalf of the delegator in wei") +} + +func bindValidatorUnstakeFlags(cmd *cobra.Command, cfg *stakeConfig) { + bindValidatorBaseFlags(cmd, &cfg.baseConfig) + cmd.Flags().StringVar(&cfg.ValidatorPubKey, "validator-pubkey", "", "Validator's base64-encoded compressed 33-byte secp256k1 public key") + cmd.Flags().StringVar(&cfg.StakeAmount, "unstake", "", "Amount to unstake on behalf of the delegator in wei") +} + +func bindValidatorUnstakeOnBehalfFlags(cmd *cobra.Command, cfg *stakeConfig) { + bindValidatorBaseFlags(cmd, &cfg.baseConfig) + cmd.Flags().StringVar(&cfg.ValidatorPubKey, "validator-pubkey", "", "Validator's base64-encoded compressed 33-byte secp256k1 public key") + cmd.Flags().StringVar(&cfg.DelegatorPubKey, "delegator-pubkey", "", "Delegator's base64-encoded compressed 33-byte secp256k1 public key") + cmd.Flags().StringVar(&cfg.StakeAmount, "unstake", "", "Amount to unstake on behalf of the delegator in wei") +} + +func bindValidatorKeyExportFlags(cmd *cobra.Command, cfg *exportKeyConfig) { + bindValidatorKeyFlags(cmd, &cfg.ValidatorKeyFile) + defaultEVMKeyFilePath := filepath.Join(config.DefaultHomeDir(), "config", "private_key.txt") + cmd.Flags().BoolVar(&cfg.ExportEVMKey, "export-evm-key", false, "Export the EVM private key") + cmd.Flags().StringVar(&cfg.EvmKeyFile, "evm-key-path", defaultEVMKeyFilePath, "Path to save the exported EVM private key") +} + +func bindValidatorKeyFlags(cmd *cobra.Command, keyFilePath *string) { + defaultKeyFilePath := filepath.Join(config.DefaultHomeDir(), "config", "priv_validator_key.json") + cmd.Flags().StringVar(keyFilePath, "keyfile", defaultKeyFilePath, "Path to the Tendermint key file") +} + +// Flag Validation + +func validateFlags(flags map[string]string) error { + var missingFlags []string + + for flag, value := range flags { + if value == "" { + missingFlags = append(missingFlags, flag) + } + } + + if len(missingFlags) > 0 { + return fmt.Errorf("missing required flag(s): %s", strings.Join(missingFlags, ", ")) + } + + return nil +} + +func validateValidatorCreateFlags(cfg createValidatorConfig) error { + return validateFlags(map[string]string{ + "rpc": cfg.RPC, + "keyfile": cfg.ValidatorKeyFile, + "stake": cfg.StakeAmount, + }) +} + +func validateOperatorFlags(cfg operatorConfig) error { + return validateFlags(map[string]string{ + "rpc": cfg.RPC, + "operator": cfg.Operator, + }) +} + +func validateWithdrawalFlags(cfg withdrawalConfig) error { + return validateFlags(map[string]string{ + "rpc": cfg.RPC, + "withdrawal-address": cfg.WithdrawalAddress, + }) +} + +func validateValidatorStakeFlags(cfg stakeConfig) error { + return validateFlags(map[string]string{ + "rpc": cfg.RPC, + "validator-pubkey": cfg.ValidatorPubKey, + "stake": cfg.StakeAmount, + }) +} + +func validateValidatorStakeOnBehalfFlags(cfg stakeConfig) error { + return validateFlags(map[string]string{ + "rpc": cfg.RPC, + "validator-pubkey": cfg.ValidatorPubKey, + "delegator-pubkey": cfg.DelegatorPubKey, + "stake": cfg.StakeAmount, + }) +} + +func validateValidatorUnstakeOnBehalfFlags(cfg stakeConfig) error { + return validateFlags(map[string]string{ + "rpc": cfg.RPC, + "validator-pubkey": cfg.ValidatorPubKey, + "delegator-pubkey": cfg.DelegatorPubKey, + "unstake": cfg.StakeAmount, + }) +} diff --git a/client/cmd/key_utils.go b/client/cmd/key_utils.go index 989b1e34..cb5b0746 100644 --- a/client/cmd/key_utils.go +++ b/client/cmd/key_utils.go @@ -1,13 +1,10 @@ package cmd import ( - "crypto/ecdsa" "crypto/elliptic" "encoding/base64" "encoding/hex" - "encoding/json" "fmt" - "os" "github.com/decred/dcrd/dcrec/secp256k1" "github.com/ethereum/go-ethereum/crypto" @@ -26,7 +23,7 @@ type KeyInfo struct { Value string `json:"value"` } -func decodeAndUncompressPubKey(compressedPubKeyBase64 string) (string, error) { +func uncompressPubKey(compressedPubKeyBase64 string) (string, error) { compressedPubKeyBytes, err := base64.StdEncoding.DecodeString(compressedPubKeyBase64) if err != nil { return "", errors.Wrap(err, "failed to decode base64 public key") @@ -46,7 +43,12 @@ func decodeAndUncompressPubKey(compressedPubKeyBase64 string) (string, error) { return uncompressedPubKeyHex, nil } -func deriveUncompressedPublicKeyFromPrivateKey(evmPrivKey *ecdsa.PrivateKey) ([]byte, error) { +func uncompressPrivateKey(privateKeyHex string) ([]byte, error) { + evmPrivKey, err := crypto.HexToECDSA(privateKeyHex) + if err != nil { + return nil, errors.Wrap(err, "invalid EVM private key") + } + pubKey := evmPrivKey.PublicKey uncompressedPubKey := elliptic.Marshal(pubKey.Curve, pubKey.X, pubKey.Y) if len(uncompressedPubKey) != 65 { @@ -55,52 +57,3 @@ func deriveUncompressedPublicKeyFromPrivateKey(evmPrivKey *ecdsa.PrivateKey) ([] return uncompressedPubKey, nil } - -func validatorKeyExport(keyFilePath string) error { - keyFileBytes, err := os.ReadFile(keyFilePath) - if err != nil { - return errors.Wrap(err, "failed to read key file") - } - - var keyData ValidatorKey - if err := json.Unmarshal(keyFileBytes, &keyData); err != nil { - return errors.Wrap(err, "failed to unmarshal key file") - } - - privKeyBytes, err := base64.StdEncoding.DecodeString(keyData.PrivKey.Value) - if err != nil { - return errors.Wrap(err, "failed to decode private key") - } - - privateKey, err := crypto.ToECDSA(privKeyBytes) - if err != nil { - return errors.Wrap(err, "invalid private key") - } - - publicKey, ok := privateKey.Public().(*ecdsa.PublicKey) - if !ok { - return errors.New("failed to cast public key to ecdsa.PublicKey") - } - evmPublicKey := crypto.PubkeyToAddress(*publicKey).Hex() - - // Handle the compressed public key - compressedPubKeyBytes, err := base64.StdEncoding.DecodeString(keyData.PubKey.Value) - if err != nil { - return errors.Wrap(err, "failed to decode base64 public key") - } - compressedPubKeyHex := hex.EncodeToString(compressedPubKeyBytes) - - // Get the uncompressed public key using the refactored function - uncompressedPubKeyHex, err := decodeAndUncompressPubKey(keyData.PubKey.Value) - if err != nil { - return err - } - - fmt.Println("------------------------------------------------------") - fmt.Println("EVM Public Key:", evmPublicKey) - fmt.Println("Compressed Public Key:", compressedPubKeyHex) - fmt.Println("Uncompressed Public Key:", uncompressedPubKeyHex) - fmt.Println("------------------------------------------------------") - - return nil -} diff --git a/client/cmd/transaction.go b/client/cmd/transaction.go index f54f4801..b996350e 100644 --- a/client/cmd/transaction.go +++ b/client/cmd/transaction.go @@ -42,8 +42,8 @@ func prepareAndSendTransaction(ctx context.Context, cfg baseConfig, contractAddr if err != nil { return errors.Wrap(err, "failed to suggest gas price") } - gasPrice = new(big.Int).Mul(gasPrice, big.NewInt(120)) - gasPrice = new(big.Int).Div(gasPrice, big.NewInt(100)) + + gasPrice.Mul(gasPrice, big.NewInt(120)).Div(gasPrice, big.NewInt(100)) gasLimit, err := estimateGas(ctx, client, fromAddress, contractAddress, gasPrice, value, data) if err != nil { diff --git a/client/cmd/validator.go b/client/cmd/validator.go index 8a62755f..18e16763 100644 --- a/client/cmd/validator.go +++ b/client/cmd/validator.go @@ -3,12 +3,12 @@ package cmd import ( "context" "crypto/ecdsa" + "encoding/base64" "encoding/hex" "encoding/json" "fmt" "math/big" "os" - "path/filepath" "strings" "github.com/ethereum/go-ethereum/accounts/abi" @@ -17,7 +17,6 @@ import ( "github.com/joho/godotenv" "github.com/spf13/cobra" - "github.com/piplabs/story/client/config" "github.com/piplabs/story/lib/errors" _ "embed" @@ -39,14 +38,19 @@ type baseConfig struct { type stakeConfig struct { baseConfig + DelegatorPubKey string ValidatorPubKey string StakeAmount string } -type unstakeConfig struct { +type operatorConfig struct { baseConfig - ValidatorPubKey string - UnstakeAmount string + Operator string +} + +type withdrawalConfig struct { + baseConfig + WithdrawalAddress string } type createValidatorConfig struct { @@ -55,6 +59,12 @@ type createValidatorConfig struct { StakeAmount string } +type exportKeyConfig struct { + ValidatorKeyFile string + EvmKeyFile string + ExportEVMKey bool +} + func loadEnv() { err := godotenv.Load() if err != nil { @@ -73,7 +83,12 @@ func newValidatorCmds() *cobra.Command { newValidatorCreateCmd(), newValidatorKeyExportCmd(), newValidatorStakeCmd(), + newValidatorStakeOnBehalfCmd(), newValidatorUnstakeCmd(), + newValidatorUnstakeOnBehalfCmd(), + newValidatorAddOperatorCmd(), + newValidatorRemoveOperatorCmd(), + newValidatorSetWithdrawalAddressCmd(), ) return cmd @@ -86,20 +101,79 @@ func newValidatorCreateCmd() *cobra.Command { Use: "create", Short: "Create a new validator", Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { - loadEnv() - if err := validateCreateFlags(cfg); err != nil { - fmt.Println("Debug: Entering cmd.Help()") - _ = cmd.Help() + PreRunE: func(_ *cobra.Command, _ []string) error { + return loadAndValidatePrivateKey(&cfg.baseConfig) + }, + RunE: runValidatorCommand( + func() error { return validateValidatorCreateFlags(cfg) }, + func(ctx context.Context) error { return createValidator(ctx, cfg) }, + ), + } + + bindValidatorCreateFlags(cmd, &cfg) + + return cmd +} + +func newValidatorAddOperatorCmd() *cobra.Command { + var cfg operatorConfig + + cmd := &cobra.Command{ + Use: "add-operator", + Short: "Add a new operator to your delegator", + Args: cobra.NoArgs, + PreRunE: func(_ *cobra.Command, _ []string) error { + return loadAndValidatePrivateKey(&cfg.baseConfig) + }, + RunE: runValidatorCommand( + func() error { return validateOperatorFlags(cfg) }, + func(ctx context.Context) error { return addOperator(ctx, cfg) }, + ), + } + + bindAddOperatorFlags(cmd, &cfg) + + return cmd +} + +func newValidatorRemoveOperatorCmd() *cobra.Command { + var cfg operatorConfig + + cmd := &cobra.Command{ + Use: "remove-operator", + Short: "Removes an existing operator from your delegator", + Args: cobra.NoArgs, + PreRunE: func(_ *cobra.Command, _ []string) error { + return loadAndValidatePrivateKey(&cfg.baseConfig) + }, + RunE: runValidatorCommand( + func() error { return validateOperatorFlags(cfg) }, + func(ctx context.Context) error { return removeOperator(ctx, cfg) }, + ), + } - return err - } + bindRemoveOperatorFlags(cmd, &cfg) - return createValidator(cmd.Context(), cfg) + return cmd +} + +func newValidatorSetWithdrawalAddressCmd() *cobra.Command { + var cfg withdrawalConfig + + cmd := &cobra.Command{ + Use: "set-withdrawal-address", + Short: "Updates the withdrawal address that receives stake and reward withdrawals", + Args: cobra.NoArgs, + PreRunE: func(_ *cobra.Command, _ []string) error { + return loadAndValidatePrivateKey(&cfg.baseConfig) }, + RunE: runValidatorCommand( + func() error { return validateWithdrawalFlags(cfg) }, + func(ctx context.Context) error { return setWithdrawalAddress(ctx, cfg) }, + ), } - bindCreateValidatorConfig(cmd, &cfg) + bindSetWithdrawalAddressFlags(cmd, &cfg) return cmd } @@ -109,147 +183,180 @@ func newValidatorStakeCmd() *cobra.Command { cmd := &cobra.Command{ Use: "stake", - Short: "Stake tokens on behalf of a delegator", + Short: "Stake tokens as the delegator", Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { - loadEnv() - fmt.Printf("Debug: RPC value before validation: '%s'\n", cfg.RPC) - if err := validateStakeFlags(cfg); err != nil { - fmt.Println("Debug: Entering cmd.Help()") - _ = cmd.Help() // Print the help message + PreRunE: func(_ *cobra.Command, _ []string) error { + return loadAndValidatePrivateKey(&cfg.baseConfig) + }, + RunE: runValidatorCommand( + func() error { + return validateValidatorStakeFlags(cfg) + }, + func(ctx context.Context) error { return stake(ctx, cfg) }, + ), + } - return err - } + bindValidatorStakeFlags(cmd, &cfg) + + return cmd +} - return stakeTokens(cmd.Context(), cfg) +func newValidatorStakeOnBehalfCmd() *cobra.Command { + var cfg stakeConfig + + cmd := &cobra.Command{ + Use: "stake-on-behalf", + Short: "Stake tokens on behalf of a delegator", + Args: cobra.NoArgs, + PreRunE: func(_ *cobra.Command, _ []string) error { + return loadAndValidatePrivateKey(&cfg.baseConfig) }, + RunE: runValidatorCommand( + func() error { + return validateValidatorStakeOnBehalfFlags(cfg) + }, + func(ctx context.Context) error { return stakeOnBehalf(ctx, cfg) }, + ), } - bindStakeConfig(cmd, &cfg) + bindValidatorStakeOnBehalfFlags(cmd, &cfg) return cmd } func newValidatorUnstakeCmd() *cobra.Command { - var cfg unstakeConfig + var cfg stakeConfig cmd := &cobra.Command{ Use: "unstake", - Short: "Unstake tokens on behalf of a delegator", + Short: "Unstake tokens as the delegator", Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { - loadEnv() - fmt.Printf("Debug: RPC value before validation: '%s'\n", cfg.RPC) + PreRunE: func(_ *cobra.Command, _ []string) error { + return loadAndValidatePrivateKey(&cfg.baseConfig) + }, + RunE: runValidatorCommand( + func() error { return validateValidatorStakeFlags(cfg) }, + func(ctx context.Context) error { return unstake(ctx, cfg) }, + ), + } + + bindValidatorUnstakeFlags(cmd, &cfg) - if err := validateUnstakeFlags(cfg); err != nil { - fmt.Println("Debug: Entering cmd.Help()") - _ = cmd.Help() // Print the help message + return cmd +} - return err - } +func newValidatorUnstakeOnBehalfCmd() *cobra.Command { + var cfg stakeConfig - return unstakeTokens(cmd.Context(), cfg) + cmd := &cobra.Command{ + Use: "unstake-on-behalf", + Short: "Unstake tokens on behalf of a delegator", + Args: cobra.NoArgs, + PreRunE: func(_ *cobra.Command, _ []string) error { + return loadAndValidatePrivateKey(&cfg.baseConfig) }, + RunE: runValidatorCommand( + func() error { return validateValidatorUnstakeOnBehalfFlags(cfg) }, + func(ctx context.Context) error { return unstakeOnBehalf(ctx, cfg) }, + ), } - bindUnstakeConfig(cmd, &cfg) + bindValidatorUnstakeOnBehalfFlags(cmd, &cfg) return cmd } func newValidatorKeyExportCmd() *cobra.Command { - var keyFilePath string + var cfg exportKeyConfig cmd := &cobra.Command{ Use: "export", Short: "Export the EVM private key from the Tendermint key file", - RunE: func(_ *cobra.Command, _ []string) error { - loadEnv() - return validatorKeyExport(keyFilePath) - }, + RunE: runValidatorCommand( + func() error { return nil }, + func(ctx context.Context) error { return exportKey(ctx, cfg) }, + ), } - bindKeyConfig(cmd, &keyFilePath) + bindValidatorKeyExportFlags(cmd, &cfg) return cmd } -func validateFlags(flags map[string]string) error { - var missingFlags []string - - for flag, value := range flags { - if value == "" { - missingFlags = append(missingFlags, flag) +func runValidatorCommand( + validate func() error, + execute func(ctx context.Context) error, +) func(cmd *cobra.Command, _ []string) error { + return func(cmd *cobra.Command, _ []string) error { + if err := validate(); err != nil { + _ = cmd.Help() + return err } + + return execute(cmd.Context()) } +} - if len(missingFlags) > 0 { - return fmt.Errorf("missing required flag(s): %s", strings.Join(missingFlags, ", ")) +func exportKey(_ context.Context, cfg exportKeyConfig) error { + keyFileBytes, err := os.ReadFile(cfg.ValidatorKeyFile) + if err != nil { + return errors.Wrap(err, "failed to read key file") } - return nil -} + var keyData ValidatorKey + if err := json.Unmarshal(keyFileBytes, &keyData); err != nil { + return errors.Wrap(err, "failed to unmarshal key file") + } -func validateCreateFlags(cfg createValidatorConfig) error { - return validateFlags(map[string]string{ - "rpc": cfg.RPC, - "keyfile": cfg.ValidatorKeyFile, - "stake": cfg.StakeAmount, - }) -} + privKeyBytes, err := base64.StdEncoding.DecodeString(keyData.PrivKey.Value) + if err != nil { + return errors.Wrap(err, "failed to decode private key") + } -func validateStakeFlags(cfg stakeConfig) error { - return validateFlags(map[string]string{ - "rpc": cfg.RPC, - "validator-pubkey": cfg.ValidatorPubKey, - "stake": cfg.StakeAmount, - }) -} + privateKey, err := crypto.ToECDSA(privKeyBytes) + if err != nil { + return errors.Wrap(err, "invalid private key") + } -func validateUnstakeFlags(cfg unstakeConfig) error { - return validateFlags(map[string]string{ - "rpc": cfg.RPC, - "validator-pubkey": cfg.ValidatorPubKey, - "unstake": cfg.UnstakeAmount, - }) -} + publicKey, ok := privateKey.Public().(*ecdsa.PublicKey) + if !ok { + return errors.New("failed to cast public key to ecdsa.PublicKey") + } + evmPublicKey := crypto.PubkeyToAddress(*publicKey).Hex() -func bindBaseConfig(cmd *cobra.Command, cfg *baseConfig) { - cmd.Flags().StringVar(&cfg.RPC, "rpc", "https://testnet.storyrpc.io", "RPC URL to connect to the testnet") - cmd.Flags().StringVar(&cfg.PrivateKey, "private-key", "", "Private key used for the transaction") - cmd.Flags().StringVar(&cfg.Explorer, "explorer", "https://testnet.storyscan.xyz", "URL of the blockchain explorer") - cmd.Flags().Int64Var(&cfg.ChainID, "chain-id", 1513, "Chain ID to use for the transaction (default 1513)") -} + compressedPubKeyBytes, err := base64.StdEncoding.DecodeString(keyData.PubKey.Value) + if err != nil { + return errors.Wrap(err, "failed to decode base64 pub key") + } + compressedPubKeyHex := hex.EncodeToString(compressedPubKeyBytes) -func bindCreateValidatorConfig(cmd *cobra.Command, cfg *createValidatorConfig) { - bindBaseConfig(cmd, &cfg.baseConfig) - bindKeyConfig(cmd, &cfg.ValidatorKeyFile) - cmd.Flags().StringVar(&cfg.StakeAmount, "stake", "", "Amount for the validator to self-delegate in wei") -} + uncompressedPubKeyHex, err := uncompressPubKey(keyData.PubKey.Value) + if err != nil { + return err + } -func bindStakeConfig(cmd *cobra.Command, cfg *stakeConfig) { - bindBaseConfig(cmd, &cfg.baseConfig) - cmd.Flags().StringVar(&cfg.ValidatorPubKey, "validator-pubkey", "", "Validator's 33 bytes compressed secp256k1 public key") - cmd.Flags().StringVar(&cfg.StakeAmount, "stake", "", "Amount to stake on behalf of the delegator in wei") -} + fmt.Println("------------------------------------------------------") + fmt.Println("EVM Public Key:", evmPublicKey) + fmt.Println("Compressed Public Key (base64):", keyData.PubKey.Value) + fmt.Println("Compressed Public Key (hex):", compressedPubKeyHex) + fmt.Println("Uncompressed Public Key:", uncompressedPubKeyHex) + fmt.Println("------------------------------------------------------") + + if cfg.ExportEVMKey { + evmPrivateKey := hex.EncodeToString(crypto.FromECDSA(privateKey)) + keyContent := "PRIVATE_KEY=" + evmPrivateKey + if err := os.WriteFile(cfg.EvmKeyFile, []byte(keyContent), 0600); err != nil { + return errors.Wrap(err, "failed to export private key") + } -func bindUnstakeConfig(cmd *cobra.Command, cfg *unstakeConfig) { - bindBaseConfig(cmd, &cfg.baseConfig) - cmd.Flags().StringVar(&cfg.ValidatorPubKey, "validator-pubkey", "", "Validator's 33 bytes compressed secp256k1 public key") - cmd.Flags().StringVar(&cfg.UnstakeAmount, "unstake", "", "Amount to unstake on behalf of the delegator in wei") -} + fmt.Printf("EVM Private Key saved to: %s\n", cfg.EvmKeyFile) + fmt.Println("WARNING: The EVM private key is highly sensitive. Store this file in a secure location.") + } -func bindKeyConfig(cmd *cobra.Command, keyFilePath *string) { - defaultKeyFilePath := filepath.Join(config.DefaultHomeDir(), "config", "priv_validator_key.json") - cmd.Flags().StringVar(keyFilePath, "keyfile", defaultKeyFilePath, "Path to the Tendermint key file") + return nil } func createValidator(ctx context.Context, cfg createValidatorConfig) error { - _, err := loadPrivateKey(&cfg.baseConfig) - if err != nil { - return err - } - keyFileBytes, err := os.ReadFile(cfg.ValidatorKeyFile) if err != nil { return errors.Wrap(err, "invalid key file") @@ -260,7 +367,7 @@ func createValidator(ctx context.Context, cfg createValidatorConfig) error { return errors.Wrap(err, "failed to unmarshal priv_validator_key.json") } - uncompressedPubKeyHex, err := decodeAndUncompressPubKey(keyFileData.PubKey.Value) + uncompressedPubKeyHex, err := uncompressPubKey(keyFileData.PubKey.Value) if err != nil { return err } @@ -284,18 +391,77 @@ func createValidator(ctx context.Context, cfg createValidatorConfig) error { return nil } -func stakeTokens(ctx context.Context, cfg stakeConfig) error { - uncompressedPubKey, err := deriveUncompressedPublicKeyFromConfig(&cfg.baseConfig) +func setWithdrawalAddress(ctx context.Context, cfg withdrawalConfig) error { + uncompressedPubKey, err := uncompressPrivateKey(cfg.PrivateKey) if err != nil { return err } + withdrawalAddress := common.HexToAddress(cfg.WithdrawalAddress) + + err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "setWithdrawalAddress", big.NewInt(0), uncompressedPubKey, withdrawalAddress) + if err != nil { + return err + } + + fmt.Println("Withdrawal address successfully set!") + + return nil +} + +func addOperator(ctx context.Context, cfg operatorConfig) error { + uncompressedPubKey, err := uncompressPrivateKey(cfg.PrivateKey) + if err != nil { + return err + } + + operatorAddress := common.HexToAddress(cfg.Operator) + + err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "addOperator", big.NewInt(0), uncompressedPubKey, operatorAddress) + if err != nil { + return err + } + + fmt.Println("Operator added successfully!") + + return nil +} + +func removeOperator(ctx context.Context, cfg operatorConfig) error { + uncompressedPubKey, err := uncompressPrivateKey(cfg.PrivateKey) + if err != nil { + return err + } + + operatorAddress := common.HexToAddress(cfg.Operator) + + err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "removeOperator", big.NewInt(0), uncompressedPubKey, operatorAddress) + if err != nil { + return err + } + + fmt.Println("Operator removed successfully!") + + return nil +} + +func stake(ctx context.Context, cfg stakeConfig) error { + uncompressedPubKey, err := uncompressPrivateKey(cfg.PrivateKey) + if err != nil { + return err + } + + validatorPubKeyBytes, err := base64.StdEncoding.DecodeString(cfg.ValidatorPubKey) + if err != nil { + return errors.Wrap(err, "failed to decode base64 pub key") + } + stakeAmount, ok := new(big.Int).SetString(cfg.StakeAmount, 10) if !ok { return errors.New("invalid stake amount", "amount", cfg.StakeAmount) } - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "stake", stakeAmount, uncompressedPubKey, common.Hex2Bytes(cfg.ValidatorPubKey)) + err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "stakeOnBehalf", stakeAmount, uncompressedPubKey, validatorPubKeyBytes) if err != nil { return err } @@ -305,69 +471,115 @@ func stakeTokens(ctx context.Context, cfg stakeConfig) error { return nil } -func unstakeTokens(ctx context.Context, cfg unstakeConfig) error { - uncompressedPubKey, err := deriveUncompressedPublicKeyFromConfig(&cfg.baseConfig) +func stakeOnBehalf(ctx context.Context, cfg stakeConfig) error { + uncompressedDelegatorPubKeyHex, err := uncompressPubKey(cfg.DelegatorPubKey) if err != nil { return err } + uncompressedDelegatorPubKeyBytes, err := hex.DecodeString(uncompressedDelegatorPubKeyHex) + if err != nil { + return errors.Wrap(err, "failed to decode uncompressed delegator public key") + } + + validatorPubKeyBytes, err := base64.StdEncoding.DecodeString(cfg.ValidatorPubKey) + if err != nil { + return errors.Wrap(err, "failed to decode validator public key") + } - unstakeAmount, ok := new(big.Int).SetString(cfg.UnstakeAmount, 10) + stakeAmount, ok := new(big.Int).SetString(cfg.StakeAmount, 10) if !ok { - return errors.New("invalid unstake amount", "amount", cfg.UnstakeAmount) + return errors.New("invalid stake amount", "amount", cfg.StakeAmount) } - err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "unstake", big.NewInt(0), uncompressedPubKey, common.Hex2Bytes(cfg.ValidatorPubKey), unstakeAmount) + err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "stakeOnBehalf", stakeAmount, uncompressedDelegatorPubKeyBytes, validatorPubKeyBytes) if err != nil { return err } - fmt.Println("Tokens unstaked successfully!") + fmt.Println("Tokens staked on behalf of delegator successfully!") 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 unstake(ctx context.Context, cfg stakeConfig) error { + uncompressedPubKey, err := uncompressPrivateKey(cfg.PrivateKey) if err != nil { - return errors.Wrap(err, "failed to parse ABI") + return err } - data, err := contractABI.Pack(methodName, args...) + + validatorPubKeyBytes, err := base64.StdEncoding.DecodeString(cfg.ValidatorPubKey) if err != nil { - return errors.Wrap(err, "failed to pack data") + return errors.Wrap(err, "failed to decode base64 pub key") } - return prepareAndSendTransaction(ctx, *cfg, contractAddress, value, data) + unstakeAmount, ok := new(big.Int).SetString(cfg.StakeAmount, 10) + if !ok { + return errors.New("invalid unstake amount", "amount", cfg.StakeAmount) + } + + err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "unstake", big.NewInt(0), uncompressedPubKey, validatorPubKeyBytes, unstakeAmount) + if err != nil { + return err + } + + fmt.Println("Tokens unstaked successfully!") + + return nil } -func deriveUncompressedPublicKeyFromConfig(cfg *baseConfig) ([]byte, error) { - evmPrivKey, err := loadPrivateKey(cfg) +func unstakeOnBehalf(ctx context.Context, cfg stakeConfig) error { + delegatorPubKeyBytes, err := base64.StdEncoding.DecodeString(cfg.DelegatorPubKey) if err != nil { - return nil, err + return errors.Wrap(err, "failed to decode base64 delegator pub key") } - uncompressedPubKey, err := deriveUncompressedPublicKeyFromPrivateKey(evmPrivKey) + validatorPubKeyBytes, err := base64.StdEncoding.DecodeString(cfg.ValidatorPubKey) if err != nil { - return nil, errors.Wrap(err, "failed to derive uncompressed public key") + return errors.Wrap(err, "failed to decode base64 validator pub key") } - fmt.Printf("Uncompressed Delegator PubKey: %x\n", uncompressedPubKey) + unstakeAmount, ok := new(big.Int).SetString(cfg.StakeAmount, 10) + if !ok { + return errors.New("invalid unstake amount", "amount", cfg.StakeAmount) + } + + err = prepareAndExecuteTransaction(ctx, &cfg.baseConfig, "unstakeOnBehalf", big.NewInt(0), delegatorPubKeyBytes, validatorPubKeyBytes, unstakeAmount) + if err != nil { + return err + } + + fmt.Println("Tokens unstaked on behalf of delegator successfully!") + + 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))) + if err != nil { + return errors.Wrap(err, "failed to parse ABI") + } + data, err := contractABI.Pack(methodName, args...) + if err != nil { + return errors.Wrap(err, "failed to pack data") + } - return uncompressedPubKey, nil + return prepareAndSendTransaction(ctx, *cfg, contractAddress, value, data) } -func loadPrivateKey(cfg *baseConfig) (*ecdsa.PrivateKey, error) { +func loadAndValidatePrivateKey(cfg *baseConfig) error { if cfg.PrivateKey == "" { + loadEnv() cfg.PrivateKey = os.Getenv("PRIVATE_KEY") if cfg.PrivateKey == "" { - return nil, errors.New("missing required flag", "private-key", "EVM private key") + return errors.New("missing required flag", "private-key", "EVM private key") } } - evmPrivKey, err := crypto.HexToECDSA(cfg.PrivateKey) + _, err := crypto.HexToECDSA(cfg.PrivateKey) if err != nil { - return nil, errors.Wrap(err, "invalid EVM private key") + return errors.Wrap(err, "invalid EVM private key") } - return evmPrivKey, nil + return nil } From 2eab69d90acead75e7d279f8314c7dc155e1e7f9 Mon Sep 17 00:00:00 2001 From: Leeren Date: Mon, 26 Aug 2024 01:15:39 -0700 Subject: [PATCH 8/9] chore(lib/netconf): update iliad default config (#59) --- lib/netconf/iliad/genesis.json | 4 ++-- lib/netconf/iliad/seeds.txt | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/netconf/iliad/genesis.json b/lib/netconf/iliad/genesis.json index 760550a8..1851eb7c 100644 --- a/lib/netconf/iliad/genesis.json +++ b/lib/netconf/iliad/genesis.json @@ -1,6 +1,6 @@ { "genesis_time": "2024-04-16T11:04:40.60280319Z", - "chain_id": "iliad-1", + "chain_id": "iliad-0", "initial_height": "1", "consensus_params": { "block": { @@ -200,7 +200,7 @@ }, "evmengine": { "params": { - "execution_block_hash": "9ohUkVHO40twer1JwywBmu+3ZtcBSI3QxmhgGpGmeXg=" + "execution_block_hash": "9evG8Jgui/f9Uys/lZ+E0Ss97dKCevjTH1OJRHvtr8Y=" } }, "genutil": { diff --git a/lib/netconf/iliad/seeds.txt b/lib/netconf/iliad/seeds.txt index a9fbc5c1..f96eef4c 100644 --- a/lib/netconf/iliad/seeds.txt +++ b/lib/netconf/iliad/seeds.txt @@ -1,3 +1,2 @@ -112f9449305b9ee5293836a073aed5e390578c61@3.209.222.188:26656 -0aa6f0e7359ed338c4d88d123874cf5eb96925af@54.183.204.164:26656 - +5a0191a6bd8f17c9d2fa52386ff409f5d796d112@b1.testnet.storyrpc.io:26656 +0e2f0d4b5204e5e92a994a1eaa745b9ccb1d747b@b2.testnet.storyrpc.io:26656 From 2a25df1094254e5337ee801817c1f3ec484f7e26 Mon Sep 17 00:00:00 2001 From: Leeren Date: Mon, 26 Aug 2024 01:23:58 -0700 Subject: [PATCH 9/9] chore(release): bump client to 0.9.11 stable (#60) --- lib/buildinfo/buildinfo.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/buildinfo/buildinfo.go b/lib/buildinfo/buildinfo.go index c980047d..5a139120 100644 --- a/lib/buildinfo/buildinfo.go +++ b/lib/buildinfo/buildinfo.go @@ -13,10 +13,10 @@ import ( ) const ( - VersionMajor = 0 // Major version component of the current release - VersionMinor = 9 // Minor version component of the current release - VersionPatch = 11 // Patch version component of the current release - VersionMeta = "unstable" // Version metadata to append to the version string + VersionMajor = 0 // Major version component of the current release + VersionMinor = 9 // Minor version component of the current release + VersionPatch = 11 // Patch version component of the current release + VersionMeta = "stable" // Version metadata to append to the version string ) // Version returns the version of the whole story-monorepo and all binaries built from this git commit.