diff --git a/.changelog/3559.feature.md b/.changelog/3559.feature.md new file mode 100644 index 00000000000..8ac921b5687 --- /dev/null +++ b/.changelog/3559.feature.md @@ -0,0 +1,3 @@ +go/oasis-node/cmd: Add `oasis-node stake account nonce` CLI command + +It can be used to get a staking account's current nonce. diff --git a/go/oasis-node/cmd/stake/account.go b/go/oasis-node/cmd/stake/account.go index 8a6cb6c779a..b5f4e9eae2c 100644 --- a/go/oasis-node/cmd/stake/account.go +++ b/go/oasis-node/cmd/stake/account.go @@ -43,7 +43,7 @@ const ( ) var ( - accountInfoFlags = flag.NewFlagSet("", flag.ContinueOnError) + commonAccountFlags = flag.NewFlagSet("", flag.ContinueOnError) amountFlags = flag.NewFlagSet("", flag.ContinueOnError) sharesFlags = flag.NewFlagSet("", flag.ContinueOnError) commonEscrowFlags = flag.NewFlagSet("", flag.ContinueOnError) @@ -58,10 +58,16 @@ var ( accountInfoCmd = &cobra.Command{ Use: "info", - Short: "query account info", + Short: "get account info", Run: doAccountInfo, } + accountNonceCmd = &cobra.Command{ + Use: "nonce", + Short: "get account nonce", + Run: doAccountNonce, + } + accountTransferCmd = &cobra.Command{ Use: "gen_transfer", Short: "generate a transfer transaction", @@ -70,25 +76,25 @@ var ( accountBurnCmd = &cobra.Command{ Use: "gen_burn", - Short: "Generate a burn transaction", + Short: "generate a burn transaction", Run: doAccountBurn, } accountEscrowCmd = &cobra.Command{ Use: "gen_escrow", - Short: "Generate an escrow (stake) transaction", + Short: "generate an escrow (stake) transaction", Run: doAccountEscrow, } accountReclaimEscrowCmd = &cobra.Command{ Use: "gen_reclaim_escrow", - Short: "Generate a reclaim_escrow (unstake) transaction", + Short: "generate a reclaim escrow (unstake) transaction", Run: doAccountReclaimEscrow, } accountAmendCommissionScheduleCmd = &cobra.Command{ Use: "gen_amend_commission_schedule", - Short: "Generate an amend_commission_schedule transaction", + Short: "generate an amend commission schedule transaction", Run: doAccountAmendCommissionSchedule, } ) @@ -128,6 +134,27 @@ func doAccountInfo(cmd *cobra.Command, args []string) { acct.PrettyPrint(ctx, "", os.Stdout) } +func doAccountNonce(cmd *cobra.Command, args []string) { + if err := cmdCommon.Init(); err != nil { + cmdCommon.EarlyLogAndExit(err) + } + + var addr api.Address + if err := addr.UnmarshalText([]byte(viper.GetString(CfgAccountAddr))); err != nil { + logger.Error("failed to parse account address", + "err", err, + ) + os.Exit(1) + } + + conn, client := doConnect(cmd) + defer conn.Close() + + ctx := context.Background() + acct := getAccount(ctx, cmd, addr, client) + fmt.Println(acct.General.Nonce) +} + func doAccountTransfer(cmd *cobra.Command, args []string) { if err := cmdCommon.Init(); err != nil { cmdCommon.EarlyLogAndExit(err) @@ -315,6 +342,7 @@ func doAccountAmendCommissionSchedule(cmd *cobra.Command, args []string) { func registerAccountCmd() { for _, v := range []*cobra.Command{ accountInfoCmd, + accountNonceCmd, accountTransferCmd, accountBurnCmd, accountEscrowCmd, @@ -324,7 +352,8 @@ func registerAccountCmd() { accountCmd.AddCommand(v) } - accountInfoCmd.Flags().AddFlagSet(accountInfoFlags) + accountInfoCmd.Flags().AddFlagSet(commonAccountFlags) + accountNonceCmd.Flags().AddFlagSet(commonAccountFlags) accountTransferCmd.Flags().AddFlagSet(accountTransferFlags) accountBurnCmd.Flags().AddFlagSet(accountBurnFlags) accountEscrowCmd.Flags().AddFlagSet(commonEscrowFlags) @@ -335,9 +364,9 @@ func registerAccountCmd() { } func init() { - accountInfoFlags.String(CfgAccountAddr, "", "account address") - _ = viper.BindPFlags(accountInfoFlags) - accountInfoFlags.AddFlagSet(cmdGrpc.ClientFlags) + commonAccountFlags.String(CfgAccountAddr, "", "account address") + _ = viper.BindPFlags(commonAccountFlags) + commonAccountFlags.AddFlagSet(cmdGrpc.ClientFlags) amountFlags.String(CfgAmount, "0", "amount of stake (in base units) for the transaction") _ = viper.BindPFlags(amountFlags) diff --git a/go/oasis-test-runner/scenario/e2e/stake_cli.go b/go/oasis-test-runner/scenario/e2e/stake_cli.go index fcddc3717ad..f99d7444a7a 100644 --- a/go/oasis-test-runner/scenario/e2e/stake_cli.go +++ b/go/oasis-test-runner/scenario/e2e/stake_cli.go @@ -272,11 +272,18 @@ func (sc *stakeCLIImpl) testPubkey2Address(childEnv *env.Env, publicKeyText, add // testTransfer tests transfer of transferAmount base units from src to dst. func (sc *stakeCLIImpl) testTransfer(childEnv *env.Env, cli *cli.Helpers, src, dst api.Address) error { - var srcNonce, dstNonce uint64 = 0, 0 + srcNonce, err := sc.getAccountNonce(childEnv, src) + if err != nil { + return fmt.Errorf("getAccountNonce for source account %s: %w", src, err) + } + dstNonce, err := sc.getAccountNonce(childEnv, src) + if err != nil { + return fmt.Errorf("getAccountNonce for destination account %s: %w", dst, err) + } ctx := contextWithTokenInfo() unsignedTransferTxPath := filepath.Join(childEnv.Dir(), "stake_transfer_unsigned.cbor") - if err := sc.genUnsignedTransferTx(childEnv, transferAmount, 0, dst, unsignedTransferTxPath); err != nil { + if err = sc.genUnsignedTransferTx(childEnv, transferAmount, srcNonce, dst, unsignedTransferTxPath); err != nil { return fmt.Errorf("genUnsignedTransferTx: %w", err) } _, teSigner, err := entity.TestEntity() @@ -345,23 +352,26 @@ func (sc *stakeCLIImpl) testTransfer(childEnv *env.Env, cli *cli.Helpers, src, d // testBurn tests burning of burnAmount base units owned by src. func (sc *stakeCLIImpl) testBurn(childEnv *env.Env, cli *cli.Helpers, src api.Address) error { - var srcNonce uint64 = 1 + srcNonce, err := sc.getAccountNonce(childEnv, src) + if err != nil { + return fmt.Errorf("getAccountNonce for source account %s: %w", src, err) + } ctx := contextWithTokenInfo() burnTxPath := filepath.Join(childEnv.Dir(), "stake_burn.json") - if err := sc.genBurnTx(childEnv, burnAmount, srcNonce, burnTxPath); err != nil { + if err = sc.genBurnTx(childEnv, burnAmount, srcNonce, burnTxPath); err != nil { return err } - if err := sc.showTx(childEnv, burnTxPath); err != nil { + if err = sc.showTx(childEnv, burnTxPath); err != nil { return err } - if err := cli.Consensus.SubmitTx(burnTxPath); err != nil { + if err = cli.Consensus.SubmitTx(burnTxPath); err != nil { return err } expectedBalance := mustInitQuantity(initBalance - transferAmount - burnAmount - 2*feeAmount) - if err := sc.checkGeneralAccount( + if err = sc.checkGeneralAccount( ctx, childEnv, src, &api.GeneralAccount{Balance: expectedBalance, Nonce: srcNonce + 1}, ); err != nil { return err @@ -379,32 +389,35 @@ func (sc *stakeCLIImpl) testBurn(childEnv *env.Env, cli *cli.Helpers, src api.Ad // testEscrow tests escrowing escrowAmount base units from src to dst. func (sc *stakeCLIImpl) testEscrow(childEnv *env.Env, cli *cli.Helpers, src, escrow api.Address) error { - var srcNonce uint64 = 2 + srcNonce, err := sc.getAccountNonce(childEnv, src) + if err != nil { + return fmt.Errorf("getAccountNonce for source account %s: %w", src, err) + } ctx := contextWithTokenInfo() escrowTxPath := filepath.Join(childEnv.Dir(), "stake_escrow.json") - if err := sc.genEscrowTx(childEnv, escrowAmount, srcNonce, escrow, escrowTxPath); err != nil { + if err = sc.genEscrowTx(childEnv, escrowAmount, srcNonce, escrow, escrowTxPath); err != nil { return err } - if err := sc.showTx(childEnv, escrowTxPath); err != nil { + if err = sc.showTx(childEnv, escrowTxPath); err != nil { return err } - if err := cli.Consensus.SubmitTx(escrowTxPath); err != nil { + if err = cli.Consensus.SubmitTx(escrowTxPath); err != nil { return err } expectedGeneralBalance := mustInitQuantity( initBalance - transferAmount - burnAmount - escrowAmount - 3*feeAmount, ) - if err := sc.checkGeneralAccount( + if err = sc.checkGeneralAccount( ctx, childEnv, src, &api.GeneralAccount{Balance: expectedGeneralBalance, Nonce: srcNonce + 1}, ); err != nil { return err } expectedEscrowActiveBalance := mustInitQuantity(escrowAmount) expectedActiveShares := mustInitQuantity(escrowShares) - if err := sc.checkEscrowAccountSharePool( + if err = sc.checkEscrowAccountSharePool( ctx, childEnv, escrow, "Active", &api.SharePool{ Balance: expectedEscrowActiveBalance, TotalShares: expectedActiveShares, }, @@ -425,29 +438,32 @@ func (sc *stakeCLIImpl) testEscrow(childEnv *env.Env, cli *cli.Helpers, src, esc // testReclaimEscrow test reclaiming reclaimEscrowShares shares from an escrow account. func (sc *stakeCLIImpl) testReclaimEscrow(childEnv *env.Env, cli *cli.Helpers, src, escrow api.Address) error { - var srcNonce uint64 = 3 + srcNonce, err := sc.getAccountNonce(childEnv, src) + if err != nil { + return fmt.Errorf("getAccountNonce for source account %s: %w", src, err) + } ctx := contextWithTokenInfo() reclaimEscrowTxPath := filepath.Join(childEnv.Dir(), "stake_reclaim_escrow.json") - if err := sc.genReclaimEscrowTx(childEnv, reclaimEscrowShares, srcNonce, escrow, reclaimEscrowTxPath); err != nil { + if err = sc.genReclaimEscrowTx(childEnv, reclaimEscrowShares, srcNonce, escrow, reclaimEscrowTxPath); err != nil { return err } - if err := sc.showTx(childEnv, reclaimEscrowTxPath); err != nil { + if err = sc.showTx(childEnv, reclaimEscrowTxPath); err != nil { return err } - if err := sc.checkEscrowAccountSharePool( + if err = sc.checkEscrowAccountSharePool( ctx, childEnv, escrow, "Debonding", &api.SharePool{Balance: qZero, TotalShares: qZero}, ); err != nil { return err } - if err := cli.Consensus.SubmitTx(reclaimEscrowTxPath); err != nil { + if err = cli.Consensus.SubmitTx(reclaimEscrowTxPath); err != nil { return err } expectedEscrowDebondingBalance := mustInitQuantity(reclaimEscrowAmount) expectedEscrowDebondingShares := mustInitQuantity(reclaimEscrowShares) - if err := sc.checkEscrowAccountSharePool( + if err = sc.checkEscrowAccountSharePool( ctx, childEnv, escrow, "Debonding", &api.SharePool{ Balance: expectedEscrowDebondingBalance, TotalShares: expectedEscrowDebondingShares, }, @@ -457,7 +473,7 @@ func (sc *stakeCLIImpl) testReclaimEscrow(childEnv *env.Env, cli *cli.Helpers, s expectedEscrowActiveBalance := mustInitQuantity(escrowAmount - reclaimEscrowAmount) expectedEscrowActiveShares := mustInitQuantity(escrowShares - reclaimEscrowShares) - if err := sc.checkEscrowAccountSharePool( + if err = sc.checkEscrowAccountSharePool( ctx, childEnv, escrow, "Active", &api.SharePool{ Balance: expectedEscrowActiveBalance, TotalShares: expectedEscrowActiveShares, }, @@ -466,11 +482,11 @@ func (sc *stakeCLIImpl) testReclaimEscrow(childEnv *env.Env, cli *cli.Helpers, s } // Advance epochs to trigger reclaim processing. - if err := sc.Net.Controller().SetEpoch(context.Background(), 1); err != nil { + if err = sc.Net.Controller().SetEpoch(context.Background(), 1); err != nil { return fmt.Errorf("failed to set epoch: %w", err) } - if err := sc.checkEscrowAccountSharePool( + if err = sc.checkEscrowAccountSharePool( ctx, childEnv, escrow, "Debonding", &api.SharePool{Balance: qZero, TotalShares: qZero}); err != nil { return err } @@ -478,7 +494,7 @@ func (sc *stakeCLIImpl) testReclaimEscrow(childEnv *env.Env, cli *cli.Helpers, s expectedGeneralBalance := mustInitQuantity( initBalance - transferAmount - burnAmount - escrowAmount + reclaimEscrowAmount - 4*feeAmount, ) - if err := sc.checkGeneralAccount( + if err = sc.checkGeneralAccount( ctx, childEnv, src, &api.GeneralAccount{Balance: expectedGeneralBalance, Nonce: srcNonce + 1}, ); err != nil { return err @@ -522,10 +538,14 @@ func (sc *stakeCLIImpl) testAmendCommissionSchedule(childEnv *env.Env, cli *cli. RateMax: mustInitQuantity(50_000), }, } + srcNonce, err := sc.getAccountNonce(childEnv, src) + if err != nil { + return fmt.Errorf("getAccountNonce for source account %s: %w", src, err) + } ctx := contextWithTokenInfo() amendCommissionScheduleTxPath := filepath.Join(childEnv.Dir(), "amend_commission_schedule.json") - if err := sc.genAmendCommissionScheduleTx(childEnv, 4, &api.CommissionSchedule{ + if err := sc.genAmendCommissionScheduleTx(childEnv, srcNonce, &api.CommissionSchedule{ Rates: rates, Bounds: bounds, }, amendCommissionScheduleTxPath); err != nil { @@ -610,6 +630,27 @@ func (sc *stakeCLIImpl) getAccountInfo(childEnv *env.Env, src api.Address) (stri return out.String(), nil } +func (sc *stakeCLIImpl) getAccountNonce(childEnv *env.Env, src api.Address) (uint64, error) { + sc.Logger.Info("checking account nonce", stake.CfgAccountAddr, src.String()) + args := []string{ + "stake", "account", "nonce", + "--" + stake.CfgAccountAddr, src.String(), + "--" + grpc.CfgAddress, "unix:" + sc.Net.Validators()[0].SocketPath(), + } + + out, err := cli.RunSubCommandWithOutput(childEnv, sc.Logger, "info", sc.Net.Config().NodeBinary, args) + if err != nil { + return 0, fmt.Errorf("failed to check account nonce: error: %w output: %s", err, out.String()) + } + + nonce, err := strconv.ParseUint(strings.TrimSpace(out.String()), 10, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse account nonce: error: %w output: %s", err, out.String()) + } + + return nonce, nil +} + func (sc *stakeCLIImpl) checkGeneralAccount( ctx context.Context, childEnv *env.Env, @@ -734,13 +775,13 @@ func (sc *stakeCLIImpl) showTx(childEnv *env.Env, txPath string) error { return nil } -func (sc *stakeCLIImpl) genUnsignedTransferTx(childEnv *env.Env, amount, nonce int, dst api.Address, txPath string) error { +func (sc *stakeCLIImpl) genUnsignedTransferTx(childEnv *env.Env, amount int, nonce uint64, dst api.Address, txPath string) error { sc.Logger.Info("generating unsigned stake transfer tx", stake.CfgTransferDestination, dst) args := []string{ "stake", "account", "gen_transfer", "--" + stake.CfgAmount, strconv.Itoa(amount), - "--" + consensus.CfgTxNonce, strconv.Itoa(nonce), + "--" + consensus.CfgTxNonce, strconv.FormatUint(nonce, 10), "--" + consensus.CfgTxFile, txPath, "--" + stake.CfgTransferDestination, dst.String(), "--" + consensus.CfgTxFeeAmount, strconv.Itoa(feeAmount), @@ -843,12 +884,12 @@ func (sc *stakeCLIImpl) genReclaimEscrowTx(childEnv *env.Env, shares int, nonce return nil } -func (sc *stakeCLIImpl) genAmendCommissionScheduleTx(childEnv *env.Env, nonce int, cs *api.CommissionSchedule, txPath string) error { +func (sc *stakeCLIImpl) genAmendCommissionScheduleTx(childEnv *env.Env, nonce uint64, cs *api.CommissionSchedule, txPath string) error { sc.Logger.Info("generating stake amend commission schedule tx", "commission_schedule", cs) args := []string{ "stake", "account", "gen_amend_commission_schedule", - "--" + consensus.CfgTxNonce, strconv.Itoa(nonce), + "--" + consensus.CfgTxNonce, strconv.FormatUint(nonce, 10), "--" + consensus.CfgTxFile, txPath, "--" + consensus.CfgTxFeeAmount, strconv.Itoa(feeAmount), "--" + consensus.CfgTxFeeGas, strconv.Itoa(feeGas),