From 2d225c8fadb8d20c1d6d46838d0408f00e6cd309 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 7 Aug 2024 11:23:36 -0500 Subject: [PATCH 01/13] configure observer relayer key for Solana; remove hardcoded solana test key from zetaclient code --- cmd/zetaclientd/init.go | 16 ++--- cmd/zetaclientd/solana_test_key.go | 37 ---------- cmd/zetae2e/config/localnet.yml | 7 ++ .../orchestrator/Dockerfile.fastbuild | 2 + .../localnet/orchestrator/start-zetae2e.sh | 12 ++++ contrib/localnet/scripts/start-zetaclientd.sh | 23 +++++++ e2e/config/config.go | 20 ++++-- zetaclient/chains/solana/signer/signer.go | 29 +++++--- zetaclient/chains/solana/signer/withdraw.go | 6 +- zetaclient/config/types.go | 41 ++--------- zetaclient/keys/relayer_keys.go | 69 +++++++++++++++++++ zetaclient/orchestrator/bootstrap.go | 9 +-- 12 files changed, 163 insertions(+), 108 deletions(-) delete mode 100644 cmd/zetaclientd/solana_test_key.go create mode 100644 zetaclient/keys/relayer_keys.go diff --git a/cmd/zetaclientd/init.go b/cmd/zetaclientd/init.go index 1b58265f90..2d3e67d698 100644 --- a/cmd/zetaclientd/init.go +++ b/cmd/zetaclientd/init.go @@ -1,8 +1,6 @@ package main import ( - "path" - "github.com/rs/zerolog" "github.com/spf13/cobra" @@ -38,7 +36,7 @@ type initArguments struct { KeyringBackend string HsmMode bool HsmHotKey string - SolanaKey string + RelayerKeyPath string } func init() { @@ -72,7 +70,8 @@ func init() { InitCmd.Flags().BoolVar(&initArgs.HsmMode, "hsm-mode", false, "enable hsm signer, default disabled") InitCmd.Flags(). StringVar(&initArgs.HsmHotKey, "hsm-hotkey", "hsm-hotkey", "name of hotkey associated with hardware security module") - InitCmd.Flags().StringVar(&initArgs.SolanaKey, "solana-key", "solana-key.json", "solana key file name") + InitCmd.Flags(). + StringVar(&initArgs.RelayerKeyPath, "relayer-key-path", "~/.zetacored/relayer-keys", "path to relayer keys") } func Initialize(_ *cobra.Command, _ []string) error { @@ -110,16 +109,9 @@ func Initialize(_ *cobra.Command, _ []string) error { configData.KeyringBackend = config.KeyringBackend(initArgs.KeyringBackend) configData.HsmMode = initArgs.HsmMode configData.HsmHotKey = initArgs.HsmHotKey - configData.SolanaKeyFile = initArgs.SolanaKey + configData.RelayerKeyPath = initArgs.RelayerKeyPath configData.ComplianceConfig = testutils.ComplianceConfigTest() - // Save solana test fee payer key file - keyFile := path.Join(rootArgs.zetaCoreHome, initArgs.SolanaKey) - err = createSolanaTestKeyFile(keyFile) - if err != nil { - return err - } - // Save config file return config.Save(&configData, rootArgs.zetaCoreHome) } diff --git a/cmd/zetaclientd/solana_test_key.go b/cmd/zetaclientd/solana_test_key.go deleted file mode 100644 index 12a266dd9d..0000000000 --- a/cmd/zetaclientd/solana_test_key.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "encoding/json" - "os" -) - -// solanaTestKey is a local test private key for Solana -// TODO: use separate keys for each zetaclient in Solana E2E tests -// https://github.com/zeta-chain/node/issues/2614 -var solanaTestKey = []uint8{ - 199, 16, 63, 28, 125, 103, 131, 13, 6, 94, 68, 109, 13, 68, 132, 17, - 71, 33, 216, 51, 49, 103, 146, 241, 245, 162, 90, 228, 71, 177, 32, 199, - 31, 128, 124, 2, 23, 207, 48, 93, 141, 113, 91, 29, 196, 95, 24, 137, - 170, 194, 90, 4, 124, 113, 12, 222, 166, 209, 119, 19, 78, 20, 99, 5, -} - -// createSolanaTestKeyFile creates a solana test key json file -func createSolanaTestKeyFile(keyFile string) error { - // marshal the byte array to JSON - keyBytes, err := json.Marshal(solanaTestKey) - if err != nil { - return err - } - - // create file (or overwrite if it already exists) - // #nosec G304 -- for E2E testing purposes only - file, err := os.Create(keyFile) - if err != nil { - return err - } - defer file.Close() - - // write the key bytes to the file - _, err = file.Write(keyBytes) - return err -} diff --git a/cmd/zetae2e/config/localnet.yml b/cmd/zetae2e/config/localnet.yml index eb6ba8fbb9..46c603de13 100644 --- a/cmd/zetae2e/config/localnet.yml +++ b/cmd/zetae2e/config/localnet.yml @@ -54,6 +54,13 @@ policy_accounts: bech32_address: "zeta142ds9x7raljv2qz9euys93e64gjmgdfnc47dwq" evm_address: "0xAa9b029BC3EFe4c50045Cf0902c73aAa25b43533" private_key: "0595CB0CD9BF5264A85A603EC8E43C30ADBB5FD2D9E2EF84C374EA4A65BB616C" +observer_relayer_accounts: + relayer_account_0: + solana_address: "2qBVcNBZCubcnSR3NyCnFjCfkCVUB3G7ECPoaW5rxVjx" + solana_private_key: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ" + relayer_account_1: + solana_address: "4kkCV8H38xirwQTkE5kL6FHNtYGHnMQQ7SkCjAxibHFK" + solana_private_key: "5SSv7jWzamtjWNKGiKf3gvCPHcq9mE5x6LhYgzJCKNSxoQ83gFpmMgmg2JS2zdKcBEdwy7y9bvWgX4LBiUpvnrPf" rpcs: zevm: "http://zetacore0:8545" evm: "http://eth:8545" diff --git a/contrib/localnet/orchestrator/Dockerfile.fastbuild b/contrib/localnet/orchestrator/Dockerfile.fastbuild index c48b8f92b6..a988221384 100644 --- a/contrib/localnet/orchestrator/Dockerfile.fastbuild +++ b/contrib/localnet/orchestrator/Dockerfile.fastbuild @@ -1,5 +1,6 @@ FROM zetanode:latest as zeta FROM ghcr.io/zeta-chain/ethereum-client-go:v1.10.26 as geth +FROM ghcr.io/zeta-chain/solana-docker:1.18.15 as solana FROM ghcr.io/zeta-chain/golang:1.22.5-bookworm as orchestrator RUN apt update && \ @@ -7,6 +8,7 @@ RUN apt update && \ rm -rf /var/lib/apt/lists/* COPY --from=geth /usr/local/bin/geth /usr/local/bin/ +COPY --from=solana /usr/bin/solana /usr/local/bin/ COPY --from=zeta /usr/local/bin/zetacored /usr/local/bin/zetaclientd /usr/local/bin/zetae2e /usr/local/bin/ COPY contrib/localnet/orchestrator/start-zetae2e.sh /work/ diff --git a/contrib/localnet/orchestrator/start-zetae2e.sh b/contrib/localnet/orchestrator/start-zetae2e.sh index dda5b12cd7..c885264c6f 100644 --- a/contrib/localnet/orchestrator/start-zetae2e.sh +++ b/contrib/localnet/orchestrator/start-zetae2e.sh @@ -91,6 +91,18 @@ address=$(yq -r '.additional_accounts.user_migration.evm_address' config.yml) echo "funding migration tester address ${address} with 10000 Ether" geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +# unlock local solana relayer accounts +solana_url=$(yq -r '.rpcs.solana' config.yml) +solana config set --url "$solana_url" > /dev/null + +relayer=$(yq -r '.observer_relayer_accounts.relayer_account_0.solana_address' config.yml) +echo "funding solana relayer address ${relayer} with 100 SOL" +solana airdrop 100 "$relayer" > /dev/null + +relayer=$(yq -r '.observer_relayer_accounts.relayer_account_1.solana_address' config.yml) +echo "funding solana relayer address ${relayer} with 100 SOL" +solana airdrop 100 "$relayer" > /dev/null + ### Run zetae2e command depending on the option passed if [ "$LOCALNET_MODE" == "upgrade" ]; then diff --git a/contrib/localnet/scripts/start-zetaclientd.sh b/contrib/localnet/scripts/start-zetaclientd.sh index 9250385853..64117621d2 100755 --- a/contrib/localnet/scripts/start-zetaclientd.sh +++ b/contrib/localnet/scripts/start-zetaclientd.sh @@ -14,6 +14,18 @@ set_sepolia_endpoint() { jq '.EVMChainConfigs."11155111".Endpoint = "http://eth2:8545"' /root/.zetacored/config/zetaclient_config.json > tmp.json && mv tmp.json /root/.zetacored/config/zetaclient_config.json } +# creates a file that contains a relayer private key (e.g. Solana relayer key) +create_relayer_key_file() { + local num="$1" + local file="$2" + + # read observer relayer private key from config + privkey_relayer=$(yq -r ".observer_relayer_accounts.relayer_account_${num}.solana_private_key" /root/config.yml) + + # create the key file that contains the private key + jq -n --arg privkey_relayer "$privkey_relayer" '{"private_key": $privkey_relayer}' > "${file}" +} + PREPARAMS_PATH="/root/preparams/${HOSTNAME}.json" if [[ -n "${ZETACLIENTD_GEN_PREPARAMS}" ]]; then # generate pre-params as early as possible @@ -54,6 +66,11 @@ done operator=$(cat $HOME/.zetacored/os.json | jq '.ObserverAddress' ) operatorAddress=$(echo "$operator" | tr -d '"') echo "operatorAddress: $operatorAddress" + +# create the path that holds observer relayer private keys (e.g. Solana relayer key) +RELAYER_KEY_PATH="$HOME/.zetacored/relayer-keys" +mkdir -p "${RELAYER_KEY_PATH}" + echo "Start zetaclientd" # skip initialization if the config file already exists (zetaclientd init has already been run) if [[ $HOSTNAME == "zetaclient0" && ! -f ~/.zetacored/config/zetaclient_config.json ]] @@ -61,6 +78,9 @@ then MYIP=$(/sbin/ip -o -4 addr list eth0 | awk '{print $4}' | cut -d/ -f1) zetaclientd init --zetacore-url zetacore0 --chain-id athens_101-1 --operator "$operatorAddress" --log-format=text --public-ip "$MYIP" --keyring-backend "$BACKEND" --pre-params "$PREPARAMS_PATH" + # create relayer key file for solana + create_relayer_key_file 0 "${RELAYER_KEY_PATH}/solana.json" + # if eth2 is enabled, set the endpoint in the zetaclient_config.json # in this case, the additional evm is represented with the sepolia chain, we set manually the eth2 endpoint to the sepolia chain (11155111 -> http://eth2:8545) # in /root/.zetacored/config/zetaclient_config.json @@ -81,6 +101,9 @@ then done zetaclientd init --peer "/ip4/172.20.0.21/tcp/6668/p2p/${SEED}" --zetacore-url "$node" --chain-id athens_101-1 --operator "$operatorAddress" --log-format=text --public-ip "$MYIP" --log-level 1 --keyring-backend "$BACKEND" --pre-params "$PREPARAMS_PATH" + # create relayer key file for solana + create_relayer_key_file "${num}" "${RELAYER_KEY_PATH}/solana.json" + # check if the option is additional-evm # in this case, the additional evm is represented with the sepolia chain, we set manually the eth2 endpoint to the sepolia chain (11155111 -> http://eth2:8545) # in /root/.zetacored/config/zetaclient_config.json diff --git a/e2e/config/config.go b/e2e/config/config.go index 8e8006c042..2200e95826 100644 --- a/e2e/config/config.go +++ b/e2e/config/config.go @@ -41,12 +41,13 @@ func (s DoubleQuotedString) AsEVMAddress() (ethcommon.Address, error) { // Config contains the configuration for the e2e test type Config struct { // Default account to use when running tests and running setup - DefaultAccount Account `yaml:"default_account"` - AdditionalAccounts AdditionalAccounts `yaml:"additional_accounts"` - PolicyAccounts PolicyAccounts `yaml:"policy_accounts"` - RPCs RPCs `yaml:"rpcs"` - Contracts Contracts `yaml:"contracts"` - ZetaChainID string `yaml:"zeta_chain_id"` + DefaultAccount Account `yaml:"default_account"` + AdditionalAccounts AdditionalAccounts `yaml:"additional_accounts"` + PolicyAccounts PolicyAccounts `yaml:"policy_accounts"` + ObserverRelayerAccounts ObserverRelayerAccounts `yaml:"observer_relayer_accounts"` + RPCs RPCs `yaml:"rpcs"` + Contracts Contracts `yaml:"contracts"` + ZetaChainID string `yaml:"zeta_chain_id"` } // Account contains configuration for an account @@ -54,6 +55,7 @@ type Account struct { RawBech32Address DoubleQuotedString `yaml:"bech32_address"` RawEVMAddress DoubleQuotedString `yaml:"evm_address"` RawPrivateKey DoubleQuotedString `yaml:"private_key"` + SolanaAddress DoubleQuotedString `yaml:"solana_address"` SolanaPrivateKey DoubleQuotedString `yaml:"solana_private_key"` } @@ -76,6 +78,12 @@ type PolicyAccounts struct { AdminPolicyAccount Account `yaml:"admin_policy_account"` } +// ObserverRelayerAccounts are the accounts used by the observers to interact with gateway contracts in non-EVM chains (e.g. Solana) +type ObserverRelayerAccounts struct { + RelayerAccount0 Account `yaml:"relayer_account_0"` + RelayerAccount1 Account `yaml:"relayer_account_1"` +} + // RPCs contains the configuration for the RPC endpoints type RPCs struct { Zevm string `yaml:"zevm"` diff --git a/zetaclient/chains/solana/signer/signer.go b/zetaclient/chains/solana/signer/signer.go index 3fb7512512..a250f31c69 100644 --- a/zetaclient/chains/solana/signer/signer.go +++ b/zetaclient/chains/solana/signer/signer.go @@ -15,6 +15,7 @@ import ( observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/keys" "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/outboundprocessor" ) @@ -28,8 +29,8 @@ type Signer struct { // client is the Solana RPC client that interacts with the Solana chain client interfaces.SolanaRPCClient - // solanaFeePayerKey is the private key of the fee payer account on Solana chain - solanaFeePayerKey solana.PrivateKey + // relayerKey is the private key of the relayer account for Solana chain + relayerKey solana.PrivateKey // gatewayID is the program ID of gateway program on Solana chain gatewayID solana.PublicKey @@ -44,7 +45,7 @@ func NewSigner( chainParams observertypes.ChainParams, solClient interfaces.SolanaRPCClient, tss interfaces.TSSSigner, - solanaKey solana.PrivateKey, + relayerKey keys.RelayerKey, ts *metrics.TelemetryServer, logger base.Logger, ) (*Signer, error) { @@ -56,15 +57,21 @@ func NewSigner( if err != nil { return nil, errors.Wrapf(err, "cannot parse gateway address %s", chainParams.GatewayAddress) } - logger.Std.Info().Msgf("Solana fee payer address: %s", solanaKey.PublicKey()) - // create solana observer + // construct Solana private key + privKey, err := solana.PrivateKeyFromBase58(relayerKey.PrivateKey) + if err != nil { + return nil, errors.Wrap(err, "unable to construct solana private key") + } + logger.Std.Info().Msgf("Solana relayer address: %s", privKey.PublicKey()) + + // create Solana signer return &Signer{ - Signer: baseSigner, - client: solClient, - solanaFeePayerKey: solanaKey, - gatewayID: gatewayID, - pda: pda, + Signer: baseSigner, + client: solClient, + relayerKey: privKey, + gatewayID: gatewayID, + pda: pda, }, nil } @@ -114,7 +121,7 @@ func (signer *Signer) TryProcessOutbound( return } - // sign the withdraw transaction by fee payer + // sign the withdraw transaction by relayer key tx, err := signer.SignWithdrawTx(ctx, *msg) if err != nil { logger.Error().Err(err).Msgf("TryProcessOutbound: SignWithdrawTx error for chain %d nonce %d", chainID, nonce) diff --git a/zetaclient/chains/solana/signer/withdraw.go b/zetaclient/chains/solana/signer/withdraw.go index 383d1c908a..9cd11c40b7 100644 --- a/zetaclient/chains/solana/signer/withdraw.go +++ b/zetaclient/chains/solana/signer/withdraw.go @@ -47,7 +47,7 @@ func (signer *Signer) SignMsgWithdraw( return msg.SetSignature(signature), nil } -// SignWithdrawTx wraps the withdraw 'msg' into a Solana transaction and signs it with the fee payer key. +// SignWithdrawTx wraps the withdraw 'msg' into a Solana transaction and signs it with the relayer key. func (signer *Signer) SignWithdrawTx(ctx context.Context, msg contracts.MsgWithdraw) (*solana.Transaction, error) { // create withdraw instruction with program call data var err error @@ -65,7 +65,7 @@ func (signer *Signer) SignWithdrawTx(ctx context.Context, msg contracts.MsgWithd } // attach required accounts to the instruction - privkey := signer.solanaFeePayerKey + privkey := signer.relayerKey attachWithdrawAccounts(&inst, privkey.PublicKey(), signer.pda, msg.To(), signer.gatewayID) // get a recent blockhash @@ -89,7 +89,7 @@ func (signer *Signer) SignWithdrawTx(ctx context.Context, msg contracts.MsgWithd return nil, errors.Wrap(err, "NewTransaction error") } - // fee payer signs the transaction + // relayer signs the transaction _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { if key.Equals(privkey.PublicKey()) { return &privkey diff --git a/zetaclient/config/types.go b/zetaclient/config/types.go index 31298b00a3..b43043e30e 100644 --- a/zetaclient/config/types.go +++ b/zetaclient/config/types.go @@ -2,15 +2,9 @@ package config import ( "encoding/json" - "fmt" - "os" - "path" "strings" "sync" - "cosmossdk.io/errors" - "github.com/gagliardetto/solana-go" - "github.com/zeta-chain/zetacore/pkg/chains" ) @@ -85,9 +79,9 @@ type Config struct { TssPath string `json:"TssPath"` TestTssKeysign bool `json:"TestTssKeysign"` KeyringBackend KeyringBackend `json:"KeyringBackend"` + RelayerKeyPath string `json:"RelayerKeyPath"` HsmMode bool `json:"HsmMode"` HsmHotKey string `json:"HsmHotKey"` - SolanaKeyFile string `json:"SolanaKeyFile"` // chain configs EVMChainConfigs map[int64]EVMConfig `json:"EVMChainConfigs"` @@ -165,34 +159,11 @@ func (c Config) GetKeyringBackend() KeyringBackend { return c.KeyringBackend } -// LoadSolanaPrivateKey loads the Solana private key from the key file -func (c Config) LoadSolanaPrivateKey() (solana.PrivateKey, error) { - // key file path - fileName := path.Join(c.ZetaCoreHome, c.SolanaKeyFile) - - // load the gateway keypair from a JSON file - // #nosec G304 -- user is allowed to specify the key file - fileContent, err := os.ReadFile(fileName) - if err != nil { - return solana.PrivateKey{}, errors.Wrapf(err, "unable to read Solana key file: %s", fileName) - } - - // unmarshal the JSON content into a slice of bytes - var keyBytes []byte - err = json.Unmarshal(fileContent, &keyBytes) - if err != nil { - return solana.PrivateKey{}, errors.Wrap(err, "unable to unmarshal Solana key bytes") - } - - // ensure the key length is 64 bytes - if len(keyBytes) != 64 { - return solana.PrivateKey{}, fmt.Errorf("invalid Solana key length: %d", len(keyBytes)) - } - - // create private key from the key bytes - privKey := solana.PrivateKey(keyBytes) - - return privKey, nil +// GetRelayerKeyPath returns the relayer key path +func (c Config) GetRelayerKeyPath() string { + c.mu.RLock() + defer c.mu.RUnlock() + return c.RelayerKeyPath } func (c EVMConfig) Empty() bool { diff --git a/zetaclient/keys/relayer_keys.go b/zetaclient/keys/relayer_keys.go new file mode 100644 index 0000000000..cf7b20f724 --- /dev/null +++ b/zetaclient/keys/relayer_keys.go @@ -0,0 +1,69 @@ +package keys + +import ( + "encoding/json" + "fmt" + "io" + "os" + "path" + + "github.com/pkg/errors" + + "github.com/zeta-chain/zetacore/pkg/chains" +) + +const ( + // RelayerKeyFileSolana is the file name for the Solana relayer key + RelayerKeyFileSolana = "solana.json" +) + +// RelayerKey is the structure that holds the relayer private key +type RelayerKey struct { + PrivateKey string `json:"private_key"` +} + +// LoadRelayerKey loads a relayer key from given path and chain +func LoadRelayerKey(keyPath string, chain chains.Chain) (RelayerKey, error) { + // determine relayer key file name based on chain + var fileName string + switch chain.Network { + case chains.Network_solana: + fileName = path.Join(keyPath, RelayerKeyFileSolana) + default: + return RelayerKey{}, errors.Errorf("relayer key not supported for network %s", chain.Network) + } + + // read the relayer key file + relayerKey, err := ReadRelayerKeyFromFile(fileName) + if err != nil { + return RelayerKey{}, errors.Wrap(err, "ReadRelayerKeyFile failed") + } + + return relayerKey, nil +} + +// ReadRelayerKeyFromFile reads the relayer key file and returns the key +func ReadRelayerKeyFromFile(fileName string) (RelayerKey, error) { + fileName = "/root/.zetacored/relayer-keys/solana.json" + fmt.Println("Reading relayer key from file: ", fileName) + file, err := os.Open(fileName) + if err != nil { + return RelayerKey{}, errors.Wrapf(err, "unable to open relayer key file: %s", fileName) + } + defer file.Close() + + // read the file contents + fileData, err := io.ReadAll(file) + if err != nil { + return RelayerKey{}, errors.Wrapf(err, "unable to read relayer key data: %s", fileName) + } + + // unmarshal the JSON data into the struct + var key RelayerKey + err = json.Unmarshal(fileData, &key) + if err != nil { + return RelayerKey{}, errors.Wrap(err, "unable to unmarshal relayer key") + } + + return key, nil +} diff --git a/zetaclient/orchestrator/bootstrap.go b/zetaclient/orchestrator/bootstrap.go index ef4920f8b5..2e65e157ad 100644 --- a/zetaclient/orchestrator/bootstrap.go +++ b/zetaclient/orchestrator/bootstrap.go @@ -21,6 +21,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/config" zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/db" + "github.com/zeta-chain/zetacore/zetaclient/keys" "github.com/zeta-chain/zetacore/zetaclient/metrics" ) @@ -162,18 +163,18 @@ func syncSignerMap( } // load the Solana private key - solanaKey, err := app.Config().LoadSolanaPrivateKey() + relayerKey, err := keys.LoadRelayerKey(app.Config().GetRelayerKeyPath(), *rawChain) if err != nil { - logger.Std.Error().Err(err).Msg("Unable to get Solana private key") + logger.Std.Error().Err(err).Msg("Unable to load Solana relayer key") + continue } var ( - chainRaw = chain.RawChain() paramsRaw = chain.Params() ) // create Solana signer - signer, err := solanasigner.NewSigner(*chainRaw, *paramsRaw, rpcClient, tss, solanaKey, ts, logger) + signer, err := solanasigner.NewSigner(*rawChain, *paramsRaw, rpcClient, tss, relayerKey, ts, logger) if err != nil { logger.Std.Error().Err(err).Msgf("Unable to construct signer for SOL chain %d", chainID) continue From 5da629e74f40115b950de2563e7bb972c59aa8e9 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Fri, 9 Aug 2024 11:24:31 -0500 Subject: [PATCH 02/13] implementation of relayer key importer, encryption and decryption --- cmd/zetaclientd/encrypt_tss.go | 34 +--- cmd/zetaclientd/import_relayer_keys.go | 178 +++++++++++++++++ .../localnet/orchestrator/start-zetae2e.sh | 20 +- pkg/chains/chain.go | 6 + pkg/chains/chain_test.go | 10 +- pkg/crypto/aes256_gcm.go | 118 +++++++++++ pkg/crypto/aes256_gcm_test.go | 187 ++++++++++++++++++ pkg/os/path.go | 33 ++++ pkg/os/path_test.go | 83 ++++++++ rpc/namespaces/ethereum/debug/api.go | 3 +- rpc/namespaces/ethereum/debug/trace.go | 4 +- rpc/namespaces/ethereum/debug/utils.go | 21 +- server/start.go | 4 +- zetaclient/keys/relayer_keys.go | 55 +++++- 14 files changed, 690 insertions(+), 66 deletions(-) create mode 100644 cmd/zetaclientd/import_relayer_keys.go create mode 100644 pkg/crypto/aes256_gcm.go create mode 100644 pkg/crypto/aes256_gcm_test.go create mode 100644 pkg/os/path.go create mode 100644 pkg/os/path_test.go diff --git a/cmd/zetaclientd/encrypt_tss.go b/cmd/zetaclientd/encrypt_tss.go index 6fca9064cb..e8e4a69807 100644 --- a/cmd/zetaclientd/encrypt_tss.go +++ b/cmd/zetaclientd/encrypt_tss.go @@ -1,17 +1,14 @@ package main import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "crypto/sha256" "encoding/json" - "errors" - "io" "os" "path/filepath" + "github.com/pkg/errors" "github.com/spf13/cobra" + + "github.com/zeta-chain/zetacore/pkg/crypto" ) var encTssCmd = &cobra.Command{ @@ -25,6 +22,7 @@ func init() { RootCmd.AddCommand(encTssCmd) } +// EncryptTSSFile encrypts the given file with the given secret key func EncryptTSSFile(_ *cobra.Command, args []string) error { filePath := args[0] secretKey := args[1] @@ -39,29 +37,11 @@ func EncryptTSSFile(_ *cobra.Command, args []string) error { return errors.New("file does not contain valid json, may already be encrypted") } - block, err := aes.NewCipher(getFragmentSeed(secretKey)) - if err != nil { - return err - } - - // Creating GCM mode - gcm, err := cipher.NewGCM(block) + // encrypt the data + cipherText, err := crypto.EncryptAES256GCM(data, secretKey) if err != nil { - return err - } - // Generating random nonce - nonce := make([]byte, gcm.NonceSize()) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return err + return errors.Wrap(err, "failed to encrypt data") } - cipherText := gcm.Seal(nonce, nonce, data, nil) return os.WriteFile(filePath, cipherText, 0o600) } - -func getFragmentSeed(password string) []byte { - h := sha256.New() - h.Write([]byte(password)) - seed := h.Sum(nil) - return seed -} diff --git a/cmd/zetaclientd/import_relayer_keys.go b/cmd/zetaclientd/import_relayer_keys.go new file mode 100644 index 0000000000..fef20f6ed1 --- /dev/null +++ b/cmd/zetaclientd/import_relayer_keys.go @@ -0,0 +1,178 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + "github.com/zeta-chain/zetacore/pkg/crypto" + zetaos "github.com/zeta-chain/zetacore/pkg/os" + "github.com/zeta-chain/zetacore/zetaclient/keys" +) + +var CmdImportRelayerKey = &cobra.Command{ + Use: "import-relayer-key [network] [private-key] [password] [relayer-key-path]", + Short: "Import a relayer private key", + Example: `zetaclientd import-relayer-key --network=7 --private-key=3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ --password=my_password`, + RunE: ImportRelayerKey, +} + +var CmdRelayerAddress = &cobra.Command{ + Use: "relayer-address [network] [password] [relayer-key-path]", + Short: "Show the relayer address", + Example: `zetaclientd relayer-address --network=7 --password=my_password`, + RunE: ShowRelayerAddress, +} + +var importArgs = importRelayerKeyArguments{} +var addressArgs = relayerAddressArguments{} + +// importRelayerKeyArguments is the struct that holds the arguments for the import command +type importRelayerKeyArguments struct { + network int32 + privateKey string + password string + relayerKeyPath string +} + +// relayerAddressArguments is the struct that holds the arguments for the show command +type relayerAddressArguments struct { + network int32 + password string + relayerKeyPath string +} + +func init() { + RootCmd.AddCommand(CmdImportRelayerKey) + RootCmd.AddCommand(CmdRelayerAddress) + + // resolve default relayer key path + defaultRelayerKeyPath := "~/.zetacored/relayer-keys" + defaultRelayerKeyPath, err := zetaos.ExpandHomeDir(defaultRelayerKeyPath) + if err != nil { + log.Fatal().Err(err).Msg("failed to resolve default relayer key path") + } + + CmdImportRelayerKey.Flags().Int32Var(&importArgs.network, "network", 7, "network id, (7: solana)") + CmdImportRelayerKey.Flags(). + StringVar(&importArgs.privateKey, "private-key", "", "the relayer private key to import") + CmdImportRelayerKey.Flags(). + StringVar(&importArgs.password, "password", "", "the password to encrypt the private key") + CmdImportRelayerKey.Flags(). + StringVar(&importArgs.relayerKeyPath, "relayer-key-path", defaultRelayerKeyPath, "path to relayer keys") + + CmdRelayerAddress.Flags().Int32Var(&addressArgs.network, "network", 7, "network id, (7:solana)") + CmdRelayerAddress.Flags(). + StringVar(&addressArgs.password, "password", "", "the password to decrypt the private key") + CmdRelayerAddress.Flags(). + StringVar(&addressArgs.relayerKeyPath, "relayer-key-path", defaultRelayerKeyPath, "path to relayer keys") +} + +// ImportRelayerKey imports a relayer private key +func ImportRelayerKey(_ *cobra.Command, _ []string) error { + // validate private key and password + if importArgs.privateKey == "" { + return errors.New("must provide a private key") + } + if importArgs.password == "" { + return errors.New("must provide a password") + } + + // resolve the relayer key file path + keyPath, fileName, err := resolveRelayerKeyPath(importArgs.network, importArgs.relayerKeyPath) + if err != nil { + return errors.Wrap(err, "failed to resolve relayer key file path") + } + + // create path (owner `rwx` permissions) if it does not exist + if _, err := os.Stat(keyPath); os.IsNotExist(err) { + if err := os.MkdirAll(keyPath, 0o700); err != nil { + return errors.Wrapf(err, "failed to create relayer key path: %s", keyPath) + } + } + + // avoid overwriting existing key file + if zetaos.FileExists(fileName) { + return errors.Errorf( + "relayer key %s already exists, please backup and remove it before importing a new key", + fileName, + ) + } + + // encrypt the private key + ciphertext, err := crypto.EncryptAES256GCMBase64(importArgs.privateKey, importArgs.password) + if err != nil { + return errors.Wrap(err, "private key encryption failed") + } + + // construct the relayer key struct and write to file as json + keyData, err := json.Marshal(keys.RelayerKey{PrivateKey: ciphertext}) + if err != nil { + return errors.Wrap(err, "failed to marshal relayer key") + } + + // create relay key file (owner `rw` permissions) + err = os.WriteFile(fileName, keyData, 0o600) + if err != nil { + return errors.Wrapf(err, "failed to create relayer key file: %s", fileName) + } + fmt.Printf("successfully imported relayer key: %s\n", fileName) + + return nil +} + +// ShowRelayerAddress shows the relayer address +func ShowRelayerAddress(_ *cobra.Command, _ []string) error { + // resolve the relayer key file path + _, fileName, err := resolveRelayerKeyPath(addressArgs.network, addressArgs.relayerKeyPath) + if err != nil { + return errors.Wrap(err, "failed to resolve relayer key file path") + } + + // read the relayer key file + relayerKey, err := keys.ReadRelayerKeyFromFile(fileName) + if err != nil { + return err + } + + // decrypt the private key + privateKey, err := crypto.DecryptAES256GCMBase64(relayerKey.PrivateKey, addressArgs.password) + if err != nil { + return errors.Wrap(err, "private key decryption failed") + } + relayerKey.PrivateKey = privateKey + + // resolve the address + networkName, address, err := relayerKey.ResolveAddress(addressArgs.network) + if err != nil { + return errors.Wrap(err, "failed to resolve relayer address") + } + fmt.Printf("relayer address (%s): %s\n", networkName, address) + + return nil +} + +// resolveRelayerKeyPath is a helper function to resolve the relayer key file path and name +func resolveRelayerKeyPath(network int32, relayerKeyPath string) (string, string, error) { + // get relayer key file name by network + name, err := keys.GetRelayerKeyFileByNetwork(network) + if err != nil { + return "", "", errors.Wrap(err, "failed to get relayer key file name") + } + + // resolve relayer key path if it contains a tilde + keyPath, err := zetaos.ExpandHomeDir(relayerKeyPath) + if err != nil { + return "", "", errors.Wrap(err, "failed to resolve relayer key path") + } + + // build file name + fileName := filepath.Join(keyPath, name) + + return keyPath, fileName, err +} diff --git a/contrib/localnet/orchestrator/start-zetae2e.sh b/contrib/localnet/orchestrator/start-zetae2e.sh index c885264c6f..614d53bc47 100644 --- a/contrib/localnet/orchestrator/start-zetae2e.sh +++ b/contrib/localnet/orchestrator/start-zetae2e.sh @@ -44,52 +44,52 @@ sleep 2 # unlock the default account account address=$(yq -r '.default_account.evm_address' config.yml) echo "funding deployer address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock erc20 tester accounts address=$(yq -r '.additional_accounts.user_erc20.evm_address' config.yml) echo "funding erc20 address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock zeta tester accounts address=$(yq -r '.additional_accounts.user_zeta_test.evm_address' config.yml) echo "funding zeta tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock zevm message passing tester accounts address=$(yq -r '.additional_accounts.user_zevm_mp_test.evm_address' config.yml) echo "funding zevm mp tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock bitcoin tester accounts address=$(yq -r '.additional_accounts.user_bitcoin.evm_address' config.yml) echo "funding bitcoin tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock solana tester accounts address=$(yq -r '.additional_accounts.user_solana.evm_address' config.yml) echo "funding solana tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock ethers tester accounts address=$(yq -r '.additional_accounts.user_ether.evm_address' config.yml) echo "funding ether tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock miscellaneous tests accounts address=$(yq -r '.additional_accounts.user_misc.evm_address' config.yml) echo "funding misc tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock admin erc20 tests accounts address=$(yq -r '.additional_accounts.user_admin.evm_address' config.yml) echo "funding admin tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock migration tests accounts address=$(yq -r '.additional_accounts.user_migration.evm_address' config.yml) echo "funding migration tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock local solana relayer accounts solana_url=$(yq -r '.rpcs.solana' config.yml) diff --git a/pkg/chains/chain.go b/pkg/chains/chain.go index 221da26a32..06ce4af289 100644 --- a/pkg/chains/chain.go +++ b/pkg/chains/chain.go @@ -147,6 +147,12 @@ func (chain Chain) IsEmpty() bool { return strings.TrimSpace(chain.String()) == "" } +// GetNetworkName returns the network name from the network ID +func GetNetworkName(network int32) (string, bool) { + name, found := Network_name[network] + return name, found +} + // GetChainFromChainID returns the chain from the chain ID // additionalChains is a list of additional chains to search from // in practice, it is used in the protocol to dynamically support new chains without doing an upgrade diff --git a/pkg/chains/chain_test.go b/pkg/chains/chain_test.go index 23bc6adf18..0bbfbc5263 100644 --- a/pkg/chains/chain_test.go +++ b/pkg/chains/chain_test.go @@ -396,11 +396,19 @@ func TestChain_IsEmpty(t *testing.T) { require.False(t, chains.ZetaChainMainnet.IsEmpty()) } +func TestGetNetworkName(t *testing.T) { + network := int32(chains.Network_solana) + name, found := chains.GetNetworkName(network) + nameExpected, foundExpected := chains.Network_name[network] + require.Equal(t, nameExpected, name) + require.Equal(t, foundExpected, found) +} + func TestGetChainFromChainID(t *testing.T) { chain, found := chains.GetChainFromChainID(chains.ZetaChainMainnet.ChainId, []chains.Chain{}) require.EqualValues(t, chains.ZetaChainMainnet, chain) require.True(t, found) - chain, found = chains.GetChainFromChainID(9999, []chains.Chain{}) + _, found = chains.GetChainFromChainID(9999, []chains.Chain{}) require.False(t, found) } diff --git a/pkg/crypto/aes256_gcm.go b/pkg/crypto/aes256_gcm.go new file mode 100644 index 0000000000..f615fa79ea --- /dev/null +++ b/pkg/crypto/aes256_gcm.go @@ -0,0 +1,118 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + io "io" + + "github.com/pkg/errors" +) + +// EncryptAES256GCMBase64 encrypts the given string plaintext using AES-256-GCM with the given key and returns the base64-encoded ciphertext. +func EncryptAES256GCMBase64(plaintext string, encryptKey string) (string, error) { + // validate the input + if plaintext == "" { + return "", errors.New("plaintext must not be empty") + } + if encryptKey == "" { + return "", errors.New("encrypt key must not be empty") + } + + // encrypt the plaintext + ciphertext, err := EncryptAES256GCM([]byte(plaintext), encryptKey) + if err != nil { + return "", errors.Wrap(err, "failed to encrypt string plaintext") + } + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// DecryptAES256GCMBase64 decrypts the given base64-encoded ciphertext using AES-256-GCM with the given key. +func DecryptAES256GCMBase64(ciphertextBase64 string, decryptKey string) (string, error) { + // validate the input + if ciphertextBase64 == "" { + return "", errors.New("ciphertext must not be empty") + } + if decryptKey == "" { + return "", errors.New("decrypt key must not be empty") + } + + // decode the base64-encoded ciphertext + ciphertext, err := base64.StdEncoding.DecodeString(ciphertextBase64) + if err != nil { + return "", errors.Wrap(err, "failed to decode base64 ciphertext") + } + + // decrypt the ciphertext + plaintext, err := DecryptAES256GCM(ciphertext, decryptKey) + if err != nil { + return "", errors.Wrap(err, "failed to decrypt ciphertext") + } + return string(plaintext), nil +} + +// EncryptAES256GCM encrypts the given plaintext using AES-256-GCM with the given key. +func EncryptAES256GCM(plaintext []byte, encryptKey string) ([]byte, error) { + block, err := aes.NewCipher(getAESKey(encryptKey)) + if err != nil { + return nil, err + } + + // create GCM mode + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + // generate random nonce + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + // encrypt the plaintext + ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) + + return ciphertext, nil +} + +// DecryptAES256GCM decrypts the given ciphertext using AES-256-GCM with the given key. +func DecryptAES256GCM(ciphertext []byte, encryptKey string) ([]byte, error) { + block, err := aes.NewCipher(getAESKey(encryptKey)) + if err != nil { + return nil, err + } + + // create GCM mode + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + // get the nonce size + nonceSize := gcm.NonceSize() + if len(ciphertext) < nonceSize { + return nil, err + } + + // extract the nonce from the ciphertext + nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] + + // decrypt the ciphertext + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +} + +// getAESKey uses SHA-256 to create a 32-byte key AES encryption. +func getAESKey(key string) []byte { + h := sha256.New() + h.Write([]byte(key)) + + return h.Sum(nil) +} diff --git a/pkg/crypto/aes256_gcm_test.go b/pkg/crypto/aes256_gcm_test.go new file mode 100644 index 0000000000..ff83698ddb --- /dev/null +++ b/pkg/crypto/aes256_gcm_test.go @@ -0,0 +1,187 @@ +package crypto_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/crypto" +) + +func Test_EncryptDecryptAES256GCM(t *testing.T) { + tests := []struct { + name string + plaintext string + encryptKey string + decryptKey string + modifyFunc func([]byte) []byte + fail bool + }{ + { + name: "Successful encryption and decryption", + plaintext: "Hello, World!", + encryptKey: "my_password", + decryptKey: "my_password", + fail: false, + }, + { + name: "Decryption with incorrect key should fail", + plaintext: "Hello, World!", + encryptKey: "my_password", + decryptKey: "my_password2", + fail: true, + }, + { + name: "Decryption with corrupted ciphertext should fail", + plaintext: "Hello, World!", + encryptKey: "my_password", + decryptKey: "my_password", + modifyFunc: func(ciphertext []byte) []byte { + // flip the last bit of the ciphertext + ciphertext[len(ciphertext)-1] ^= 0x01 + return ciphertext + }, + fail: true, + }, + { + name: "Decryption with incorrect nonce should fail", + plaintext: "Hello, World!", + encryptKey: "my_password", + decryptKey: "my_password", + modifyFunc: func(ciphertext []byte) []byte { + // flip the first bit of the nonce + ciphertext[0] ^= 0x01 + return ciphertext + }, + fail: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + encrypted, err := crypto.EncryptAES256GCM([]byte(tt.plaintext), tt.encryptKey) + require.NoError(t, err) + + // modify the encrypted data if needed + if tt.modifyFunc != nil { + encrypted = tt.modifyFunc(encrypted) + } + + // decrypt the data + decrypted, err := crypto.DecryptAES256GCM(encrypted, tt.decryptKey) + if tt.fail { + require.Error(t, err) + return + } + + require.True(t, bytes.Equal(decrypted, []byte(tt.plaintext)), "decrypted plaintext does not match") + }) + } +} + +func Test_EncryptAES256GCMBase64(t *testing.T) { + tests := []struct { + name string + plaintext string + encryptKey string + decryptKey string + errorMessage string + }{ + { + name: "Successful encryption and decryption", + plaintext: "Hello, World!", + encryptKey: "my_password", + decryptKey: "my_password", + }, + { + name: "Encryption with empty plaintext should fail", + plaintext: "", + errorMessage: "plaintext must not be empty", + }, + { + name: "Encryption with empty encrypt key should fail", + plaintext: "Hello, World!", + encryptKey: "", + errorMessage: "encrypt key must not be empty", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // encrypt the data + ciphertextBase64, err := crypto.EncryptAES256GCMBase64(tt.plaintext, tt.encryptKey) + if tt.errorMessage != "" { + require.ErrorContains(t, err, tt.errorMessage) + return + } + + // decrypt the data + decrypted, err := crypto.DecryptAES256GCMBase64(ciphertextBase64, tt.decryptKey) + require.NoError(t, err) + + require.Equal(t, tt.plaintext, decrypted) + }) + } +} + +func Test_DecryptAES256GCMBase64(t *testing.T) { + tests := []struct { + name string + ciphertextBase64 string + plaintext string + decryptKey string + modifyFunc func(string) string + errorMessage string + }{ + { + name: "Successful decryption", + ciphertextBase64: "CXLWgHdVeZQwVOZZyHeZ5n5VB+eVSLaWFF0v0QOm9DyB7XSiHDwhNwQ=", + plaintext: "Hello, World!", + decryptKey: "my_password", + }, + { + name: "Decryption with empty ciphertext should fail", + ciphertextBase64: "", + decryptKey: "my_password", + errorMessage: "ciphertext must not be empty", + }, + { + name: "Decryption with empty decrypt key should fail", + ciphertextBase64: "CXLWgHdVeZQwVOZZyHeZ5n5VB+eVSLaWFF0v0QOm9DyB7XSiHDwhNwQ=", + decryptKey: "", + errorMessage: "decrypt key must not be empty", + }, + { + name: "Decryption with invalid base64 ciphertext should fail", + ciphertextBase64: "CXLWgHdVeZQwVOZZyHeZ5n5VB*eVSLaWFF0v0QOm9DyB7XSiHDwhNwQ=", // use '*' instead of '+' + decryptKey: "my_password", + errorMessage: "failed to decode base64 ciphertext", + }, + { + name: "Decryption with incorrect decrypt key should fail", + ciphertextBase64: "CXLWgHdVeZQwVOZZyHeZ5n5VB+eVSLaWFF0v0QOm9DyB7XSiHDwhNwQ=", + decryptKey: "my_password2", + errorMessage: "failed to decrypt ciphertext", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ciphertextBase64 := tt.ciphertextBase64 + + // modify the encrypted data if needed + if tt.modifyFunc != nil { + ciphertextBase64 = tt.modifyFunc(ciphertextBase64) + } + + // decrypt the data + decrypted, err := crypto.DecryptAES256GCMBase64(ciphertextBase64, tt.decryptKey) + if tt.errorMessage != "" { + require.ErrorContains(t, err, tt.errorMessage) + return + } + + require.Equal(t, tt.plaintext, decrypted) + }) + } +} diff --git a/pkg/os/path.go b/pkg/os/path.go new file mode 100644 index 0000000000..abf8368c64 --- /dev/null +++ b/pkg/os/path.go @@ -0,0 +1,33 @@ +package os + +import ( + "os" + "os/user" + "path/filepath" + "strings" +) + +// ExpandHomeDir expands a leading tilde in the path to the home directory of the current user. +// ~someuser/tmp will not be expanded. +func ExpandHomeDir(p string) (string, error) { + if p == "~" || + strings.HasPrefix(p, "~/") || + strings.HasPrefix(p, "~\\") { + usr, err := user.Current() + if err != nil { + return p, err + } + + p = filepath.Join(usr.HomeDir, p[1:]) + } + return filepath.Clean(p), nil +} + +// FileExists checks if a file exists. +func FileExists(filePath string) bool { + _, err := os.Stat(filePath) + if os.IsNotExist(err) { + return false + } + return err == nil +} diff --git a/pkg/os/path_test.go b/pkg/os/path_test.go new file mode 100644 index 0000000000..d02c55ef4e --- /dev/null +++ b/pkg/os/path_test.go @@ -0,0 +1,83 @@ +package os_test + +import ( + "os" + "os/user" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + zetaos "github.com/zeta-chain/zetacore/pkg/os" + "github.com/zeta-chain/zetacore/testutil/sample" +) + +func TestResolveHome(t *testing.T) { + usr, err := user.Current() + require.NoError(t, err) + + testCases := []struct { + name string + pathIn string + expected string + fail bool + }{ + { + name: `should resolve home with leading "~/"`, + pathIn: "~/tmp/file.json", + expected: filepath.Clean(filepath.Join(usr.HomeDir, "tmp/file.json")), + }, + { + name: "should resolve '~'", + pathIn: `~`, + expected: filepath.Clean(filepath.Join(usr.HomeDir, "")), + }, + { + name: "should not resolve '~someuser/tmp'", + pathIn: `~someuser/tmp`, + expected: `~someuser/tmp`, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + pathOut, err := zetaos.ExpandHomeDir(tc.pathIn) + require.NoError(t, err) + require.Equal(t, tc.expected, pathOut) + }) + } +} + +func TestFileExists(t *testing.T) { + path := sample.CreateTempDir(t) + + // create a test file + existingFile := filepath.Join(path, "test.txt") + _, err := os.Create(existingFile) + require.NoError(t, err) + + testCases := []struct { + name string + file string + expected bool + }{ + { + name: "should return true for existing file", + file: existingFile, + expected: true, + }, + { + name: "should return false for non-existing file", + file: filepath.Join(path, "non-existing.txt"), + expected: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + exists := zetaos.FileExists(tc.file) + require.Equal(t, tc.expected, exists) + }) + } +} diff --git a/rpc/namespaces/ethereum/debug/api.go b/rpc/namespaces/ethereum/debug/api.go index 3496da92d8..1b07828b80 100644 --- a/rpc/namespaces/ethereum/debug/api.go +++ b/rpc/namespaces/ethereum/debug/api.go @@ -37,6 +37,7 @@ import ( evmtypes "github.com/evmos/ethermint/x/evm/types" stderrors "github.com/pkg/errors" + zetaos "github.com/zeta-chain/zetacore/pkg/os" "github.com/zeta-chain/zetacore/rpc/backend" rpctypes "github.com/zeta-chain/zetacore/rpc/types" ) @@ -199,7 +200,7 @@ func (a *API) StartCPUProfile(file string) error { a.logger.Debug("CPU profiling already in progress") return errors.New("CPU profiling already in progress") default: - fp, err := ExpandHome(file) + fp, err := zetaos.ExpandHomeDir(file) if err != nil { a.logger.Debug("failed to get filepath for the CPU profile file", "error", err.Error()) return err diff --git a/rpc/namespaces/ethereum/debug/trace.go b/rpc/namespaces/ethereum/debug/trace.go index 28ba1c8043..ae35b16fc2 100644 --- a/rpc/namespaces/ethereum/debug/trace.go +++ b/rpc/namespaces/ethereum/debug/trace.go @@ -25,6 +25,8 @@ import ( "runtime/trace" stderrors "github.com/pkg/errors" + + zetaos "github.com/zeta-chain/zetacore/pkg/os" ) // StartGoTrace turns on tracing, writing to the given file. @@ -37,7 +39,7 @@ func (a *API) StartGoTrace(file string) error { a.logger.Debug("trace already in progress") return errors.New("trace already in progress") } - fp, err := ExpandHome(file) + fp, err := zetaos.ExpandHomeDir(file) if err != nil { a.logger.Debug("failed to get filepath for the CPU profile file", "error", err.Error()) return err diff --git a/rpc/namespaces/ethereum/debug/utils.go b/rpc/namespaces/ethereum/debug/utils.go index 277c37df56..ae3f0c5ba5 100644 --- a/rpc/namespaces/ethereum/debug/utils.go +++ b/rpc/namespaces/ethereum/debug/utils.go @@ -17,13 +17,12 @@ package debug import ( "os" - "os/user" - "path/filepath" "runtime/pprof" - "strings" "github.com/cometbft/cometbft/libs/log" "github.com/cosmos/cosmos-sdk/server" + + zetaos "github.com/zeta-chain/zetacore/pkg/os" ) // isCPUProfileConfigurationActivated checks if cpuprofile was configured via flag @@ -33,25 +32,11 @@ func isCPUProfileConfigurationActivated(ctx *server.Context) bool { return ctx.Viper.GetString("cpu-profile") != "" } -// ExpandHome expands home directory in file paths. -// ~someuser/tmp will not be expanded. -func ExpandHome(p string) (string, error) { - if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") { - usr, err := user.Current() - if err != nil { - return p, err - } - home := usr.HomeDir - p = home + p[1:] - } - return filepath.Clean(p), nil -} - // writeProfile writes the data to a file func writeProfile(name, file string, log log.Logger) error { p := pprof.Lookup(name) log.Info("Writing profile records", "count", p.Count(), "type", name, "dump", file) - fp, err := ExpandHome(file) + fp, err := zetaos.ExpandHomeDir(file) if err != nil { return err } diff --git a/server/start.go b/server/start.go index 64bb1db4e6..79abab78bf 100644 --- a/server/start.go +++ b/server/start.go @@ -58,7 +58,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - ethdebug "github.com/zeta-chain/zetacore/rpc/namespaces/ethereum/debug" + zetaos "github.com/zeta-chain/zetacore/pkg/os" "github.com/zeta-chain/zetacore/server/config" srvflags "github.com/zeta-chain/zetacore/server/flags" ) @@ -337,7 +337,7 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt logger := ctx.Logger if cpuProfile := ctx.Viper.GetString(srvflags.CPUProfile); cpuProfile != "" { - fp, err := ethdebug.ExpandHome(cpuProfile) + fp, err := zetaos.ExpandHomeDir(cpuProfile) if err != nil { ctx.Logger.Debug("failed to get filepath for the CPU profile file", "error", err.Error()) return err diff --git a/zetaclient/keys/relayer_keys.go b/zetaclient/keys/relayer_keys.go index cf7b20f724..97dd7ca8b2 100644 --- a/zetaclient/keys/relayer_keys.go +++ b/zetaclient/keys/relayer_keys.go @@ -2,14 +2,15 @@ package keys import ( "encoding/json" - "fmt" "io" "os" "path" + "github.com/gagliardetto/solana-go" "github.com/pkg/errors" "github.com/zeta-chain/zetacore/pkg/chains" + zetaos "github.com/zeta-chain/zetacore/pkg/os" ) const ( @@ -22,6 +23,26 @@ type RelayerKey struct { PrivateKey string `json:"private_key"` } +// ResolveAddress returns the network name and address of the relayer key +func (rk RelayerKey) ResolveAddress(network int32) (string, string, error) { + // get network name + networkName, found := chains.GetNetworkName(network) + if !found { + return "", "", errors.Errorf("network name not found for network %d", network) + } + + switch chains.Network(network) { + case chains.Network_solana: + privKey, err := solana.PrivateKeyFromBase58(rk.PrivateKey) + if err != nil { + return "", "", errors.Wrap(err, "unable to construct solana private key") + } + return networkName, privKey.PublicKey().String(), nil + default: + return "", "", errors.Errorf("cannot derive relayer address for unsupported network %d", network) + } +} + // LoadRelayerKey loads a relayer key from given path and chain func LoadRelayerKey(keyPath string, chain chains.Chain) (RelayerKey, error) { // determine relayer key file name based on chain @@ -44,18 +65,23 @@ func LoadRelayerKey(keyPath string, chain chains.Chain) (RelayerKey, error) { // ReadRelayerKeyFromFile reads the relayer key file and returns the key func ReadRelayerKeyFromFile(fileName string) (RelayerKey, error) { - fileName = "/root/.zetacored/relayer-keys/solana.json" - fmt.Println("Reading relayer key from file: ", fileName) - file, err := os.Open(fileName) + // expand home directory in the file path if it exists + fileNameFull, err := zetaos.ExpandHomeDir(fileName) if err != nil { - return RelayerKey{}, errors.Wrapf(err, "unable to open relayer key file: %s", fileName) + return RelayerKey{}, errors.Wrapf(err, "ExpandHome failed for file: %s", fileName) + } + + // open the file + file, err := os.Open(fileNameFull) + if err != nil { + return RelayerKey{}, errors.Wrapf(err, "unable to open relayer key file: %s", fileNameFull) } defer file.Close() // read the file contents fileData, err := io.ReadAll(file) if err != nil { - return RelayerKey{}, errors.Wrapf(err, "unable to read relayer key data: %s", fileName) + return RelayerKey{}, errors.Wrapf(err, "unable to read relayer key data: %s", fileNameFull) } // unmarshal the JSON data into the struct @@ -67,3 +93,20 @@ func ReadRelayerKeyFromFile(fileName string) (RelayerKey, error) { return key, nil } + +// GetRelayerKeyFileByNetwork returns the relayer key file name based on network +func GetRelayerKeyFileByNetwork(network int32) (string, error) { + // get network name + networkName, found := chains.GetNetworkName(network) + if !found { + return "", errors.Errorf("network name not found for network %d", network) + } + + // return file name for supported networks only + switch chains.Network(network) { + case chains.Network_solana: + return networkName + ".json", nil + default: + return "", errors.Errorf("network %d does not support relayer key", network) + } +} From 6a64051689640dafd1ddbb3279f7b3ad184aba89 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Sun, 11 Aug 2024 23:14:25 -0500 Subject: [PATCH 03/13] integrate relayer key into E2E and Solana signer --- cmd/zetaclientd-supervisor/lib.go | 22 -- cmd/zetaclientd-supervisor/main.go | 8 +- cmd/zetaclientd/import_relayer_keys.go | 68 ++--- cmd/zetaclientd/start.go | 38 +-- contrib/localnet/scripts/password.file | 1 + contrib/localnet/scripts/start-zetaclientd.sh | 21 +- pkg/os/console.go | 29 ++ pkg/os/console_test.go | 65 +++++ testutil/sample/crypto.go | 11 +- zetaclient/chains/solana/signer/signer.go | 47 +++- zetaclient/chains/solana/signer/withdraw.go | 2 +- zetaclient/context/app.go | 39 ++- zetaclient/keys/relayer_key.go | 149 ++++++++++ zetaclient/keys/relayer_key_test.go | 258 ++++++++++++++++++ zetaclient/keys/relayer_keys.go | 112 -------- zetaclient/orchestrator/bootstrap.go | 11 +- 16 files changed, 628 insertions(+), 253 deletions(-) create mode 100644 pkg/os/console.go create mode 100644 pkg/os/console_test.go create mode 100644 zetaclient/keys/relayer_key.go create mode 100644 zetaclient/keys/relayer_key_test.go delete mode 100644 zetaclient/keys/relayer_keys.go diff --git a/cmd/zetaclientd-supervisor/lib.go b/cmd/zetaclientd-supervisor/lib.go index 71f492e88b..fe62e0c07a 100644 --- a/cmd/zetaclientd-supervisor/lib.go +++ b/cmd/zetaclientd-supervisor/lib.go @@ -1,7 +1,6 @@ package main import ( - "bufio" "context" "encoding/json" "errors" @@ -10,7 +9,6 @@ import ( "os" "path" "runtime" - "strings" "sync" "syscall" "time" @@ -383,23 +381,3 @@ func (s *zetaclientdSupervisor) downloadZetaclientd(ctx context.Context, plan *u } return nil } - -func promptPasswords() (string, string, error) { - reader := bufio.NewReader(os.Stdin) - fmt.Print("HotKey Password: ") - hotKeyPass, err := reader.ReadString('\n') - if err != nil { - return "", "", err - } - fmt.Print("TSS Password: ") - tssKeyPass, err := reader.ReadString('\n') - if err != nil { - return "", "", err - } - - //trim delimiters - hotKeyPass = strings.TrimSuffix(hotKeyPass, "\n") - tssKeyPass = strings.TrimSuffix(tssKeyPass, "\n") - - return hotKeyPass, tssKeyPass, err -} diff --git a/cmd/zetaclientd-supervisor/main.go b/cmd/zetaclientd-supervisor/main.go index ee1e247be4..955a0097f1 100644 --- a/cmd/zetaclientd-supervisor/main.go +++ b/cmd/zetaclientd-supervisor/main.go @@ -7,12 +7,14 @@ import ( "os" "os/exec" "os/signal" + "strings" "syscall" "time" "golang.org/x/sync/errgroup" "github.com/zeta-chain/zetacore/app" + zetaos "github.com/zeta-chain/zetacore/pkg/os" "github.com/zeta-chain/zetacore/zetaclient/config" ) @@ -36,7 +38,9 @@ func main() { shutdownChan := make(chan os.Signal, 1) signal.Notify(shutdownChan, syscall.SIGINT, syscall.SIGTERM) - hotkeyPassword, tssPassword, err := promptPasswords() + // prompt for all necessary passwords + titles := []string{"HotKey", "TSS", "Solana Relayer Key"} + passwords, err := zetaos.PromptPasswords(titles) if err != nil { logger.Error().Err(err).Msg("unable to get passwords") os.Exit(1) @@ -65,7 +69,7 @@ func main() { cmd.Stderr = os.Stderr // must reset the passwordInputBuffer every iteration because reads are stateful (seek to end) passwordInputBuffer := bytes.Buffer{} - passwordInputBuffer.Write([]byte(hotkeyPassword + "\n" + tssPassword + "\n")) + passwordInputBuffer.Write([]byte(strings.Join(passwords, "\n") + "\n")) cmd.Stdin = &passwordInputBuffer eg, ctx := errgroup.WithContext(ctx) diff --git a/cmd/zetaclientd/import_relayer_keys.go b/cmd/zetaclientd/import_relayer_keys.go index fef20f6ed1..0f2f713336 100644 --- a/cmd/zetaclientd/import_relayer_keys.go +++ b/cmd/zetaclientd/import_relayer_keys.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "fmt" "os" "path/filepath" @@ -10,6 +9,7 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/crypto" zetaos "github.com/zeta-chain/zetacore/pkg/os" "github.com/zeta-chain/zetacore/zetaclient/keys" @@ -62,13 +62,13 @@ func init() { CmdImportRelayerKey.Flags(). StringVar(&importArgs.privateKey, "private-key", "", "the relayer private key to import") CmdImportRelayerKey.Flags(). - StringVar(&importArgs.password, "password", "", "the password to encrypt the private key") + StringVar(&importArgs.password, "password", "", "the password to encrypt the relayer private key") CmdImportRelayerKey.Flags(). StringVar(&importArgs.relayerKeyPath, "relayer-key-path", defaultRelayerKeyPath, "path to relayer keys") CmdRelayerAddress.Flags().Int32Var(&addressArgs.network, "network", 7, "network id, (7:solana)") CmdRelayerAddress.Flags(). - StringVar(&addressArgs.password, "password", "", "the password to decrypt the private key") + StringVar(&addressArgs.password, "password", "", "the password to decrypt the relayer private key") CmdRelayerAddress.Flags(). StringVar(&addressArgs.relayerKeyPath, "relayer-key-path", defaultRelayerKeyPath, "path to relayer keys") } @@ -84,12 +84,13 @@ func ImportRelayerKey(_ *cobra.Command, _ []string) error { } // resolve the relayer key file path - keyPath, fileName, err := resolveRelayerKeyPath(importArgs.network, importArgs.relayerKeyPath) + fileName, err := keys.ResolveRelayerKeyFile(importArgs.relayerKeyPath, chains.Network(importArgs.network)) if err != nil { return errors.Wrap(err, "failed to resolve relayer key file path") } // create path (owner `rwx` permissions) if it does not exist + keyPath := filepath.Dir(fileName) if _, err := os.Stat(keyPath); os.IsNotExist(err) { if err := os.MkdirAll(keyPath, 0o700); err != nil { return errors.Wrapf(err, "failed to create relayer key path: %s", keyPath) @@ -110,14 +111,8 @@ func ImportRelayerKey(_ *cobra.Command, _ []string) error { return errors.Wrap(err, "private key encryption failed") } - // construct the relayer key struct and write to file as json - keyData, err := json.Marshal(keys.RelayerKey{PrivateKey: ciphertext}) - if err != nil { - return errors.Wrap(err, "failed to marshal relayer key") - } - - // create relay key file (owner `rw` permissions) - err = os.WriteFile(fileName, keyData, 0o600) + // create the relayer key file + err = keys.WriteRelayerKeyToFile(fileName, keys.RelayerKey{PrivateKey: ciphertext}) if err != nil { return errors.Wrapf(err, "failed to create relayer key file: %s", fileName) } @@ -128,27 +123,24 @@ func ImportRelayerKey(_ *cobra.Command, _ []string) error { // ShowRelayerAddress shows the relayer address func ShowRelayerAddress(_ *cobra.Command, _ []string) error { - // resolve the relayer key file path - _, fileName, err := resolveRelayerKeyPath(addressArgs.network, addressArgs.relayerKeyPath) - if err != nil { - return errors.Wrap(err, "failed to resolve relayer key file path") - } - - // read the relayer key file - relayerKey, err := keys.ReadRelayerKeyFromFile(fileName) + // try loading the relayer key if present + network := chains.Network(addressArgs.network) + relayerKey, err := keys.LoadRelayerKey(addressArgs.relayerKeyPath, network, addressArgs.password) if err != nil { - return err + return errors.Wrap(err, "failed to load relayer key") } - // decrypt the private key - privateKey, err := crypto.DecryptAES256GCMBase64(relayerKey.PrivateKey, addressArgs.password) - if err != nil { - return errors.Wrap(err, "private key decryption failed") + // relayer key does not exist, return error + if relayerKey == nil { + return fmt.Errorf( + "relayer key not found for network %d in path: %s", + addressArgs.network, + addressArgs.relayerKeyPath, + ) } - relayerKey.PrivateKey = privateKey - // resolve the address - networkName, address, err := relayerKey.ResolveAddress(addressArgs.network) + // resolve the relayer address + networkName, address, err := relayerKey.ResolveAddress(network) if err != nil { return errors.Wrap(err, "failed to resolve relayer address") } @@ -156,23 +148,3 @@ func ShowRelayerAddress(_ *cobra.Command, _ []string) error { return nil } - -// resolveRelayerKeyPath is a helper function to resolve the relayer key file path and name -func resolveRelayerKeyPath(network int32, relayerKeyPath string) (string, string, error) { - // get relayer key file name by network - name, err := keys.GetRelayerKeyFileByNetwork(network) - if err != nil { - return "", "", errors.Wrap(err, "failed to get relayer key file name") - } - - // resolve relayer key path if it contains a tilde - keyPath, err := zetaos.ExpandHomeDir(relayerKeyPath) - if err != nil { - return "", "", errors.Wrap(err, "failed to resolve relayer key path") - } - - // build file name - fileName := filepath.Join(keyPath, name) - - return keyPath, fileName, err -} diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 281043cb27..6b357db38f 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -1,7 +1,6 @@ package main import ( - "bufio" "context" "encoding/json" "fmt" @@ -21,7 +20,9 @@ import ( "github.com/spf13/cobra" "github.com/zeta-chain/zetacore/pkg/authz" + "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/constant" + zetaos "github.com/zeta-chain/zetacore/pkg/os" observerTypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/config" @@ -49,10 +50,15 @@ func start(_ *cobra.Command, _ []string) error { SetupConfigForTest() - //Prompt for Hotkey and TSS key-share passwords - hotkeyPass, tssKeyPass, err := promptPasswords() + // Prompt for Hotkey, TSS key-share and relayer key passwords + titles := []string{"HotKey", "TSS", "Solana Relayer Key"} + passwords, err := zetaos.PromptPasswords(titles) if err != nil { - return err + return errors.Wrap(err, "unable to get passwords") + } + hotkeyPass, tssKeyPass, solanaKeyPass := passwords[0], passwords[1], passwords[2] + relayerKeyPasswords := map[chains.Network]string{ + chains.Network_solana: solanaKeyPass, } //Load Config file given path @@ -77,6 +83,7 @@ func start(_ *cobra.Command, _ []string) error { startLogger := logger.Std.With().Str("module", "startup").Logger() appContext := zctx.New(cfg, masterLogger) + appContext.SetRelayerKeyPasswords(relayerKeyPasswords) ctx := zctx.WithAppContext(context.Background(), appContext) // Wait until zetacore is up @@ -397,29 +404,6 @@ func initPreParams(path string) { } } -// promptPasswords() This function will prompt for passwords which will be used to decrypt two key files: -// 1. HotKey -// 2. TSS key-share -func promptPasswords() (string, string, error) { - reader := bufio.NewReader(os.Stdin) - fmt.Print("HotKey Password: ") - hotKeyPass, err := reader.ReadString('\n') - if err != nil { - return "", "", err - } - fmt.Print("TSS Password: ") - TSSKeyPass, err := reader.ReadString('\n') - if err != nil { - return "", "", err - } - - //trim delimiters - hotKeyPass = strings.TrimSuffix(hotKeyPass, "\n") - TSSKeyPass = strings.TrimSuffix(TSSKeyPass, "\n") - - return hotKeyPass, TSSKeyPass, err -} - // isObserverNode checks whether THIS node is an observer node. func isObserverNode(ctx context.Context, client *zetacore.Client) (bool, error) { observers, err := client.GetObserverList(ctx) diff --git a/contrib/localnet/scripts/password.file b/contrib/localnet/scripts/password.file index 96b3814661..efedb37b66 100644 --- a/contrib/localnet/scripts/password.file +++ b/contrib/localnet/scripts/password.file @@ -1,2 +1,3 @@ password pass2 +pass_relayerkey diff --git a/contrib/localnet/scripts/start-zetaclientd.sh b/contrib/localnet/scripts/start-zetaclientd.sh index 64117621d2..df61ce48f5 100755 --- a/contrib/localnet/scripts/start-zetaclientd.sh +++ b/contrib/localnet/scripts/start-zetaclientd.sh @@ -14,16 +14,13 @@ set_sepolia_endpoint() { jq '.EVMChainConfigs."11155111".Endpoint = "http://eth2:8545"' /root/.zetacored/config/zetaclient_config.json > tmp.json && mv tmp.json /root/.zetacored/config/zetaclient_config.json } -# creates a file that contains a relayer private key (e.g. Solana relayer key) -create_relayer_key_file() { +# import a relayer private key (e.g. Solana relayer key) +import_relayer_key() { local num="$1" - local file="$2" - # read observer relayer private key from config - privkey_relayer=$(yq -r ".observer_relayer_accounts.relayer_account_${num}.solana_private_key" /root/config.yml) - - # create the key file that contains the private key - jq -n --arg privkey_relayer "$privkey_relayer" '{"private_key": $privkey_relayer}' > "${file}" + # import solana (network=7) relayer private key + privkey_solana=$(yq -r ".observer_relayer_accounts.relayer_account_${num}.solana_private_key" /root/config.yml) + zetaclientd import-relayer-key --network=7 --private-key="$privkey_solana" --password=pass_relayerkey } PREPARAMS_PATH="/root/preparams/${HOSTNAME}.json" @@ -78,8 +75,8 @@ then MYIP=$(/sbin/ip -o -4 addr list eth0 | awk '{print $4}' | cut -d/ -f1) zetaclientd init --zetacore-url zetacore0 --chain-id athens_101-1 --operator "$operatorAddress" --log-format=text --public-ip "$MYIP" --keyring-backend "$BACKEND" --pre-params "$PREPARAMS_PATH" - # create relayer key file for solana - create_relayer_key_file 0 "${RELAYER_KEY_PATH}/solana.json" + # import relayer private key for zetaclient0 + import_relayer_key 0 # if eth2 is enabled, set the endpoint in the zetaclient_config.json # in this case, the additional evm is represented with the sepolia chain, we set manually the eth2 endpoint to the sepolia chain (11155111 -> http://eth2:8545) @@ -101,8 +98,8 @@ then done zetaclientd init --peer "/ip4/172.20.0.21/tcp/6668/p2p/${SEED}" --zetacore-url "$node" --chain-id athens_101-1 --operator "$operatorAddress" --log-format=text --public-ip "$MYIP" --log-level 1 --keyring-backend "$BACKEND" --pre-params "$PREPARAMS_PATH" - # create relayer key file for solana - create_relayer_key_file "${num}" "${RELAYER_KEY_PATH}/solana.json" + # import relayer private key for zetaclient{$num} + import_relayer_key "${num}" # check if the option is additional-evm # in this case, the additional evm is represented with the sepolia chain, we set manually the eth2 endpoint to the sepolia chain (11155111 -> http://eth2:8545) diff --git a/pkg/os/console.go b/pkg/os/console.go new file mode 100644 index 0000000000..28102ef1fd --- /dev/null +++ b/pkg/os/console.go @@ -0,0 +1,29 @@ +package os + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +// PromptPasswords prompts the user for passwords with the given titles +func PromptPasswords(passwordTitles []string) ([]string, error) { + reader := bufio.NewReader(os.Stdin) + passwords := make([]string, len(passwordTitles)) + + // iterate over password titles and prompt for each + for i, title := range passwordTitles { + fmt.Printf("%s Password: ", title) + password, err := reader.ReadString('\n') + if err != nil { + return nil, err + } + + // trim delimiters + password = strings.TrimSuffix(password, "\n") + passwords[i] = password + } + + return passwords, nil +} diff --git a/pkg/os/console_test.go b/pkg/os/console_test.go new file mode 100644 index 0000000000..bdacfa2a39 --- /dev/null +++ b/pkg/os/console_test.go @@ -0,0 +1,65 @@ +package os_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + zetaos "github.com/zeta-chain/zetacore/pkg/os" +) + +// Test function for PromptPasswords +func Test_PromptPasswords(t *testing.T) { + tests := []struct { + name string + passwordTitles []string + input string + expected []string + }{ + { + name: "Single password prompt", + passwordTitles: []string{"HotKey"}, + input: "pass123\n", + expected: []string{"pass123"}, + }, + { + name: "Multiple password prompts", + passwordTitles: []string{"HotKey", "TSS", "RelayerKey"}, + input: "pass_hotkey\npass_tss\npass_relayer\n", + expected: []string{"pass_hotkey", "pass_tss", "pass_relayer"}, + }, + { + name: "Empty input for passwords is allowed", + passwordTitles: []string{"HotKey", "TSS", "RelayerKey"}, + input: "\n\n\n", + expected: []string{"", "", ""}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a pipe to simulate stdin + r, w, err := os.Pipe() + require.NoError(t, err) + + // Write the test input to the pipe + _, err = w.Write([]byte(tt.input)) + require.NoError(t, err) + w.Close() // Close the write end of the pipe + + // Backup the original stdin and restore it after the test + oldStdin := os.Stdin + defer func() { os.Stdin = oldStdin }() + + // Redirect stdin to the read end of the pipe + os.Stdin = r + + // Call the function with the test case data + passwords, err := zetaos.PromptPasswords(tt.passwordTitles) + + // Check the returned passwords + require.NoError(t, err) + require.Equal(t, tt.expected, passwords) + }) + } +} diff --git a/testutil/sample/crypto.go b/testutil/sample/crypto.go index a46310fb25..906b8f6ee0 100644 --- a/testutil/sample/crypto.go +++ b/testutil/sample/crypto.go @@ -57,11 +57,18 @@ func EthAddress() ethcommon.Address { return ethcommon.BytesToAddress(sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()).Bytes()) } +// SolanaPrivateKey returns a sample solana private key +func SolanaPrivateKey(t *testing.T) solana.PrivateKey { + privKey, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + return privKey +} + // SolanaAddress returns a sample solana address func SolanaAddress(t *testing.T) string { - keypair, err := solana.NewRandomPrivateKey() + privKey, err := solana.NewRandomPrivateKey() require.NoError(t, err) - return keypair.PublicKey().String() + return privKey.PublicKey().String() } // SolanaSignature returns a sample solana signature diff --git a/zetaclient/chains/solana/signer/signer.go b/zetaclient/chains/solana/signer/signer.go index a250f31c69..cc9b6e14f1 100644 --- a/zetaclient/chains/solana/signer/signer.go +++ b/zetaclient/chains/solana/signer/signer.go @@ -30,7 +30,8 @@ type Signer struct { client interfaces.SolanaRPCClient // relayerKey is the private key of the relayer account for Solana chain - relayerKey solana.PrivateKey + // relayerKey is optional, the signer will not relay transactions if it is not set + relayerKey *solana.PrivateKey // gatewayID is the program ID of gateway program on Solana chain gatewayID solana.PublicKey @@ -45,7 +46,7 @@ func NewSigner( chainParams observertypes.ChainParams, solClient interfaces.SolanaRPCClient, tss interfaces.TSSSigner, - relayerKey keys.RelayerKey, + relayerKey *keys.RelayerKey, ts *metrics.TelemetryServer, logger base.Logger, ) (*Signer, error) { @@ -58,21 +59,32 @@ func NewSigner( return nil, errors.Wrapf(err, "cannot parse gateway address %s", chainParams.GatewayAddress) } - // construct Solana private key - privKey, err := solana.PrivateKeyFromBase58(relayerKey.PrivateKey) - if err != nil { - return nil, errors.Wrap(err, "unable to construct solana private key") + // create Solana signer + signer := &Signer{ + Signer: baseSigner, + client: solClient, + gatewayID: gatewayID, + pda: pda, } - logger.Std.Info().Msgf("Solana relayer address: %s", privKey.PublicKey()) - // create Solana signer - return &Signer{ - Signer: baseSigner, - client: solClient, - relayerKey: privKey, - gatewayID: gatewayID, - pda: pda, - }, nil + // construct Solana private key if present + if relayerKey != nil { + privKey, err := solana.PrivateKeyFromBase58(relayerKey.PrivateKey) + if err != nil { + return nil, errors.Wrap(err, "unable to construct solana private key") + } + signer.relayerKey = &privKey + logger.Std.Info().Msgf("Solana relayer address: %s", privKey.PublicKey()) + } else { + logger.Std.Info().Msg("Solana relayer key is not provided") + } + + return signer, nil +} + +// HasRelayerKey returns true if the signer has a relayer key +func (signer *Signer) HasRelayerKey() bool { + return signer.relayerKey != nil } // TryProcessOutbound - signer interface implementation @@ -121,6 +133,11 @@ func (signer *Signer) TryProcessOutbound( return } + // skip relaying the transaction if this signer hasn't set the relayer key + if !signer.HasRelayerKey() { + return + } + // sign the withdraw transaction by relayer key tx, err := signer.SignWithdrawTx(ctx, *msg) if err != nil { diff --git a/zetaclient/chains/solana/signer/withdraw.go b/zetaclient/chains/solana/signer/withdraw.go index 9cd11c40b7..f44dc3fc30 100644 --- a/zetaclient/chains/solana/signer/withdraw.go +++ b/zetaclient/chains/solana/signer/withdraw.go @@ -92,7 +92,7 @@ func (signer *Signer) SignWithdrawTx(ctx context.Context, msg contracts.MsgWithd // relayer signs the transaction _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { if key.Equals(privkey.PublicKey()) { - return &privkey + return privkey } return nil }) diff --git a/zetaclient/context/app.go b/zetaclient/context/app.go index 032d0b759c..3bb4ab698d 100644 --- a/zetaclient/context/app.go +++ b/zetaclient/context/app.go @@ -19,14 +19,26 @@ import ( // AppContext represents application (zetaclient) context. type AppContext struct { + // config is the config of the app config config.Config + + // logger is the logger of the app logger zerolog.Logger + // chainRegistry is a registry of supported chains chainRegistry *ChainRegistry + // currentTssPubKey is the current tss pubKey currentTssPubKey string - crosschainFlags observertypes.CrosschainFlags - keygen observertypes.Keygen + + // crosschainFlags is the current crosschain flags state + crosschainFlags observertypes.CrosschainFlags + + // keygen is the current tss keygen state + keygen observertypes.Keygen + + // relayerKeyPasswords maps network id to relayer key password + relayerKeyPasswords map[chains.Network]string mu sync.RWMutex } @@ -39,9 +51,10 @@ func New(cfg config.Config, logger zerolog.Logger) *AppContext { chainRegistry: NewChainRegistry(), - crosschainFlags: observertypes.CrosschainFlags{}, - currentTssPubKey: "", - keygen: observertypes.Keygen{}, + crosschainFlags: observertypes.CrosschainFlags{}, + currentTssPubKey: "", + keygen: observertypes.Keygen{}, + relayerKeyPasswords: make(map[chains.Network]string), mu: sync.RWMutex{}, } @@ -127,6 +140,22 @@ func (a *AppContext) GetCrossChainFlags() observertypes.CrosschainFlags { return a.crosschainFlags } +// SetRelayerKeyPasswords sets the relayer key passwords for given networks +func (a *AppContext) SetRelayerKeyPasswords(relayerKeyPasswords map[chains.Network]string) { + a.mu.Lock() + defer a.mu.Unlock() + + a.relayerKeyPasswords = relayerKeyPasswords +} + +// GetRelayerKeyPassword returns the relayer key password for the given network +func (a *AppContext) GetRelayerKeyPassword(network chains.Network) string { + a.mu.RLock() + defer a.mu.RUnlock() + + return a.relayerKeyPasswords[network] +} + // Update updates AppContext and params for all chains // this must be the ONLY function that writes to AppContext func (a *AppContext) Update( diff --git a/zetaclient/keys/relayer_key.go b/zetaclient/keys/relayer_key.go new file mode 100644 index 0000000000..8de6cc9853 --- /dev/null +++ b/zetaclient/keys/relayer_key.go @@ -0,0 +1,149 @@ +package keys + +import ( + "encoding/json" + "io" + "os" + "path/filepath" + + "github.com/gagliardetto/solana-go" + "github.com/pkg/errors" + + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/crypto" + zetaos "github.com/zeta-chain/zetacore/pkg/os" +) + +// RelayerKey is the structure that holds the relayer private key +type RelayerKey struct { + PrivateKey string `json:"private_key"` +} + +// ResolveAddress returns the network name and address of the relayer key +func (rk RelayerKey) ResolveAddress(network chains.Network) (string, string, error) { + // get network name + networkName, found := chains.GetNetworkName(int32(network)) + if !found { + return "", "", errors.Errorf("network name not found for network %d", network) + } + + switch network { + case chains.Network_solana: + privKey, err := solana.PrivateKeyFromBase58(rk.PrivateKey) + if err != nil { + return "", "", errors.Wrap(err, "unable to construct solana private key") + } + return networkName, privKey.PublicKey().String(), nil + default: + return "", "", errors.Errorf("cannot derive relayer address for unsupported network %d", network) + } +} + +// LoadRelayerKey loads the relayer key for given network and password +func LoadRelayerKey(relayerKeyPath string, network chains.Network, password string) (*RelayerKey, error) { + // resolve the relayer key file name + fileName, err := ResolveRelayerKeyFile(relayerKeyPath, network) + if err != nil { + return nil, errors.Wrap(err, "failed to resolve relayer key file name") + } + + // load the relayer key if it is present + if zetaos.FileExists(fileName) { + // read the relayer key file + relayerKey, err := ReadRelayerKeyFromFile(fileName) + if err != nil { + return nil, errors.Wrapf(err, "failed to read relayer key file: %s", fileName) + } + + // decrypt the private key + privateKey, err := crypto.DecryptAES256GCMBase64(relayerKey.PrivateKey, password) + if err != nil { + return nil, errors.Wrap(err, "failed to decrypt private key") + } + + relayerKey.PrivateKey = privateKey + return relayerKey, nil + } + + // relayer key is optional, so it's okay if the relayer key is not provided + return nil, nil +} + +// ResolveRelayerKeyFile is a helper function to resolve the relayer key file with full path +func ResolveRelayerKeyFile(relayerKeyPath string, network chains.Network) (string, error) { + // resolve relayer key path if it contains a tilde + keyPath, err := zetaos.ExpandHomeDir(relayerKeyPath) + if err != nil { + return "", errors.Wrap(err, "failed to resolve relayer key path") + } + + // get relayer key file name by network + name, err := relayerKeyFileByNetwork(network) + if err != nil { + return "", errors.Wrap(err, "failed to get relayer key file name") + } + + return filepath.Join(keyPath, name), nil +} + +// WriteRelayerKeyToFile writes the relayer key to a file +func WriteRelayerKeyToFile(fileName string, relayerKey RelayerKey) error { + keyData, err := json.Marshal(relayerKey) + if err != nil { + return errors.Wrap(err, "failed to marshal relayer key") + } + + // create relay key file (owner `rw` permissions) + return os.WriteFile(fileName, keyData, 0o600) +} + +// ReadRelayerKeyFromFile reads the relayer key file and returns the key +func ReadRelayerKeyFromFile(fileName string) (*RelayerKey, error) { + // expand home directory in the file path if it exists + fileNameFull, err := zetaos.ExpandHomeDir(fileName) + if err != nil { + return nil, errors.Wrapf(err, "ExpandHome failed for file: %s", fileName) + } + + // open the file + file, err := os.Open(fileNameFull) + if err != nil { + return nil, errors.Wrapf(err, "unable to open relayer key file: %s", fileNameFull) + } + defer file.Close() + + // read the file contents + fileData, err := io.ReadAll(file) + if err != nil { + return nil, errors.Wrapf(err, "unable to read relayer key data: %s", fileNameFull) + } + + // unmarshal the JSON data into the struct + var key RelayerKey + err = json.Unmarshal(fileData, &key) + if err != nil { + return nil, errors.Wrap(err, "unable to unmarshal relayer key") + } + + return &key, nil +} + +// relayerKeyFileByNetwork returns the relayer key JSON file name based on network +func relayerKeyFileByNetwork(network chains.Network) (string, error) { + // get network name + networkName, found := chains.GetNetworkName(int32(network)) + if !found { + return "", errors.Errorf("network name not found for network %d", network) + } + + // JSONFileSuffix is the suffix for the relayer key file + const JSONFileSuffix = ".json" + + // return file name for supported networks only + switch network { + case chains.Network_solana: + return networkName + JSONFileSuffix, nil + default: + return "", errors.Errorf("network %d does not support relayer key", network) + } +} diff --git a/zetaclient/keys/relayer_key_test.go b/zetaclient/keys/relayer_key_test.go new file mode 100644 index 0000000000..3d5c8a1d1e --- /dev/null +++ b/zetaclient/keys/relayer_key_test.go @@ -0,0 +1,258 @@ +package keys_test + +import ( + "os" + "os/user" + "path" + "testing" + + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/crypto" + "github.com/zeta-chain/zetacore/testutil/sample" + "github.com/zeta-chain/zetacore/zetaclient/keys" +) + +// createRelayerKeyFile creates a relayer key file for testing +func createRelayerKeyFile(t *testing.T, fileName, privKey, password string) { + // encrypt the private key + ciphertext, err := crypto.EncryptAES256GCMBase64(privKey, password) + require.NoError(t, err) + + // create relayer key file + err = keys.WriteRelayerKeyToFile(fileName, keys.RelayerKey{PrivateKey: ciphertext}) + require.NoError(t, err) +} + +// createBadRelayerKeyFile creates a bad relayer key file for testing +func createBadRelayerKeyFile(t *testing.T, fileName string) { + err := os.WriteFile(fileName, []byte("arbitrary data"), 0o600) + require.NoError(t, err) +} + +func Test_ResolveAddress(t *testing.T) { + // sample test keys + solanaPrivKey := sample.SolanaPrivateKey(t) + + tests := []struct { + name string + network chains.Network + relayerKey keys.RelayerKey + expectedNetworkName string + expectedAddress string + expectedError string + }{ + { + name: "should resolve solana address", + network: chains.Network_solana, + relayerKey: keys.RelayerKey{ + PrivateKey: solanaPrivKey.String(), + }, + expectedNetworkName: "solana", + expectedAddress: solanaPrivKey.PublicKey().String(), + }, + { + name: "should return error if network name not found", + network: chains.Network(999), + relayerKey: keys.RelayerKey{ + PrivateKey: solanaPrivKey.String(), + }, + expectedError: "network name not found", + }, + { + name: "should return error if private key is invalid", + network: chains.Network_solana, + relayerKey: keys.RelayerKey{ + PrivateKey: "invalid", + }, + expectedError: "unable to construct solana private key", + }, + { + name: "should return error if network is unsupported", + network: chains.Network_eth, + relayerKey: keys.RelayerKey{ + PrivateKey: solanaPrivKey.String(), + }, + expectedError: "cannot derive relayer address for unsupported network", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + networkName, address, err := tt.relayerKey.ResolveAddress(tt.network) + if tt.expectedError != "" { + require.Empty(t, networkName) + require.Empty(t, address) + require.ErrorContains(t, err, tt.expectedError) + return + } + + require.NoError(t, err) + require.Equal(t, tt.expectedNetworkName, networkName) + require.Equal(t, tt.expectedAddress, address) + }) + } +} + +func Test_LoadRelayerKey(t *testing.T) { + // sample test key and temp path + solanaPrivKey := sample.SolanaPrivateKey(t) + keyPath := sample.CreateTempDir(t) + fileName := path.Join(keyPath, "solana.json") + + // create relayer key file + createRelayerKeyFile(t, fileName, solanaPrivKey.String(), "password") + + // create a bad relayer key file + keyPath2 := sample.CreateTempDir(t) + badKeyFile := path.Join(keyPath2, "solana.json") + createBadRelayerKeyFile(t, badKeyFile) + + // test cases + tests := []struct { + name string + keyPath string + network chains.Network + password string + expectedKey *keys.RelayerKey + expectError string + }{ + { + name: "should load relayer key successfully", + keyPath: keyPath, + network: chains.Network_solana, + password: "password", + expectedKey: &keys.RelayerKey{PrivateKey: solanaPrivKey.String()}, + }, + { + name: "it's okay if relayer key is not provided", + keyPath: sample.CreateTempDir(t), // create a empty directory + network: chains.Network_solana, + password: "", + expectedKey: nil, + expectError: "", + }, + { + name: "should return error if network is unsupported", + keyPath: keyPath, + network: chains.Network_eth, + password: "", + expectedKey: nil, + expectError: "failed to resolve relayer key file name", + }, + { + name: "should return error if unable to read relayer key file", + keyPath: keyPath2, + network: chains.Network_solana, + password: "", + expectedKey: nil, + expectError: "failed to read relayer key file", + }, + { + name: "should return error if password is incorrect", + keyPath: keyPath, + network: chains.Network_solana, + password: "incorrect", + expectedKey: nil, + expectError: "failed to decrypt private key", + }, + } + + // Iterate over the test cases and run them + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + relayerKey, err := keys.LoadRelayerKey(tt.keyPath, tt.network, tt.password) + + if tt.expectError != "" { + require.ErrorContains(t, err, tt.expectError) + require.Nil(t, relayerKey) + } else { + require.NoError(t, err) + if tt.expectedKey != nil { + require.Equal(t, tt.expectedKey.PrivateKey, relayerKey.PrivateKey) + } + } + }) + } +} + +func Test_ResolveRelayerKeyPath(t *testing.T) { + usr, err := user.Current() + require.NoError(t, err) + + tests := []struct { + name string + relayerKeyPath string + network chains.Network + expectedName string + errMessage string + }{ + { + name: "should resolve relayer key path", + relayerKeyPath: "~/.zetacored/relayer-keys", + network: chains.Network_solana, + expectedName: path.Join(usr.HomeDir, ".zetacored/relayer-keys/solana.json"), + }, + { + name: "should return error if network is not found", + relayerKeyPath: "~/.zetacored/relayer-keys", + network: chains.Network(999), + errMessage: "failed to get relayer key file name", + }, + { + name: "should return error if network does not support relayer key", + relayerKeyPath: "~/.zetacored/relayer-keys", + network: chains.Network_eth, + errMessage: "does not support relayer key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + name, err := keys.ResolveRelayerKeyFile(tt.relayerKeyPath, tt.network) + if tt.errMessage != "" { + require.Empty(t, name) + require.ErrorContains(t, err, tt.errMessage) + return + } + + require.NoError(t, err) + require.Equal(t, tt.expectedName, name) + }) + } +} + +func Test_ReadWriteRelayerKeyFile(t *testing.T) { + // sample test key and temp path + solanaPrivKey := sample.SolanaPrivateKey(t) + keyPath := sample.CreateTempDir(t) + fileName := path.Join(keyPath, "solana.json") + + t.Run("should write and read relayer key file", func(t *testing.T) { + // create relayer key file + err := keys.WriteRelayerKeyToFile(fileName, keys.RelayerKey{PrivateKey: solanaPrivKey.String()}) + require.NoError(t, err) + + // read relayer key file + relayerKey, err := keys.ReadRelayerKeyFromFile(fileName) + require.NoError(t, err) + require.Equal(t, solanaPrivKey.String(), relayerKey.PrivateKey) + }) + + t.Run("should return error if relayer key file does not exist", func(t *testing.T) { + noFileName := path.Join(keyPath, "non-existing.json") + _, err := keys.ReadRelayerKeyFromFile(noFileName) + require.ErrorContains(t, err, "unable to open relayer key file") + }) + + t.Run("should return error if unmarsalling fails", func(t *testing.T) { + // create a bad key file + badKeyFile := path.Join(keyPath, "bad.json") + createBadRelayerKeyFile(t, badKeyFile) + + // try reading bad key file + key, err := keys.ReadRelayerKeyFromFile(badKeyFile) + require.ErrorContains(t, err, "unable to unmarshal relayer key") + require.Nil(t, key) + }) +} diff --git a/zetaclient/keys/relayer_keys.go b/zetaclient/keys/relayer_keys.go deleted file mode 100644 index 97dd7ca8b2..0000000000 --- a/zetaclient/keys/relayer_keys.go +++ /dev/null @@ -1,112 +0,0 @@ -package keys - -import ( - "encoding/json" - "io" - "os" - "path" - - "github.com/gagliardetto/solana-go" - "github.com/pkg/errors" - - "github.com/zeta-chain/zetacore/pkg/chains" - zetaos "github.com/zeta-chain/zetacore/pkg/os" -) - -const ( - // RelayerKeyFileSolana is the file name for the Solana relayer key - RelayerKeyFileSolana = "solana.json" -) - -// RelayerKey is the structure that holds the relayer private key -type RelayerKey struct { - PrivateKey string `json:"private_key"` -} - -// ResolveAddress returns the network name and address of the relayer key -func (rk RelayerKey) ResolveAddress(network int32) (string, string, error) { - // get network name - networkName, found := chains.GetNetworkName(network) - if !found { - return "", "", errors.Errorf("network name not found for network %d", network) - } - - switch chains.Network(network) { - case chains.Network_solana: - privKey, err := solana.PrivateKeyFromBase58(rk.PrivateKey) - if err != nil { - return "", "", errors.Wrap(err, "unable to construct solana private key") - } - return networkName, privKey.PublicKey().String(), nil - default: - return "", "", errors.Errorf("cannot derive relayer address for unsupported network %d", network) - } -} - -// LoadRelayerKey loads a relayer key from given path and chain -func LoadRelayerKey(keyPath string, chain chains.Chain) (RelayerKey, error) { - // determine relayer key file name based on chain - var fileName string - switch chain.Network { - case chains.Network_solana: - fileName = path.Join(keyPath, RelayerKeyFileSolana) - default: - return RelayerKey{}, errors.Errorf("relayer key not supported for network %s", chain.Network) - } - - // read the relayer key file - relayerKey, err := ReadRelayerKeyFromFile(fileName) - if err != nil { - return RelayerKey{}, errors.Wrap(err, "ReadRelayerKeyFile failed") - } - - return relayerKey, nil -} - -// ReadRelayerKeyFromFile reads the relayer key file and returns the key -func ReadRelayerKeyFromFile(fileName string) (RelayerKey, error) { - // expand home directory in the file path if it exists - fileNameFull, err := zetaos.ExpandHomeDir(fileName) - if err != nil { - return RelayerKey{}, errors.Wrapf(err, "ExpandHome failed for file: %s", fileName) - } - - // open the file - file, err := os.Open(fileNameFull) - if err != nil { - return RelayerKey{}, errors.Wrapf(err, "unable to open relayer key file: %s", fileNameFull) - } - defer file.Close() - - // read the file contents - fileData, err := io.ReadAll(file) - if err != nil { - return RelayerKey{}, errors.Wrapf(err, "unable to read relayer key data: %s", fileNameFull) - } - - // unmarshal the JSON data into the struct - var key RelayerKey - err = json.Unmarshal(fileData, &key) - if err != nil { - return RelayerKey{}, errors.Wrap(err, "unable to unmarshal relayer key") - } - - return key, nil -} - -// GetRelayerKeyFileByNetwork returns the relayer key file name based on network -func GetRelayerKeyFileByNetwork(network int32) (string, error) { - // get network name - networkName, found := chains.GetNetworkName(network) - if !found { - return "", errors.Errorf("network name not found for network %d", network) - } - - // return file name for supported networks only - switch chains.Network(network) { - case chains.Network_solana: - return networkName + ".json", nil - default: - return "", errors.Errorf("network %d does not support relayer key", network) - } -} diff --git a/zetaclient/orchestrator/bootstrap.go b/zetaclient/orchestrator/bootstrap.go index 2e65e157ad..2df07902a9 100644 --- a/zetaclient/orchestrator/bootstrap.go +++ b/zetaclient/orchestrator/bootstrap.go @@ -162,19 +162,16 @@ func syncSignerMap( continue } - // load the Solana private key - relayerKey, err := keys.LoadRelayerKey(app.Config().GetRelayerKeyPath(), *rawChain) + // try loading Solana relayer key if present + password := app.GetRelayerKeyPassword(rawChain.Network) + relayerKey, err := keys.LoadRelayerKey(app.Config().GetRelayerKeyPath(), rawChain.Network, password) if err != nil { logger.Std.Error().Err(err).Msg("Unable to load Solana relayer key") continue } - var ( - paramsRaw = chain.Params() - ) - // create Solana signer - signer, err := solanasigner.NewSigner(*rawChain, *paramsRaw, rpcClient, tss, relayerKey, ts, logger) + signer, err := solanasigner.NewSigner(*rawChain, *chain.Params(), rpcClient, tss, relayerKey, ts, logger) if err != nil { logger.Std.Error().Err(err).Msgf("Unable to construct signer for SOL chain %d", chainID) continue From 66b7027f9601eebd0af68728eb115afc4798babf Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Mon, 12 Aug 2024 22:50:57 -0500 Subject: [PATCH 04/13] add relayer_key_balance metrics and unit tests --- pkg/crypto/privkey.go | 23 +++ pkg/crypto/privkey_test.go | 67 +++++++++ zetaclient/chains/interfaces/interfaces.go | 5 + zetaclient/chains/solana/signer/signer.go | 22 ++- .../chains/solana/signer/signer_test.go | 142 ++++++++++++++++++ zetaclient/keys/relayer_key.go | 3 +- zetaclient/metrics/metrics.go | 7 + zetaclient/metrics/metrics_test.go | 11 +- zetaclient/testutils/constant.go | 34 +++-- zetaclient/testutils/mocks/solana_rpc.go | 30 ++++ 10 files changed, 326 insertions(+), 18 deletions(-) create mode 100644 pkg/crypto/privkey.go create mode 100644 pkg/crypto/privkey_test.go create mode 100644 zetaclient/chains/solana/signer/signer_test.go diff --git a/pkg/crypto/privkey.go b/pkg/crypto/privkey.go new file mode 100644 index 0000000000..2acbf1c609 --- /dev/null +++ b/pkg/crypto/privkey.go @@ -0,0 +1,23 @@ +package crypto + +import ( + fmt "fmt" + + "github.com/gagliardetto/solana-go" + "github.com/pkg/errors" +) + +// SolanaPrivateKeyFromString converts a base58 encoded private key to a solana.PrivateKey +func SolanaPrivateKeyFromString(privKeyBase58 string) (*solana.PrivateKey, error) { + privateKey, err := solana.PrivateKeyFromBase58(privKeyBase58) + if err != nil { + return nil, errors.Wrap(err, "invalid base58 private key") + } + + // Solana private keys are 64 bytes long + if len(privateKey) != 64 { + return nil, fmt.Errorf("invalid private key length: %d", len(privateKey)) + } + + return &privateKey, nil +} diff --git a/pkg/crypto/privkey_test.go b/pkg/crypto/privkey_test.go new file mode 100644 index 0000000000..4a0182b134 --- /dev/null +++ b/pkg/crypto/privkey_test.go @@ -0,0 +1,67 @@ +package crypto_test + +import ( + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/crypto" +) + +func Test_IsValidSolanaPrivateKey(t *testing.T) { + tests := []struct { + name string + input string + output *solana.PrivateKey + errMsg string + }{ + { + name: "valid private key", + input: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", + output: func() *solana.PrivateKey { + privKey, _ := solana.PrivateKeyFromBase58( + "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", + ) + return &privKey + }(), + }, + { + name: "invalid private key - too short", + input: "oR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", + output: nil, + errMsg: "invalid private key length: 38", + }, + { + name: "invalid private key - too long", + input: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQdJ", + output: nil, + errMsg: "invalid private key length: 66", + }, + { + name: "invalid private key - bad base58 encoding", + input: "!!!InvalidBase58!!!", + output: nil, + errMsg: "invalid base58 private key", + }, + { + name: "invalid private key - empty string", + input: "", + output: nil, + errMsg: "invalid base58 private key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := crypto.SolanaPrivateKeyFromString(tt.input) + if tt.errMsg != "" { + require.ErrorContains(t, err, tt.errMsg) + require.Nil(t, result) + return + } + + require.NoError(t, err) + require.Equal(t, tt.output.String(), result.String()) + }) + } +} diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index 8377d2ba33..edd656979b 100644 --- a/zetaclient/chains/interfaces/interfaces.go +++ b/zetaclient/chains/interfaces/interfaces.go @@ -193,6 +193,11 @@ type SolanaRPCClient interface { GetHealth(ctx context.Context) (string, error) GetSlot(ctx context.Context, commitment solrpc.CommitmentType) (uint64, error) GetAccountInfo(ctx context.Context, account solana.PublicKey) (*solrpc.GetAccountInfoResult, error) + GetBalance( + ctx context.Context, + account solana.PublicKey, + commitment solrpc.CommitmentType, + ) (*solrpc.GetBalanceResult, error) GetRecentBlockhash(ctx context.Context, commitment solrpc.CommitmentType) (*solrpc.GetRecentBlockhashResult, error) GetRecentPrioritizationFees( ctx context.Context, diff --git a/zetaclient/chains/solana/signer/signer.go b/zetaclient/chains/solana/signer/signer.go index cc9b6e14f1..ca3dfb0acb 100644 --- a/zetaclient/chains/solana/signer/signer.go +++ b/zetaclient/chains/solana/signer/signer.go @@ -11,6 +11,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" contracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" + "github.com/zeta-chain/zetacore/pkg/crypto" "github.com/zeta-chain/zetacore/x/crosschain/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" @@ -69,12 +70,11 @@ func NewSigner( // construct Solana private key if present if relayerKey != nil { - privKey, err := solana.PrivateKeyFromBase58(relayerKey.PrivateKey) + signer.relayerKey, err = crypto.SolanaPrivateKeyFromString(relayerKey.PrivateKey) if err != nil { return nil, errors.Wrap(err, "unable to construct solana private key") } - signer.relayerKey = &privKey - logger.Std.Info().Msgf("Solana relayer address: %s", privKey.PublicKey()) + logger.Std.Info().Msgf("Solana relayer address: %s", signer.relayerKey.PublicKey()) } else { logger.Std.Info().Msg("Solana relayer key is not provided") } @@ -138,6 +138,9 @@ func (signer *Signer) TryProcessOutbound( return } + // set relayer balance metrics + signer.SetRelayerBalanceMetrics(ctx) + // sign the withdraw transaction by relayer key tx, err := signer.SignWithdrawTx(ctx, *msg) if err != nil { @@ -191,6 +194,19 @@ func (signer *Signer) GetGatewayAddress() string { return signer.gatewayID.String() } +// SetRelayerBalanceMetrics sets the relayer balance metrics +func (signer *Signer) SetRelayerBalanceMetrics(ctx context.Context) { + if signer.HasRelayerKey() { + result, err := signer.client.GetBalance(ctx, signer.relayerKey.PublicKey(), rpc.CommitmentFinalized) + if err != nil { + signer.Logger().Std.Error().Err(err).Msg("GetBalance error") + return + } + solBalance := float64(result.Value) / float64(solana.LAMPORTS_PER_SOL) + metrics.RelayerKeyBalance.WithLabelValues(signer.Chain().Name).Set(solBalance) + } +} + // TODO: get rid of below four functions for Solana and Bitcoin // https://github.com/zeta-chain/node/issues/2532 func (signer *Signer) SetZetaConnectorAddress(_ ethcommon.Address) { diff --git a/zetaclient/chains/solana/signer/signer_test.go b/zetaclient/chains/solana/signer/signer_test.go new file mode 100644 index 0000000000..f3e1799d61 --- /dev/null +++ b/zetaclient/chains/solana/signer/signer_test.go @@ -0,0 +1,142 @@ +package signer_test + +import ( + "context" + "errors" + "testing" + + "github.com/gagliardetto/solana-go/rpc" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/testutil/sample" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/chains/solana/signer" + "github.com/zeta-chain/zetacore/zetaclient/keys" + "github.com/zeta-chain/zetacore/zetaclient/metrics" + "github.com/zeta-chain/zetacore/zetaclient/testutils" + "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" +) + +func Test_NewSigner(t *testing.T) { + // test parameters + chain := chains.SolanaDevnet + chainParams := sample.ChainParams(chain.ChainId) + chainParams.GatewayAddress = testutils.GatewayAddresses[chain.ChainId] + + tests := []struct { + name string + chain chains.Chain + chainParams observertypes.ChainParams + solClient interfaces.SolanaRPCClient + tss interfaces.TSSSigner + relayerKey *keys.RelayerKey + ts *metrics.TelemetryServer + logger base.Logger + errMessage string + }{ + { + name: "should create solana signer successfully with relayer key", + chain: chain, + chainParams: *chainParams, + solClient: nil, + tss: nil, + relayerKey: &keys.RelayerKey{ + PrivateKey: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", + }, + ts: nil, + logger: base.DefaultLogger(), + }, + { + name: "should create solana signer successfully without relayer key", + chainParams: *chainParams, + solClient: nil, + tss: nil, + relayerKey: nil, + ts: nil, + logger: base.DefaultLogger(), + }, + { + name: "should fail to create solana signer with invalid gateway address", + chainParams: func() observertypes.ChainParams { + cp := *chainParams + cp.GatewayAddress = "invalid" + return cp + }(), + solClient: nil, + tss: nil, + relayerKey: nil, + ts: nil, + logger: base.DefaultLogger(), + errMessage: "cannot parse gateway address", + }, + { + name: "should fail to create solana signer with invalid relayer key", + chainParams: *chainParams, + solClient: nil, + tss: nil, + relayerKey: &keys.RelayerKey{ + PrivateKey: "3EMjCcCJg53fMEGVj13", // too short + }, + ts: nil, + logger: base.DefaultLogger(), + errMessage: "unable to construct solana private key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, err := signer.NewSigner(tt.chain, tt.chainParams, tt.solClient, tt.tss, tt.relayerKey, tt.ts, tt.logger) + if tt.errMessage != "" { + require.ErrorContains(t, err, tt.errMessage) + require.Nil(t, s) + return + } + + require.NoError(t, err) + require.NotNil(t, s) + }) + } +} + +func Test_SetRelayerBalanceMetrics(t *testing.T) { + // test parameters + chain := chains.SolanaDevnet + chainParams := sample.ChainParams(chain.ChainId) + chainParams.GatewayAddress = testutils.GatewayAddresses[chain.ChainId] + relayerKey := &keys.RelayerKey{ + PrivateKey: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", + } + ctx := context.Background() + + // mock solana client with RPC error + mckClient := mocks.NewSolanaRPCClient(t) + mckClient.On("GetBalance", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("rpc error")) + + // create signer and set relayer balance metrics + s, err := signer.NewSigner(chain, *chainParams, mckClient, nil, relayerKey, nil, base.DefaultLogger()) + require.NoError(t, err) + s.SetRelayerBalanceMetrics(ctx) + + // assert that relayer key balance metrics is not set (due to RPC error) + balance := testutil.ToFloat64(metrics.RelayerKeyBalance.WithLabelValues(chain.Name)) + require.Equal(t, 0.0, balance) + + // mock solana client with balance + mckClient = mocks.NewSolanaRPCClient(t) + mckClient.On("GetBalance", mock.Anything, mock.Anything, mock.Anything).Return(&rpc.GetBalanceResult{ + Value: 123400000, + }, nil) + + // create signer and set relayer balance metrics again + s, err = signer.NewSigner(chain, *chainParams, mckClient, nil, relayerKey, nil, base.DefaultLogger()) + require.NoError(t, err) + s.SetRelayerBalanceMetrics(ctx) + + // assert that relayer key balance metrics is set correctly + balance = testutil.ToFloat64(metrics.RelayerKeyBalance.WithLabelValues(chain.Name)) + require.Equal(t, 0.1234, balance) +} diff --git a/zetaclient/keys/relayer_key.go b/zetaclient/keys/relayer_key.go index 8de6cc9853..9eb0cb3205 100644 --- a/zetaclient/keys/relayer_key.go +++ b/zetaclient/keys/relayer_key.go @@ -6,7 +6,6 @@ import ( "os" "path/filepath" - "github.com/gagliardetto/solana-go" "github.com/pkg/errors" "github.com/zeta-chain/zetacore/pkg/chains" @@ -29,7 +28,7 @@ func (rk RelayerKey) ResolveAddress(network chains.Network) (string, string, err switch network { case chains.Network_solana: - privKey, err := solana.PrivateKeyFromBase58(rk.PrivateKey) + privKey, err := crypto.SolanaPrivateKeyFromString(rk.PrivateKey) if err != nil { return "", "", errors.Wrap(err, "unable to construct solana private key") } diff --git a/zetaclient/metrics/metrics.go b/zetaclient/metrics/metrics.go index df956daa90..a0a7341f94 100644 --- a/zetaclient/metrics/metrics.go +++ b/zetaclient/metrics/metrics.go @@ -50,6 +50,13 @@ var ( Help: "Tss node blame counter per pubkey", }, []string{"pubkey"}) + // RelayerKeyBalance is a gauge that contains the relayer key balance of the chain + RelayerKeyBalance = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: ZetaClientNamespace, + Name: "relayer_key_balance", + Help: "Relayer key balance of the chain", + }, []string{"chain"}) + // HotKeyBurnRate is a gauge that contains the fee burn rate of the hotkey HotKeyBurnRate = promauto.NewGauge(prometheus.GaugeOpts{ Namespace: ZetaClientNamespace, diff --git a/zetaclient/metrics/metrics_test.go b/zetaclient/metrics/metrics_test.go index 6be8bc30c0..239a6391c4 100644 --- a/zetaclient/metrics/metrics_test.go +++ b/zetaclient/metrics/metrics_test.go @@ -10,6 +10,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/zeta-chain/zetacore/pkg/chains" . "gopkg.in/check.v1" ) @@ -39,7 +40,7 @@ func (ms *MetricsSuite) TestCurryWith(c *C) { RPCCount.Reset() } -func (ms *MetricsSuite) TestMetrics(c *C) { +func (ms *MetricsSuite) Test_RPCCount(c *C) { GetFilterLogsPerChain.WithLabelValues("chain1").Inc() GetFilterLogsPerChain.WithLabelValues("chain2").Inc() GetFilterLogsPerChain.WithLabelValues("chain2").Inc() @@ -77,3 +78,11 @@ func (ms *MetricsSuite) TestMetrics(c *C) { rpcCount = testutil.ToFloat64(RPCCount.With(prometheus.Labels{"host": "127.0.0.1:8886", "code": "502"})) c.Assert(rpcCount, Equals, 0.0) } + +func (ms *MetricsSuite) Test_RelayerKeyBalance(c *C) { + RelayerKeyBalance.WithLabelValues(chains.SolanaDevnet.Name).Set(2.1564) + + // assert that relayer key balance is being set correctly + balance := testutil.ToFloat64(RelayerKeyBalance.WithLabelValues(chains.SolanaDevnet.Name)) + c.Assert(balance, Equals, 2.1564) +} diff --git a/zetaclient/testutils/constant.go b/zetaclient/testutils/constant.go index ad8302577d..3036035db4 100644 --- a/zetaclient/testutils/constant.go +++ b/zetaclient/testutils/constant.go @@ -1,6 +1,10 @@ package testutils -import ethcommon "github.com/ethereum/go-ethereum/common" +import ( + ethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/zeta-chain/zetacore/pkg/chains" +) const ( // TSSAddressEVMMainnet the EVM TSS address for test purposes @@ -29,33 +33,39 @@ const ( EventERC20Withdraw = "Withdrawn" ) +// GatewayAddresses contains constants gateway addresses for testing +var GatewayAddresses = map[int64]string{ + // Gateway address on Solana devnet + chains.SolanaDevnet.ChainId: "94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d", +} + // ConnectorAddresses contains constants ERC20 connector addresses for testing var ConnectorAddresses = map[int64]ethcommon.Address{ // Connector address on Ethereum mainnet - 1: ethcommon.HexToAddress("0x000007Cf399229b2f5A4D043F20E90C9C98B7C6a"), + chains.Ethereum.ChainId: ethcommon.HexToAddress("0x000007Cf399229b2f5A4D043F20E90C9C98B7C6a"), // Connector address on Binance Smart Chain mainnet - 56: ethcommon.HexToAddress("0x000063A6e758D9e2f438d430108377564cf4077D"), + chains.BscMainnet.ChainId: ethcommon.HexToAddress("0x000063A6e758D9e2f438d430108377564cf4077D"), // testnet - 5: ethcommon.HexToAddress("0x00005E3125aBA53C5652f9F0CE1a4Cf91D8B15eA"), - 97: ethcommon.HexToAddress("0x0000ecb8cdd25a18F12DAA23f6422e07fBf8B9E1"), - 11155111: ethcommon.HexToAddress("0x3963341dad121c9CD33046089395D66eBF20Fb03"), + chains.Goerli.ChainId: ethcommon.HexToAddress("0x00005E3125aBA53C5652f9F0CE1a4Cf91D8B15eA"), + chains.BscTestnet.ChainId: ethcommon.HexToAddress("0x0000ecb8cdd25a18F12DAA23f6422e07fBf8B9E1"), + chains.Sepolia.ChainId: ethcommon.HexToAddress("0x3963341dad121c9CD33046089395D66eBF20Fb03"), // localnet - 1337: ethcommon.HexToAddress("0xD28D6A0b8189305551a0A8bd247a6ECa9CE781Ca"), + chains.GoerliLocalnet.ChainId: ethcommon.HexToAddress("0xD28D6A0b8189305551a0A8bd247a6ECa9CE781Ca"), } // CustodyAddresses contains constants ERC20 custody addresses for testing var CustodyAddresses = map[int64]ethcommon.Address{ // ERC20 custody address on Ethereum mainnet - 1: ethcommon.HexToAddress("0x0000030Ec64DF25301d8414eE5a29588C4B0dE10"), + chains.Ethereum.ChainId: ethcommon.HexToAddress("0x0000030Ec64DF25301d8414eE5a29588C4B0dE10"), // ERC20 custody address on Binance Smart Chain mainnet - 56: ethcommon.HexToAddress("0x00000fF8fA992424957F97688015814e707A0115"), + chains.BscMainnet.ChainId: ethcommon.HexToAddress("0x00000fF8fA992424957F97688015814e707A0115"), // testnet - 5: ethcommon.HexToAddress("0x000047f11C6E42293F433C82473532E869Ce4Ec5"), - 97: ethcommon.HexToAddress("0x0000a7Db254145767262C6A81a7eE1650684258e"), - 11155111: ethcommon.HexToAddress("0x84725b70a239d3Faa7C6EF0C6C8E8b6c8e28338b"), + chains.Goerli.ChainId: ethcommon.HexToAddress("0x000047f11C6E42293F433C82473532E869Ce4Ec5"), + chains.BscTestnet.ChainId: ethcommon.HexToAddress("0x0000a7Db254145767262C6A81a7eE1650684258e"), + chains.Sepolia.ChainId: ethcommon.HexToAddress("0x84725b70a239d3Faa7C6EF0C6C8E8b6c8e28338b"), } diff --git a/zetaclient/testutils/mocks/solana_rpc.go b/zetaclient/testutils/mocks/solana_rpc.go index 953bde87e5..fad147037c 100644 --- a/zetaclient/testutils/mocks/solana_rpc.go +++ b/zetaclient/testutils/mocks/solana_rpc.go @@ -47,6 +47,36 @@ func (_m *SolanaRPCClient) GetAccountInfo(ctx context.Context, account solana.Pu return r0, r1 } +// GetBalance provides a mock function with given fields: ctx, account, commitment +func (_m *SolanaRPCClient) GetBalance(ctx context.Context, account solana.PublicKey, commitment rpc.CommitmentType) (*rpc.GetBalanceResult, error) { + ret := _m.Called(ctx, account, commitment) + + if len(ret) == 0 { + panic("no return value specified for GetBalance") + } + + var r0 *rpc.GetBalanceResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) (*rpc.GetBalanceResult, error)); ok { + return rf(ctx, account, commitment) + } + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) *rpc.GetBalanceResult); ok { + r0 = rf(ctx, account, commitment) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*rpc.GetBalanceResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, rpc.CommitmentType) error); ok { + r1 = rf(ctx, account, commitment) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetConfirmedTransactionWithOpts provides a mock function with given fields: ctx, signature, opts func (_m *SolanaRPCClient) GetConfirmedTransactionWithOpts(ctx context.Context, signature solana.Signature, opts *rpc.GetTransactionOpts) (*rpc.TransactionWithMeta, error) { ret := _m.Called(ctx, signature, opts) From 5d114881b9236c1b4cc7b0a4c1431832c2ad8988 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 13 Aug 2024 00:22:39 -0500 Subject: [PATCH 05/13] use TrimSpace to trim password --- contrib/localnet/orchestrator/Dockerfile.fastbuild | 2 +- pkg/os/console.go | 2 +- pkg/os/console_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/localnet/orchestrator/Dockerfile.fastbuild b/contrib/localnet/orchestrator/Dockerfile.fastbuild index d89125333b..85e0296dfc 100644 --- a/contrib/localnet/orchestrator/Dockerfile.fastbuild +++ b/contrib/localnet/orchestrator/Dockerfile.fastbuild @@ -2,7 +2,7 @@ # check=error=true FROM zetanode:latest AS zeta FROM ghcr.io/zeta-chain/ethereum-client-go:v1.10.26 AS geth -FROM ghcr.io/zeta-chain/solana-docker:1.18.15 as solana +FROM ghcr.io/zeta-chain/solana-docker:1.18.15 AS solana FROM ghcr.io/zeta-chain/golang:1.22.5-bookworm AS orchestrator RUN apt update && \ diff --git a/pkg/os/console.go b/pkg/os/console.go index 28102ef1fd..3b4327cba4 100644 --- a/pkg/os/console.go +++ b/pkg/os/console.go @@ -21,7 +21,7 @@ func PromptPasswords(passwordTitles []string) ([]string, error) { } // trim delimiters - password = strings.TrimSuffix(password, "\n") + password = strings.TrimSpace(password) passwords[i] = password } diff --git a/pkg/os/console_test.go b/pkg/os/console_test.go index bdacfa2a39..c86897fe90 100644 --- a/pkg/os/console_test.go +++ b/pkg/os/console_test.go @@ -19,7 +19,7 @@ func Test_PromptPasswords(t *testing.T) { { name: "Single password prompt", passwordTitles: []string{"HotKey"}, - input: "pass123\n", + input: " pass123\n", expected: []string{"pass123"}, }, { From e8736a01a19d55f403b695f07c1136790182b713 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 13 Aug 2024 00:24:40 -0500 Subject: [PATCH 06/13] add changelog entry --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 7cf196b8c8..d0d667e9fa 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,7 @@ * [2634](https://github.com/zeta-chain/node/pull/2634) - add support for EIP-1559 gas fees * [2597](https://github.com/zeta-chain/node/pull/2597) - Add generic rpc metrics to zetaclient * [2538](https://github.com/zeta-chain/node/pull/2538) - add background worker routines to shutdown zetaclientd when needed for tss migration +* [2673](https://github.com/zeta-chain/node/pull/2673) - add relayer key importer, encryption and decryption ### Refactor From 98f041a755690e1cfd6c94e35f96763be753f6e1 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 13 Aug 2024 16:16:44 -0500 Subject: [PATCH 07/13] use relayer account array in E2E config; a few renaming; add private key validation when importing --- cmd/zetaclientd/encrypt_tss.go | 4 +- cmd/zetaclientd/import_relayer_keys.go | 9 +- cmd/zetae2e/config/localnet.yml | 11 +-- cmd/zetae2e/local/local.go | 3 +- .../localnet/orchestrator/start-zetae2e.sh | 4 +- contrib/localnet/scripts/start-zetaclientd.sh | 2 +- e2e/config/config.go | 4 +- pkg/crypto/aes256_gcm.go | 38 ++++---- pkg/crypto/aes256_gcm_test.go | 96 +++++++++++-------- pkg/crypto/privkey_test.go | 2 +- zetaclient/keys/relayer_key.go | 32 +++++-- zetaclient/keys/relayer_key_test.go | 47 ++++++++- zetaclient/orchestrator/bootstrap.go | 7 +- 13 files changed, 168 insertions(+), 91 deletions(-) diff --git a/cmd/zetaclientd/encrypt_tss.go b/cmd/zetaclientd/encrypt_tss.go index e8e4a69807..99322c0ecd 100644 --- a/cmd/zetaclientd/encrypt_tss.go +++ b/cmd/zetaclientd/encrypt_tss.go @@ -25,7 +25,7 @@ func init() { // EncryptTSSFile encrypts the given file with the given secret key func EncryptTSSFile(_ *cobra.Command, args []string) error { filePath := args[0] - secretKey := args[1] + password := args[1] filePath = filepath.Clean(filePath) data, err := os.ReadFile(filePath) @@ -38,7 +38,7 @@ func EncryptTSSFile(_ *cobra.Command, args []string) error { } // encrypt the data - cipherText, err := crypto.EncryptAES256GCM(data, secretKey) + cipherText, err := crypto.EncryptAES256GCM(data, password) if err != nil { return errors.Wrap(err, "failed to encrypt data") } diff --git a/cmd/zetaclientd/import_relayer_keys.go b/cmd/zetaclientd/import_relayer_keys.go index 0f2f713336..caf2db9538 100644 --- a/cmd/zetaclientd/import_relayer_keys.go +++ b/cmd/zetaclientd/import_relayer_keys.go @@ -16,14 +16,14 @@ import ( ) var CmdImportRelayerKey = &cobra.Command{ - Use: "import-relayer-key [network] [private-key] [password] [relayer-key-path]", + Use: "import-relayer-key --network= --private-key= --password= --relayer-key-path=", Short: "Import a relayer private key", - Example: `zetaclientd import-relayer-key --network=7 --private-key=3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ --password=my_password`, + Example: `zetaclientd import-relayer-key --network=7 --private-key= --password=`, RunE: ImportRelayerKey, } var CmdRelayerAddress = &cobra.Command{ - Use: "relayer-address [network] [password] [relayer-key-path]", + Use: "relayer-address --network= --password= --relayer-key-path=", Short: "Show the relayer address", Example: `zetaclientd relayer-address --network=7 --password=my_password`, RunE: ShowRelayerAddress, @@ -82,6 +82,9 @@ func ImportRelayerKey(_ *cobra.Command, _ []string) error { if importArgs.password == "" { return errors.New("must provide a password") } + if !keys.IsRelayerPrivateKeyValid(importArgs.privateKey, chains.Network(importArgs.network)) { + return errors.New("invalid private key") + } // resolve the relayer key file path fileName, err := keys.ResolveRelayerKeyFile(importArgs.relayerKeyPath, chains.Network(importArgs.network)) diff --git a/cmd/zetae2e/config/localnet.yml b/cmd/zetae2e/config/localnet.yml index 46c603de13..98410bc368 100644 --- a/cmd/zetae2e/config/localnet.yml +++ b/cmd/zetae2e/config/localnet.yml @@ -55,12 +55,11 @@ policy_accounts: evm_address: "0xAa9b029BC3EFe4c50045Cf0902c73aAa25b43533" private_key: "0595CB0CD9BF5264A85A603EC8E43C30ADBB5FD2D9E2EF84C374EA4A65BB616C" observer_relayer_accounts: - relayer_account_0: - solana_address: "2qBVcNBZCubcnSR3NyCnFjCfkCVUB3G7ECPoaW5rxVjx" - solana_private_key: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ" - relayer_account_1: - solana_address: "4kkCV8H38xirwQTkE5kL6FHNtYGHnMQQ7SkCjAxibHFK" - solana_private_key: "5SSv7jWzamtjWNKGiKf3gvCPHcq9mE5x6LhYgzJCKNSxoQ83gFpmMgmg2JS2zdKcBEdwy7y9bvWgX4LBiUpvnrPf" + relayer_accounts: + - solana_address: "2qBVcNBZCubcnSR3NyCnFjCfkCVUB3G7ECPoaW5rxVjx" + solana_private_key: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ" + - solana_address: "4kkCV8H38xirwQTkE5kL6FHNtYGHnMQQ7SkCjAxibHFK" + solana_private_key: "5SSv7jWzamtjWNKGiKf3gvCPHcq9mE5x6LhYgzJCKNSxoQ83gFpmMgmg2JS2zdKcBEdwy7y9bvWgX4LBiUpvnrPf" rpcs: zevm: "http://zetacore0:8545" evm: "http://eth:8545" diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index cb24096859..8b93f2da9c 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -353,8 +353,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if testTSSMigration { runTSSMigrationTest(deployerRunner, logger, verbose, conf) } - // Verify that there are no trackers left over after tests complete - deployerRunner.EnsureNoTrackers() + // print and validate report networkReport, err := deployerRunner.GenerateNetworkReport() if err != nil { diff --git a/contrib/localnet/orchestrator/start-zetae2e.sh b/contrib/localnet/orchestrator/start-zetae2e.sh index 614d53bc47..8d634ca11d 100644 --- a/contrib/localnet/orchestrator/start-zetae2e.sh +++ b/contrib/localnet/orchestrator/start-zetae2e.sh @@ -95,11 +95,11 @@ geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: w solana_url=$(yq -r '.rpcs.solana' config.yml) solana config set --url "$solana_url" > /dev/null -relayer=$(yq -r '.observer_relayer_accounts.relayer_account_0.solana_address' config.yml) +relayer=$(yq -r '.observer_relayer_accounts.relayer_accounts[0].solana_address' config.yml) echo "funding solana relayer address ${relayer} with 100 SOL" solana airdrop 100 "$relayer" > /dev/null -relayer=$(yq -r '.observer_relayer_accounts.relayer_account_1.solana_address' config.yml) +relayer=$(yq -r '.observer_relayer_accounts.relayer_accounts[1].solana_address' config.yml) echo "funding solana relayer address ${relayer} with 100 SOL" solana airdrop 100 "$relayer" > /dev/null diff --git a/contrib/localnet/scripts/start-zetaclientd.sh b/contrib/localnet/scripts/start-zetaclientd.sh index df61ce48f5..71ca33f589 100755 --- a/contrib/localnet/scripts/start-zetaclientd.sh +++ b/contrib/localnet/scripts/start-zetaclientd.sh @@ -19,7 +19,7 @@ import_relayer_key() { local num="$1" # import solana (network=7) relayer private key - privkey_solana=$(yq -r ".observer_relayer_accounts.relayer_account_${num}.solana_private_key" /root/config.yml) + privkey_solana=$(yq -r ".observer_relayer_accounts.relayer_accounts[${num}].solana_private_key" /root/config.yml) zetaclientd import-relayer-key --network=7 --private-key="$privkey_solana" --password=pass_relayerkey } diff --git a/e2e/config/config.go b/e2e/config/config.go index 2200e95826..e1e8d54611 100644 --- a/e2e/config/config.go +++ b/e2e/config/config.go @@ -80,8 +80,8 @@ type PolicyAccounts struct { // ObserverRelayerAccounts are the accounts used by the observers to interact with gateway contracts in non-EVM chains (e.g. Solana) type ObserverRelayerAccounts struct { - RelayerAccount0 Account `yaml:"relayer_account_0"` - RelayerAccount1 Account `yaml:"relayer_account_1"` + // RelayerAccounts contains two relayer accounts used by zetaclient0 and zetaclient1 + RelayerAccounts [2]Account `yaml:"relayer_accounts"` } // RPCs contains the configuration for the RPC endpoints diff --git a/pkg/crypto/aes256_gcm.go b/pkg/crypto/aes256_gcm.go index f615fa79ea..e4fba7de7c 100644 --- a/pkg/crypto/aes256_gcm.go +++ b/pkg/crypto/aes256_gcm.go @@ -11,32 +11,32 @@ import ( "github.com/pkg/errors" ) -// EncryptAES256GCMBase64 encrypts the given string plaintext using AES-256-GCM with the given key and returns the base64-encoded ciphertext. -func EncryptAES256GCMBase64(plaintext string, encryptKey string) (string, error) { +// EncryptAES256GCMBase64 encrypts the given string plaintext using AES-256-GCM with the given password and returns the base64-encoded ciphertext. +func EncryptAES256GCMBase64(plaintext string, password string) (string, error) { // validate the input if plaintext == "" { return "", errors.New("plaintext must not be empty") } - if encryptKey == "" { - return "", errors.New("encrypt key must not be empty") + if password == "" { + return "", errors.New("password must not be empty") } // encrypt the plaintext - ciphertext, err := EncryptAES256GCM([]byte(plaintext), encryptKey) + ciphertext, err := EncryptAES256GCM([]byte(plaintext), password) if err != nil { return "", errors.Wrap(err, "failed to encrypt string plaintext") } return base64.StdEncoding.EncodeToString(ciphertext), nil } -// DecryptAES256GCMBase64 decrypts the given base64-encoded ciphertext using AES-256-GCM with the given key. -func DecryptAES256GCMBase64(ciphertextBase64 string, decryptKey string) (string, error) { +// DecryptAES256GCMBase64 decrypts the given base64-encoded ciphertext using AES-256-GCM with the given password. +func DecryptAES256GCMBase64(ciphertextBase64 string, password string) (string, error) { // validate the input if ciphertextBase64 == "" { return "", errors.New("ciphertext must not be empty") } - if decryptKey == "" { - return "", errors.New("decrypt key must not be empty") + if password == "" { + return "", errors.New("password must not be empty") } // decode the base64-encoded ciphertext @@ -46,16 +46,17 @@ func DecryptAES256GCMBase64(ciphertextBase64 string, decryptKey string) (string, } // decrypt the ciphertext - plaintext, err := DecryptAES256GCM(ciphertext, decryptKey) + plaintext, err := DecryptAES256GCM(ciphertext, password) if err != nil { return "", errors.Wrap(err, "failed to decrypt ciphertext") } return string(plaintext), nil } -// EncryptAES256GCM encrypts the given plaintext using AES-256-GCM with the given key. -func EncryptAES256GCM(plaintext []byte, encryptKey string) ([]byte, error) { - block, err := aes.NewCipher(getAESKey(encryptKey)) +// EncryptAES256GCM encrypts the given plaintext using AES-256-GCM with the given password. +func EncryptAES256GCM(plaintext []byte, password string) ([]byte, error) { + // create AES cipher + block, err := aes.NewCipher(getAESKey(password)) if err != nil { return nil, err } @@ -78,9 +79,10 @@ func EncryptAES256GCM(plaintext []byte, encryptKey string) ([]byte, error) { return ciphertext, nil } -// DecryptAES256GCM decrypts the given ciphertext using AES-256-GCM with the given key. -func DecryptAES256GCM(ciphertext []byte, encryptKey string) ([]byte, error) { - block, err := aes.NewCipher(getAESKey(encryptKey)) +// DecryptAES256GCM decrypts the given ciphertext using AES-256-GCM with the given password. +func DecryptAES256GCM(ciphertext []byte, password string) ([]byte, error) { + // create AES cipher + block, err := aes.NewCipher(getAESKey(password)) if err != nil { return nil, err } @@ -94,7 +96,7 @@ func DecryptAES256GCM(ciphertext []byte, encryptKey string) ([]byte, error) { // get the nonce size nonceSize := gcm.NonceSize() if len(ciphertext) < nonceSize { - return nil, err + return nil, errors.New("ciphertext too short") } // extract the nonce from the ciphertext @@ -109,7 +111,7 @@ func DecryptAES256GCM(ciphertext []byte, encryptKey string) ([]byte, error) { return plaintext, nil } -// getAESKey uses SHA-256 to create a 32-byte key AES encryption. +// getAESKey uses SHA-256 to create a 32-byte key for AES encryption. func getAESKey(key string) []byte { h := sha256.New() h.Write([]byte(key)) diff --git a/pkg/crypto/aes256_gcm_test.go b/pkg/crypto/aes256_gcm_test.go index ff83698ddb..92c1e4f1f0 100644 --- a/pkg/crypto/aes256_gcm_test.go +++ b/pkg/crypto/aes256_gcm_test.go @@ -10,32 +10,45 @@ import ( func Test_EncryptDecryptAES256GCM(t *testing.T) { tests := []struct { - name string - plaintext string - encryptKey string - decryptKey string - modifyFunc func([]byte) []byte - fail bool + name string + plaintext string + encryptPass string + decryptPass string + modifyFunc func([]byte) []byte + fail bool + errMsg string }{ { - name: "Successful encryption and decryption", - plaintext: "Hello, World!", - encryptKey: "my_password", - decryptKey: "my_password", - fail: false, + name: "Successful encryption and decryption", + plaintext: "Hello, World!", + encryptPass: "my_password", + decryptPass: "my_password", + fail: false, }, { - name: "Decryption with incorrect key should fail", - plaintext: "Hello, World!", - encryptKey: "my_password", - decryptKey: "my_password2", - fail: true, + name: "Decryption with incorrect key should fail", + plaintext: "Hello, World!", + encryptPass: "my_password", + decryptPass: "my_password2", + fail: true, }, { - name: "Decryption with corrupted ciphertext should fail", - plaintext: "Hello, World!", - encryptKey: "my_password", - decryptKey: "my_password", + name: "Decryption with ciphertext too short should fail", + plaintext: "Hello, World!", + encryptPass: "my_password", + decryptPass: "my_password", + modifyFunc: func(ciphertext []byte) []byte { + // truncate the ciphertext, nonce size is 12 bytes + return ciphertext[:10] + }, + fail: true, + errMsg: "ciphertext too short", + }, + { + name: "Decryption with corrupted ciphertext should fail", + plaintext: "Hello, World!", + encryptPass: "my_password", + decryptPass: "my_password", modifyFunc: func(ciphertext []byte) []byte { // flip the last bit of the ciphertext ciphertext[len(ciphertext)-1] ^= 0x01 @@ -44,10 +57,10 @@ func Test_EncryptDecryptAES256GCM(t *testing.T) { fail: true, }, { - name: "Decryption with incorrect nonce should fail", - plaintext: "Hello, World!", - encryptKey: "my_password", - decryptKey: "my_password", + name: "Decryption with incorrect nonce should fail", + plaintext: "Hello, World!", + encryptPass: "my_password", + decryptPass: "my_password", modifyFunc: func(ciphertext []byte) []byte { // flip the first bit of the nonce ciphertext[0] ^= 0x01 @@ -59,7 +72,7 @@ func Test_EncryptDecryptAES256GCM(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - encrypted, err := crypto.EncryptAES256GCM([]byte(tt.plaintext), tt.encryptKey) + encrypted, err := crypto.EncryptAES256GCM([]byte(tt.plaintext), tt.encryptPass) require.NoError(t, err) // modify the encrypted data if needed @@ -68,9 +81,12 @@ func Test_EncryptDecryptAES256GCM(t *testing.T) { } // decrypt the data - decrypted, err := crypto.DecryptAES256GCM(encrypted, tt.decryptKey) + decrypted, err := crypto.DecryptAES256GCM(encrypted, tt.decryptPass) if tt.fail { require.Error(t, err) + if tt.errMsg != "" { + require.Contains(t, err.Error(), tt.errMsg) + } return } @@ -83,15 +99,15 @@ func Test_EncryptAES256GCMBase64(t *testing.T) { tests := []struct { name string plaintext string - encryptKey string - decryptKey string + encryptPass string + decryptPass string errorMessage string }{ { - name: "Successful encryption and decryption", - plaintext: "Hello, World!", - encryptKey: "my_password", - decryptKey: "my_password", + name: "Successful encryption and decryption", + plaintext: "Hello, World!", + encryptPass: "my_password", + decryptPass: "my_password", }, { name: "Encryption with empty plaintext should fail", @@ -99,24 +115,24 @@ func Test_EncryptAES256GCMBase64(t *testing.T) { errorMessage: "plaintext must not be empty", }, { - name: "Encryption with empty encrypt key should fail", + name: "Encryption with empty password should fail", plaintext: "Hello, World!", - encryptKey: "", - errorMessage: "encrypt key must not be empty", + encryptPass: "", + errorMessage: "password must not be empty", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // encrypt the data - ciphertextBase64, err := crypto.EncryptAES256GCMBase64(tt.plaintext, tt.encryptKey) + ciphertextBase64, err := crypto.EncryptAES256GCMBase64(tt.plaintext, tt.encryptPass) if tt.errorMessage != "" { require.ErrorContains(t, err, tt.errorMessage) return } // decrypt the data - decrypted, err := crypto.DecryptAES256GCMBase64(ciphertextBase64, tt.decryptKey) + decrypted, err := crypto.DecryptAES256GCMBase64(ciphertextBase64, tt.decryptPass) require.NoError(t, err) require.Equal(t, tt.plaintext, decrypted) @@ -146,10 +162,10 @@ func Test_DecryptAES256GCMBase64(t *testing.T) { errorMessage: "ciphertext must not be empty", }, { - name: "Decryption with empty decrypt key should fail", + name: "Decryption with empty password should fail", ciphertextBase64: "CXLWgHdVeZQwVOZZyHeZ5n5VB+eVSLaWFF0v0QOm9DyB7XSiHDwhNwQ=", decryptKey: "", - errorMessage: "decrypt key must not be empty", + errorMessage: "password must not be empty", }, { name: "Decryption with invalid base64 ciphertext should fail", @@ -158,7 +174,7 @@ func Test_DecryptAES256GCMBase64(t *testing.T) { errorMessage: "failed to decode base64 ciphertext", }, { - name: "Decryption with incorrect decrypt key should fail", + name: "Decryption with incorrect password should fail", ciphertextBase64: "CXLWgHdVeZQwVOZZyHeZ5n5VB+eVSLaWFF0v0QOm9DyB7XSiHDwhNwQ=", decryptKey: "my_password2", errorMessage: "failed to decrypt ciphertext", diff --git a/pkg/crypto/privkey_test.go b/pkg/crypto/privkey_test.go index 4a0182b134..cf8921b454 100644 --- a/pkg/crypto/privkey_test.go +++ b/pkg/crypto/privkey_test.go @@ -8,7 +8,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/crypto" ) -func Test_IsValidSolanaPrivateKey(t *testing.T) { +func Test_SolanaPrivateKeyFromString(t *testing.T) { tests := []struct { name string input string diff --git a/zetaclient/keys/relayer_key.go b/zetaclient/keys/relayer_key.go index 9eb0cb3205..20608e3643 100644 --- a/zetaclient/keys/relayer_key.go +++ b/zetaclient/keys/relayer_key.go @@ -2,7 +2,6 @@ package keys import ( "encoding/json" - "io" "os" "path/filepath" @@ -34,7 +33,7 @@ func (rk RelayerKey) ResolveAddress(network chains.Network) (string, string, err } return networkName, privKey.PublicKey().String(), nil default: - return "", "", errors.Errorf("cannot derive relayer address for unsupported network %d", network) + return "", "", errors.Errorf("unsupported network %d: unable to derive relayer address", network) } } @@ -54,6 +53,11 @@ func LoadRelayerKey(relayerKeyPath string, network chains.Network, password stri return nil, errors.Wrapf(err, "failed to read relayer key file: %s", fileName) } + // password must be set by operator + if password == "" { + return nil, errors.New("password is required to decrypt the private key") + } + // decrypt the private key privateKey, err := crypto.DecryptAES256GCMBase64(relayerKey.PrivateKey, password) if err != nil { @@ -104,15 +108,8 @@ func ReadRelayerKeyFromFile(fileName string) (*RelayerKey, error) { return nil, errors.Wrapf(err, "ExpandHome failed for file: %s", fileName) } - // open the file - file, err := os.Open(fileNameFull) - if err != nil { - return nil, errors.Wrapf(err, "unable to open relayer key file: %s", fileNameFull) - } - defer file.Close() - // read the file contents - fileData, err := io.ReadAll(file) + fileData, err := os.ReadFile(fileNameFull) if err != nil { return nil, errors.Wrapf(err, "unable to read relayer key data: %s", fileNameFull) } @@ -127,6 +124,21 @@ func ReadRelayerKeyFromFile(fileName string) (*RelayerKey, error) { return &key, nil } +// IsRelayerPrivateKeyValid checks if the given private key is valid for the given network +func IsRelayerPrivateKeyValid(privateKey string, network chains.Network) bool { + switch network { + case chains.Network_solana: + _, err := crypto.SolanaPrivateKeyFromString(privateKey) + if err != nil { + return false + } + default: + // unsupported network + return false + } + return true +} + // relayerKeyFileByNetwork returns the relayer key JSON file name based on network func relayerKeyFileByNetwork(network chains.Network) (string, error) { // get network name diff --git a/zetaclient/keys/relayer_key_test.go b/zetaclient/keys/relayer_key_test.go index 3d5c8a1d1e..0d3f8aeae1 100644 --- a/zetaclient/keys/relayer_key_test.go +++ b/zetaclient/keys/relayer_key_test.go @@ -73,7 +73,7 @@ func Test_ResolveAddress(t *testing.T) { relayerKey: keys.RelayerKey{ PrivateKey: solanaPrivKey.String(), }, - expectedError: "cannot derive relayer address for unsupported network", + expectedError: "unsupported network", }, } @@ -148,6 +148,14 @@ func Test_LoadRelayerKey(t *testing.T) { expectedKey: nil, expectError: "failed to read relayer key file", }, + { + name: "should return error if password is missing", + keyPath: keyPath, + network: chains.Network_solana, + password: "", + expectedKey: nil, + expectError: "password is required to decrypt the private key", + }, { name: "should return error if password is incorrect", keyPath: keyPath, @@ -242,7 +250,7 @@ func Test_ReadWriteRelayerKeyFile(t *testing.T) { t.Run("should return error if relayer key file does not exist", func(t *testing.T) { noFileName := path.Join(keyPath, "non-existing.json") _, err := keys.ReadRelayerKeyFromFile(noFileName) - require.ErrorContains(t, err, "unable to open relayer key file") + require.ErrorContains(t, err, "unable to read relayer key data") }) t.Run("should return error if unmarsalling fails", func(t *testing.T) { @@ -256,3 +264,38 @@ func Test_ReadWriteRelayerKeyFile(t *testing.T) { require.Nil(t, key) }) } + +func Test_IsRelayerPrivateKeyValid(t *testing.T) { + tests := []struct { + name string + privKey string + network chains.Network + result bool + }{ + { + name: "valid private key - solana", + privKey: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", + network: chains.Network(7), // solana + result: true, + }, + { + name: "invalid private key - unsupported network", + privKey: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", + network: chains.Network(0), // eth + result: false, + }, + { + name: "invalid private key - invalid solana private key", + privKey: "3EMjCcCJg53fMEGVj13UPQpo6p", // too short + network: chains.Network(7), // solana + result: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := keys.IsRelayerPrivateKeyValid(tt.privKey, chains.Network(tt.network)) + require.Equal(t, tt.result, result) + }) + } +} diff --git a/zetaclient/orchestrator/bootstrap.go b/zetaclient/orchestrator/bootstrap.go index 2df07902a9..5741c04a97 100644 --- a/zetaclient/orchestrator/bootstrap.go +++ b/zetaclient/orchestrator/bootstrap.go @@ -100,7 +100,10 @@ func syncSignerMap( continue } - rawChain := chain.RawChain() + var ( + params = chain.Params() + rawChain = chain.RawChain() + ) switch { case chain.IsEVM(): @@ -171,7 +174,7 @@ func syncSignerMap( } // create Solana signer - signer, err := solanasigner.NewSigner(*rawChain, *chain.Params(), rpcClient, tss, relayerKey, ts, logger) + signer, err := solanasigner.NewSigner(*rawChain, *params, rpcClient, tss, relayerKey, ts, logger) if err != nil { logger.Std.Error().Err(err).Msgf("Unable to construct signer for SOL chain %d", chainID) continue From 2e2b8a94ac104904afe66a2e34b41bc38fe9885f Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 14 Aug 2024 10:16:15 -0500 Subject: [PATCH 08/13] fix linter --- zetaclient/keys/relayer_key.go | 1 + 1 file changed, 1 insertion(+) diff --git a/zetaclient/keys/relayer_key.go b/zetaclient/keys/relayer_key.go index 20608e3643..0ca963b4f2 100644 --- a/zetaclient/keys/relayer_key.go +++ b/zetaclient/keys/relayer_key.go @@ -109,6 +109,7 @@ func ReadRelayerKeyFromFile(fileName string) (*RelayerKey, error) { } // read the file contents + // #nosec G304 -- relayer key file is controlled by the operator fileData, err := os.ReadFile(fileNameFull) if err != nil { return nil, errors.Wrapf(err, "unable to read relayer key data: %s", fileNameFull) From 3a415d21bc20cf6e6ce8d79cfc0ae1249d1a3201 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 14 Aug 2024 13:27:48 -0500 Subject: [PATCH 09/13] remove GetNetworkName method for simplification --- pkg/chains/chain.go | 6 ------ pkg/chains/chain_test.go | 8 -------- zetaclient/chains/solana/signer/signer.go | 18 ++++++++++-------- zetaclient/keys/relayer_key.go | 20 +++++++------------- zetaclient/keys/relayer_key_test.go | 10 +--------- 5 files changed, 18 insertions(+), 44 deletions(-) diff --git a/pkg/chains/chain.go b/pkg/chains/chain.go index 06ce4af289..221da26a32 100644 --- a/pkg/chains/chain.go +++ b/pkg/chains/chain.go @@ -147,12 +147,6 @@ func (chain Chain) IsEmpty() bool { return strings.TrimSpace(chain.String()) == "" } -// GetNetworkName returns the network name from the network ID -func GetNetworkName(network int32) (string, bool) { - name, found := Network_name[network] - return name, found -} - // GetChainFromChainID returns the chain from the chain ID // additionalChains is a list of additional chains to search from // in practice, it is used in the protocol to dynamically support new chains without doing an upgrade diff --git a/pkg/chains/chain_test.go b/pkg/chains/chain_test.go index 0bbfbc5263..d097d3946c 100644 --- a/pkg/chains/chain_test.go +++ b/pkg/chains/chain_test.go @@ -396,14 +396,6 @@ func TestChain_IsEmpty(t *testing.T) { require.False(t, chains.ZetaChainMainnet.IsEmpty()) } -func TestGetNetworkName(t *testing.T) { - network := int32(chains.Network_solana) - name, found := chains.GetNetworkName(network) - nameExpected, foundExpected := chains.Network_name[network] - require.Equal(t, nameExpected, name) - require.Equal(t, foundExpected, found) -} - func TestGetChainFromChainID(t *testing.T) { chain, found := chains.GetChainFromChainID(chains.ZetaChainMainnet.ChainId, []chains.Chain{}) require.EqualValues(t, chains.ZetaChainMainnet, chain) diff --git a/zetaclient/chains/solana/signer/signer.go b/zetaclient/chains/solana/signer/signer.go index ca3dfb0acb..7fa989a071 100644 --- a/zetaclient/chains/solana/signer/signer.go +++ b/zetaclient/chains/solana/signer/signer.go @@ -196,15 +196,17 @@ func (signer *Signer) GetGatewayAddress() string { // SetRelayerBalanceMetrics sets the relayer balance metrics func (signer *Signer) SetRelayerBalanceMetrics(ctx context.Context) { - if signer.HasRelayerKey() { - result, err := signer.client.GetBalance(ctx, signer.relayerKey.PublicKey(), rpc.CommitmentFinalized) - if err != nil { - signer.Logger().Std.Error().Err(err).Msg("GetBalance error") - return - } - solBalance := float64(result.Value) / float64(solana.LAMPORTS_PER_SOL) - metrics.RelayerKeyBalance.WithLabelValues(signer.Chain().Name).Set(solBalance) + if !signer.HasRelayerKey() { + return + } + + result, err := signer.client.GetBalance(ctx, signer.relayerKey.PublicKey(), rpc.CommitmentFinalized) + if err != nil { + signer.Logger().Std.Error().Err(err).Msg("GetBalance error") + return } + solBalance := float64(result.Value) / float64(solana.LAMPORTS_PER_SOL) + metrics.RelayerKeyBalance.WithLabelValues(signer.Chain().Name).Set(solBalance) } // TODO: get rid of below four functions for Solana and Bitcoin diff --git a/zetaclient/keys/relayer_key.go b/zetaclient/keys/relayer_key.go index 0ca963b4f2..935d11ac84 100644 --- a/zetaclient/keys/relayer_key.go +++ b/zetaclient/keys/relayer_key.go @@ -19,11 +19,7 @@ type RelayerKey struct { // ResolveAddress returns the network name and address of the relayer key func (rk RelayerKey) ResolveAddress(network chains.Network) (string, string, error) { - // get network name - networkName, found := chains.GetNetworkName(int32(network)) - if !found { - return "", "", errors.Errorf("network name not found for network %d", network) - } + var address string switch network { case chains.Network_solana: @@ -31,10 +27,13 @@ func (rk RelayerKey) ResolveAddress(network chains.Network) (string, string, err if err != nil { return "", "", errors.Wrap(err, "unable to construct solana private key") } - return networkName, privKey.PublicKey().String(), nil + address = privKey.PublicKey().String() default: return "", "", errors.Errorf("unsupported network %d: unable to derive relayer address", network) } + + // return network name and address + return network.String(), address, nil } // LoadRelayerKey loads the relayer key for given network and password @@ -142,19 +141,14 @@ func IsRelayerPrivateKeyValid(privateKey string, network chains.Network) bool { // relayerKeyFileByNetwork returns the relayer key JSON file name based on network func relayerKeyFileByNetwork(network chains.Network) (string, error) { - // get network name - networkName, found := chains.GetNetworkName(int32(network)) - if !found { - return "", errors.Errorf("network name not found for network %d", network) - } - // JSONFileSuffix is the suffix for the relayer key file const JSONFileSuffix = ".json" // return file name for supported networks only switch network { case chains.Network_solana: - return networkName + JSONFileSuffix, nil + // return network name + '.json' + return network.String() + JSONFileSuffix, nil default: return "", errors.Errorf("network %d does not support relayer key", network) } diff --git a/zetaclient/keys/relayer_key_test.go b/zetaclient/keys/relayer_key_test.go index 0d3f8aeae1..08e82863d1 100644 --- a/zetaclient/keys/relayer_key_test.go +++ b/zetaclient/keys/relayer_key_test.go @@ -51,14 +51,6 @@ func Test_ResolveAddress(t *testing.T) { expectedNetworkName: "solana", expectedAddress: solanaPrivKey.PublicKey().String(), }, - { - name: "should return error if network name not found", - network: chains.Network(999), - relayerKey: keys.RelayerKey{ - PrivateKey: solanaPrivKey.String(), - }, - expectedError: "network name not found", - }, { name: "should return error if private key is invalid", network: chains.Network_solana, @@ -202,7 +194,7 @@ func Test_ResolveRelayerKeyPath(t *testing.T) { expectedName: path.Join(usr.HomeDir, ".zetacored/relayer-keys/solana.json"), }, { - name: "should return error if network is not found", + name: "should return error if network is invalid", relayerKeyPath: "~/.zetacored/relayer-keys", network: chains.Network(999), errMessage: "failed to get relayer key file name", From 3d2ebad119709cd782f8846e5cc9d6722066ab37 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 14 Aug 2024 14:34:07 -0500 Subject: [PATCH 10/13] added PromptPassword method to prompt single password --- pkg/os/console.go | 30 +++++++++++++++++++++------ pkg/os/console_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/pkg/os/console.go b/pkg/os/console.go index 3b4327cba4..c4a7c505c7 100644 --- a/pkg/os/console.go +++ b/pkg/os/console.go @@ -7,23 +7,41 @@ import ( "strings" ) -// PromptPasswords prompts the user for passwords with the given titles +// PromptPassword prompts the user for a password with the given title +func PromptPassword(passwordTitle string) (string, error) { + reader := bufio.NewReader(os.Stdin) + + return readPassword(reader, passwordTitle) +} + +// PromptPasswords is a convenience function that prompts the user for multiple passwords func PromptPasswords(passwordTitles []string) ([]string, error) { reader := bufio.NewReader(os.Stdin) passwords := make([]string, len(passwordTitles)) // iterate over password titles and prompt for each for i, title := range passwordTitles { - fmt.Printf("%s Password: ", title) - password, err := reader.ReadString('\n') + password, err := readPassword(reader, title) if err != nil { return nil, err } - - // trim delimiters - password = strings.TrimSpace(password) passwords[i] = password } return passwords, nil } + +// readPassword is a helper function that reads a password from bufio.Reader +func readPassword(reader *bufio.Reader, passwordTitle string) (string, error) { + const delimitor = '\n' + + // prompt for password + fmt.Printf("%s Password: ", passwordTitle) + password, err := reader.ReadString(delimitor) + if err != nil { + return "", err + } + + // trim leading and trailing spaces + return strings.TrimSpace(password), nil +} diff --git a/pkg/os/console_test.go b/pkg/os/console_test.go index c86897fe90..d5733744d2 100644 --- a/pkg/os/console_test.go +++ b/pkg/os/console_test.go @@ -8,6 +8,52 @@ import ( zetaos "github.com/zeta-chain/zetacore/pkg/os" ) +func Test_PromptPassword(t *testing.T) { + tests := []struct { + name string + input string + output string + }{ + { + name: "Valid password", + input: " pass123\n", + output: "pass123", + }, + { + name: "Empty password", + input: "\n", + output: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a pipe to simulate stdin + r, w, err := os.Pipe() + require.NoError(t, err) + + // Write the test input to the pipe + _, err = w.Write([]byte(tt.input)) + require.NoError(t, err) + w.Close() // Close the write end of the pipe + + // Backup the original stdin and restore it after the test + oldStdin := os.Stdin + defer func() { os.Stdin = oldStdin }() + + // Redirect stdin to the read end of the pipe + os.Stdin = r + + // Call the function with the test case data + password, err := zetaos.PromptPassword("anyTitle") + + // Check the returned passwords + require.NoError(t, err) + require.Equal(t, tt.output, password) + }) + } +} + // Test function for PromptPasswords func Test_PromptPasswords(t *testing.T) { tests := []struct { From ad8773ffd130522be972d94e699a74c76144f7d4 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 15 Aug 2024 13:21:15 -0500 Subject: [PATCH 11/13] use network name as map index to store relayer key passwords --- cmd/zetaclientd/start.go | 4 ++-- zetaclient/context/app.go | 12 ++++++------ zetaclient/orchestrator/bootstrap.go | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 0646963e49..afea8291b3 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -58,8 +58,8 @@ func start(_ *cobra.Command, _ []string) error { return errors.Wrap(err, "unable to get passwords") } hotkeyPass, tssKeyPass, solanaKeyPass := passwords[0], passwords[1], passwords[2] - relayerKeyPasswords := map[chains.Network]string{ - chains.Network_solana: solanaKeyPass, + relayerKeyPasswords := map[string]string{ + chains.Network_solana.String(): solanaKeyPass, } //Load Config file given path diff --git a/zetaclient/context/app.go b/zetaclient/context/app.go index 3bb4ab698d..24dc7b26d3 100644 --- a/zetaclient/context/app.go +++ b/zetaclient/context/app.go @@ -37,8 +37,8 @@ type AppContext struct { // keygen is the current tss keygen state keygen observertypes.Keygen - // relayerKeyPasswords maps network id to relayer key password - relayerKeyPasswords map[chains.Network]string + // relayerKeyPasswords maps network name to relayer key password + relayerKeyPasswords map[string]string mu sync.RWMutex } @@ -54,7 +54,7 @@ func New(cfg config.Config, logger zerolog.Logger) *AppContext { crosschainFlags: observertypes.CrosschainFlags{}, currentTssPubKey: "", keygen: observertypes.Keygen{}, - relayerKeyPasswords: make(map[chains.Network]string), + relayerKeyPasswords: make(map[string]string), mu: sync.RWMutex{}, } @@ -141,7 +141,7 @@ func (a *AppContext) GetCrossChainFlags() observertypes.CrosschainFlags { } // SetRelayerKeyPasswords sets the relayer key passwords for given networks -func (a *AppContext) SetRelayerKeyPasswords(relayerKeyPasswords map[chains.Network]string) { +func (a *AppContext) SetRelayerKeyPasswords(relayerKeyPasswords map[string]string) { a.mu.Lock() defer a.mu.Unlock() @@ -149,11 +149,11 @@ func (a *AppContext) SetRelayerKeyPasswords(relayerKeyPasswords map[chains.Netwo } // GetRelayerKeyPassword returns the relayer key password for the given network -func (a *AppContext) GetRelayerKeyPassword(network chains.Network) string { +func (a *AppContext) GetRelayerKeyPassword(networkName string) string { a.mu.RLock() defer a.mu.RUnlock() - return a.relayerKeyPasswords[network] + return a.relayerKeyPasswords[networkName] } // Update updates AppContext and params for all chains diff --git a/zetaclient/orchestrator/bootstrap.go b/zetaclient/orchestrator/bootstrap.go index 5741c04a97..5efc0cb47b 100644 --- a/zetaclient/orchestrator/bootstrap.go +++ b/zetaclient/orchestrator/bootstrap.go @@ -166,7 +166,7 @@ func syncSignerMap( } // try loading Solana relayer key if present - password := app.GetRelayerKeyPassword(rawChain.Network) + password := app.GetRelayerKeyPassword(rawChain.Network.String()) relayerKey, err := keys.LoadRelayerKey(app.Config().GetRelayerKeyPath(), rawChain.Network, password) if err != nil { logger.Std.Error().Err(err).Msg("Unable to load Solana relayer key") From 6a4399ce1b26b4c772398a5c69895a63f2c47632 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 15 Aug 2024 14:14:08 -0500 Subject: [PATCH 12/13] moved relayer passwords to chain registry --- cmd/zetaclientd/debug.go | 2 +- cmd/zetaclientd/start.go | 3 +- cmd/zetae2e/local/local.go | 1 - zetaclient/chains/base/observer_test.go | 2 +- .../chains/evm/observer/inbound_test.go | 2 +- .../chains/evm/observer/observer_test.go | 2 +- zetaclient/chains/evm/signer/signer_test.go | 2 +- zetaclient/context/app.go | 30 ++++--------------- zetaclient/context/app_test.go | 2 +- zetaclient/context/chain.go | 19 +++++++++--- zetaclient/context/chain_test.go | 2 +- zetaclient/context/context_test.go | 4 +-- zetaclient/orchestrator/bootstap_test.go | 4 +-- zetaclient/orchestrator/bootstrap.go | 2 +- zetaclient/orchestrator/orchestrator_test.go | 2 +- zetaclient/zetacore/tx_test.go | 2 +- 16 files changed, 35 insertions(+), 46 deletions(-) diff --git a/cmd/zetaclientd/debug.go b/cmd/zetaclientd/debug.go index 6fc46f71f9..9d7ece9a0c 100644 --- a/cmd/zetaclientd/debug.go +++ b/cmd/zetaclientd/debug.go @@ -80,7 +80,7 @@ func debugCmd(_ *cobra.Command, args []string) error { return err } - appContext := zctx.New(cfg, zerolog.Nop()) + appContext := zctx.New(cfg, nil, zerolog.Nop()) ctx := zctx.WithAppContext(context.Background(), appContext) if err := client.UpdateAppContext(ctx, appContext, zerolog.Nop()); err != nil { diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index afea8291b3..cf6a8e4d9e 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -83,8 +83,7 @@ func start(_ *cobra.Command, _ []string) error { masterLogger := logger.Std startLogger := logger.Std.With().Str("module", "startup").Logger() - appContext := zctx.New(cfg, masterLogger) - appContext.SetRelayerKeyPasswords(relayerKeyPasswords) + appContext := zctx.New(cfg, relayerKeyPasswords, masterLogger) ctx := zctx.WithAppContext(context.Background(), appContext) // Wait until zetacore is up diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 521c53569e..86adfac7c4 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -302,7 +302,6 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestUpdateBytecodeConnectorName, e2etests.TestDepositEtherLiquidityCapName, e2etests.TestCriticalAdminTransactionsName, - e2etests.TestMigrateERC20CustodyFundsName, // TestMigrateChainSupportName tests EVM chain migration. Currently this test doesn't work with Anvil because pre-EIP1559 txs are not supported // See issue below for details diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index 50148a551b..009d9a53cf 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -54,7 +54,7 @@ func TestNewObserver(t *testing.T) { // constructor parameters chain := chains.Ethereum chainParams := *sample.ChainParams(chain.ChainId) - appContext := zctx.New(config.New(false), zerolog.Nop()) + appContext := zctx.New(config.New(false), nil, zerolog.Nop()) zetacoreClient := mocks.NewZetacoreClient(t) tss := mocks.NewTSSMainnet() blockCacheSize := base.DefaultBlockCacheSize diff --git a/zetaclient/chains/evm/observer/inbound_test.go b/zetaclient/chains/evm/observer/inbound_test.go index 231a3ae6c7..26290fc6c0 100644 --- a/zetaclient/chains/evm/observer/inbound_test.go +++ b/zetaclient/chains/evm/observer/inbound_test.go @@ -501,7 +501,7 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { func makeAppContext(t *testing.T) (context.Context, *zctx.AppContext) { var ( - app = zctx.New(config.New(false), zerolog.New(zerolog.NewTestWriter(t))) + app = zctx.New(config.New(false), nil, zerolog.New(zerolog.NewTestWriter(t))) ctx = context.Background() ) diff --git a/zetaclient/chains/evm/observer/observer_test.go b/zetaclient/chains/evm/observer/observer_test.go index 95d2ed2140..69ff6a977d 100644 --- a/zetaclient/chains/evm/observer/observer_test.go +++ b/zetaclient/chains/evm/observer/observer_test.go @@ -59,7 +59,7 @@ func getAppContext( logger := zerolog.New(zerolog.NewTestWriter(t)) // create AppContext - appContext := zctx.New(cfg, logger) + appContext := zctx.New(cfg, nil, logger) chainParams := map[int64]*observertypes.ChainParams{ evmChain.ChainId: evmChainParams, chains.ZetaChainMainnet.ChainId: ptr.Ptr( diff --git a/zetaclient/chains/evm/signer/signer_test.go b/zetaclient/chains/evm/signer/signer_test.go index 570cd57042..23c7bc2fcf 100644 --- a/zetaclient/chains/evm/signer/signer_test.go +++ b/zetaclient/chains/evm/signer/signer_test.go @@ -463,7 +463,7 @@ func TestSigner_SignerErrorMsg(t *testing.T) { } func makeCtx(t *testing.T) context.Context { - app := zctx.New(config.New(false), zerolog.Nop()) + app := zctx.New(config.New(false), nil, zerolog.Nop()) bscParams := mocks.MockChainParams(chains.BscMainnet.ChainId, 10) diff --git a/zetaclient/context/app.go b/zetaclient/context/app.go index 24dc7b26d3..e3a219f00b 100644 --- a/zetaclient/context/app.go +++ b/zetaclient/context/app.go @@ -37,24 +37,20 @@ type AppContext struct { // keygen is the current tss keygen state keygen observertypes.Keygen - // relayerKeyPasswords maps network name to relayer key password - relayerKeyPasswords map[string]string - mu sync.RWMutex } // New creates and returns new empty AppContext -func New(cfg config.Config, logger zerolog.Logger) *AppContext { +func New(cfg config.Config, relayerKeyPasswords map[string]string, logger zerolog.Logger) *AppContext { return &AppContext{ config: cfg, logger: logger.With().Str("module", "appcontext").Logger(), - chainRegistry: NewChainRegistry(), + chainRegistry: NewChainRegistry(relayerKeyPasswords), - crosschainFlags: observertypes.CrosschainFlags{}, - currentTssPubKey: "", - keygen: observertypes.Keygen{}, - relayerKeyPasswords: make(map[string]string), + crosschainFlags: observertypes.CrosschainFlags{}, + currentTssPubKey: "", + keygen: observertypes.Keygen{}, mu: sync.RWMutex{}, } @@ -140,22 +136,6 @@ func (a *AppContext) GetCrossChainFlags() observertypes.CrosschainFlags { return a.crosschainFlags } -// SetRelayerKeyPasswords sets the relayer key passwords for given networks -func (a *AppContext) SetRelayerKeyPasswords(relayerKeyPasswords map[string]string) { - a.mu.Lock() - defer a.mu.Unlock() - - a.relayerKeyPasswords = relayerKeyPasswords -} - -// GetRelayerKeyPassword returns the relayer key password for the given network -func (a *AppContext) GetRelayerKeyPassword(networkName string) string { - a.mu.RLock() - defer a.mu.RUnlock() - - return a.relayerKeyPasswords[networkName] -} - // Update updates AppContext and params for all chains // this must be the ONLY function that writes to AppContext func (a *AppContext) Update( diff --git a/zetaclient/context/app_test.go b/zetaclient/context/app_test.go index 0dd8e2daed..d3bba4f041 100644 --- a/zetaclient/context/app_test.go +++ b/zetaclient/context/app_test.go @@ -57,7 +57,7 @@ func TestAppContext(t *testing.T) { t.Run("Update", func(t *testing.T) { // Given AppContext - appContext := New(testCfg, logger) + appContext := New(testCfg, nil, logger) // With expected default behavior _, err := appContext.GetChain(123) diff --git a/zetaclient/context/chain.go b/zetaclient/context/chain.go index a5c17768f1..4271fdb46e 100644 --- a/zetaclient/context/chain.go +++ b/zetaclient/context/chain.go @@ -20,6 +20,9 @@ type ChainRegistry struct { // chain IDs. It's stored in the protocol to dynamically support new chains without doing an upgrade additionalChains []chains.Chain + // relayerKeyPasswords maps network name to relayer key password + relayerKeyPasswords map[string]string + mu sync.Mutex } @@ -39,11 +42,12 @@ var ( ) // NewChainRegistry constructs a new ChainRegistry -func NewChainRegistry() *ChainRegistry { +func NewChainRegistry(relayerKeyPasswords map[string]string) *ChainRegistry { return &ChainRegistry{ - chains: make(map[int64]Chain), - additionalChains: []chains.Chain{}, - mu: sync.Mutex{}, + chains: make(map[int64]Chain), + additionalChains: []chains.Chain{}, + relayerKeyPasswords: relayerKeyPasswords, + mu: sync.Mutex{}, } } @@ -161,6 +165,13 @@ func (c Chain) IsSolana() bool { return chains.IsSolanaChain(c.ID(), c.registry.additionalChains) } +// RelayerKeyPassword returns the relayer key password for the chain +func (c Chain) RelayerKeyPassword() string { + network := c.RawChain().Network + + return c.registry.relayerKeyPasswords[network.String()] +} + func validateNewChain(chainID int64, chain *chains.Chain, params *observer.ChainParams) error { switch { case chainID < 1: diff --git a/zetaclient/context/chain_test.go b/zetaclient/context/chain_test.go index a679ed020a..29d1ecef7c 100644 --- a/zetaclient/context/chain_test.go +++ b/zetaclient/context/chain_test.go @@ -35,7 +35,7 @@ func TestChainRegistry(t *testing.T) { t.Run("Sample Flow", func(t *testing.T) { // Given registry - r := NewChainRegistry() + r := NewChainRegistry(nil) // With some chains added require.NoError(t, r.Set(btc.ChainId, btc, btcParams)) diff --git a/zetaclient/context/context_test.go b/zetaclient/context/context_test.go index be9dab83a4..d0f623a86f 100644 --- a/zetaclient/context/context_test.go +++ b/zetaclient/context/context_test.go @@ -24,7 +24,7 @@ func TestFromContext(t *testing.T) { // ARRANGE #2 // Given basic app - app := context.New(config.New(false), zerolog.Nop()) + app := context.New(config.New(false), nil, zerolog.Nop()) // That is included in the ctx ctx = context.WithAppContext(ctx, app) @@ -42,7 +42,7 @@ func TestFromContext(t *testing.T) { func TestCopy(t *testing.T) { // ARRANGE var ( - app = context.New(config.New(false), zerolog.Nop()) + app = context.New(config.New(false), nil, zerolog.Nop()) ctx1 = context.WithAppContext(goctx.Background(), app) ) diff --git a/zetaclient/orchestrator/bootstap_test.go b/zetaclient/orchestrator/bootstap_test.go index 55b6f47614..bac8507f7c 100644 --- a/zetaclient/orchestrator/bootstap_test.go +++ b/zetaclient/orchestrator/bootstap_test.go @@ -51,7 +51,7 @@ func TestCreateSignerMap(t *testing.T) { cfg.BitcoinConfig = btcConfig // Given AppContext - app := zctx.New(cfg, log) + app := zctx.New(cfg, nil, log) ctx := zctx.WithAppContext(context.Background(), app) // Given chain & chainParams "fetched" from zetacore @@ -230,7 +230,7 @@ func TestCreateChainObserverMap(t *testing.T) { cfg.SolanaConfig = solConfig // Given AppContext - app := zctx.New(cfg, log) + app := zctx.New(cfg, nil, log) ctx := zctx.WithAppContext(context.Background(), app) // Given chain & chainParams "fetched" from zetacore diff --git a/zetaclient/orchestrator/bootstrap.go b/zetaclient/orchestrator/bootstrap.go index 5efc0cb47b..61d2960468 100644 --- a/zetaclient/orchestrator/bootstrap.go +++ b/zetaclient/orchestrator/bootstrap.go @@ -166,7 +166,7 @@ func syncSignerMap( } // try loading Solana relayer key if present - password := app.GetRelayerKeyPassword(rawChain.Network.String()) + password := chain.RelayerKeyPassword() relayerKey, err := keys.LoadRelayerKey(app.Config().GetRelayerKeyPath(), rawChain.Network, password) if err != nil { logger.Std.Error().Err(err).Msg("Unable to load Solana relayer key") diff --git a/zetaclient/orchestrator/orchestrator_test.go b/zetaclient/orchestrator/orchestrator_test.go index 3594accc2a..21d3998a84 100644 --- a/zetaclient/orchestrator/orchestrator_test.go +++ b/zetaclient/orchestrator/orchestrator_test.go @@ -535,7 +535,7 @@ func createAppContext(t *testing.T, chainsOrParams ...any) *zctx.AppContext { } // new AppContext - appContext := zctx.New(cfg, zerolog.New(zerolog.NewTestWriter(t))) + appContext := zctx.New(cfg, nil, zerolog.New(zerolog.NewTestWriter(t))) ccFlags := sample.CrosschainFlags() diff --git a/zetaclient/zetacore/tx_test.go b/zetaclient/zetacore/tx_test.go index a01c11be2b..05cdff6417 100644 --- a/zetaclient/zetacore/tx_test.go +++ b/zetaclient/zetacore/tx_test.go @@ -357,7 +357,7 @@ func TestZetacore_UpdateAppContext(t *testing.T) { t.Run("zetacore update success", func(t *testing.T) { cfg := config.New(false) - appContext := zctx.New(cfg, zerolog.Nop()) + appContext := zctx.New(cfg, nil, zerolog.Nop()) err := client.UpdateAppContext(ctx, appContext, zerolog.New(zerolog.NewTestWriter(t))) require.NoError(t, err) }) From c9f0b42d134df43a0182fd6d22422973b3d85b42 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 15 Aug 2024 14:58:30 -0500 Subject: [PATCH 13/13] airdrop SOL token only if solana local node is available --- .../localnet/orchestrator/Dockerfile.fastbuild | 2 +- contrib/localnet/orchestrator/start-zetae2e.sh | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/contrib/localnet/orchestrator/Dockerfile.fastbuild b/contrib/localnet/orchestrator/Dockerfile.fastbuild index 85e0296dfc..d260d4a1f0 100644 --- a/contrib/localnet/orchestrator/Dockerfile.fastbuild +++ b/contrib/localnet/orchestrator/Dockerfile.fastbuild @@ -6,7 +6,7 @@ FROM ghcr.io/zeta-chain/solana-docker:1.18.15 AS solana FROM ghcr.io/zeta-chain/golang:1.22.5-bookworm AS orchestrator RUN apt update && \ - apt install -yq jq yq curl tmux python3 openssh-server iputils-ping iproute2 && \ + apt install -yq jq yq curl tmux python3 openssh-server iputils-ping iproute2 bind9-host && \ rm -rf /var/lib/apt/lists/* COPY --from=geth /usr/local/bin/geth /usr/local/bin/ diff --git a/contrib/localnet/orchestrator/start-zetae2e.sh b/contrib/localnet/orchestrator/start-zetae2e.sh index 7d6dcdeca5..4ee8192c6d 100644 --- a/contrib/localnet/orchestrator/start-zetae2e.sh +++ b/contrib/localnet/orchestrator/start-zetae2e.sh @@ -92,16 +92,18 @@ echo "funding migration tester address ${address} with 10000 Ether" geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock local solana relayer accounts -solana_url=$(yq -r '.rpcs.solana' config.yml) -solana config set --url "$solana_url" > /dev/null +if host solana > /dev/null; then + solana_url=$(yq -r '.rpcs.solana' config.yml) + solana config set --url "$solana_url" > /dev/null -relayer=$(yq -r '.observer_relayer_accounts.relayer_accounts[0].solana_address' config.yml) -echo "funding solana relayer address ${relayer} with 100 SOL" -solana airdrop 100 "$relayer" > /dev/null + relayer=$(yq -r '.observer_relayer_accounts.relayer_accounts[0].solana_address' config.yml) + echo "funding solana relayer address ${relayer} with 100 SOL" + solana airdrop 100 "$relayer" > /dev/null -relayer=$(yq -r '.observer_relayer_accounts.relayer_accounts[1].solana_address' config.yml) -echo "funding solana relayer address ${relayer} with 100 SOL" -solana airdrop 100 "$relayer" > /dev/null + relayer=$(yq -r '.observer_relayer_accounts.relayer_accounts[1].solana_address' config.yml) + echo "funding solana relayer address ${relayer} with 100 SOL" + solana airdrop 100 "$relayer" > /dev/null +fi ### Run zetae2e command depending on the option passed