diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index 3f976a547..f5529b72b 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -1,61 +1,28 @@ use crate::{ config::{ - generate_and_write_backup_encryption_key_to_disk, load_config_file, BlockArgs, CKDConfig, - ConfigFile, ForeignChainsConfig, IndexerConfig, KeygenConfig, PersistentSecrets, - PresignatureConfig, RespondConfig, SecretsConfig, SignatureConfig, SyncMode, TripleConfig, - }, - coordinator::Coordinator, - db::SecretDB, - indexer::{ - real::spawn_real_indexer, tx_sender::TransactionSender, IndexerAPI, ReadForeignChainPolicy, + load_config_file, BlockArgs, CKDConfig, ConfigFile, ForeignChainsConfig, GcpStartConfig, + IndexerConfig, KeygenConfig, PersistentSecrets, PresignatureConfig, SecretsStartConfig, + SignatureConfig, StartConfig, SyncMode, TeeAuthorityStartConfig, TeeStartConfig, + TripleConfig, }, keyshare::{ compat::legacy_ecdsa_key_from_keyshares, local::LocalPermanentKeyStorageBackend, permanent::{PermanentKeyStorage, PermanentKeyStorageBackend, PermanentKeyshareData}, - GcpPermanentKeyStorageConfig, KeyStorageConfig, KeyshareStorage, }, - migration_service::spawn_recovery_server_and_run_onboarding, p2p::testing::{generate_test_p2p_configs, PortSeed}, - profiler, - tracking::{self, start_root_task}, - web::{start_web_server, static_web_data, DebugRequest}, + run::run_mpc_node, }; -use anyhow::{anyhow, Context}; use clap::{Args, Parser, Subcommand, ValueEnum}; use hex::FromHex; -use mpc_attestation::report_data::ReportDataV1; -use mpc_contract::state::ProtocolContractState; use near_account_id::AccountId; use near_indexer_primitives::types::Finality; -use near_time::Clock; use std::{ - collections::BTreeMap, net::{Ipv4Addr, SocketAddr}, - sync::Mutex, -}; -use std::{path::PathBuf, sync::Arc, sync::OnceLock, time::Duration}; -use tee_authority::tee_authority::{ - DstackTeeAuthorityConfig, LocalTeeAuthorityConfig, TeeAuthority, DEFAULT_DSTACK_ENDPOINT, - DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL, + path::PathBuf, }; -use tokio::sync::{broadcast, mpsc, oneshot, watch, RwLock}; -use tokio_util::sync::CancellationToken; +use tee_authority::tee_authority::{DEFAULT_DSTACK_ENDPOINT, DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL}; use url::Url; - -use contract_interface::types::Ed25519PublicKey; -use { - crate::tee::{ - monitor_allowed_image_hashes, - remote_attestation::{monitor_attestation_removal, periodic_attestation_submission}, - AllowedImageHashesFile, - }, - mpc_contract::tee::proposal::MpcDockerImageHash, - tracing::info, -}; - -pub const ATTESTATION_RESUBMISSION_INTERVAL: Duration = Duration::from_secs(60 * 60); // 1 hour - #[derive(Parser, Debug)] #[command(name = "mpc-node")] #[command(about = "MPC Node for Near Protocol")] @@ -77,6 +44,13 @@ pub enum LogFormat { #[derive(Subcommand, Debug)] pub enum CliCommand { + /// Starts the MPC node using a single JSON configuration file instead of + /// environment variables and CLI flags. + StartWithConfigFile { + /// Path to a JSON configuration file containing all settings needed to + /// start the MPC node. + config_path: PathBuf, + }, Start(StartCmd), /// Generates/downloads required files for Near node to run Init(InitConfigArgs), @@ -108,38 +82,10 @@ pub enum CliCommand { migrating_nodes: Vec, }, } - -#[derive(Args, Debug)] -pub struct InitConfigArgs { - #[arg(long, env("MPC_HOME_DIR"))] - pub dir: std::path::PathBuf, - /// chain/network id (localnet, testnet, devnet, betanet) - #[arg(long)] - pub chain_id: Option, - /// Genesis file to use when initialize testnet (including downloading) - #[arg(long)] - pub genesis: Option, - /// Download the verified NEAR config file automatically. - #[arg(long)] - pub download_config: bool, - #[arg(long)] - pub download_config_url: Option, - /// Download the verified NEAR genesis file automatically. - #[arg(long)] - pub download_genesis: bool, - /// Specify a custom download URL for the genesis-file. - #[arg(long)] - pub download_genesis_url: Option, - #[arg(long)] - pub download_genesis_records_url: Option, - #[arg(long)] - pub boot_nodes: Option, -} - #[derive(Args, Debug)] pub struct StartCmd { #[arg(long, env("MPC_HOME_DIR"))] - pub home_dir: String, + pub home_dir: PathBuf, /// Hex-encoded 16 byte AES key for local storage encryption. /// This key should come from a secure secret storage. /// TODO(#444): After TEE integration decide on what to do with AES encryption key @@ -153,17 +99,17 @@ pub struct StartCmd { pub gcp_project_id: Option, /// TEE authority config #[command(subcommand)] - pub tee_authority: TeeAuthorityConfig, + pub tee_authority: CliTeeAuthorityConfig, /// TEE related configuration settings. #[command(flatten)] - pub image_hash_config: MpcImageHashConfig, + pub image_hash_config: CliImageHashConfig, /// Hex-encoded 32 byte AES key for backup encryption. #[arg(env("MPC_BACKUP_ENCRYPTION_KEY_HEX"))] pub backup_encryption_key_hex: Option, } #[derive(Subcommand, Debug, Clone)] -pub enum TeeAuthorityConfig { +pub enum CliTeeAuthorityConfig { Local, Dstack { #[arg(long, env("DSTACK_ENDPOINT"), default_value = DEFAULT_DSTACK_ENDPOINT)] @@ -173,24 +119,8 @@ pub enum TeeAuthorityConfig { }, } -impl TryFrom for TeeAuthority { - type Error = anyhow::Error; - - fn try_from(cmd: TeeAuthorityConfig) -> Result { - let authority_config = match cmd { - TeeAuthorityConfig::Local => LocalTeeAuthorityConfig::default().into(), - TeeAuthorityConfig::Dstack { - dstack_endpoint, - quote_upload_url, - } => DstackTeeAuthorityConfig::new(dstack_endpoint, quote_upload_url).into(), - }; - - Ok(authority_config) - } -} - #[derive(Args, Debug)] -pub struct MpcImageHashConfig { +pub struct CliImageHashConfig { #[arg( long, env("MPC_IMAGE_HASH"), @@ -205,6 +135,66 @@ pub struct MpcImageHashConfig { pub latest_allowed_hash_file: Option, } +impl StartCmd { + fn into_start_config(self, config: ConfigFile) -> StartConfig { + let gcp = match (self.gcp_keyshare_secret_id, self.gcp_project_id) { + (Some(keyshare_secret_id), Some(project_id)) => Some(GcpStartConfig { + keyshare_secret_id, + project_id, + }), + _ => None, + }; + StartConfig { + home_dir: self.home_dir, + secrets: SecretsStartConfig { + secret_store_key_hex: self.secret_store_key_hex, + backup_encryption_key_hex: self.backup_encryption_key_hex, + }, + tee: TeeStartConfig { + authority: match self.tee_authority { + CliTeeAuthorityConfig::Local => TeeAuthorityStartConfig::Local, + CliTeeAuthorityConfig::Dstack { + dstack_endpoint, + quote_upload_url, + } => TeeAuthorityStartConfig::Dstack { + dstack_endpoint, + quote_upload_url: quote_upload_url.to_string(), + }, + }, + image_hash: self.image_hash_config.image_hash, + latest_allowed_hash_file: self.image_hash_config.latest_allowed_hash_file, + }, + gcp, + node: config, + } + } +} +#[derive(Args, Debug)] +pub struct InitConfigArgs { + #[arg(long, env("MPC_HOME_DIR"))] + pub dir: std::path::PathBuf, + /// chain/network id (localnet, testnet, devnet, betanet) + #[arg(long)] + pub chain_id: Option, + /// Genesis file to use when initialize testnet (including downloading) + #[arg(long)] + pub genesis: Option, + /// Download the verified NEAR config file automatically. + #[arg(long)] + pub download_config: bool, + #[arg(long)] + pub download_config_url: Option, + /// Download the verified NEAR genesis file automatically. + #[arg(long)] + pub download_genesis: bool, + /// Specify a custom download URL for the genesis-file. + #[arg(long)] + pub download_genesis_url: Option, + #[arg(long)] + pub download_genesis_records_url: Option, + #[arg(long)] + pub boot_nodes: Option, +} #[derive(Args, Debug)] pub struct ImportKeyshareCmd { /// Path to home directory @@ -232,293 +222,21 @@ pub struct ExportKeyshareCmd { #[arg(help = "Hex-encoded 16 byte AES key for local storage encryption")] pub local_encryption_key_hex: String, } - -impl StartCmd { - async fn run(self) -> anyhow::Result<()> { - let root_runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .worker_threads(1) - .build()?; - - let _tokio_enter_guard = root_runtime.enter(); - - // Load configuration and initialize persistent secrets - let home_dir = PathBuf::from(self.home_dir.clone()); - let config = load_config_file(&home_dir)?; - let persistent_secrets = PersistentSecrets::generate_or_get_existing( - &home_dir, - config.number_of_responder_keys, - )?; - - profiler::web_server::start_web_server(config.pprof_bind_address).await?; - root_runtime.spawn(crate::metrics::tokio_task_metrics::run_monitor_loop()); - - // TODO(#1296): Decide if the MPC responder account is actually needed - let respond_config = RespondConfig::from_parts(&config, &persistent_secrets); - - let backup_encryption_key_hex = match &self.backup_encryption_key_hex { - Some(key) => key.clone(), - None => generate_and_write_backup_encryption_key_to_disk(&home_dir)?, - }; - - // Load secrets from configuration and persistent storage - let secrets = SecretsConfig::from_parts( - &self.secret_store_key_hex, - persistent_secrets.clone(), - &backup_encryption_key_hex, - )?; - - // Generate attestation - let tee_authority = TeeAuthority::try_from(self.tee_authority.clone())?; - let tls_public_key = &secrets.persistent_secrets.p2p_private_key.verifying_key(); - - let account_public_key = &secrets.persistent_secrets.near_signer_key.verifying_key(); - - let report_data = ReportDataV1::new( - *Ed25519PublicKey::from(tls_public_key).as_bytes(), - *Ed25519PublicKey::from(account_public_key).as_bytes(), - ) - .into(); - - let attestation = tee_authority.generate_attestation(report_data).await?; - - // Create communication channels and runtime - let (debug_request_sender, _) = tokio::sync::broadcast::channel(10); - let root_task_handle = Arc::new(OnceLock::new()); - - let (protocol_state_sender, protocol_state_receiver) = - watch::channel(ProtocolContractState::NotInitialized); - - let (migration_state_sender, migration_state_receiver) = - watch::channel((0, BTreeMap::new())); - let web_server = root_runtime - .block_on(start_web_server( - root_task_handle.clone(), - debug_request_sender.clone(), - config.web_ui, - static_web_data(&secrets, Some(attestation)), - protocol_state_receiver, - migration_state_receiver, - )) - .context("Failed to create web server.")?; - - let _web_server_join_handle = root_runtime.spawn(web_server); - - // Create Indexer and wait for indexer to be synced. - let (indexer_exit_sender, indexer_exit_receiver) = oneshot::channel(); - let indexer_api = spawn_real_indexer( - home_dir.clone(), - config.indexer.clone(), - config.my_near_account_id.clone(), - persistent_secrets.near_signer_key.clone(), - respond_config, - indexer_exit_sender, - protocol_state_sender, - migration_state_sender, - *tls_public_key, - ); - - let (shutdown_signal_sender, mut shutdown_signal_receiver) = mpsc::channel(1); - let cancellation_token = CancellationToken::new(); - - let image_hash_watcher_handle = if let (Some(image_hash), Some(latest_allowed_hash_file)) = ( - &self.image_hash_config.image_hash, - &self.image_hash_config.latest_allowed_hash_file, - ) { - let current_image_hash_bytes: [u8; 32] = hex::decode(image_hash) - .expect("The currently running image is a hex string.") - .try_into() - .expect("The currently running image hash hex representation is 32 bytes."); - - let allowed_hashes_in_contract = indexer_api.allowed_docker_images_receiver.clone(); - let image_hash_storage = AllowedImageHashesFile::from(latest_allowed_hash_file.clone()); - - Some(root_runtime.spawn(monitor_allowed_image_hashes( - cancellation_token.child_token(), - MpcDockerImageHash::from(current_image_hash_bytes), - allowed_hashes_in_contract, - image_hash_storage, - shutdown_signal_sender.clone(), - ))) - } else { - tracing::info!( - "MPC_IMAGE_HASH and/or MPC_LATEST_ALLOWED_HASH_FILE not set, skipping TEE image hash monitoring" - ); - None - }; - - let root_future = self.create_root_future( - home_dir.clone(), - config.clone(), - secrets.clone(), - indexer_api, - debug_request_sender, - root_task_handle, - tee_authority, - ); - - let root_task = root_runtime.spawn(start_root_task("root", root_future).0); - - let exit_reason = tokio::select! { - root_task_result = root_task => { - root_task_result? - } - indexer_exit_response = indexer_exit_receiver => { - indexer_exit_response.context("Indexer thread dropped response channel.")? - } - Some(()) = shutdown_signal_receiver.recv() => { - Err(anyhow!("TEE allowed image hashes watcher is sending shutdown signal.")) - } - }; - - // Perform graceful shutdown - cancellation_token.cancel(); - - if let Some(handle) = image_hash_watcher_handle { - info!("Waiting for image hash watcher to gracefully exit."); - let exit_result = handle.await; - info!(?exit_result, "Image hash watcher exited."); - } - - exit_reason - } - - #[allow(clippy::too_many_arguments)] - async fn create_root_future( - self, - home_dir: PathBuf, - config: ConfigFile, - secrets: SecretsConfig, - indexer_api: IndexerAPI, - debug_request_sender: broadcast::Sender, - // Cloning a OnceLock returns a new cell, which is why we have to wrap it in an arc. - // Otherwise we would not write to the same cell/lock. - root_task_handle_once_lock: Arc>>, - tee_authority: TeeAuthority, - ) -> anyhow::Result<()> - where - TransactionSenderImpl: TransactionSender + 'static, - ForeignChainPolicyReader: ReadForeignChainPolicy + Clone + Send + Sync + 'static, - { - let root_task_handle = tracking::current_task(); - - root_task_handle_once_lock - .set(root_task_handle.clone()) - .map_err(|_| anyhow!("Root task handle was already set"))?; - - let tls_public_key = - Ed25519PublicKey::from(&secrets.persistent_secrets.p2p_private_key.verifying_key()); - let account_public_key = - Ed25519PublicKey::from(&secrets.persistent_secrets.near_signer_key.verifying_key()); - - let secret_db = SecretDB::new(&home_dir.join("assets"), secrets.local_storage_aes_key)?; - - let key_storage_config = KeyStorageConfig { - home_dir: home_dir.clone(), - local_encryption_key: secrets.local_storage_aes_key, - gcp: if let Some(secret_id) = self.gcp_keyshare_secret_id { - let project_id = self.gcp_project_id.ok_or_else(|| { - anyhow::anyhow!( - "GCP_PROJECT_ID must be specified to use GCP_KEYSHARE_SECRET_ID" - ) - })?; - Some(GcpPermanentKeyStorageConfig { - project_id, - secret_id, - }) - } else { - None - }, - }; - - // Spawn periodic attestation submission task - let tee_authority_clone = tee_authority.clone(); - let tx_sender_clone = indexer_api.txn_sender.clone(); - let tls_public_key_clone = tls_public_key.clone(); - let account_public_key_clone = account_public_key.clone(); - let allowed_docker_images_receiver_clone = - indexer_api.allowed_docker_images_receiver.clone(); - let allowed_launcher_compose_receiver_clone = - indexer_api.allowed_launcher_compose_receiver.clone(); - tokio::spawn(async move { - if let Err(e) = periodic_attestation_submission( - tee_authority_clone, - tx_sender_clone, - tls_public_key_clone, - account_public_key_clone, - allowed_docker_images_receiver_clone, - allowed_launcher_compose_receiver_clone, - tokio::time::interval(ATTESTATION_RESUBMISSION_INTERVAL), - ) - .await - { - tracing::error!( - error = ?e, - "periodic attestation submission task failed" - ); - } - }); - - // Spawn TEE attestation monitoring task - let tx_sender_clone = indexer_api.txn_sender.clone(); - let tee_accounts_receiver = indexer_api.attested_nodes_receiver.clone(); - let account_id_clone = config.my_near_account_id.clone(); - let allowed_docker_images_receiver_clone = - indexer_api.allowed_docker_images_receiver.clone(); - let allowed_launcher_compose_receiver_clone = - indexer_api.allowed_launcher_compose_receiver.clone(); - tokio::spawn(async move { - if let Err(e) = monitor_attestation_removal( - account_id_clone, - tee_authority, - tx_sender_clone, - tls_public_key, - account_public_key, - allowed_docker_images_receiver_clone, - allowed_launcher_compose_receiver_clone, - tee_accounts_receiver, - ) - .await - { - tracing::error!( - error = ?e, - "attestation removal monitoring task failed" - ); - } - }); - - let keyshare_storage: Arc> = - RwLock::new(key_storage_config.create().await?).into(); - - spawn_recovery_server_and_run_onboarding( - config.migration_web_ui, - (&secrets).into(), - config.my_near_account_id.clone(), - keyshare_storage.clone(), - indexer_api.my_migration_info_receiver.clone(), - indexer_api.contract_state_receiver.clone(), - indexer_api.txn_sender.clone(), - ) - .await?; - - let coordinator = Coordinator { - clock: Clock::real(), - config_file: config, - secrets, - secret_db, - keyshare_storage, - indexer: indexer_api, - currently_running_job_name: Arc::new(Mutex::new(String::new())), - debug_request_sender, - }; - coordinator.run().await - } -} - impl Cli { pub async fn run(self) -> anyhow::Result<()> { match self.command { - CliCommand::Start(start) => start.run().await, + CliCommand::StartWithConfigFile { config_path } => { + let node_configuration = StartConfig::from_json_file(&config_path)?; + run_mpc_node(node_configuration).await + } + // TODO(#2334): deprecate this + CliCommand::Start(start) => { + let home_dir = std::path::Path::new(&start.home_dir); + let config_file = load_config_file(home_dir)?; + + let node_configuration = start.into_start_config(config_file); + run_mpc_node(node_configuration).await + } CliCommand::Init(config) => { let (download_config_type, download_config_url) = if config.download_config { ( @@ -565,7 +283,7 @@ impl Cli { participants.len() == responders.len(), "Number of participants must match number of responders" ); - self.run_generate_test_configs( + run_generate_test_configs( output_dir, participants.clone(), responders.clone(), @@ -578,132 +296,7 @@ impl Cli { } } } - - fn duplicate_migrating_accounts( - mut accounts: Vec, - migrating_nodes: &[usize], - ) -> anyhow::Result> { - for migrating_node_idx in migrating_nodes { - let migrating_node_account: AccountId = accounts - .get(*migrating_node_idx) - .ok_or_else(|| { - anyhow::anyhow!("index {} out of bounds for accounts", migrating_node_idx) - })? - .clone(); - - accounts.push(migrating_node_account); - } - Ok(accounts) - } - - #[allow(clippy::too_many_arguments)] - fn run_generate_test_configs( - &self, - output_dir: &str, - participants: Vec, - responders: Vec, - threshold: usize, - desired_triples_to_buffer: usize, - desired_presignatures_to_buffer: usize, - desired_responder_keys_per_participant: usize, - migrating_nodes: &[usize], - ) -> anyhow::Result<()> { - let participants = Self::duplicate_migrating_accounts(participants, migrating_nodes)?; - let responders = Self::duplicate_migrating_accounts(responders, migrating_nodes)?; - - let p2p_key_pairs = participants - .iter() - .enumerate() - .map(|(idx, _account_id)| { - let subdir = PathBuf::from(output_dir).join(idx.to_string()); - PersistentSecrets::generate_or_get_existing( - &subdir, - desired_responder_keys_per_participant, - ) - .map(|secret| secret.p2p_private_key) - }) - .collect::, _>>()?; - let configs = generate_test_p2p_configs( - &participants, - threshold, - PortSeed::CLI_FOR_PYTEST, - Some(p2p_key_pairs), - )?; - let participants_config = configs[0].0.participants.clone(); - for (i, (_config, _p2p_private_key)) in configs.into_iter().enumerate() { - let subdir = format!("{}/{}", output_dir, i); - std::fs::create_dir_all(&subdir)?; - let file_config = self.create_file_config( - &participants[i], - &responders[i], - i, - desired_triples_to_buffer, - desired_presignatures_to_buffer, - )?; - std::fs::write( - format!("{}/config.yaml", subdir), - serde_yaml::to_string(&file_config)?, - )?; - } - std::fs::write( - format!("{}/participants.json", output_dir), - serde_json::to_string(&participants_config)?, - )?; - Ok(()) - } - - fn create_file_config( - &self, - participant: &AccountId, - responder: &AccountId, - index: usize, - desired_triples_to_buffer: usize, - desired_presignatures_to_buffer: usize, - ) -> anyhow::Result { - Ok(ConfigFile { - my_near_account_id: participant.clone(), - near_responder_account_id: responder.clone(), - number_of_responder_keys: 1, - web_ui: SocketAddr::new( - Ipv4Addr::LOCALHOST.into(), - PortSeed::CLI_FOR_PYTEST.web_port(index), - ), - migration_web_ui: SocketAddr::new( - Ipv4Addr::LOCALHOST.into(), - PortSeed::CLI_FOR_PYTEST.migration_web_port(index), - ), - pprof_bind_address: SocketAddr::new( - Ipv4Addr::LOCALHOST.into(), - PortSeed::CLI_FOR_PYTEST.pprof_web_port(index), - ), - indexer: IndexerConfig { - validate_genesis: true, - sync_mode: SyncMode::Block(BlockArgs { height: 0 }), - concurrency: 1.try_into().unwrap(), - mpc_contract_id: "test0".parse().unwrap(), - finality: Finality::None, - port_override: None, - }, - triple: TripleConfig { - concurrency: 2, - desired_triples_to_buffer, - timeout_sec: 60, - parallel_triple_generation_stagger_time_sec: 1, - }, - presignature: PresignatureConfig { - concurrency: 2, - desired_presignatures_to_buffer, - timeout_sec: 60, - }, - signature: SignatureConfig { timeout_sec: 60 }, - ckd: CKDConfig { timeout_sec: 60 }, - keygen: KeygenConfig { timeout_sec: 60 }, - foreign_chains: ForeignChainsConfig::default(), - cores: Some(4), - }) - } } - impl ImportKeyshareCmd { pub async fn run(&self) -> anyhow::Result<()> { let runtime = tokio::runtime::Runtime::new()?; @@ -800,7 +393,127 @@ impl ExportKeyshareCmd { }) } } +fn duplicate_migrating_accounts( + mut accounts: Vec, + migrating_nodes: &[usize], +) -> anyhow::Result> { + for migrating_node_idx in migrating_nodes { + let migrating_node_account: AccountId = accounts + .get(*migrating_node_idx) + .ok_or_else(|| { + anyhow::anyhow!("index {} out of bounds for accounts", migrating_node_idx) + })? + .clone(); + + accounts.push(migrating_node_account); + } + Ok(accounts) +} + +#[allow(clippy::too_many_arguments)] +fn run_generate_test_configs( + output_dir: &str, + participants: Vec, + responders: Vec, + threshold: usize, + desired_triples_to_buffer: usize, + desired_presignatures_to_buffer: usize, + desired_responder_keys_per_participant: usize, + migrating_nodes: &[usize], +) -> anyhow::Result<()> { + let participants = duplicate_migrating_accounts(participants, migrating_nodes)?; + let responders = duplicate_migrating_accounts(responders, migrating_nodes)?; + + let p2p_key_pairs = participants + .iter() + .enumerate() + .map(|(idx, _account_id)| { + let subdir = PathBuf::from(output_dir).join(idx.to_string()); + PersistentSecrets::generate_or_get_existing( + &subdir, + desired_responder_keys_per_participant, + ) + .map(|secret| secret.p2p_private_key) + }) + .collect::, _>>()?; + let configs = generate_test_p2p_configs( + &participants, + threshold, + PortSeed::CLI_FOR_PYTEST, + Some(p2p_key_pairs), + )?; + let participants_config = configs[0].0.participants.clone(); + for (i, (_config, _p2p_private_key)) in configs.into_iter().enumerate() { + let subdir = format!("{}/{}", output_dir, i); + std::fs::create_dir_all(&subdir)?; + let file_config = create_file_config( + &participants[i], + &responders[i], + i, + desired_triples_to_buffer, + desired_presignatures_to_buffer, + ); + std::fs::write( + format!("{}/mpc_node_config.json", subdir), + serde_json::to_string_pretty(&file_config)?, + )?; + } + std::fs::write( + format!("{}/participants.json", output_dir), + serde_json::to_string(&participants_config)?, + )?; + Ok(()) +} +fn create_file_config( + participant: &AccountId, + responder: &AccountId, + index: usize, + desired_triples_to_buffer: usize, + desired_presignatures_to_buffer: usize, +) -> ConfigFile { + ConfigFile { + my_near_account_id: participant.clone(), + near_responder_account_id: responder.clone(), + number_of_responder_keys: 1, + web_ui: SocketAddr::new( + Ipv4Addr::LOCALHOST.into(), + PortSeed::CLI_FOR_PYTEST.web_port(index), + ), + migration_web_ui: SocketAddr::new( + Ipv4Addr::LOCALHOST.into(), + PortSeed::CLI_FOR_PYTEST.migration_web_port(index), + ), + pprof_bind_address: SocketAddr::new( + Ipv4Addr::LOCALHOST.into(), + PortSeed::CLI_FOR_PYTEST.pprof_web_port(index), + ), + indexer: IndexerConfig { + validate_genesis: true, + sync_mode: SyncMode::Block(BlockArgs { height: 0 }), + concurrency: 1.try_into().unwrap(), + mpc_contract_id: "test0".parse().unwrap(), + finality: Finality::None, + port_override: None, + }, + triple: TripleConfig { + concurrency: 2, + desired_triples_to_buffer, + timeout_sec: 60, + parallel_triple_generation_stagger_time_sec: 1, + }, + presignature: PresignatureConfig { + concurrency: 2, + desired_presignatures_to_buffer, + timeout_sec: 60, + }, + signature: SignatureConfig { timeout_sec: 60 }, + ckd: CKDConfig { timeout_sec: 60 }, + keygen: KeygenConfig { timeout_sec: 60 }, + foreign_chains: ForeignChainsConfig::default(), + cores: Some(4), + } +} #[cfg(test)] mod tests { use super::*; diff --git a/crates/node/src/config.rs b/crates/node/src/config.rs index fdabd98bf..f48c9f922 100644 --- a/crates/node/src/config.rs +++ b/crates/node/src/config.rs @@ -13,6 +13,11 @@ use std::{ path::Path, }; +mod start; +pub use start::{ + GcpStartConfig, SecretsStartConfig, StartConfig, TeeAuthorityStartConfig, TeeStartConfig, +}; + mod foreign_chains; pub use foreign_chains::{ AbstractApiVariant, AbstractChainConfig, AbstractProviderConfig, AuthConfig, BitcoinApiVariant, diff --git a/crates/node/src/config/foreign_chains/auth.rs b/crates/node/src/config/foreign_chains/auth.rs index 5269b3130..3540fe837 100644 --- a/crates/node/src/config/foreign_chains/auth.rs +++ b/crates/node/src/config/foreign_chains/auth.rs @@ -52,6 +52,10 @@ pub enum TokenConfig { impl TokenConfig { pub fn resolve(&self) -> anyhow::Result { match self { + // TODO(#2335): do not resolve env variables this deep in the binary. + // Should be resolved at start, preferably in the config so we can kill env configs + // + // One option is to have a separate secrets config file. TokenConfig::Env { env } => { std::env::var(env).with_context(|| format!("environment variable {env} is not set")) } diff --git a/crates/node/src/config/start.rs b/crates/node/src/config/start.rs new file mode 100644 index 000000000..78aa8be3d --- /dev/null +++ b/crates/node/src/config/start.rs @@ -0,0 +1,113 @@ +use super::ConfigFile; +use anyhow::Context; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use tee_authority::tee_authority::{ + DstackTeeAuthorityConfig, LocalTeeAuthorityConfig, TeeAuthority, DEFAULT_DSTACK_ENDPOINT, + DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL, +}; +use url::Url; + +/// Configuration for starting the MPC node. This is the canonical type used +/// by the run logic. Both `StartCmd` (CLI flags) and `StartWithConfigFileCmd` +/// (JSON file) convert into this type. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StartConfig { + pub home_dir: PathBuf, + /// Encryption keys and backup settings. + pub secrets: SecretsStartConfig, + /// TEE authority and image hash monitoring settings. + pub tee: TeeStartConfig, + /// GCP keyshare storage settings. Optional — omit if not using GCP. + #[serde(default)] + pub gcp: Option, + /// Node configuration (indexer, protocol parameters, etc.). + pub node: ConfigFile, +} + +/// Encryption keys needed at startup. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SecretsStartConfig { + /// Hex-encoded 16 byte AES key for local storage encryption. + pub secret_store_key_hex: String, + /// Hex-encoded 32 byte AES key for backup encryption. + /// If not provided, a key is generated and written to disk. + #[serde(default)] + pub backup_encryption_key_hex: Option, +} + +/// TEE-related configuration. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TeeStartConfig { + /// TEE authority configuration. + pub authority: TeeAuthorityStartConfig, + /// Hex representation of the hash of the running image. Only required in TEE. + #[serde(default)] + pub image_hash: Option, + /// Path to the file where the node writes the latest allowed hash. + /// If not set, assumes running outside of TEE and skips image hash monitoring. + #[serde(default)] + pub latest_allowed_hash_file: Option, +} + +/// GCP keyshare storage configuration. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GcpStartConfig { + /// GCP secret ID for storing the root keyshare. + pub keyshare_secret_id: String, + /// GCP project ID. + pub project_id: String, +} + +/// TEE authority configuration for JSON deserialization. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum TeeAuthorityStartConfig { + Local, + Dstack { + #[serde(default = "default_dstack_endpoint")] + dstack_endpoint: String, + #[serde(default = "default_quote_upload_url")] + // TODO(#2333): use URL type for this type + quote_upload_url: String, + }, +} + +fn default_dstack_endpoint() -> String { + DEFAULT_DSTACK_ENDPOINT.to_string() +} + +fn default_quote_upload_url() -> String { + DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL.to_string() +} + +impl TeeAuthorityStartConfig { + pub fn into_tee_authority(self) -> anyhow::Result { + Ok(match self { + TeeAuthorityStartConfig::Local => LocalTeeAuthorityConfig::default().into(), + TeeAuthorityStartConfig::Dstack { + dstack_endpoint, + quote_upload_url, + } => { + let url: Url = quote_upload_url + .parse() + .context("invalid quote_upload_url")?; + DstackTeeAuthorityConfig::new(dstack_endpoint, url).into() + } + }) + } +} + +impl StartConfig { + pub fn from_json_file(path: &std::path::Path) -> anyhow::Result { + let content = std::fs::read_to_string(path) + .with_context(|| format!("failed to read config file: {}", path.display()))?; + let config: Self = serde_json::from_str(&content) + .with_context(|| format!("failed to parse config file: {}", path.display()))?; + config + .node + .validate() + .context("invalid node config in config file")?; + Ok(config) + } +} diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index 71778f6fe..dffd498fe 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -39,6 +39,7 @@ mod protocol; mod protocol_version; mod providers; pub mod requests; +mod run; mod runtime; mod storage; pub mod tracing; diff --git a/crates/node/src/run.rs b/crates/node/src/run.rs new file mode 100644 index 000000000..93b9c99fe --- /dev/null +++ b/crates/node/src/run.rs @@ -0,0 +1,308 @@ +use crate::{ + config::{ + generate_and_write_backup_encryption_key_to_disk, ConfigFile, PersistentSecrets, + RespondConfig, SecretsConfig, StartConfig, + }, + coordinator::Coordinator, + db::SecretDB, + indexer::{ + real::spawn_real_indexer, tx_sender::TransactionSender, IndexerAPI, ReadForeignChainPolicy, + }, + keyshare::{GcpPermanentKeyStorageConfig, KeyStorageConfig, KeyshareStorage}, + migration_service::spawn_recovery_server_and_run_onboarding, + profiler, + tracking::{self, start_root_task}, + web::{start_web_server, static_web_data, DebugRequest}, +}; +use anyhow::{anyhow, Context}; +use contract_interface::types::Ed25519PublicKey; +use mpc_attestation::report_data::ReportDataV1; +use mpc_contract::state::ProtocolContractState; +use mpc_contract::tee::proposal::MpcDockerImageHash; +use near_time::Clock; +use std::{ + collections::BTreeMap, + path::PathBuf, + sync::{Arc, Mutex, OnceLock}, + time::Duration, +}; +use tee_authority::tee_authority::TeeAuthority; +use tokio::sync::{broadcast, mpsc, oneshot, watch, RwLock}; +use tokio_util::sync::CancellationToken; +use tracing::info; + +use crate::tee::{ + monitor_allowed_image_hashes, + remote_attestation::{monitor_attestation_removal, periodic_attestation_submission}, + AllowedImageHashesFile, +}; + +pub const ATTESTATION_RESUBMISSION_INTERVAL: Duration = Duration::from_secs(60 * 60); // 1 hour + +pub async fn run_mpc_node(config: StartConfig) -> anyhow::Result<()> { + let root_runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .worker_threads(1) + .build()?; + + let _tokio_enter_guard = root_runtime.enter(); + + // Load configuration and initialize persistent secrets + let node_config = config.node.clone(); + let persistent_secrets = PersistentSecrets::generate_or_get_existing( + &config.home_dir, + node_config.number_of_responder_keys, + )?; + + profiler::web_server::start_web_server(node_config.pprof_bind_address).await?; + root_runtime.spawn(crate::metrics::tokio_task_metrics::run_monitor_loop()); + + // TODO(#1296): Decide if the MPC responder account is actually needed + let respond_config = RespondConfig::from_parts(&node_config, &persistent_secrets); + + let backup_encryption_key_hex = match &config.secrets.backup_encryption_key_hex { + Some(key) => key.clone(), + None => generate_and_write_backup_encryption_key_to_disk(&config.home_dir)?, + }; + + // Load secrets from configuration and persistent storage + let secrets = SecretsConfig::from_parts( + &config.secrets.secret_store_key_hex, + persistent_secrets.clone(), + &backup_encryption_key_hex, + )?; + + // Generate attestation + let tee_authority = config.tee.authority.clone().into_tee_authority()?; + let tls_public_key = &secrets.persistent_secrets.p2p_private_key.verifying_key(); + + let account_public_key = &secrets.persistent_secrets.near_signer_key.verifying_key(); + + let report_data = ReportDataV1::new( + *Ed25519PublicKey::from(tls_public_key).as_bytes(), + *Ed25519PublicKey::from(account_public_key).as_bytes(), + ) + .into(); + + let attestation = tee_authority.generate_attestation(report_data).await?; + + // Create communication channels and runtime + let (debug_request_sender, _) = tokio::sync::broadcast::channel(10); + let root_task_handle = Arc::new(OnceLock::new()); + + let (protocol_state_sender, protocol_state_receiver) = + watch::channel(ProtocolContractState::NotInitialized); + + let (migration_state_sender, migration_state_receiver) = watch::channel((0, BTreeMap::new())); + let web_server = root_runtime + .block_on(start_web_server( + root_task_handle.clone(), + debug_request_sender.clone(), + node_config.web_ui, + static_web_data(&secrets, Some(attestation)), + protocol_state_receiver, + migration_state_receiver, + )) + .context("Failed to create web server.")?; + + let _web_server_join_handle = root_runtime.spawn(web_server); + + // Create Indexer and wait for indexer to be synced. + let (indexer_exit_sender, indexer_exit_receiver) = oneshot::channel(); + let indexer_api = spawn_real_indexer( + config.home_dir.clone(), + node_config.indexer.clone(), + node_config.my_near_account_id.clone(), + persistent_secrets.near_signer_key.clone(), + respond_config, + indexer_exit_sender, + protocol_state_sender, + migration_state_sender, + *tls_public_key, + ); + + let (shutdown_signal_sender, mut shutdown_signal_receiver) = mpsc::channel(1); + let cancellation_token = CancellationToken::new(); + + let image_hash_watcher_handle = if let (Some(image_hash), Some(latest_allowed_hash_file)) = + (&config.tee.image_hash, &config.tee.latest_allowed_hash_file) + { + let current_image_hash_bytes: [u8; 32] = hex::decode(image_hash) + .expect("The currently running image is a hex string.") + .try_into() + .expect("The currently running image hash hex representation is 32 bytes."); + + let allowed_hashes_in_contract = indexer_api.allowed_docker_images_receiver.clone(); + let image_hash_storage = AllowedImageHashesFile::from(latest_allowed_hash_file.clone()); + + Some(root_runtime.spawn(monitor_allowed_image_hashes( + cancellation_token.child_token(), + MpcDockerImageHash::from(current_image_hash_bytes), + allowed_hashes_in_contract, + image_hash_storage, + shutdown_signal_sender.clone(), + ))) + } else { + tracing::info!( + "image_hash and/or latest_allowed_hash_file not set, skipping TEE image hash monitoring" + ); + None + }; + + let home_dir = config.home_dir.clone(); + let root_future = create_root_future( + config, + home_dir.clone(), + node_config.clone(), + secrets.clone(), + indexer_api, + debug_request_sender, + root_task_handle, + tee_authority, + ); + + let root_task = root_runtime.spawn(start_root_task("root", root_future).0); + + let exit_reason = tokio::select! { + root_task_result = root_task => { + root_task_result? + } + indexer_exit_response = indexer_exit_receiver => { + indexer_exit_response.context("Indexer thread dropped response channel.")? + } + Some(()) = shutdown_signal_receiver.recv() => { + Err(anyhow!("TEE allowed image hashes watcher is sending shutdown signal.")) + } + }; + + // Perform graceful shutdown + cancellation_token.cancel(); + + if let Some(handle) = image_hash_watcher_handle { + info!("Waiting for image hash watcher to gracefully exit."); + let exit_result = handle.await; + info!(?exit_result, "Image hash watcher exited."); + } + + exit_reason +} + +#[allow(clippy::too_many_arguments)] +async fn create_root_future( + start_config: StartConfig, + home_dir: PathBuf, + config: ConfigFile, + secrets: SecretsConfig, + indexer_api: IndexerAPI, + debug_request_sender: broadcast::Sender, + // Cloning a OnceLock returns a new cell, which is why we have to wrap it in an arc. + // Otherwise we would not write to the same cell/lock. + root_task_handle_once_lock: Arc>>, + tee_authority: TeeAuthority, +) -> anyhow::Result<()> +where + TransactionSenderImpl: TransactionSender + 'static, + ForeignChainPolicyReader: ReadForeignChainPolicy + Clone + Send + Sync + 'static, +{ + let root_task_handle = tracking::current_task(); + + root_task_handle_once_lock + .set(root_task_handle.clone()) + .map_err(|_| anyhow!("Root task handle was already set"))?; + + let tls_public_key = + Ed25519PublicKey::from(&secrets.persistent_secrets.p2p_private_key.verifying_key()); + let account_public_key = + Ed25519PublicKey::from(&secrets.persistent_secrets.near_signer_key.verifying_key()); + + let secret_db = SecretDB::new(&home_dir.join("assets"), secrets.local_storage_aes_key)?; + + let key_storage_config = KeyStorageConfig { + home_dir: home_dir.clone(), + local_encryption_key: secrets.local_storage_aes_key, + gcp: start_config.gcp.map(|gcp| GcpPermanentKeyStorageConfig { + project_id: gcp.project_id, + secret_id: gcp.keyshare_secret_id, + }), + }; + + // Spawn periodic attestation submission task + let tee_authority_clone = tee_authority.clone(); + let tx_sender_clone = indexer_api.txn_sender.clone(); + let tls_public_key_clone = tls_public_key.clone(); + let account_public_key_clone = account_public_key.clone(); + let allowed_docker_images_receiver_clone = indexer_api.allowed_docker_images_receiver.clone(); + let allowed_launcher_compose_receiver_clone = + indexer_api.allowed_launcher_compose_receiver.clone(); + tokio::spawn(async move { + if let Err(e) = periodic_attestation_submission( + tee_authority_clone, + tx_sender_clone, + tls_public_key_clone, + account_public_key_clone, + allowed_docker_images_receiver_clone, + allowed_launcher_compose_receiver_clone, + tokio::time::interval(ATTESTATION_RESUBMISSION_INTERVAL), + ) + .await + { + tracing::error!( + error = ?e, + "periodic attestation submission task failed" + ); + } + }); + + // Spawn TEE attestation monitoring task + let tx_sender_clone = indexer_api.txn_sender.clone(); + let tee_accounts_receiver = indexer_api.attested_nodes_receiver.clone(); + let account_id_clone = config.my_near_account_id.clone(); + let allowed_docker_images_receiver_clone = indexer_api.allowed_docker_images_receiver.clone(); + let allowed_launcher_compose_receiver_clone = + indexer_api.allowed_launcher_compose_receiver.clone(); + tokio::spawn(async move { + if let Err(e) = monitor_attestation_removal( + account_id_clone, + tee_authority, + tx_sender_clone, + tls_public_key, + account_public_key, + allowed_docker_images_receiver_clone, + allowed_launcher_compose_receiver_clone, + tee_accounts_receiver, + ) + .await + { + tracing::error!( + error = ?e, + "attestation removal monitoring task failed" + ); + } + }); + + let keyshare_storage: Arc> = + RwLock::new(key_storage_config.create().await?).into(); + + spawn_recovery_server_and_run_onboarding( + config.migration_web_ui, + (&secrets).into(), + config.my_near_account_id.clone(), + keyshare_storage.clone(), + indexer_api.my_migration_info_receiver.clone(), + indexer_api.contract_state_receiver.clone(), + indexer_api.txn_sender.clone(), + ) + .await?; + + let coordinator = Coordinator { + clock: Clock::real(), + config_file: config, + secrets, + secret_db, + keyshare_storage, + indexer: indexer_api, + currently_running_job_name: Arc::new(Mutex::new(String::new())), + debug_request_sender, + }; + coordinator.run().await +} diff --git a/docs/localnet/localnet.md b/docs/localnet/localnet.md index 24dc69ce8..90045af21 100644 --- a/docs/localnet/localnet.md +++ b/docs/localnet/localnet.md @@ -191,65 +191,13 @@ Since this is not a validator node, we can remove `validator_key.json` rm ~/.near/mpc-frodo/validator_key.json ``` -Next we'll create a `config.yaml` for the MPC-indexer: - -```shell -cat > ~/.near/mpc-frodo/config.yaml << 'EOF' -my_near_account_id: frodo.test.near -near_responder_account_id: frodo.test.near -number_of_responder_keys: 1 -web_ui: 127.0.0.1:8081 -migration_web_ui: 127.0.0.1:8079 -pprof_bind_address: 127.0.0.1:34001 -triple: - concurrency: 2 - desired_triples_to_buffer: 128 - timeout_sec: 60 - parallel_triple_generation_stagger_time_sec: 1 -presignature: - concurrency: 4 - desired_presignatures_to_buffer: 64 - timeout_sec: 60 -signature: - timeout_sec: 60 -indexer: - validate_genesis: false - sync_mode: Latest - concurrency: 1 - mpc_contract_id: mpc-contract.test.near - finality: optimistic -ckd: - timeout_sec: 60 -cores: 4 -foreign_chains: - bitcoin: - timeout_sec: 30 - max_retries: 3 - providers: - public: - api_variant: esplora - rpc_url: "https://bitcoin-rpc.publicnode.com" - auth: - kind: none - abstract: - timeout_sec: 30 - max_retries: 3 - providers: - public: - api_variant: standard - rpc_url: "https://api.testnet.abs.xyz" - auth: - kind: none - starknet: - timeout_sec: 30 - max_retries: 3 - providers: - public: - api_variant: standard - rpc_url: "https://starknet-rpc.publicnode.com" - auth: - kind: none -EOF +Next we'll create a JSON configuration file for Frodo's MPC node using the +shared template at `docs/localnet/mpc-config.template.json`. This single file +contains all settings (secrets, TEE config, and node parameters): + +```shell +env MPC_NODE_ID=mpc-frodo NEAR_ACCOUNT_ID=frodo.test.near WEB_UI_PORT=8081 MIGRATION_WEB_UI_PORT=8079 PPROF_PORT=34001 \ + envsubst < docs/localnet/mpc-config.template.json > ~/.near/mpc-frodo/mpc-config.json ``` ### Initialize Sam's node @@ -273,79 +221,24 @@ rm ~/.near/mpc-sam/validator_key.json ``` ```shell -cat > ~/.near/mpc-sam/config.yaml << 'EOF' -my_near_account_id: sam.test.near -near_responder_account_id: sam.test.near -number_of_responder_keys: 1 -web_ui: 127.0.0.1:8082 -migration_web_ui: 127.0.0.1:8078 -pprof_bind_address: 127.0.0.1:34002 -triple: - concurrency: 2 - desired_triples_to_buffer: 128 - timeout_sec: 60 - parallel_triple_generation_stagger_time_sec: 1 -presignature: - concurrency: 4 - desired_presignatures_to_buffer: 64 - timeout_sec: 60 -signature: - timeout_sec: 60 -indexer: - validate_genesis: false - sync_mode: Latest - concurrency: 1 - mpc_contract_id: mpc-contract.test.near - finality: optimistic -ckd: - timeout_sec: 60 -cores: 4 -foreign_chains: - bitcoin: - timeout_sec: 30 - max_retries: 3 - providers: - public: - api_variant: esplora - rpc_url: "https://bitcoin-rpc.publicnode.com" - auth: - kind: none - abstract: - timeout_sec: 30 - max_retries: 3 - providers: - public: - api_variant: standard - rpc_url: "https://api.testnet.abs.xyz" - auth: - kind: none - starknet: - timeout_sec: 30 - max_retries: 3 - providers: - public: - api_variant: standard - rpc_url: "https://starknet-rpc.publicnode.com" - auth: - kind: none -EOF +env MPC_NODE_ID=mpc-sam NEAR_ACCOUNT_ID=sam.test.near WEB_UI_PORT=8082 MIGRATION_WEB_UI_PORT=8078 PPROF_PORT=34002 \ + envsubst < docs/localnet/mpc-config.template.json > ~/.near/mpc-sam/mpc-config.json ``` ### Run the MPC binary -In two separate shells run the MPC binary for frodo and sam. Note the last argument repeating (`11111111111111111111111111111111`) is the encryption key for the secret storage, and can be any arbitrary value. +In two separate shells run the MPC binary for Frodo and Sam using their JSON config files: ```shell -RUST_LOG=info mpc-node start --home-dir ~/.near/mpc-sam/ 11111111111111111111111111111111 --image-hash "8b40f81f77b8c22d6c777a6e14d307a1d11cb55ab83541fbb8575d02d86a74b0" --latest-allowed-hash-file /temp/LATEST_ALLOWED_HASH_FILE.txt local +RUST_LOG=info mpc-node start-with-config-file ~/.near/mpc-sam/mpc-config.json ``` ```shell -RUST_LOG=info mpc-node start --home-dir ~/.near/mpc-frodo/ 11111111111111111111111111111111 --image-hash "8b40f81f77b8c22d6c777a6e14d307a1d11cb55ab83541fbb8575d02d86a74b0" --latest-allowed-hash-file /temp/LATEST_ALLOWED_HASH_FILE.txt local +RUST_LOG=info mpc-node start-with-config-file ~/.near/mpc-frodo/mpc-config.json ``` Notes: -- `8b40f81f77b8c22d6c777a6e14d307a1d11cb55ab83541fbb8575d02d86a74b0` is just an arbitrary hash. - If you get the following error: ```console diff --git a/docs/localnet/mpc-config.template.json b/docs/localnet/mpc-config.template.json new file mode 100644 index 000000000..07e15ff27 --- /dev/null +++ b/docs/localnet/mpc-config.template.json @@ -0,0 +1,75 @@ +{ + "home_dir": "$HOME/.near/$MPC_NODE_ID", + "secrets": { + "secret_store_key_hex": "11111111111111111111111111111111" + }, + "tee": { + "authority": { "type": "local" }, + "image_hash": "8b40f81f77b8c22d6c777a6e14d307a1d11cb55ab83541fbb8575d02d86a74b0", + "latest_allowed_hash_file": "/tmp/LATEST_ALLOWED_HASH_FILE.txt" + }, + "node": { + "my_near_account_id": "$NEAR_ACCOUNT_ID", + "near_responder_account_id": "$NEAR_ACCOUNT_ID", + "number_of_responder_keys": 1, + "web_ui": "127.0.0.1:$WEB_UI_PORT", + "migration_web_ui": "127.0.0.1:$MIGRATION_WEB_UI_PORT", + "pprof_bind_address": "127.0.0.1:$PPROF_PORT", + "triple": { + "concurrency": 2, + "desired_triples_to_buffer": 128, + "timeout_sec": 60, + "parallel_triple_generation_stagger_time_sec": 1 + }, + "presignature": { + "concurrency": 4, + "desired_presignatures_to_buffer": 64, + "timeout_sec": 60 + }, + "signature": { "timeout_sec": 60 }, + "indexer": { + "validate_genesis": false, + "sync_mode": "Latest", + "concurrency": 1, + "mpc_contract_id": "mpc-contract.test.near", + "finality": "optimistic" + }, + "ckd": { "timeout_sec": 60 }, + "cores": 4, + "foreign_chains": { + "bitcoin": { + "timeout_sec": 30, + "max_retries": 3, + "providers": { + "public": { + "api_variant": "esplora", + "rpc_url": "https://bitcoin-rpc.publicnode.com", + "auth": { "kind": "none" } + } + } + }, + "abstract": { + "timeout_sec": 30, + "max_retries": 3, + "providers": { + "public": { + "api_variant": "standard", + "rpc_url": "https://api.testnet.abs.xyz", + "auth": { "kind": "none" } + } + } + }, + "starknet": { + "timeout_sec": 30, + "max_retries": 3, + "providers": { + "public": { + "api_variant": "standard", + "rpc_url": "https://starknet-rpc.publicnode.com", + "auth": { "kind": "none" } + } + } + } + } + } +}