Repository: informalsystems/ibc-starknet
Pull request #334: fix: exclude starknet signing key from relayer config file 
Author: login: rnbguy
Created: 2025-02-28T11:19:34Z | Merged: 2025-03-04T13:09:51Z

closes #318

included an example relayer config file. after filling out the empty fields, we can relay packets via

```bash
RUST_LOG=trace hermes-starknet -c relayer.toml start cosmos-with-starknet \
  --chain-id-a 393402133025997798000961 --client-id-a 07-tendermint-3 \
  --chain-id-b osmo-test-5 --client-id-b 08-wasm-4459
```

Linked issue #318: exclude Starknet wallet's signing key in relayer config
Currently, the `StarknetWallet` takes `signing_key` directly.

https://github.com/informalsystems/ibc-starknet/blob/f721ccf98a925b81e897d11d43666df4f033da58/relayer/crates/starknet-chain-components/src/types/wallet.rs#L7-L11

This makes the relayer configuration file include the private secret. Ideally, this should a file path. While creating the `LocalWallet` or other wallets, the secret should be read from it.

This will allow us to include our testnet configuration file in the repo.

Diff:
diff --git a/.gitignore b/.gitignore
index c99de4e3e..606a8c4ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ target/
 .snfoundry_versioned_programs/
 
 .env
+relayer.toml
 
 # vscode
 .vscode/
diff --git a/relayer.toml.example b/relayer.toml.example
new file mode 100644
index 000000000..ced0cfb40
--- /dev/null
+++ b/relayer.toml.example
@@ -0,0 +1,62 @@
+[starknet_chain_config]
+poll_interval = "40s"
+block_time    = { secs = 30, nanos = 0 }
+
+json_rpc_url = "https://starknet-sepolia.public.blastapi.io/rpc/v0_7"
+# json_rpc_url = "https://starknet-sepolia.reddio.com/rpc/v0_7"
+
+# -- to be filled
+# private information
+# toml file containing the `account_address`, `public_key` and `signing_key` as Felt
+relayer_wallet = ""
+
+[starknet_chain_config.contract_classes]
+erc20      = "0x04570481e743c3fdd442c44d968030b375cce8ec1471ccfd1a1dc48613060c3c"
+ics20      = "0x04d2f862a3e23f094983b7e2df83f4875fbd6b8571c5a53478e0be8962f87b30"
+ibc_client = "0x0305e76f607a3c39f2955902a41d820a4b2559e9e3e8bed681f32c2e87831901"
+
+[starknet_chain_config.contract_addresses]
+ibc_client = "0x05d78cad8c0efc496d5ce0bdce66bbae7aa9d97860448a5af84019227383bb2a"
+ibc_core   = "0x04cf1183281f8e60abcb58466b0d01f3eb273e64799d8e6ad2cea1578ef5e91a"
+
+[cosmos_chain_config]
+id             = 'osmo-test-5'
+account_prefix = 'osmo'
+store_prefix   = 'ibc'
+rpc_timeout    = { secs = 10, nanos = 0 }
+address_type   = 'cosmos'
+max_msg_num    = 4
+max_tx_size    = 1048576
+
+rpc_addr     = 'https://rpc.testnet.osmosis.zone'
+# rpc_addr     = 'https://rpc.osmotest5.osmosis.zone'
+grpc_addr    = 'https://grpc.testnet.osmosis.zone'
+event_source = { mode = 'push', url = 'wss://rpc.testnet.osmosis.zone/websocket' }
+
+# -- to be filled
+# private information
+# hermes-v1 key should be present at `<key_store_folder>/<key_name>.json`
+key_store_folder = ""
+key_name         = ''
+
+# https://www.mintscan.io/osmosis-testnet
+block_time        = { secs = 0, nanos = 520000000 }
+clock_drift       = { secs = 1, nanos = 0 }
+max_block_time    = { secs = 10, nanos = 0 }
+compat_mode       = "v0.37"
+extension_options = [  ]
+poll_interval     = { secs = 1, nanos = 0 }
+
+[cosmos_chain_config.gas_config]
+default_gas        = 200000
+max_gas            = 400000
+gas_multiplier     = 1.25
+gas_price          = { price = 0.0025, denom = 'uosmo' }
+fee_granter        = ''
+dynamic_gas_config = { multiplier = 1.1, max = 1.6, eip_query_type = "Osmosis", denom = "uosmo" }
+
+[cosmos_chain_config.gas_config.max_fee]
+amount    = [ { amount = "25000", denom = 'uosmo' } ]
+gas_limit = 400000
+payer     = ''
+granter   = ''
diff --git a/relayer/Cargo.lock b/relayer/Cargo.lock
index f3918179b..80ae56e22 100644
--- a/relayer/Cargo.lock
+++ b/relayer/Cargo.lock
@@ -2523,6 +2523,7 @@ dependencies = [
  "serde",
  "starknet",
  "tiny-bip39",
+ "toml",
  "url",
 ]
 
