summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWill Winder <wwinder.unh@gmail.com>2022-02-28 13:54:56 -0500
committerGitHub <noreply@github.com>2022-02-28 13:54:56 -0500
commit6b7dfb635d100d20c67259f091f0390e4498d9c3 (patch)
tree5e0504f6c438dd901b6756fcc49d3fd45d78c136
parent6ad1e686172bf967b72fb11f6657c1f5b9f78fb6 (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.go258
-rw-r--r--cmd/algokey/part.go1
-rwxr-xr-xtest/scripts/e2e_subs/keyreg.sh33
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(&params.fee, "fee", minFee, "transaction fee")
+ keyregCmd.Flags().Uint64Var(&params.firstValid, "firstvalid", 0, "first round where the transaction may be committed to the ledger")
+ keyregCmd.MarkFlagRequired("firstvalid") // nolint:errcheck
+ keyregCmd.Flags().Uint64Var(&params.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(&params.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(&params.offline, "offline", false, "set to bring an account offline")
+ keyregCmd.Flags().StringVarP(&params.txFile, "outputFile", "o", "", fmt.Sprintf("write signed transaction to this file, or '%s' to write to stdout", stdoutFilenameValue))
+ keyregCmd.Flags().StringVar(&params.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(&params.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}"