diff --git a/.claude/commands/pegin-request.md b/.claude/commands/pegin-request.md index 9d3e716d6..72907854e 100644 --- a/.claude/commands/pegin-request.md +++ b/.claude/commands/pegin-request.md @@ -8,7 +8,7 @@ Send a pegin request to GoatChain via the `pegin-request` binary. - `prepare` - Build, sign and broadcast the pegin deposit tx on Bitcoin - `request-prepare` - Request then auto-prepare after waiting ~12 minutes - `cancel` - Build, sign and broadcast the pegin refund tx on Bitcoin - - **network**: Bitcoin network (`bitcoin`, `testnet`, `testnet4`, `signet`, `regtest`). Default: `testnet4` + - **network**: Bitcoin network (`bitcoin`, `testnet4`, `signet`, `regtest`). Default: `BITCOIN_NETWORK`, or `testnet4`. Must match `BITCOIN_NETWORK` when both are set - **esplora_url**: Esplora API URL. Default: `https://mempool.space/testnet4/api` - For `request` / `request-prepare`: - **pegin_amount_sats**: Amount in satoshis. Default: `1000000` (0.01 BTC) diff --git a/crates/bitvm-gc/src/keys.rs b/crates/bitvm-gc/src/keys.rs index 237cd3c78..14e369628 100644 --- a/crates/bitvm-gc/src/keys.rs +++ b/crates/bitvm-gc/src/keys.rs @@ -27,11 +27,56 @@ use rand::{RngCore, rngs::OsRng}; use secp256k1::schnorr::Signature as SchnorrSignature; use serde::{Deserialize, Serialize}; use sha2::Sha256; -use std::os::unix::fs::PermissionsExt; +use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::time::{SystemTime, UNIX_EPOCH}; use std::{fs, io::Write, path::Path}; use uuid::Uuid; +/// Writes `contents` to `path` so that after a crash the file is either complete or absent. +/// The bytes go to a 0600 temporary file in the same directory, which is synced, renamed +/// over `path`, and the directory is synced so the rename itself survives power loss. +/// The instance key is answered on chain right after this returns; it is the only copy. +fn write_file_durably(path: &Path, contents: &[u8]) -> anyhow::Result<()> { + let parent = path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .with_context(|| format!("envelope path has no directory: {}", path.display()))?; + fs::create_dir_all(parent) + .with_context(|| format!("create envelope directory failed: {}", parent.display()))?; + let file_name = path + .file_name() + .with_context(|| format!("envelope path has no file name: {}", path.display()))?; + let mut temp_name = file_name.to_os_string(); + temp_name.push(".tmp"); + let temp_path = parent.join(temp_name); + + let result = (|| { + let mut file = fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(&temp_path) + .with_context(|| format!("create envelope file failed: {}", temp_path.display()))?; + // `mode` only applies to a new file; a leftover temporary file keeps its old mode. + file.set_permissions(fs::Permissions::from_mode(0o600)) + .with_context(|| format!("set envelope permissions failed: {}", temp_path.display()))?; + file.write_all(contents) + .with_context(|| format!("write envelope failed: {}", temp_path.display()))?; + file.sync_all() + .with_context(|| format!("sync envelope failed: {}", temp_path.display()))?; + fs::rename(&temp_path, path) + .with_context(|| format!("rename envelope failed: {}", path.display()))?; + fs::File::open(parent) + .and_then(|directory| directory.sync_all()) + .with_context(|| format!("sync envelope directory failed: {}", parent.display())) + })(); + if result.is_err() { + let _ = fs::remove_file(&temp_path); + } + result +} + pub type OperatorAssertWotsSecretKey = OperatorAssertSecretKey; pub type OperatorAssertWotsPublicKey = OperatorAssertPublicKey; pub type OperatorAssertWotsKeypair = (OperatorAssertWotsSecretKey, OperatorAssertWotsPublicKey); @@ -248,18 +293,7 @@ impl CommitteeMasterKey { let serialized = serde_json::to_vec_pretty(&envelope) .context("serialize committee instance key envelope")?; - if let Some(parent) = envelope_path.parent() { - fs::create_dir_all(parent).with_context(|| { - format!("create envelope directory failed: {}", parent.display()) - })?; - } - let mut file = fs::File::create(envelope_path) - .with_context(|| format!("create envelope file failed: {}", envelope_path.display()))?; - file.write_all(&serialized) - .with_context(|| format!("write envelope failed: {}", envelope_path.display()))?; - fs::set_permissions(envelope_path, fs::Permissions::from_mode(0o600)).with_context( - || format!("set envelope permissions failed: {}", envelope_path.display()), - )?; + write_file_durably(envelope_path, &serialized)?; Ok(committee_pubkey) } @@ -372,6 +406,13 @@ impl CommitteeMasterKey { Ok(self.nonces_for_graph_context_with_keypair(context, signer_keypair)) } + /// The nonces are derived from the key and the signed context, so the node needs no + /// nonce state of its own. That is safe only while the record of what it signed is never + /// rolled back: a MuSig2 partial signature also depends on the other signers' nonces, so + /// after a rollback a co-signer can answer the same context with a new nonce, and this node + /// would sign again with the same secret nonce. Two partial signatures under one nonce reveal + /// the key. Keep the database and the key envelopes on storage that is not restored from + /// older snapshots. fn nonces_for_graph_context_with_keypair( &self, context: CommitteeGraphNonceContext, @@ -531,6 +572,29 @@ mod tests { assert!(!envelope_path.exists(), "envelope file should be removed"); } + #[test] + fn committee_instance_key_envelope_is_written_whole_with_owner_only_access() { + let instance_id = Uuid::new_v4(); + let master = CommitteeMasterKey::new(test_master_keypair("seed:test-committee-master")); + let directory = std::env::temp_dir().join(format!("bitvm-gc-envelope-dir-{instance_id}")); + let envelope_path = directory.join("nested").join(format!("{instance_id}.json")); + let temp_path = envelope_path.with_file_name(format!("{instance_id}.json.tmp")); + // A temporary file left by an interrupted write is replaced, not trusted. + fs::create_dir_all(temp_path.parent().unwrap()).unwrap(); + fs::write(&temp_path, b"partial").unwrap(); + fs::set_permissions(&temp_path, fs::Permissions::from_mode(0o644)).unwrap(); + + let created = master.create_instance_keypair_envelope(instance_id, &envelope_path).unwrap(); + assert!(!temp_path.exists(), "the temporary file is renamed into place"); + let mode = fs::metadata(&envelope_path).unwrap().permissions().mode() & 0o777; + assert_eq!(mode, 0o600); + let loaded: PublicKey = + master.load_instance_keypair(instance_id, &envelope_path).unwrap().public_key().into(); + assert_eq!(created, loaded); + + fs::remove_dir_all(&directory).unwrap(); + } + #[test] fn committee_instance_key_envelope_load_mismatch_instance_id_should_fail() { let instance_id = Uuid::new_v4(); diff --git a/deployment/README.md b/deployment/README.md index 779ad15b8..f71b437c1 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -22,6 +22,10 @@ cargo build -r -p bitvm-noded --bin bitvm-noded Bitcoin and GOAT accounts. 3. Choose the proof service and peer network shared by the participants. 4. Give each node isolated persistent state and distinct listening addresses. + A Committee node keeps its instance keys in `cache/committee-instance-keys` + next to its database file, or in `COMMITTEE_INSTANCE_KEYS_DIR`. Keep that + directory on the same persistent storage as the database, and never restore + either from an older snapshot. 5. Start the nodes and confirm they are ready before initiating bridge activity. All participants must agree on the graph parameters, Verifier slot count, and diff --git a/node/src/bin/send_pegin_request.rs b/node/src/bin/send_pegin_request.rs index 641e1c993..b6c0f9759 100644 --- a/node/src/bin/send_pegin_request.rs +++ b/node/src/bin/send_pegin_request.rs @@ -21,7 +21,7 @@ use alloy::primitives::Address as EvmAddress; use alloy::providers::{Provider, ProviderBuilder}; -use anyhow::{Result, anyhow}; +use anyhow::{Result, anyhow, bail}; use bitcoin::hashes::Hash; use bitcoin::sighash::EcdsaSighashType; use bitcoin::{Address, Amount, key::Keypair}; @@ -43,7 +43,7 @@ use tracing_subscriber::EnvFilter; use bitvm_noded::env::{ ENV_BITCOIN_NETWORK, ENV_BITVM_SECRET, ENV_GOAT_ADDRESS, get_goat_network, - get_node_goat_address, goat_config_from_env, + get_node_goat_address, goat_config_from_env, parse_bitcoin_network, }; use bitvm_noded::utils::{ broadcast_tx, get_fee_rate, get_proper_utxo_set, node_p2wsh_address, node_sign, @@ -148,20 +148,13 @@ async fn main() -> Result<()> { let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); tracing_subscriber::fmt().with_env_filter(filter).init(); let args = Args::parse(); - let network = args.network.as_str(); - let network = match network { - "bitcoin" => Network::Bitcoin, - "testnet" => Network::Testnet, - "testnet4" => Network::Testnet4, - "signet" => Network::Signet, - "regtest" => Network::Regtest, - _ => { - tracing::warn!( - "Unknown BTC network: {network}, expect bitcoin, testnet, signet or regtest, return testnet by default" - ); - Network::Testnet4 - } - }; + let network = parse_bitcoin_network(&args.network)?; + // Addresses are also checked by node code that reads BITCOIN_NETWORK; both must agree. + if let Ok(configured) = std::env::var(ENV_BITCOIN_NETWORK) + && parse_bitcoin_network(&configured)? != network + { + bail!("--network {} disagrees with {ENV_BITCOIN_NETWORK}={configured}", args.network); + } let btc_client = BTCClient::new(network, Some(&args.esplora_url)); let goat_client = { diff --git a/node/src/env.rs b/node/src/env.rs index 4dc0b095a..65484fec0 100644 --- a/node/src/env.rs +++ b/node/src/env.rs @@ -3,6 +3,7 @@ use alloy::primitives::Address as EvmAddress; use alloy::primitives::Address; use alloy::providers::{Provider, ProviderBuilder}; use alloy::signers::local::PrivateKeySigner; +use anyhow::{Context, Result, bail}; use base64::Engine; use bitcoin::{Network, PublicKey, key::Keypair}; use bitvm_lib::actors::Actor; @@ -18,8 +19,9 @@ use client::timeout_config::get_goat_rpc_timeout_secs; use libp2p::PeerId; use reqwest::Url; use sha2::{Digest, Sha256}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::str::FromStr; +use std::sync::OnceLock; use std::time::SystemTime; use strum::{Display, EnumString}; use tracing::{info, warn}; @@ -67,7 +69,10 @@ pub const ENV_VERIFIER_CANDIDATE_BACKUP_COUNT: &str = "VERIFIER_CANDIDATE_BACKUP pub const ENV_VERIFIER_CANDIDATE_COLLECTION_WINDOW_SECS: &str = "VERIFIER_CANDIDATE_COLLECTION_WINDOW_SECS"; pub const ENV_EXTERNAL_SOCKET_ADDR: &str = "EXTERNAL_SOCKET_ADDR"; -pub const COMMITTEE_INSTANCE_KEYS_DIR: &str = "cache/committee-instance-keys/"; +/// Directory of the committee instance key envelopes. Defaults to +/// `/cache/committee-instance-keys`. +pub const ENV_COMMITTEE_INSTANCE_KEYS_DIR: &str = "COMMITTEE_INSTANCE_KEYS_DIR"; +const COMMITTEE_INSTANCE_KEYS_SUBDIR: &str = "cache/committee-instance-keys"; pub const DUST_AMOUNT: u64 = goat::transactions::base::DUST_AMOUNT; pub const MAX_CUSTOM_INPUTS: usize = 100; @@ -160,20 +165,104 @@ pub const ENV_COMMITTEE_INSTANCE_KEY_DELETE_TIMELOCK_BLOCKS: &str = "COMMITTEE_INSTANCE_KEY_DELETE_TIMELOCK_BLOCKS"; pub const DEFAULT_COMMITTEE_INSTANCE_KEY_DELETE_TIMELOCK_BLOCKS: i64 = 32; +static COMMITTEE_INSTANCE_KEYS_PATH: OnceLock = OnceLock::new(); +static DISCOVERED_EXTERNAL_SOCKET_ADDR: OnceLock = OnceLock::new(); + +/// Records the public RPC address found at startup; `EXTERNAL_SOCKET_ADDR` still wins. +pub fn set_discovered_external_socket_addr(addr: std::net::SocketAddr) { + let _ = DISCOVERED_EXTERNAL_SOCKET_ADDR.set(addr); +} + +/// The directory holding the file of a sqlite database URL or path, as an absolute path. +/// `None` for an in-memory database. +fn sqlite_database_dir(db_path: &str) -> Option { + let path = db_path + .strip_prefix("sqlite://") + .or_else(|| db_path.strip_prefix("sqlite:")) + .unwrap_or(db_path); + let path = path.split('?').next().unwrap_or_default(); + if path.is_empty() || path == ":memory:" { + return None; + } + let file = std::path::absolute(path).ok()?; + file.parent().map(Path::to_path_buf) +} + +fn resolve_committee_instance_keys_dir( + configured: Option, + db_path: Option<&str>, +) -> Result { + if let Some(dir) = configured + && !dir.trim().is_empty() + { + return std::path::absolute(dir.trim()) + .with_context(|| format!("invalid {ENV_COMMITTEE_INSTANCE_KEYS_DIR} {dir}")); + } + let base = match db_path { + Some(db_path) => sqlite_database_dir(db_path).with_context(|| { + format!( + "the database {db_path} has no directory; set {ENV_COMMITTEE_INSTANCE_KEYS_DIR}" + ) + })?, + None => std::env::current_dir().context("read the current directory")?, + }; + Ok(base.join(COMMITTEE_INSTANCE_KEYS_SUBDIR)) +} + +/// Fixes the committee instance key directory for this process: `COMMITTEE_INSTANCE_KEYS_DIR`, +/// or next to the database. A committee node also checks that it can write there, since it +/// answers a peg-in only after its key is on disk. +pub fn init_committee_instance_keys_dir(db_path: &str, actor: &Actor) -> Result { + let dir = resolve_committee_instance_keys_dir( + std::env::var(ENV_COMMITTEE_INSTANCE_KEYS_DIR).ok(), + Some(db_path), + )?; + if *actor == Actor::Committee { + check_writable_dir(&dir)?; + } + let dir = COMMITTEE_INSTANCE_KEYS_PATH.get_or_init(|| dir).clone(); + info!(path = %dir.display(), "committee instance key directory"); + Ok(dir) +} + +fn check_writable_dir(dir: &Path) -> Result<()> { + std::fs::create_dir_all(dir) + .with_context(|| format!("create key directory {}", dir.display()))?; + let probe = dir.join(".write-check"); + std::fs::write(&probe, b"") + .and_then(|()| std::fs::remove_file(&probe)) + .with_context(|| format!("key directory {} is not writable", dir.display())) +} + +/// The committee instance key directory fixed at startup. Tools that never call +/// [`init_committee_instance_keys_dir`] get the environment value or the current directory. +pub fn committee_instance_keys_dir() -> &'static Path { + COMMITTEE_INSTANCE_KEYS_PATH.get_or_init(|| { + resolve_committee_instance_keys_dir( + std::env::var(ENV_COMMITTEE_INSTANCE_KEYS_DIR).ok(), + None, + ) + .expect("resolve the committee instance key directory") + }) +} + +/// Parses a Bitcoin network name accepted by the node. +pub fn parse_bitcoin_network(network: &str) -> Result { + match network { + "bitcoin" => Ok(Network::Bitcoin), + "testnet4" => Ok(Network::Testnet4), + "signet" => Ok(Network::Signet), + "regtest" => Ok(Network::Regtest), + _ => bail!( + "unknown Bitcoin network {network:?}: expected bitcoin, testnet4, signet or regtest" + ), + } +} + +/// The configured Bitcoin network. An unknown name is a configuration error, not testnet4. pub fn get_network() -> Network { let network = std::env::var(ENV_BITCOIN_NETWORK).unwrap_or("testnet4".to_string()); - match network.as_str() { - "bitcoin" => Network::Bitcoin, - "testnet4" => Network::Testnet4, - "signet" => Network::Signet, - "regtest" => Network::Regtest, - _ => { - warn!( - "Unknown BTC network: {network}, expect bitcoin, testnet4, signet or regtest, return testnet by default" - ); - Network::Testnet4 - } - } + parse_bitcoin_network(&network).unwrap_or_else(|error| panic!("{ENV_BITCOIN_NETWORK}: {error}")) } pub const ENV_PEGIN_ABANDON_BUFFER_BLOCKS: &str = "PEGIN_ABANDON_BUFFER_BLOCKS"; @@ -199,16 +288,30 @@ pub fn get_operator_graph_start_cutoff_blocks() -> i64 { .unwrap_or_else(|| i64::from(default_connector_z_timelock_blocks(get_network())) * 2 / 3) } +/// The configured GOAT network. An unknown name is a configuration error, not test. pub fn get_goat_network() -> GoatNetwork { let network = std::env::var(ENV_GOAT_NETWORK).unwrap_or("test".to_string()); match network.as_str() { "main" => GoatNetwork::Main, "test" => GoatNetwork::Test, - _ => { - warn!("Unknown GOAT network: {network}, expect main, or test, return test by default"); - GoatNetwork::Test - } + _ => panic!("{ENV_GOAT_NETWORK}: unknown GOAT network {network:?}: expected main or test"), + } +} + +/// Reads both network settings once, so a bad name stops the node before it starts. +pub fn validate_network_env() -> Result<()> { + validate_network_names( + &std::env::var(ENV_BITCOIN_NETWORK).unwrap_or("testnet4".to_string()), + &std::env::var(ENV_GOAT_NETWORK).unwrap_or("test".to_string()), + ) +} + +fn validate_network_names(bitcoin: &str, goat: &str) -> Result<()> { + parse_bitcoin_network(bitcoin).with_context(|| ENV_BITCOIN_NETWORK)?; + if !matches!(goat, "main" | "test") { + bail!("{ENV_GOAT_NETWORK}: unknown GOAT network {goat:?}: expected main or test"); } + Ok(()) } /// Get the entropy of WOTS keys for current node @@ -302,52 +405,44 @@ pub fn get_node_goat_address() -> Option { } } -pub async fn check_node_info() { - if [Actor::Committee.to_string()].contains(&get_actor().to_string()) - && std::env::var(ENV_GOAT_PRIVATE_KEY).is_err() - { - panic!("Relayer and Committee must set goat secret key"); +/// Checks the node's identity settings and, for a committee member, its registration on Goat. +/// Goat reads are bounded and retried like the other boot reads. +pub async fn check_node_info() -> Result<()> { + if get_actor() == Actor::Committee && std::env::var(ENV_GOAT_PRIVATE_KEY).is_err() { + bail!("Relayer and Committee must set goat secret key"); } let node_info = get_local_node_info(); if [Actor::Operator.to_string(), Actor::Verifier.to_string()].contains(&node_info.actor) && node_info.goat_addr.is_empty() { - panic!("Operator and Verifier must set goat address or goat secret key"); + bail!("Operator and Verifier must set goat address or goat secret key"); } - if Actor::Committee.to_string() == node_info.actor - || Actor::Operator.to_string() == node_info.actor - { - let rpc_url = get_goat_url_from_env(); - let gateway_address = get_goat_gateway_contract_from_env(); - let provider = ProviderBuilder::new().connect_http(rpc_url); - let committee_management_address = + if node_info.actor != Actor::Committee.to_string() { + return Ok(()); + } + let rpc_url = get_goat_url_from_env(); + let gateway_address = get_goat_gateway_contract_from_env(); + let provider = ProviderBuilder::new().connect_http(rpc_url.clone()); + let committee_management_address = + retry_at_boot("committee management address", &rpc_url, || { get_committee_management_contract(&provider, gateway_address) - .await - .expect("fail to get committee manager address"); - let peer_id = PeerId::from_str(&node_info.peer_id).expect("fail to decode"); - - if node_info.actor == Actor::Committee.to_string() { - match is_validate_committee( - &provider, - committee_management_address, - &peer_id.to_bytes(), - ) - .await - { - Ok(is_legal) => { - if is_legal { - info!("Committee is legal!"); - } else { - panic!("Committee is illegal as not finish register! ") - } - } - Err(err) => { - panic!("Committee validate failed, err:{err:?}") - } - } - } + }) + .await + .map_err(|error| anyhow::anyhow!("get committee management address: {error}"))?; + let peer_id = + PeerId::from_str(&node_info.peer_id).context("decode the node's peer id")?.to_bytes(); + let registered = retry_at_boot("committee registration", &rpc_url, || { + is_validate_committee(&provider, committee_management_address, &peer_id) + }) + .await + .map_err(|error| anyhow::anyhow!("check committee registration: {error}"))?; + if !registered { + bail!("Committee is not registered on Goat"); } + info!("Committee is registered on Goat"); + Ok(()) } + pub fn get_local_node_info() -> NodeInfo { let actor = get_actor(); let peer_key = get_peer_id(); @@ -366,7 +461,10 @@ pub fn get_local_node_info() -> NodeInfo { addr_op }; - let socket_addr = std::env::var(ENV_EXTERNAL_SOCKET_ADDR).unwrap_or("".to_string()); + let socket_addr = std::env::var(ENV_EXTERNAL_SOCKET_ADDR) + .ok() + .or_else(|| DISCOVERED_EXTERNAL_SOCKET_ADDR.get().map(ToString::to_string)) + .unwrap_or_default(); // Sign this node's peer ID binding with its master key. let issued_at = SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -985,4 +1083,51 @@ mod tests { unsafe { std::env::remove_var(ENV_SOLDERING_PROOF_PAYLOAD_STORE_PATH) }; } + + #[test] + fn key_directory_follows_the_database_or_the_environment() { + let cwd = std::env::current_dir().unwrap(); + for (db_path, base) in [ + ("sqlite:/var/data/bitvm-node.db", PathBuf::from("/var/data")), + ("sqlite:///var/data/bitvm-node.db?mode=rwc", PathBuf::from("/var/data")), + ("/var/data/bitvm-node-0.db", PathBuf::from("/var/data")), + ("sqlite:bitvm-node.db", cwd.clone()), + ("sqlite:nodes/a/bitvm-node.db", cwd.join("nodes/a")), + ] { + assert_eq!( + resolve_committee_instance_keys_dir(None, Some(db_path)).unwrap(), + base.join(COMMITTEE_INSTANCE_KEYS_SUBDIR), + "{db_path}" + ); + } + // An in-memory database has no directory to put keys next to. + assert!(resolve_committee_instance_keys_dir(None, Some("sqlite::memory:")).is_err()); + // The environment wins, made absolute; a blank value is ignored. + assert_eq!( + resolve_committee_instance_keys_dir(Some("keys".into()), Some("sqlite::memory:")) + .unwrap(), + cwd.join("keys") + ); + assert_eq!( + resolve_committee_instance_keys_dir(Some(" ".into()), Some("sqlite:/d/x.db")).unwrap(), + PathBuf::from("/d").join(COMMITTEE_INSTANCE_KEYS_SUBDIR) + ); + + let dir = std::env::temp_dir().join(format!("bitvm-key-dir-{}", std::process::id())); + check_writable_dir(&dir.join("nested")).unwrap(); + assert!(dir.join("nested").is_dir()); + std::fs::remove_dir_all(&dir).unwrap(); + } + + #[test] + fn unknown_network_names_are_rejected() { + assert_eq!(parse_bitcoin_network("testnet4").unwrap(), Network::Testnet4); + assert_eq!(parse_bitcoin_network("regtest").unwrap(), Network::Regtest); + for name in ["testnet", "Testnet4", "mainnet", ""] { + assert!(parse_bitcoin_network(name).is_err(), "{name}"); + } + assert!(validate_network_names("regtest", "main").is_ok()); + assert!(validate_network_names("regtest", "mainnet").is_err()); + assert!(validate_network_names("testnet", "test").is_err()); + } } diff --git a/node/src/main.rs b/node/src/main.rs index 0b0f1cc12..90510b223 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -113,6 +113,7 @@ enum KeyCommands { #[tokio::main] async fn main() -> Result<(), Box> { dotenv::dotenv().ok(); + env::validate_network_env()?; let actor = env::get_actor(); let opt = Opts::parse(); if let Some(Commands::Key(key_arg)) = opt.cmd { @@ -197,6 +198,8 @@ async fn main() -> Result<(), Box> { version = env!("CARGO_PKG_VERSION"), ); let _node_span_guard = node_span.enter(); + // Fixed before any job can read or write a committee instance key. + env::init_committee_instance_keys_dir(&opt.db_path, &actor)?; let local_db = store::create_local_db_with_busy_timeout(&opt.db_path, engine_bridge::db_busy_timeout()) .await; @@ -292,7 +295,7 @@ async fn main() -> Result<(), Box> { set_node_external_socket_addr_env(&opt.rpc_addr).await?; } // validate node info - check_node_info().await; + check_node_info().await?; save_local_info(&local_db).await; metrics_state.mark_startup_ready(); diff --git a/node/src/utils/committee_keys.rs b/node/src/utils/committee_keys.rs index 1d1d646ea..33d182fed 100644 --- a/node/src/utils/committee_keys.rs +++ b/node/src/utils/committee_keys.rs @@ -12,9 +12,7 @@ use uuid::Uuid; /// Path of the committee instance key envelope for `instance_id`. pub fn committee_instance_keys_envelope_path(instance_id: Uuid) -> PathBuf { - let mut envelope_path = PathBuf::from(COMMITTEE_INSTANCE_KEYS_DIR); - envelope_path.push(format!("{instance_id}.json")); - envelope_path + committee_instance_keys_dir().join(format!("{instance_id}.json")) } /// Whether this node signs for an instance: it holds the instance key diff --git a/node/src/utils/node_info.rs b/node/src/utils/node_info.rs index 68ade44b8..11ce8c60c 100644 --- a/node/src/utils/node_info.rs +++ b/node/src/utils/node_info.rs @@ -157,6 +157,9 @@ pub async fn detect_heart_beat(swarm: &mut Swarm) -> Result<()> { /// Retrieve the server's public IP via NAT protocol and combine it with /// the configured RPC monitoring port`rpc_addr` to generate the external RPC service address. +/// Discovers this operator's public RPC address with STUN when neither the proof server URL nor +/// `EXTERNAL_SOCKET_ADDR` is configured. The result is kept for [`get_local_node_info`]; the +/// environment is not modified, since other threads are already running. pub async fn set_node_external_socket_addr_env(rpc_addr: &str) -> Result<()> { if get_proof_server_url().is_some() || std::env::var(ENV_EXTERNAL_SOCKET_ADDR).is_ok() { // not provide proof server @@ -178,12 +181,7 @@ pub async fn set_node_external_socket_addr_env(rpc_addr: &str) -> Result<()> { return Ok(()); } if let Some(socket_addr) = Attribute::get_xor_mapped_address(&message) { - unsafe { - std::env::set_var( - ENV_EXTERNAL_SOCKET_ADDR, - SocketAddr::new(socket_addr.ip(), addr.port()).to_string(), - ); - } + set_discovered_external_socket_addr(SocketAddr::new(socket_addr.ip(), addr.port())); } Ok(()) }