diff --git a/Cargo.lock b/Cargo.lock index 23a44d1abeb..26a7cebef95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4545,6 +4545,8 @@ dependencies = [ "near-epoch-manager", "near-indexer", "near-indexer-primitives", + "near-jsonrpc-client", + "near-jsonrpc-primitives", "near-network", "near-o11y", "near-primitives", diff --git a/tools/mirror/Cargo.toml b/tools/mirror/Cargo.toml index 30816b75970..77813903458 100644 --- a/tools/mirror/Cargo.toml +++ b/tools/mirror/Cargo.toml @@ -41,6 +41,8 @@ near-chain-primitives.workspace = true near-client.workspace = true near-client-primitives.workspace = true near-epoch-manager.workspace = true +near-jsonrpc-client.workspace = true +near-jsonrpc-primitives.workspace = true near-indexer-primitives.workspace = true near-indexer.workspace = true near-network.workspace = true @@ -60,6 +62,8 @@ nightly = [ "near-epoch-manager/nightly", "near-indexer-primitives/nightly", "near-indexer/nightly", + "near-jsonrpc-client/nightly", + "near-jsonrpc-primitives/nightly", "near-network/nightly", "near-o11y/nightly", "near-primitives-core/nightly", @@ -77,6 +81,8 @@ nightly_protocol = [ "near-epoch-manager/nightly_protocol", "near-indexer-primitives/nightly_protocol", "near-indexer/nightly_protocol", + "near-jsonrpc-client/nightly_protocol", + "near-jsonrpc-primitives/nightly_protocol", "near-network/nightly_protocol", "near-o11y/nightly_protocol", "near-primitives-core/nightly_protocol", diff --git a/tools/mirror/src/cli.rs b/tools/mirror/src/cli.rs index c0d69193486..299953ccac8 100644 --- a/tools/mirror/src/cli.rs +++ b/tools/mirror/src/cli.rs @@ -1,8 +1,10 @@ use anyhow::Context; -use near_primitives::types::BlockHeight; use std::cell::Cell; use std::path::PathBuf; +use near_primitives::types::BlockHeight; +use near_primitives::views::AccessKeyPermissionView; + #[derive(clap::Parser)] pub struct MirrorCommand { #[clap(subcommand)] @@ -13,6 +15,7 @@ pub struct MirrorCommand { enum SubCommand { Prepare(PrepareCmd), Run(RunCmd), + ShowKeys(ShowKeysCmd), } /// initialize a target chain with genesis records from the source chain, and @@ -50,7 +53,6 @@ struct RunCmd { impl RunCmd { fn run(self) -> anyhow::Result<()> { openssl_probe::init_ssl_cert_env_vars(); - let runtime = tokio::runtime::Runtime::new().context("failed to start tokio runtime")?; let secret = if let Some(secret_file) = &self.secret_file { let secret = crate::secret::load(secret_file) @@ -68,25 +70,14 @@ impl RunCmd { None }; - let system = new_actix_system(runtime); - system - .block_on(async move { - let _subscriber_guard = near_o11y::default_subscriber( - near_o11y::EnvFilterBuilder::from_env().finish().unwrap(), - &near_o11y::Options::default(), - ) - .global(); - actix::spawn(crate::run( - self.source_home, - self.target_home, - secret, - self.stop_height, - self.online_source, - self.config_path, - )) - .await - }) - .unwrap() + run_async(crate::run( + self.source_home, + self.target_home, + secret, + self.stop_height, + self.online_source, + self.config_path, + )) } } @@ -131,6 +122,136 @@ impl PrepareCmd { } } +/// Given a source chain NEAR home dir, read and map access keys corresponding to +/// a given account ID and optional block height. +#[derive(clap::Parser)] +struct ShowKeysFromSourceDBCmd { + #[clap(long)] + home: PathBuf, + #[clap(long)] + account_id: String, + #[clap(long)] + block_height: Option, +} + +/// Given an RPC URL, request and map access keys corresponding to +/// a given account ID and optional block height. +#[derive(clap::Parser)] +struct ShowKeysFromRPCCmd { + #[clap(long)] + rpc_url: String, + #[clap(long)] + account_id: String, + #[clap(long)] + block_height: Option, +} + +/// Map the given public key +#[derive(clap::Parser)] +struct ShowKeyFromKeyCmd { + #[clap(long)] + public_key: String, +} + +/// Show the default extra key. This key should exist for any account that does not have +/// any full access keys in the source chain (e.g. validators with staking pools) +#[derive(clap::Parser)] +struct ShowDefaultExtraKeyCmd; + +#[derive(clap::Parser)] +enum ShowKeysSubCommand { + FromSourceDB(ShowKeysFromSourceDBCmd), + FromRPC(ShowKeysFromRPCCmd), + FromPubKey(ShowKeyFromKeyCmd), + DefaultExtraKey(ShowDefaultExtraKeyCmd), +} + +/// Print the secret keys that correspond to source chain public keys +#[derive(clap::Parser)] +struct ShowKeysCmd { + /// file containing an optional secret as generated by the + /// `prepare` command. + #[clap(long)] + secret_file: Option, + #[clap(subcommand)] + subcmd: ShowKeysSubCommand, +} + +impl ShowKeysCmd { + fn run(self) -> anyhow::Result<()> { + let secret = if let Some(secret_file) = &self.secret_file { + let secret = crate::secret::load(secret_file) + .with_context(|| format!("Failed to load secret from {:?}", secret_file))?; + secret + } else { + None + }; + let mut probably_extra_key = false; + let keys = match self.subcmd { + ShowKeysSubCommand::FromSourceDB(c) => { + let keys = crate::key_util::keys_from_source_db( + &c.home, + &c.account_id, + c.block_height, + secret.as_ref(), + )?; + probably_extra_key = keys.iter().all(|key| { + key.permission + .as_ref() + .map_or(true, |p| *p != AccessKeyPermissionView::FullAccess) + }); + keys + } + ShowKeysSubCommand::FromRPC(c) => { + let keys = run_async(async move { + crate::key_util::keys_from_rpc( + &c.rpc_url, + &c.account_id, + c.block_height, + secret.as_ref(), + ) + .await + })?; + probably_extra_key = keys.iter().all(|key| { + key.permission + .as_ref() + .map_or(true, |p| *p != AccessKeyPermissionView::FullAccess) + }); + keys + } + ShowKeysSubCommand::FromPubKey(c) => { + vec![crate::key_util::map_pub_key(&c.public_key, secret.as_ref())?] + } + ShowKeysSubCommand::DefaultExtraKey(_c) => { + vec![crate::key_util::default_extra_key(secret.as_ref())] + } + }; + for key in keys.iter() { + if let Some(k) = &key.original_key { + println!("original pub key: {}", k); + } + println!( + "mapped secret key: {}\nmapped public key: {}", + &key.mapped_key, + key.mapped_key.public_key() + ); + if let Some(a) = &key.permission { + println!("access: {:?}", a); + } + println!("------------") + } + if probably_extra_key { + let extra_key = crate::key_mapping::default_extra_key(secret.as_ref()); + println!( + "{} account probably has an extra full access key added:\nmapped secret key: {}\npublic key: {}", + if keys.is_empty() { "If it exists, this" } else { "This" }, + &extra_key, extra_key.public_key(), + ); + } + Ok(()) + } +} + // copied from neard/src/cli.rs fn new_actix_system(runtime: tokio::runtime::Runtime) -> actix::SystemRunner { // `with_tokio_rt()` accepts an `Fn()->Runtime`, however we know that this function is called exactly once. @@ -144,6 +265,21 @@ fn new_actix_system(runtime: tokio::runtime::Runtime) -> actix::SystemRunner { }) } +fn run_async(f: F) -> F::Output { + let runtime = tokio::runtime::Runtime::new().unwrap(); + let system = new_actix_system(runtime); + system + .block_on(async move { + let _subscriber_guard = near_o11y::default_subscriber( + near_o11y::EnvFilterBuilder::from_env().finish().unwrap(), + &near_o11y::Options::default(), + ) + .global(); + actix::spawn(f).await + }) + .unwrap() +} + impl MirrorCommand { pub fn run(self) -> anyhow::Result<()> { tracing::warn!(target: "mirror", "the mirror command is not stable, and may be removed or changed arbitrarily at any time"); @@ -151,6 +287,7 @@ impl MirrorCommand { match self.subcmd { SubCommand::Prepare(r) => r.run(), SubCommand::Run(r) => r.run(), + SubCommand::ShowKeys(r) => r.run(), } } } diff --git a/tools/mirror/src/key_util.rs b/tools/mirror/src/key_util.rs new file mode 100644 index 00000000000..02afd420d5a --- /dev/null +++ b/tools/mirror/src/key_util.rs @@ -0,0 +1,152 @@ +use anyhow::Context; +use std::path::Path; + +use near_chain::types::RuntimeAdapter; +use near_chain::{ChainStore, ChainStoreAccess}; +use near_chain_configs::GenesisValidationMode; +use near_crypto::{PublicKey, SecretKey}; +use near_epoch_manager::{EpochManager, EpochManagerAdapter}; +use near_jsonrpc_primitives::types::query::{ + QueryResponseKind as RpcQueryResponseKind, RpcQueryRequest, +}; +use near_primitives::types::{AccountId, BlockHeight, BlockId, BlockReference, Finality}; +use near_primitives::views::{AccessKeyPermissionView, QueryRequest, QueryResponseKind}; +use nearcore::{NightshadeRuntime, NightshadeRuntimeExt}; + +pub(crate) struct SecretAccessKey { + pub(crate) original_key: Option, + pub(crate) mapped_key: SecretKey, + pub(crate) permission: Option, +} + +pub(crate) fn default_extra_key( + secret: Option<&[u8; crate::secret::SECRET_LEN]>, +) -> SecretAccessKey { + SecretAccessKey { + original_key: None, + mapped_key: crate::key_mapping::default_extra_key(secret), + permission: None, + } +} + +pub(crate) fn map_pub_key( + public_key: &str, + secret: Option<&[u8; crate::secret::SECRET_LEN]>, +) -> anyhow::Result { + let public_key: PublicKey = public_key.parse().context("Could not parse public key")?; + // we say original_key is None here because the user provided it on the command line in this case, so no need to print it again. + Ok(SecretAccessKey { + original_key: None, + mapped_key: crate::key_mapping::map_key(&public_key, secret), + permission: None, + }) +} + +pub(crate) fn keys_from_source_db( + home: &Path, + account_id: &str, + block_height: Option, + secret: Option<&[u8; crate::secret::SECRET_LEN]>, +) -> anyhow::Result> { + let account_id: AccountId = account_id.parse().context("bad account ID")?; + + let mut config = + nearcore::config::load_config(home.as_ref(), GenesisValidationMode::UnsafeFast) + .with_context(|| format!("Error loading config from {}", home.display()))?; + let node_storage = + nearcore::open_storage(home.as_ref(), &mut config).context("failed opening storage")?; + let store = node_storage.get_hot_store(); + let chain = ChainStore::new( + store.clone(), + config.genesis.config.genesis_height, + config.client_config.save_trie_changes, + ); + let epoch_manager = EpochManager::new_arc_handle(store.clone(), &config.genesis.config); + let runtime = + NightshadeRuntime::from_config(home.as_ref(), store, &config, epoch_manager.clone()) + .context("could not create the transaction runtime")?; + let block_height = match block_height { + Some(h) => h, + None => { + let head = chain.head().context("failed getting chain head")?; + head.height + } + }; + + let header = chain + .get_block_header_by_height(block_height) + .with_context(|| format!("failed getting block header #{}", block_height))?; + let shard_id = epoch_manager + .account_id_to_shard_id(&account_id, header.epoch_id()) + .with_context(|| format!("failed finding shard for {}", &account_id))?; + let shard_uid = epoch_manager + .shard_id_to_uid(shard_id, header.epoch_id()) + .context("failed mapping ShardID to ShardUID")?; + let chunk_extra = + chain.get_chunk_extra(header.hash(), &shard_uid).context("failed getting chunk extra")?; + match runtime + .query( + shard_uid, + chunk_extra.state_root(), + header.height(), + header.raw_timestamp(), + header.prev_hash(), + header.hash(), + header.epoch_id(), + &QueryRequest::ViewAccessKeyList { account_id: account_id.clone() }, + ) + .with_context(|| format!("failed fetching access keys for {}", &account_id))? + .kind + { + QueryResponseKind::AccessKeyList(l) => Ok(l + .keys + .into_iter() + .map(|k| SecretAccessKey { + mapped_key: crate::key_mapping::map_key(&k.public_key, secret), + original_key: Some(k.public_key), + permission: Some(k.access_key.permission), + }) + .collect()), + _ => unreachable!(), + } +} + +pub(crate) async fn keys_from_rpc( + rpc_url: &str, + account_id: &str, + block_height: Option, + secret: Option<&[u8; crate::secret::SECRET_LEN]>, +) -> anyhow::Result> { + let account_id: AccountId = account_id.parse().context("bad account ID")?; + + let rpc_client = near_jsonrpc_client::new_client(rpc_url); + + let block_reference = match block_height { + Some(h) => BlockReference::BlockId(BlockId::Height(h)), + None => BlockReference::Finality(Finality::None), + }; + let request = RpcQueryRequest { + block_reference, + request: QueryRequest::ViewAccessKeyList { account_id: account_id.clone() }, + }; + + let response = match rpc_client.query(request).await { + Ok(r) => r, + Err(e) => anyhow::bail!("failed making RPC request: {:?}", e), + }; + + match response.kind { + RpcQueryResponseKind::AccessKeyList(l) => Ok(l + .keys + .into_iter() + .map(|k| SecretAccessKey { + mapped_key: crate::key_mapping::map_key(&k.public_key, secret), + original_key: Some(k.public_key), + permission: Some(k.access_key.permission), + }) + .collect()), + k => { + anyhow::bail!("received unexpected RPC response for access key query: {:?}", k); + } + } +} diff --git a/tools/mirror/src/lib.rs b/tools/mirror/src/lib.rs index 97f0003f6cb..5ab58c02f5d 100644 --- a/tools/mirror/src/lib.rs +++ b/tools/mirror/src/lib.rs @@ -41,6 +41,7 @@ mod chain_tracker; pub mod cli; pub mod genesis; pub mod key_mapping; +mod key_util; mod metrics; mod offline; mod online;