-
Notifications
You must be signed in to change notification settings - Fork 518
Tools: Add 'algokey part keyreg' #3689
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
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
f769c05
Initial implementation of 'algokey part keyreg'
winder b4c5641
Make sure bad hashes fail during init functin.
winder 46367db
more small changes.
winder 1af8dbc
Small fixes, add e2e test.
winder dde7c04
review dog changes
winder c776598
PR Feedback
winder 7c7c8a0
Fix e2e test shellcheck warnings.
winder 14ed2ef
Add min fee check to algokey part rekey
winder e6b9c11
PR feedback
winder File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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}, | ||
id-ms marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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}" |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.