Skip to content

Commit

Permalink
Adding cmd to generate/add/remove access keys for key rotation functi…
Browse files Browse the repository at this point in the history
…onality
  • Loading branch information
spilin committed Oct 11, 2023
1 parent 077edb5 commit bded80c
Show file tree
Hide file tree
Showing 5 changed files with 364 additions and 7 deletions.
76 changes: 75 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
280 changes: 280 additions & 0 deletions cmd/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
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 attemots 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("")
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")
}
7 changes: 3 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit bded80c

Please sign in to comment.