diff options
author | Will Winder <wwinder.unh@gmail.com> | 2022-02-28 13:54:56 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-28 13:54:56 -0500 |
commit | 6b7dfb635d100d20c67259f091f0390e4498d9c3 (patch) | |
tree | 5e0504f6c438dd901b6756fcc49d3fd45d78c136 | |
parent | 6ad1e686172bf967b72fb11f6657c1f5b9f78fb6 (diff) |
Tools: Add 'algokey part keyreg' (#3689)
New algokey subcommand to generate transactions for bringing an account online or offline.
Usage:
```
~$ algokey part keyreg -h
Make key registration transaction
Usage:
algokey part keyreg [flags]
Flags:
--account string account address to bring offline, mutually exclusive with keyfile
--fee uint transaction fee (default 1000)
--firstvalid uint first round where the transaction may be committed to the ledger
-h, --help help for keyreg
--keyfile string participation keys to register, file is opened to fetch metadata for the transaction, mutually exclusive with account
--lastvalid uint last round where the generated transaction may be committed to the ledger, defaults to firstvalid + 1000
--network string the network where the provided keys will be registered, one of mainnet/testnet/betanet (default "mainnet")
--offline set to bring an account offline
-o, --outputFile string write signed transaction to this file, or '-' to write to stdout
```
-rw-r--r-- | cmd/algokey/keyreg.go | 258 | ||||
-rw-r--r-- | cmd/algokey/part.go | 1 | ||||
-rwxr-xr-x | test/scripts/e2e_subs/keyreg.sh | 33 |
3 files changed, 292 insertions, 0 deletions
diff --git a/cmd/algokey/keyreg.go b/cmd/algokey/keyreg.go new file mode 100644 index 000000000..445a07827 --- /dev/null +++ b/cmd/algokey/keyreg.go @@ -0,0 +1,258 @@ +// Copyright (C) 2019-2022 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see <https://www.gnu.org/licenses/>. + +package main + +import ( + "encoding/base64" + "errors" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/account" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/util" + "github.com/algorand/go-algorand/util/db" +) + +var keyregCmd *cobra.Command + +type keyregCmdParams struct { + fee uint64 + firstValid uint64 + lastValid uint64 + network string + offline bool + txFile string + partkeyFile string + addr string +} + +// There is no node to query, so we do our best here. +const ( + txnLife uint64 = 1000 + minFee uint64 = 1000 +) + +var validNetworks map[string]crypto.Digest +var validNetworkList []string + +func init() { + var params keyregCmdParams + + keyregCmd = &cobra.Command{ + Use: "keyreg", + Short: "Make key registration transaction", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, _ []string) { + err := run(params) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n\n", err.Error()) + os.Exit(1) + } + }, + } + + keyregCmd.Flags().Uint64Var(¶ms.fee, "fee", minFee, "transaction fee") + keyregCmd.Flags().Uint64Var(¶ms.firstValid, "firstvalid", 0, "first round where the transaction may be committed to the ledger") + keyregCmd.MarkFlagRequired("firstvalid") // nolint:errcheck + keyregCmd.Flags().Uint64Var(¶ms.lastValid, "lastvalid", 0, fmt.Sprintf("last round where the generated transaction may be committed to the ledger, defaults to firstvalid + %d", txnLife)) + keyregCmd.Flags().StringVar(¶ms.network, "network", "mainnet", "the network where the provided keys will be registered, one of mainnet/testnet/betanet") + keyregCmd.MarkFlagRequired("network") // nolint:errcheck + keyregCmd.Flags().BoolVar(¶ms.offline, "offline", false, "set to bring an account offline") + keyregCmd.Flags().StringVarP(¶ms.txFile, "outputFile", "o", "", fmt.Sprintf("write signed transaction to this file, or '%s' to write to stdout", stdoutFilenameValue)) + keyregCmd.Flags().StringVar(¶ms.partkeyFile, "keyfile", "", "participation keys to register, file is opened to fetch metadata for the transaction; only specify when bringing an account online to vote in Algorand consensus") + keyregCmd.Flags().StringVar(¶ms.addr, "account", "", "account address to bring offline; only specify when taking an account offline from voting in Algorand consensus") + + // TODO: move 'bundleGenesisInject' into something that can be imported here instead of using constants. + validNetworks = map[string]crypto.Digest{ + "mainnet": mustConvertB64ToDigest("wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8="), + "testnet": mustConvertB64ToDigest("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="), + "betanet": mustConvertB64ToDigest("mFgazF+2uRS1tMiL9dsj01hJGySEmPN28B/TjjvpVW0="), + "devnet": mustConvertB64ToDigest("sC3P7e2SdbqKJK0tbiCdK9tdSpbe6XeCGKdoNzmlj0E="), + } + validNetworkList = make([]string, 0, len(validNetworks)) + for k := range validNetworks { + validNetworkList = append(validNetworkList, k) + } +} + +func mustConvertB64ToDigest(b64 string) (digest crypto.Digest) { + data, err := base64.StdEncoding.DecodeString(b64) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to decode digest '%s': %s\n\n", b64, err) + os.Exit(1) + } + if len(data) != len(digest[:]) { + fmt.Fprintf(os.Stderr, "Unexpected decoded digest length decoding '%s'.\n\n", b64) + os.Exit(1) + } + copy(digest[:], data) + return +} + +func getGenesisInformation(network string) (crypto.Digest, error) { + // For testing purposes, there is a secret option to override the genesis information. + hashOverride := os.Getenv("ALGOKEY_GENESIS_HASH") + if hashOverride != "" { + return mustConvertB64ToDigest(hashOverride), nil + } + + // Otherwise check that network matches one of the known networks. + gen, ok := validNetworks[strings.ToLower(network)] + if !ok { + return crypto.Digest{}, fmt.Errorf("unknown network '%s' provided. Supported networks: %s", + network, + strings.Join(validNetworkList, ", ")) + } + + return gen, nil +} + +func run(params keyregCmdParams) error { + // Implicit last valid + if params.lastValid == 0 { + params.lastValid = params.firstValid + txnLife + } + + if params.fee < minFee { + return fmt.Errorf("the provided transaction fee (%d) is too low, the minimum fee is %d", params.fee, minFee) + } + + if !params.offline { + if params.partkeyFile == "" { + return errors.New("must provide --keyfile when registering participation keys") + } + if params.addr != "" { + return errors.New("do not provide --address when registering participation keys") + } + } else { + if params.addr == "" { + return errors.New("must provide --address when bringing an account offline") + } + if params.partkeyFile != "" { + return errors.New("do not provide --keyfile when bringing an account offline") + } + } + + var accountAddress basics.Address + if params.addr != "" { + var err error + accountAddress, err = basics.UnmarshalChecksumAddress(params.addr) + if err != nil { + return fmt.Errorf("unable to parse --address: %w", err) + } + } + + if params.partkeyFile != "" && !util.FileExists(params.partkeyFile) { + return fmt.Errorf("cannot access keyfile '%s'", params.partkeyFile) + } + + if params.txFile == "" { + params.txFile = fmt.Sprintf("%s.tx", params.partkeyFile) + } + + if util.FileExists(params.txFile) || params.txFile == stdoutFilenameValue { + return fmt.Errorf("outputFile '%s' already exists", params.partkeyFile) + } + + // Lookup information from partkey file + var part *account.Participation + if params.partkeyFile != "" { + partDB, err := db.MakeErasableAccessor(params.partkeyFile) + if err != nil { + return fmt.Errorf("cannot open keyfile %s: %v", params.partkeyFile, err) + } + + partkey, err := account.RestoreParticipation(partDB) + if err != nil { + return fmt.Errorf("cannot load keyfile %s: %v", params.partkeyFile, err) + } + defer partkey.Close() + + part = &partkey.Participation + + if params.firstValid < uint64(part.FirstValid) { + return fmt.Errorf("the transaction's firstvalid round (%d) field should be set greater than or equal to the participation key's first valid round (%d). The network will reject key registration transactions that are set to take effect before the participation key's first valid round", params.firstValid, part.FirstValid) + } + } + + validRange := params.lastValid - params.firstValid + if validRange > txnLife { + return fmt.Errorf("the transaction's specified validity range must be less than or equal to 1000 rounds due to security constraints. Please enter a first valid round (%d) and last valid round (%d) whose difference is no more than 1000 rounds", params.firstValid, params.lastValid) + } + + var txn transactions.Transaction + if !params.offline { + // Generate go-online transaction + txn = part.GenerateRegistrationTransaction( + basics.MicroAlgos{Raw: params.fee}, + basics.Round(params.firstValid), + basics.Round(params.lastValid), + [32]byte{}, + part.StateProofSecrets != nil) + } else { + // Generate go-offline transaction + txn = transactions.Transaction{ + Type: protocol.KeyRegistrationTx, + Header: transactions.Header{ + Sender: accountAddress, + Fee: basics.MicroAlgos{Raw: params.fee}, + FirstValid: basics.Round(params.firstValid), + LastValid: basics.Round(params.lastValid), + }, + } + } + + var err error + txn.GenesisHash, err = getGenesisInformation(params.network) + if err != nil { + return err + } + + // Wrap in a transactions.SignedTxn with an empty sig. + // This way protocol.Encode will encode the transaction type + stxn, err := transactions.AssembleSignedTxn(txn, crypto.Signature{}, crypto.MultisigSig{}) + if err != nil { + return fmt.Errorf("failed to assemble transaction: %w", err) + } + + data := protocol.Encode(&stxn) + if params.txFile == stdoutFilenameValue { + // Write to Stdout + if _, err = os.Stdout.Write(data); err != nil { + return fmt.Errorf("failed to write transaction to stdout: %w", err) + } + } else { + if err = ioutil.WriteFile(params.txFile, data, 0600); err != nil { + return fmt.Errorf("failed to write transaction to '%s': %w", params.txFile, err) + } + } + + if params.offline { + fmt.Printf("Account key go offline transaction written to '%s'.\n", params.txFile) + } else { + fmt.Printf("Key registration transaction written to '%s'.\n", params.txFile) + } + return nil +} diff --git a/cmd/algokey/part.go b/cmd/algokey/part.go index 5a29f9d71..57a4ddedc 100644 --- a/cmd/algokey/part.go +++ b/cmd/algokey/part.go @@ -175,6 +175,7 @@ func init() { partCmd.AddCommand(partGenerateCmd) partCmd.AddCommand(partInfoCmd) partCmd.AddCommand(partReparentCmd) + partCmd.AddCommand(keyregCmd) partGenerateCmd.Flags().StringVar(&partKeyfile, "keyfile", "", "Participation key filename") partGenerateCmd.Flags().Uint64Var(&partFirstRound, "first", 0, "First round for participation key") diff --git a/test/scripts/e2e_subs/keyreg.sh b/test/scripts/e2e_subs/keyreg.sh new file mode 100755 index 000000000..b3d852f26 --- /dev/null +++ b/test/scripts/e2e_subs/keyreg.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +date '+e2e_subs/keyreg.sh start %Y%m%d_%H%M%S' + +set -exo pipefail +export SHELLOPTS + +WALLET=$1 + +gcmd="goal -w ${WALLET}" + +ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') + +# secret algokey override +ALGOKEY_GENESIS_HASH=$(goal node status | grep 'Genesis hash:'|awk '{ print $3 }') +export ALGOKEY_GENESIS_HASH +# Test key registration +KEYS="${TEMPDIR}/foo.keys" +TXN="${TEMPDIR}/keyreg.txn" +STXN="${TEMPDIR}/keyreg.stxn" +algokey part generate --first 1 --last 1000 --parent "${ACCOUNT}" --keyfile "${KEYS}" +algokey part keyreg --network placeholder --keyfile "${KEYS}" --firstvalid 1 --outputFile "${TXN}" +# technically algokey could be used to sign at this point, that would require +# exporting secrets from the wallet. +${gcmd} clerk sign -i "${TXN}" -o "${STXN}" +${gcmd} clerk rawsend -f "${STXN}" + +TXN2="${TEMPDIR}/keydereg.txn" +STXN2="${TEMPDIR}/keydereg.stxn" +# Test key de-registration +algokey part keyreg --network placeholder --offline --account "${ACCOUNT}" --firstvalid 1 --outputFile "${TXN2}" +${gcmd} clerk sign -i "${TXN2}" -o "${STXN2}" +${gcmd} clerk rawsend -f "${STXN2}" |