From a7fc161b0a34d3810df28ad5708689354dd69411 Mon Sep 17 00:00:00 2001 From: spilin Date: Thu, 12 Oct 2023 14:42:27 +0300 Subject: [PATCH] Adding cmd to generate/add/remove access keys for key rotation. (#113) This pull request introduces enhancements to the command-line interface with the addition of three new commands: - generate-key: A command dedicated to generating a key pair for the signer. - add-keys: This command allows users to add keys to a specified account. - delete-keys: Facilitates the removal of keys from a designated account. ### Documentation Updates The documentation has been updated to detail the functionality and purpose of these newly introduced commands, ensuring that users are well-informed of their utility and operation. --- README.md | 76 +++++++++++++- cmd/keys.go | 284 ++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 7 +- go.sum | 4 +- main.go | 4 + 5 files changed, 368 insertions(+), 7 deletions(-) create mode 100644 cmd/keys.go diff --git a/README.md b/README.md index e4b68e4..14e859f 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,79 @@ To be able to call `eth_sendRawTransaction`, you must have a NEAR account and signing key on the network you are relaying to, and said NEAR account must have a sufficient Ⓝ balance to be able to send transactions. -To configure the signing account and private key: +For those who prefer not to use personal accounts, a new signing key can be generated. + +#### Setting the Signer Key Location +Ensure the signer key location is defined in the configuration file: +Ex. : +```yaml +endpoint: + ... + engine: + ... + signerKey: config/relayer.json +``` + +#### Generating the Signer Key +Execute the following command to generate a new signer key: + +```bash +./relayer generate-key +``` + +> Note: The default configuration file used is config/testnet.yaml. + +Upon successful execution, a `relayer.json` file will be generated. An example of its structure is as follows: +```json +{ + "account_id": "703f78e5355ae6fedf6384257a18e4cfb55bada321f6e7a35b1e21a3803b03e0", + "public_key": "ed25519:8ZAoFCzzj7FwADapMyRCctau295MpvKT5Y1z6cPySCko", + "secret_key": "ed25519:3j8Rxcnx6BcVvphxJpxKMJGWDfjTrSZTvDx7EdWm7L223dwkK8ZgebXieiadAZ3v5Xfg9AKx4XYsaPPcfmncFNo1" +} +``` + +Update the configuration file's `signer` field with the `account_id` value from the generated `relayer.json` file: + +```yaml +endpoint: + ... + engine: + ... + signer: 703f78e5355ae6fedf6384257a18e4cfb55bada321f6e7a35b1e21a3803b03e0 + signerKey: config/relayer.json +``` + +#### Activating implicit account + +In the NEAR Protocol, implicit accounts require activation. This is achieved when a contract-based account or an externally-owned account transfers funds to the implicit account. This step is crucial as the signer account balance is utilized to cover the costs for `eth_sendRawTransaction`. Utilize any NEAR-compatible wallet or the [near send] command. Below is an example that transfers 0.5 NEAR to the generated account: + +```bash +near send myaccount.near 703f78e5355ae6fedf6384257a18e4cfb55bada321f6e7a35b1e21a3803b03e0 0.5 +``` + +### Handling Multiple Access Keys + +When dispatching multiple transactions concurrently, each signed by a different EOA, you may encounter the `ERR_INCORRECT_NONCE` error. The solution is for the relayer to use multiple access keys, signing each transaction with a distinct key. + +#### Generating Multiple Keys + +After [configuring and activating the access key](#configuring-a-signing-key), execute the following: + +```bash +./relayer add-keys --config config/mainnet.yaml -n 100 +``` + +This command generates 100 keys, storing them in the same directory as the `config/relayer.json` file. Upon restarting, the relayer will leverage these access keys for transaction signing. + +#### Removing Keys + +To delete these keys from the account, run: + +```bash +./relayer delete-keys --config config/mainnet.yaml +``` + +You can also use ENV variables to configure the signing account and private key: #### Mainnet ```bash @@ -300,6 +372,8 @@ using distribution project [Standalone Aurora Relayer and Refiner]. [NEAR CLI]: https://docs.near.org/docs/tools/near-cli [Standalone Aurora Relayer and Refiner]: https://github.com/aurora-is-near/standalone-rpc +[near send]: https://docs.near.org/tools/near-cli#near-send + [`web3_clientVersion`]: https://docs.infura.io/infura/networks/ethereum/json-rpc-methods/web3_clientversion [`web3_sha3`]: https://openethereum.github.io/JSONRPC-web3-module#web3_sha3 [`net_listening`]: https://docs.infura.io/infura/networks/ethereum/json-rpc-methods/net_listening diff --git a/cmd/keys.go b/cmd/keys.go new file mode 100644 index 0000000..703da1e --- /dev/null +++ b/cmd/keys.go @@ -0,0 +1,284 @@ +package cmd + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/aurora-is-near/near-api-go" + "github.com/aurora-is-near/near-api-go/keystore" + "github.com/aurora-is-near/near-api-go/utils" + "github.com/btcsuite/btcutil/base58" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var defaultConfigFile = "config/testnet.yaml" +var numberOfKeys uint64 +var prefix = "ed25519:" + +type KeyFile struct { + AccountID string `json:"account_id"` + PublicKey string `json:"public_key"` + SecretKey string `json:"secret_key"` +} + +type Config struct { + Signer string `mapstructure:"signer"` + SignerKey string `mapstructure:"signerKey"` + FunctionKeyPrefixPattern string `mapstructure:"functionKeyPrefixPattern"` + NearNetworkID string `mapstructure:"networkID"` + NearNodeURL string `mapstructure:"nearNodeURL"` + NearArchivalNodeURL string `mapstructure:"nearArchivalNodeURL"` + NearConfig near.Config +} + +// GenerateKeysCmd generates a new key pair and attempts to save it to the file +func GenerateKeysCmd() *cobra.Command { + generateKey := &cobra.Command{ + Use: "generate-key", + Short: "Command to generate signer key pair", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + return bindConfiguration(cmd) + }, + + RunE: func(cmd *cobra.Command, args []string) error { + config := loadConfig() + key := newKey("") + if config.SignerKey == "" { + fmt.Println("signerKey is not set, please provide it in the config file") + os.Exit(1) + } + dumpJson(config.SignerKey, key) + fmt.Printf("key generated: [%s]", config.SignerKey) + return nil + }, + } + acceptConfigFilePath(generateKey) + return generateKey +} + +// AddKeysCmd adds keys to the account via batch transaction +func AddKeysCmd() *cobra.Command { + addKeys := &cobra.Command{ + Use: "add-keys", + Short: "Command to add keys to the account", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + return bindConfiguration(cmd) + }, + + RunE: func(cmd *cobra.Command, args []string) error { + config := loadConfig() + + if numberOfKeys == 0 { + fmt.Println("number-of-keys is not set, please provide it using -n flag") + os.Exit(1) + } + + destDir := filepath.Dir(config.SignerKey) + baseName := filepath.Base(config.SignerKey) + + nearConn := near.NewConnection(config.NearConfig.NodeURL) + nearAccount, err := near.LoadAccount(nearConn, &config.NearConfig, config.Signer) + if err != nil { + fmt.Printf("failed to load Near account from path: [%s](%s)", config.SignerKey, err) + os.Exit(1) + } + pubKeys := make([]utils.PublicKey, 0) + + for i := uint64(0); i < numberOfKeys; i++ { + key := newKey(config.Signer) + pubKeys = append(pubKeys, publicKeyFromBase58(key.PublicKey)) + filename := fmt.Sprintf("%s/fk%v.%s", destDir, i, baseName) + err = dumpJson(filename, key) + if err != nil { + fmt.Printf("failed to dump key to file: [%s](%s) \n", filename, err) + os.Exit(1) + } + } + result, err := nearAccount.AddKeys(pubKeys...) + if err != nil { + fmt.Printf("failed to add key: [%s]", err) + os.Exit(1) + } + status := result["status"].(map[string]interface{}) + if status["Failure"] != nil { + fmt.Printf("failed to add key: [%s]", status["Failure"]) + os.Exit(1) + } + + fmt.Printf("%v keys where added", len(pubKeys)) + + return nil + }, + } + acceptConfigFilePath(addKeys) + addKeys.PersistentFlags().Uint64VarP(&numberOfKeys, "number-of-keys", "n", 0, "Amount of access keys to generate and add to the account") + return addKeys +} + +// DeleteKeysCmd deletes keys from the account via batch transaction +func DeleteKeysCmd() *cobra.Command { + deleteKeys := &cobra.Command{ + Use: "delete-keys", + Short: "Command to delete keys from the account", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + return bindConfiguration(cmd) + }, + + RunE: func(cmd *cobra.Command, args []string) error { + config := loadConfig() + + keyPairFilePaths := getFunctionCallKeyPairFilePaths(config.SignerKey, config.FunctionKeyPrefixPattern) + nearConn := near.NewConnection(config.NearConfig.NodeURL) + nearAccount, err := near.LoadAccount(nearConn, &config.NearConfig, config.Signer) + if err != nil { + fmt.Printf("failed to load Near account from path: [%s](%s)", config.SignerKey, err) + os.Exit(1) + } + + pubKeys := make([]utils.PublicKey, 0) + for _, keyPairFilePath := range keyPairFilePaths { + keyPair, err := keystore.LoadKeyPairFromPath(keyPairFilePath, config.Signer) + if err != nil { + fmt.Printf("failed to load key pair from path: [%s](%s)", keyPairFilePath, err) + os.Exit(1) + } + pubKeys = append(pubKeys, publicKeyFromBase58(keyPair.PublicKey)) + } + + result, err := nearAccount.DeleteKeys(pubKeys...) + if err != nil { + fmt.Printf("failed to delete key: [%s]", err) + os.Exit(1) + } + status := result["status"].(map[string]interface{}) + if status["Failure"] != nil { + fmt.Printf("failed to delete key: [%s]", status["Failure"]) + os.Exit(1) + } + for _, keyPairFilePath := range keyPairFilePaths { + err := os.Remove(keyPairFilePath) + if err != nil { + fmt.Printf("failed to remove key pair file: [%s](%s)", keyPairFilePath, err) + os.Exit(1) + } + } + + fmt.Printf("%v keys where deleted", len(keyPairFilePaths)) + + os.Exit(0) + + return nil + }, + } + acceptConfigFilePath(deleteKeys) + return deleteKeys +} + +func bindConfiguration(cmd *cobra.Command) error { + configFile, _ := cmd.Flags().GetString("config") + if configFile != "" { + viper.SetConfigFile(configFile) + } else { + viper.SetConfigFile(defaultConfigFile) + } + + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + return err + } + } + + return nil +} + +// publicKeyFromBase58 converts a base58 encoded public key to a PublicKey struct +func publicKeyFromBase58(pub string) utils.PublicKey { + var pubKey utils.PublicKey + pubKey.KeyType = utils.ED25519 + pub = strings.TrimPrefix(pub, prefix) + decoded := base58.Decode(pub) + copy(pubKey.Data[:], decoded) + return pubKey +} + +// newKey generates a new key pair and returns the key file +func newKey(accountid string) *KeyFile { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + panic(err) + } + if accountid == "" { + accountid = hex.EncodeToString(pub) + } + kf := &KeyFile{ + AccountID: accountid, + PublicKey: prefix + base58.Encode(pub), + SecretKey: prefix + base58.Encode(priv), + } + return kf +} + +// dumpJson dumps the key file to a json file +// if the file already exists, it returns an error +func dumpJson(fileName string, keyFile *KeyFile) error { + if _, err := os.Stat(fileName); os.IsNotExist(err) { + file, err := os.Create(fileName) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + err = encoder.Encode(keyFile) + if err != nil { + return err + } + return nil + } else { + return fmt.Errorf("file already exists: [%s]", fileName) + } +} + +// loadConfig loads the configuration from the config file +func loadConfig() Config { + var config Config + err := viper.UnmarshalKey("endpoint.engine", &config) + if err != nil { + panic(err) + } + if config.NearNodeURL == "" { + config.NearNodeURL = config.NearArchivalNodeURL + } + + config.NearConfig = near.Config{ + NetworkID: config.NearNetworkID, + NodeURL: config.NearNodeURL, + KeyPath: config.SignerKey, + } + return config +} + +// getFunctionCallKeyPairFilePaths returns the file paths of the key pairs that match the pattern +func getFunctionCallKeyPairFilePaths(path, prefixPattern string) []string { + dir, file := filepath.Split(path) + pattern := filepath.Join(dir, prefixPattern+file) + + keyPairFiles := make([]string, 0) + files, err := filepath.Glob(pattern) + if err == nil && len(files) > 0 { + keyPairFiles = append(keyPairFiles, files...) + } + return keyPairFiles +} + +func acceptConfigFilePath(cmd *cobra.Command) { + cmd.PersistentFlags().StringP("config", "c", "config/testnet.yaml", "Path of the configuration file") +} diff --git a/go.mod b/go.mod index 5ae1132..99bbabb 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,16 @@ module github.com/aurora-is-near/relayer2-public go 1.18 //replace github.com/aurora-is-near/relayer2-base => github.com/aurora-is-near/relayer2-base v1.1.3-0.20230914105446-42f23496919e - -// replace github.com/aurora-is-near/near-api-go => /Users/spilin/sandbox/aurora/near-api-go +//replace github.com/aurora-is-near/near-api-go => github.com/aurora-is-near/near-api-go // The following package had a conflicting dependency. // Fixed by pointing the dependency to the latest version tag. replace github.com/btcsuite/btcd => github.com/btcsuite/btcd v0.23.2 require ( - github.com/aurora-is-near/near-api-go v0.0.13 + github.com/aurora-is-near/near-api-go v0.0.14 github.com/aurora-is-near/relayer2-base v1.1.3 + github.com/btcsuite/btcutil v1.0.2 github.com/buger/jsonparser v1.1.1 github.com/ethereum/go-ethereum v1.10.25 github.com/google/uuid v1.3.0 @@ -30,7 +30,6 @@ require ( github.com/andybalholm/brotli v1.0.5 // indirect github.com/aurora-is-near/go-jsonrpc/v3 v3.1.2 // indirect github.com/aurora-is-near/stream-backup v0.0.0-20221212013533-1e06e263c3f7 // indirect - github.com/btcsuite/btcutil v1.0.2 // indirect github.com/carlmjohnson/versioninfo v0.22.4 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/dgraph-io/badger/v3 v3.2103.2 // indirect diff --git a/go.sum b/go.sum index e822f1e..11cafb5 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aurora-is-near/go-jsonrpc/v3 v3.1.2 h1:GKX/ga2vElkJYHSIpWEGsW3Z5c0xFx6bnXkSsU30GXg= github.com/aurora-is-near/go-jsonrpc/v3 v3.1.2/go.mod h1:Li013EFlPu3crtlFQtWJAeE7VmdhSsxOpRoop1J0icw= -github.com/aurora-is-near/near-api-go v0.0.13 h1:iIMlDIA/NqGS0D6j1TeP4DIdnyboifx2Y65UsJtUnBo= -github.com/aurora-is-near/near-api-go v0.0.13/go.mod h1:k1fyeUePNSpC8VZTkbUaGbjMY8jexe4s5LffFejplyA= +github.com/aurora-is-near/near-api-go v0.0.14 h1:6BHZ0jtUQ3frNx0h6Kucd2tOR/TZ5I7lScgN5khNhd8= +github.com/aurora-is-near/near-api-go v0.0.14/go.mod h1:k1fyeUePNSpC8VZTkbUaGbjMY8jexe4s5LffFejplyA= github.com/aurora-is-near/relayer2-base v1.1.3 h1:NNPzgopDXatKjf4MPNILtx81GHvPjXwo8NkvkRrgtuE= github.com/aurora-is-near/relayer2-base v1.1.3/go.mod h1:kvEh47ywen00njN0XWPWj2RDGa1CXDF13EjDjLiRojo= github.com/aurora-is-near/stream-backup v0.0.0-20221212013533-1e06e263c3f7 h1:aHMsjwM2KJ6EO9H7f1UgFD95c+d9BTpA43NQgZ6hiaM= diff --git a/main.go b/main.go index cee5959..4c4f625 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ import ( "github.com/aurora-is-near/relayer2-base/indexer/tar" "github.com/aurora-is-near/relayer2-base/log" "github.com/aurora-is-near/relayer2-base/rpc/node" + publicCmd "github.com/aurora-is-near/relayer2-public/cmd" "github.com/aurora-is-near/relayer2-public/endpoint" "github.com/aurora-is-near/relayer2-public/indexer" "github.com/aurora-is-near/relayer2-public/middleware" @@ -28,6 +29,9 @@ import ( func main() { c := cmd.RootCmd() c.AddCommand(cmd.VersionCmd()) + c.AddCommand(publicCmd.AddKeysCmd()) + c.AddCommand(publicCmd.DeleteKeysCmd()) + c.AddCommand(publicCmd.GenerateKeysCmd()) c.AddCommand(cmd.StartCmd(func(cmd *cobra.Command, args []string) { logger := log.Log() bh, err := badger.NewBlockHandler()