diff --git a/relayer/crates/starknet-chain-components/src/impls/types/config.rs b/relayer/crates/starknet-chain-components/src/impls/types/config.rs
index d9bc22207..037508992 100644
--- a/relayer/crates/starknet-chain-components/src/impls/types/config.rs
+++ b/relayer/crates/starknet-chain-components/src/impls/types/config.rs
@@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize};
 use starknet::core::types::Felt;
 
 use crate::impls::types::address::StarknetAddress;
-use crate::types::wallet::StarknetWallet;
 
 #[derive(Debug, Serialize, Deserialize)]
 pub struct StarknetRelayerConfig {
@@ -16,7 +15,7 @@ pub struct StarknetRelayerConfig {
 #[derive(Debug, Serialize, Deserialize)]
 pub struct StarknetChainConfig {
     pub json_rpc_url: String,
-    pub relayer_wallet: StarknetWallet,
+    pub relayer_wallet: String,
     #[serde(with = "humantime_serde")]
     pub poll_interval: Duration,
     pub block_time: Duration,
diff --git a/relayer/crates/starknet-cli/src/contexts/app.rs b/relayer/crates/starknet-cli/src/contexts/app.rs
index 7e977b087..46b14da8f 100644
--- a/relayer/crates/starknet-cli/src/contexts/app.rs
+++ b/relayer/crates/starknet-cli/src/contexts/app.rs
@@ -259,12 +259,18 @@ impl ConfigUpdater<StarknetChainDriver, StarknetRelayerConfig> for UpdateStarkne
                 .next(),
         };
 
+        let relayer_wallet_path = chain_driver
+            .chain_store_dir
+            .join("wallets/relayer.toml")
+            .display()
+            .to_string();
+
         let chain_config = StarknetChainConfig {
             json_rpc_url: format!(
                 "http://{}:{}/",
                 chain_driver.node_config.rpc_addr, chain_driver.node_config.rpc_port
             ),
-            relayer_wallet,
+            relayer_wallet: relayer_wallet_path,
             poll_interval: chain_driver.chain.poll_interval,
             block_time: chain_driver.chain.block_time,
             contract_addresses,
diff --git a/relayer/crates/starknet-integration-tests/src/contexts/bootstrap.rs b/relayer/crates/starknet-integration-tests/src/contexts/bootstrap.rs
index 422b309bf..4bb28b07d 100644
--- a/relayer/crates/starknet-integration-tests/src/contexts/bootstrap.rs
+++ b/relayer/crates/starknet-integration-tests/src/contexts/bootstrap.rs
@@ -26,6 +26,8 @@ use hermes_cosmos_test_components::bootstrap::traits::types::genesis_config::Cha
 use hermes_error::impls::ProvideHermesError;
 use hermes_error::types::HermesError;
 use hermes_runtime::types::runtime::HermesRuntime;
+use hermes_runtime_components::traits::fs::create_dir::CanCreateDir;
+use hermes_runtime_components::traits::fs::write_file::CanWriteStringToFile;
 use hermes_runtime_components::traits::runtime::{
     HasRuntime, RuntimeGetterComponent, RuntimeTypeProviderComponent,
 };
@@ -116,6 +118,19 @@ impl ChainDriverBuilder<StarknetBootstrap> for StarknetBootstrapComponents {
     ) -> Result<StarknetChainDriver, HermesError> {
         let runtime = bootstrap.runtime.clone();
 
+        let chain_store_dir = bootstrap.chain_store_dir.clone();
+
+        runtime.create_dir(&chain_store_dir.join("wallets")).await?;
+
+        for (name, wallet) in wallets.iter() {
+            let wallet_str = toml::to_string_pretty(wallet)?;
+            let wallet_path = chain_store_dir.join(format!("wallets/{name}.toml"));
+
+            runtime
+                .write_string_to_file(&wallet_path, &wallet_str)
+                .await?;
+        }
+
         let relayer_wallet = wallets
             .get("relayer")
             .ok_or_else(|| StarknetBootstrap::raise_error("expect relayer wallet to be present"))?
@@ -181,6 +196,7 @@ impl ChainDriverBuilder<StarknetBootstrap> for StarknetBootstrapComponents {
         let chain_driver = StarknetChainDriver {
             runtime,
             chain,
+            chain_store_dir,
             genesis_config,
             node_config,
             wallets,
diff --git a/relayer/crates/starknet-integration-tests/src/contexts/chain_driver.rs b/relayer/crates/starknet-integration-tests/src/contexts/chain_driver.rs
index 9b1410cc5..376640c8b 100644
--- a/relayer/crates/starknet-integration-tests/src/contexts/chain_driver.rs
+++ b/relayer/crates/starknet-integration-tests/src/contexts/chain_driver.rs
@@ -1,4 +1,5 @@
 use std::collections::BTreeMap;
+use std::path::PathBuf;
 
 use cgp::core::component::UseDelegate;
 use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent};
@@ -28,6 +29,7 @@ use tokio::process::Child;
 pub struct StarknetChainDriver {
     pub runtime: HermesRuntime,
     pub chain: StarknetChain,
+    pub chain_store_dir: PathBuf,
     pub genesis_config: StarknetGenesisConfig,
     pub node_config: StarknetNodeConfig,
     pub wallets: BTreeMap<String, StarknetWallet>,
diff --git a/relayer/crates/starknet-relayer/Cargo.toml b/relayer/crates/starknet-relayer/Cargo.toml
index 4032bde97..7c80f600e 100644
--- a/relayer/crates/starknet-relayer/Cargo.toml
+++ b/relayer/crates/starknet-relayer/Cargo.toml
@@ -33,5 +33,6 @@ eyre    = { workspace = true }
 serde   = { workspace = true }
 url     = { workspace = true }
 futures = { workspace = true }
+toml    = { workspace = true }
 
 tiny-bip39 = { workspace = true }
diff --git a/relayer/crates/starknet-relayer/src/contexts/builder.rs b/relayer/crates/starknet-relayer/src/contexts/builder.rs
index da1ec7dd1..204bb1483 100644
--- a/relayer/crates/starknet-relayer/src/contexts/builder.rs
+++ b/relayer/crates/starknet-relayer/src/contexts/builder.rs
@@ -1,6 +1,7 @@
 use alloc::sync::Arc;
 use core::marker::PhantomData;
 use core::ops::Deref;
+use std::path::PathBuf;
 
 use cgp::core::component::UseDelegate;
 use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent};
@@ -25,11 +26,13 @@ use hermes_relayer_components::multi::traits::birelay_at::BiRelayTypeAtComponent
 use hermes_relayer_components::multi::traits::chain_at::ChainTypeAtComponent;
 use hermes_relayer_components::multi::traits::relay_at::RelayTypeAtComponent;
 use hermes_runtime::types::runtime::HermesRuntime;
+use hermes_runtime_components::traits::fs::read_file::CanReadFileAsString;
 use hermes_runtime_components::traits::runtime::{
     RuntimeGetterComponent, RuntimeTypeProviderComponent,
 };
 use hermes_starknet_chain_components::impls::types::config::StarknetChainConfig;
 use hermes_starknet_chain_components::types::client_id::ClientId as StarknetClientId;
+use hermes_starknet_chain_components::types::wallet::StarknetWallet;
 use hermes_starknet_chain_context::contexts::chain::{StarknetChain, StarknetChainFields};
 use hermes_starknet_chain_context::contexts::encoding::event::StarknetEventEncoding;
 use hermes_starknet_chain_context::impls::error::HandleStarknetChainError;
@@ -226,23 +229,26 @@ impl StarknetBuilder {
             return Err(eyre!("Starknet chain has a different ID as configured. Expected: {expected_chain_id}, got: {chain_id}").into());
         }
 
+        let wallet_path = PathBuf::from(self.starknet_chain_config.relayer_wallet.clone());
+
+        let wallet_str = self.runtime.read_file_as_string(&wallet_path).await?;
+
+        let relayer_wallet: StarknetWallet = toml::from_str(&wallet_str)
+            .map_err(|e| eyre!("Failed to parse relayer wallet: {e}"))?;
+
         let account = SingleOwnerAccount::new(
             rpc_client.clone(),
             LocalWallet::from_signing_key(SigningKey::from_secret_scalar(
-                self.starknet_chain_config.relayer_wallet.signing_key,
+                relayer_wallet.signing_key,
             )),
-            *self.starknet_chain_config.relayer_wallet.account_address,
+            *relayer_wallet.account_address,
             chain_id_felt,
             ExecutionEncoding::New,
         );
 
         let proof_signer = Secp256k1KeyPair::from_mnemonic(
             bip39::Mnemonic::from_entropy(
-                &self
-                    .starknet_chain_config
-                    .relayer_wallet
-                    .signing_key
-                    .to_bytes_be(),
+                &relayer_wallet.signing_key.to_bytes_be(),
                 bip39::Language::English,
             )
             .expect("valid mnemonic")


Reviews:
user: login: soareschen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-28T13:54:08Z
user: login: soareschen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-28T13:54:44Z
user: login: rnbguy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:33:08Z
user: login: soareschen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:35:21Z
user: login: rnbguy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T10:25:33Z
user: login: soareschen
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T13:09:45Z

Review comments:
diff_hunk: @@ -3,9 +3,37 @@ use starknet::core::types::Felt;
 
 use crate::impls::types::address::StarknetAddress;
 
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum SigningKey {
+    File { file: String },
+    Felt { felt: Felt },
+}
+
+impl TryFrom<SigningKey> for Felt {
+    type Error = String;
+
+    fn try_from(value: SigningKey) -> Result<Self, Self::Error> {
+        match value {
+            SigningKey::File { file } => Ok(std::fs::read_to_string(file)
+                .map_err(|e| format!("{e}"))?
+                .trim()
+                .parse()
+                .map_err(|e| format!("{e}"))?),
+            SigningKey::Felt { felt } => Ok(felt),
+        }
+    }
+}
+
+impl From<Felt> for SigningKey {
+    fn from(felt: Felt) -> Self {
+        Self::Felt { felt }
+    }
+}
+
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct StarknetWallet {
     pub account_address: StarknetAddress,
-    pub signing_key: Felt,
+    pub signing_key: SigningKey,
path: relayer/crates/starknet-chain-components/src/types/wallet.rs
user: login: soareschen
body: I think the signing key here should always be `Felt`. We should update the config to load the key from a path, and load the key before initializing the chain or wallet.
created_at: 2025-02-28T13:54:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,9 +3,37 @@ use starknet::core::types::Felt;
 
 use crate::impls::types::address::StarknetAddress;
 
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum SigningKey {
+    File { file: String },
+    Felt { felt: Felt },
+}
+
+impl TryFrom<SigningKey> for Felt {
+    type Error = String;
+
+    fn try_from(value: SigningKey) -> Result<Self, Self::Error> {
+        match value {
+            SigningKey::File { file } => Ok(std::fs::read_to_string(file)
path: relayer/crates/starknet-chain-components/src/types/wallet.rs
user: login: soareschen
body: This is an unexpected side effect, some more with blocking I/O, which would happen every time the field is used, which is not good.
created_at: 2025-02-28T13:54:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,9 +3,37 @@ use starknet::core::types::Felt;
 
 use crate::impls::types::address::StarknetAddress;
 
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum SigningKey {
+    File { file: String },
+    Felt { felt: Felt },
+}
+
+impl TryFrom<SigningKey> for Felt {
+    type Error = String;
+
+    fn try_from(value: SigningKey) -> Result<Self, Self::Error> {
+        match value {
+            SigningKey::File { file } => Ok(std::fs::read_to_string(file)
path: relayer/crates/starknet-chain-components/src/types/wallet.rs
user: login: rnbguy
body: Maybe I have to do the same way you do it for `CosmosChainConfig`

https://github.com/informalsystems/hermes-sdk/blob/27168d1ee8c004d87b39d769e80c686d79ed53c5/crates/cosmos/cosmos-chain-components/src/impls/types/config.rs#L21-L22

Can you please take over this PR since I am off sick?
created_at: 2025-03-03T11:33:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,9 +3,37 @@ use starknet::core::types::Felt;
 
 use crate::impls::types::address::StarknetAddress;
 
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum SigningKey {
+    File { file: String },
+    Felt { felt: Felt },
+}
+
+impl TryFrom<SigningKey> for Felt {
+    type Error = String;
+
+    fn try_from(value: SigningKey) -> Result<Self, Self::Error> {
+        match value {
+            SigningKey::File { file } => Ok(std::fs::read_to_string(file)
path: relayer/crates/starknet-chain-components/src/types/wallet.rs
user: login: soareschen
body: No problem, get some rest. I think this is lower priority for now.
created_at: 2025-03-03T11:35:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,9 +3,37 @@ use starknet::core::types::Felt;
 
 use crate::impls::types::address::StarknetAddress;
 
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum SigningKey {
+    File { file: String },
+    Felt { felt: Felt },
+}
+
+impl TryFrom<SigningKey> for Felt {
+    type Error = String;
+
+    fn try_from(value: SigningKey) -> Result<Self, Self::Error> {
+        match value {
+            SigningKey::File { file } => Ok(std::fs::read_to_string(file)
+                .map_err(|e| format!("{e}"))?
+                .trim()
+                .parse()
+                .map_err(|e| format!("{e}"))?),
+            SigningKey::Felt { felt } => Ok(felt),
+        }
+    }
+}
+
+impl From<Felt> for SigningKey {
+    fn from(felt: Felt) -> Self {
+        Self::Felt { felt }
+    }
+}
+
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct StarknetWallet {
     pub account_address: StarknetAddress,
-    pub signing_key: Felt,
+    pub signing_key: SigningKey,
path: relayer/crates/starknet-chain-components/src/types/wallet.rs
user: login: rnbguy
body: 5884df0
created_at: 2025-03-04T10:25:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: soareschen
created_at: 2025-03-04T11:55:03Z
body: @rnbguy I have pushed a commit to properly save and load the wallet files using async I/O. Please test it and see if it works.
author_association: CONTRIBUTOR
user: login: rnbguy
created_at: 2025-03-04T12:27:12Z
body: thanks @soareschen ! the changes work on testnet 🙏🏼 we can merge. 
author_association: CONTRIBUTOR