Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

go/oasis-node/cmd: Add oasis-node stake account nonce CLI command #3559

Merged
merged 3 commits into from
Dec 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/3559.feature.md
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering what's the use case for this, since the stake account info command already includes this in response?

The only thing i can come up with is using this in some automation - which i think we actually want to discourage and instead defer to the grpc api.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering what's the use case for this, since the stake account info command already includes this in response?

The use case I have in mind is to have an elegant way of getting an account's nonce instead of having to parse the stake account info command's response which is not guaranteed to stay stable.

The only thing i can come up with is using this in some automation - which i think we actually want to discourage and instead defer to the grpc api.

You guessed it 🙂. I've had some occasions where it was the most convenient to just write a simple Bash script to generate, sign and submit a batch of staking transactions.

Something like a more proper Go program acting as a gRPC client and a CLI would be nice, but out of scope for these smallish projects.

So, I would say that we probably don't want to discourage all such usage of our CLI tools, just not relying on their output being stable and using them to actually write a backend, e.g. a gateway, a staking dashboard, ...

49 changes: 39 additions & 10 deletions go/oasis-node/cmd/stake/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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",
Expand All @@ -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,
}
)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -315,6 +342,7 @@ func doAccountAmendCommissionSchedule(cmd *cobra.Command, args []string) {
func registerAccountCmd() {
for _, v := range []*cobra.Command{
accountInfoCmd,
accountNonceCmd,
accountTransferCmd,
accountBurnCmd,
accountEscrowCmd,
Expand All @@ -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)
Expand All @@ -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)
Expand Down
97 changes: 69 additions & 28 deletions go/oasis-test-runner/scenario/e2e/stake_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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,
},
Expand All @@ -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,
},
Expand All @@ -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,
},
Expand All @@ -466,19 +482,19 @@ 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
}

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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down