diff --git a/.changelog/3003.feature.md b/.changelog/3003.feature.md new file mode 100644 index 00000000000..0d6cc00f9cb --- /dev/null +++ b/.changelog/3003.feature.md @@ -0,0 +1,4 @@ +go/oasis-node/cmd/stake: Add `pubkey2address` commmand + +Add `oasis-node stake pubkey2address` CLI command for converting a public key +(e.g. an entity's ID) to a staking account address. diff --git a/go/oasis-node/cmd/stake/stake.go b/go/oasis-node/cmd/stake/stake.go index 7162edcbe66..b07274b3528 100644 --- a/go/oasis-node/cmd/stake/stake.go +++ b/go/oasis-node/cmd/stake/stake.go @@ -9,8 +9,10 @@ import ( "github.com/spf13/cobra" flag "github.com/spf13/pflag" + "github.com/spf13/viper" "google.golang.org/grpc" + "github.com/oasisprotocol/oasis-core/go/common/crypto/signature" "github.com/oasisprotocol/oasis-core/go/common/errors" "github.com/oasisprotocol/oasis-core/go/common/logging" "github.com/oasisprotocol/oasis-core/go/common/quantity" @@ -19,8 +21,12 @@ import ( cmdFlags "github.com/oasisprotocol/oasis-core/go/oasis-node/cmd/common/flags" cmdGrpc "github.com/oasisprotocol/oasis-core/go/oasis-node/cmd/common/grpc" "github.com/oasisprotocol/oasis-core/go/staking/api" + staking "github.com/oasisprotocol/oasis-core/go/staking/api" ) +// CfgPublicKey configures the public key. +const CfgPublicKey = "public_key" + var ( stakeCmd = &cobra.Command{ Use: "stake", @@ -39,10 +45,17 @@ var ( Run: doList, } + pubkey2AddressCmd = &cobra.Command{ + Use: "pubkey2address", + Short: "convert a public key (e.g. entity's ID) to an account address", + Run: doPubkey2Address, + } + logger = logging.GetLogger("cmd/stake") - infoFlags = flag.NewFlagSet("", flag.ContinueOnError) - listFlags = flag.NewFlagSet("", flag.ContinueOnError) + infoFlags = flag.NewFlagSet("", flag.ContinueOnError) + listFlags = flag.NewFlagSet("", flag.ContinueOnError) + pubkey2AddressFlags = flag.NewFlagSet("", flag.ContinueOnError) ) func doConnect(cmd *cobra.Command) (*grpc.ClientConn, api.Backend) { @@ -207,12 +220,35 @@ func getAccount(ctx context.Context, cmd *cobra.Command, addr api.Address, clien return acct } +func doPubkey2Address(cmd *cobra.Command, args []string) { + if err := cmdCommon.Init(); err != nil { + cmdCommon.EarlyLogAndExit(err) + } + + pkString := viper.GetString(CfgPublicKey) + if pkString == "" { + logger.Error("cannot convert an empty public key") + os.Exit(1) + } + + var pk signature.PublicKey + if err := pk.UnmarshalText([]byte(pkString)); err != nil { + logger.Error("failed to parse public key", + "err", err, + ) + os.Exit(1) + } + + fmt.Printf("%v\n", staking.NewAddress(pk)) +} + // Register registers the stake sub-command and all of it's children. func Register(parentCmd *cobra.Command) { registerAccountCmd() for _, v := range []*cobra.Command{ infoCmd, listCmd, + pubkey2AddressCmd, accountCmd, } { stakeCmd.AddCommand(v) @@ -220,6 +256,7 @@ func Register(parentCmd *cobra.Command) { infoCmd.Flags().AddFlagSet(infoFlags) listCmd.Flags().AddFlagSet(listFlags) + pubkey2AddressCmd.Flags().AddFlagSet(pubkey2AddressFlags) parentCmd.AddCommand(stakeCmd) } @@ -231,4 +268,7 @@ func init() { listFlags.AddFlagSet(cmdFlags.RetriesFlags) listFlags.AddFlagSet(cmdFlags.VerboseFlags) listFlags.AddFlagSet(cmdGrpc.ClientFlags) + + pubkey2AddressFlags.String(CfgPublicKey, "", "Public key (Base64-encoded)") + _ = viper.BindPFlags(pubkey2AddressFlags) } diff --git a/go/oasis-test-runner/scenario/e2e/stake_cli.go b/go/oasis-test-runner/scenario/e2e/stake_cli.go index 0bb1f10855a..1b5059c66f7 100644 --- a/go/oasis-test-runner/scenario/e2e/stake_cli.go +++ b/go/oasis-test-runner/scenario/e2e/stake_cli.go @@ -45,21 +45,26 @@ const ( // Transaction fee gas. feeGas = 10000 + + // Testing source account public key (hex-encoded). + srcPubkeyHex = "4ea5328f943ef6f66daaed74cb0e99c3b1c45f76307b425003dbc7cb3638ed35" + + // Testing destination account public key (hex-encoded). + dstPubkeyHex = "5ea5328f943ef6f66daaed74cb0e99c3b1c45f76307b425003dbc7cb3638ed35" + + // Testing escrow account public key (hex-encoded). + escrowPubkeyHex = "6ea5328f943ef6f66daaed74cb0e99c3b1c45f76307b425003dbc7cb3638ed35" ) var ( // Testing source account address. - srcAddress = api.NewAddress( - signature.NewPublicKey("4ea5328f943ef6f66daaed74cb0e99c3b1c45f76307b425003dbc7cb3638ed35"), - ) + srcAddress = api.NewAddress(signature.NewPublicKey(srcPubkeyHex)) + // Testing destination account address. - dstAddress = api.NewAddress( - signature.NewPublicKey("5ea5328f943ef6f66daaed74cb0e99c3b1c45f76307b425003dbc7cb3638ed35"), - ) + dstAddress = api.NewAddress(signature.NewPublicKey(dstPubkeyHex)) + // Testing escrow account address. - escrowAddress = api.NewAddress( - signature.NewPublicKey("6ea5328f943ef6f66daaed74cb0e99c3b1c45f76307b425003dbc7cb3638ed35"), - ) + escrowAddress = api.NewAddress(signature.NewPublicKey(escrowPubkeyHex)) // StakeCLI is the staking scenario. StakeCLI scenario.Scenario = &stakeCLIImpl{ @@ -137,6 +142,32 @@ func (s *stakeCLIImpl) Run(childEnv *env.Env) error { } // Run the tests + + // Ensure converting public keys to staking account addresses works. + pubkey2AddressTestVectors := []struct { + publicKeyText string + addressText string + expectError bool + }{ + {signature.NewPublicKey(srcPubkeyHex).String(), srcAddress.String(), false}, + {signature.NewPublicKey(dstPubkeyHex).String(), dstAddress.String(), false}, + {signature.NewPublicKey(escrowPubkeyHex).String(), escrowAddress.String(), false}, + // Empty public key. + {"", "", true}, + // Invalid public key. + {"BadPubKey=", "", true}, + } + s.logger.Info("test converting public keys to staking account addresses") + for _, vector := range pubkey2AddressTestVectors { + err = s.testPubkey2Address(childEnv, vector.publicKeyText, vector.addressText) + if err != nil && !vector.expectError { + return fmt.Errorf("scenario/e2e/stake: unexpected pubkey2address error: %w", err) + } + if err == nil && vector.expectError { + return fmt.Errorf("scenario/e2e/stake: pubkey2address for public key '%s' should error", vector.publicKeyText) + } + } + // Transfer if err = s.testTransfer(childEnv, cli, srcAddress, dstAddress); err != nil { return fmt.Errorf("scenario/e2e/stake: error while running Transfer test: %w", err) @@ -169,6 +200,31 @@ func (s *stakeCLIImpl) Run(childEnv *env.Env) error { return nil } +func (s *stakeCLIImpl) testPubkey2Address(childEnv *env.Env, publicKeyText string, addressText string) error { + args := []string{ + "stake", "pubkey2address", + "--" + stake.CfgPublicKey, publicKeyText, + } + + out, err := cli.RunSubCommandWithOutput(childEnv, s.logger, "info", s.runtimeImpl.net.Config().NodeBinary, args) + if err != nil { + return fmt.Errorf("failed to convert public key to address: error: %w output: %s", err, out.String()) + } + + var addr api.Address + if err = addr.UnmarshalText(bytes.TrimSpace(out.Bytes())); err != nil { + return err + } + + if addr.String() != addressText { + return fmt.Errorf("pubkey2address converted public key %s to address %s (expected address: %s)", + publicKeyText, addr, addressText, + ) + } + + return nil +} + // testTransfer tests transfer of transferAmount tokens from src to dst. func (s *stakeCLIImpl) testTransfer(childEnv *env.Env, cli *cli.Helpers, src api.Address, dst api.Address) error { transferTxPath := filepath.Join(childEnv.Dir(), "stake_transfer.json")