rust: Add mnemonic phrase inspection to `zcash-inspect`
This commit is contained in:
parent
b0bf639760
commit
fd7cb4bb4a
|
@ -950,6 +950,7 @@ version = "0.2.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"backtrace",
|
||||
"bech32",
|
||||
"bellman",
|
||||
"blake2b_simd",
|
||||
"blake2s_simd",
|
||||
|
|
|
@ -81,6 +81,7 @@ metrics-exporter-prometheus = "0.10"
|
|||
gumdrop = "0.8"
|
||||
|
||||
# zcash-inspect tool
|
||||
bech32 = "0.8"
|
||||
equihash = "0.2"
|
||||
hex = "0.4"
|
||||
lazy_static = "1.4"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
@ -9,6 +10,7 @@ use zcash_primitives::{
|
|||
consensus::Network,
|
||||
legacy::Script,
|
||||
transaction::components::{transparent, Amount},
|
||||
zip32::AccountId,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
|
@ -47,6 +49,68 @@ impl<'de> Deserialize<'de> for JsonNetwork {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct JsonAccountId(AccountId);
|
||||
|
||||
struct JsonAccountIdVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for JsonAccountIdVisitor {
|
||||
type Value = JsonAccountId;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a u31")
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
u32::try_from(v)
|
||||
.map_err(|_| E::custom(format!("u32 out of range: {}", v)))
|
||||
.map(AccountId::from)
|
||||
.map(JsonAccountId)
|
||||
}
|
||||
|
||||
fn visit_i128<E>(self, v: i128) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
u32::try_from(v)
|
||||
.map_err(|_| E::custom(format!("u32 out of range: {}", v)))
|
||||
.map(AccountId::from)
|
||||
.map(JsonAccountId)
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
u32::try_from(v)
|
||||
.map_err(|_| E::custom(format!("u32 out of range: {}", v)))
|
||||
.map(AccountId::from)
|
||||
.map(JsonAccountId)
|
||||
}
|
||||
|
||||
fn visit_u128<E>(self, v: u128) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
u32::try_from(v)
|
||||
.map_err(|_| E::custom(format!("u32 out of range: {}", v)))
|
||||
.map(AccountId::from)
|
||||
.map(JsonAccountId)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for JsonAccountId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_u32(JsonAccountIdVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct ZUint256(pub [u8; 32]);
|
||||
|
||||
|
@ -177,6 +241,7 @@ struct ZTxOut {
|
|||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct Context {
|
||||
network: Option<JsonNetwork>,
|
||||
accounts: Option<Vec<JsonAccountId>>,
|
||||
pub(crate) chainhistoryroot: Option<ZUint256>,
|
||||
transparentcoins: Option<Vec<ZTxOut>>,
|
||||
}
|
||||
|
@ -201,6 +266,12 @@ impl Context {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn accounts(&self) -> Option<Vec<AccountId>> {
|
||||
self.accounts
|
||||
.as_ref()
|
||||
.map(|accounts| accounts.iter().map(|id| id.0).collect())
|
||||
}
|
||||
|
||||
pub(crate) fn transparent_coins(&self) -> Option<Vec<transparent::TxOut>> {
|
||||
self.transparentcoins.as_ref().map(|coins| {
|
||||
coins
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
use std::convert::TryInto;
|
||||
use std::iter;
|
||||
|
||||
use bech32::ToBase32;
|
||||
use secrecy::Zeroize;
|
||||
use zcash_address::{
|
||||
unified::{self, Encoding},
|
||||
ToAddress, ZcashAddress,
|
||||
};
|
||||
use zcash_primitives::{
|
||||
consensus::Parameters,
|
||||
legacy::{
|
||||
keys::{AccountPrivKey, IncomingViewingKey},
|
||||
TransparentAddress,
|
||||
},
|
||||
zip32, zip339,
|
||||
};
|
||||
|
||||
use crate::Context;
|
||||
|
||||
pub(crate) fn inspect_mnemonic(
|
||||
mnemonic: zip339::Mnemonic,
|
||||
lang: zip339::Language,
|
||||
context: Option<Context>,
|
||||
) {
|
||||
eprintln!("Mnemonic phrase");
|
||||
eprintln!(" - Language: {}", lang);
|
||||
|
||||
if let Some(((network, addr_net), accounts)) =
|
||||
context.and_then(|c| c.network().zip(c.addr_network()).zip(c.accounts()))
|
||||
{
|
||||
let mut seed = mnemonic.to_seed("");
|
||||
for account in accounts {
|
||||
eprintln!(" - Account {}:", u32::from(account));
|
||||
|
||||
let orchard_fvk = match orchard::keys::SpendingKey::from_zip32_seed(
|
||||
&seed,
|
||||
network.coin_type(),
|
||||
account.into(),
|
||||
) {
|
||||
Ok(sk) => Some(orchard::keys::FullViewingKey::from(&sk)),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
" ⚠️ No valid Orchard key for this account under this seed: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
eprintln!(" - Sapling:");
|
||||
let sapling_master = zip32::ExtendedSpendingKey::master(&seed);
|
||||
let sapling_extsk = zip32::ExtendedSpendingKey::from_path(
|
||||
&sapling_master,
|
||||
&[
|
||||
zip32::ChildIndex::Hardened(32),
|
||||
zip32::ChildIndex::Hardened(network.coin_type()),
|
||||
zip32::ChildIndex::Hardened(account.into()),
|
||||
],
|
||||
);
|
||||
let sapling_extfvk = zip32::ExtendedFullViewingKey::from(&sapling_extsk);
|
||||
let sapling_default_addr = sapling_extfvk.default_address();
|
||||
|
||||
let mut sapling_extsk_bytes = vec![];
|
||||
sapling_extsk.write(&mut sapling_extsk_bytes).unwrap();
|
||||
eprintln!(
|
||||
" - ExtSK: {}",
|
||||
bech32::encode(
|
||||
network.hrp_sapling_extended_spending_key(),
|
||||
sapling_extsk_bytes.to_base32(),
|
||||
bech32::Variant::Bech32,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let mut sapling_extfvk_bytes = vec![];
|
||||
sapling_extfvk.write(&mut sapling_extfvk_bytes).unwrap();
|
||||
eprintln!(
|
||||
" - ExtFVK: {}",
|
||||
bech32::encode(
|
||||
network.hrp_sapling_extended_full_viewing_key(),
|
||||
sapling_extfvk_bytes.to_base32(),
|
||||
bech32::Variant::Bech32,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let sapling_addr_bytes = sapling_default_addr.1.to_bytes();
|
||||
eprintln!(
|
||||
" - Default address: {}",
|
||||
bech32::encode(
|
||||
network.hrp_sapling_payment_address(),
|
||||
sapling_addr_bytes.to_base32(),
|
||||
bech32::Variant::Bech32,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let transparent_fvk = match AccountPrivKey::from_seed(&network, &seed, account)
|
||||
.map(|sk| sk.to_account_pubkey())
|
||||
{
|
||||
Ok(fvk) => {
|
||||
eprintln!(" - Transparent:");
|
||||
match fvk.derive_external_ivk().map(|ivk| ivk.default_address().0) {
|
||||
Ok(addr) => eprintln!(
|
||||
" - Default address: {}",
|
||||
match addr {
|
||||
TransparentAddress::PublicKey(data) => ZcashAddress::from_transparent_p2pkh(addr_net, data),
|
||||
TransparentAddress::Script(_) => unreachable!(),
|
||||
}.encode(),
|
||||
),
|
||||
Err(e) => eprintln!(
|
||||
" ⚠️ No valid transparent default address for this account under this seed: {:?}",
|
||||
e
|
||||
),
|
||||
}
|
||||
|
||||
Some(fvk)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
" ⚠️ No valid transparent key for this account under this seed: {:?}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let items: Vec<_> = iter::empty()
|
||||
.chain(
|
||||
orchard_fvk
|
||||
.map(|fvk| fvk.to_bytes())
|
||||
.map(unified::Fvk::Orchard),
|
||||
)
|
||||
.chain(Some(unified::Fvk::Sapling(
|
||||
sapling_extfvk_bytes[41..].try_into().unwrap(),
|
||||
)))
|
||||
.chain(
|
||||
transparent_fvk
|
||||
.map(|fvk| fvk.serialize()[..].try_into().unwrap())
|
||||
.map(unified::Fvk::P2pkh),
|
||||
)
|
||||
.collect();
|
||||
let item_names: Vec<_> = items
|
||||
.iter()
|
||||
.map(|item| match item {
|
||||
unified::Fvk::Orchard(_) => "Orchard",
|
||||
unified::Fvk::Sapling(_) => "Sapling",
|
||||
unified::Fvk::P2pkh(_) => "Transparent",
|
||||
unified::Fvk::Unknown { .. } => unreachable!(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
eprintln!(" - Unified ({}):", item_names.join(", "));
|
||||
let ufvk = unified::Ufvk::try_from_items(items).unwrap();
|
||||
eprintln!(" - UFVK: {}", ufvk.encode(&addr_net));
|
||||
}
|
||||
seed.zeroize();
|
||||
} else {
|
||||
eprintln!("🔎 To show account details, add \"network\" (either \"main\" or \"test\") and \"accounts\" array to context");
|
||||
}
|
||||
|
||||
eprintln!();
|
||||
eprintln!(
|
||||
"WARNING: This mnemonic phrase is now likely cached in your terminal's history buffer."
|
||||
);
|
||||
}
|
|
@ -5,7 +5,9 @@ use std::process;
|
|||
|
||||
use gumdrop::{Options, ParsingStyle};
|
||||
use lazy_static::lazy_static;
|
||||
use secrecy::Zeroize;
|
||||
use zcash_address::ZcashAddress;
|
||||
use zcash_primitives::zip339;
|
||||
use zcash_primitives::{block::BlockHeader, consensus::BranchId, transaction::Transaction};
|
||||
use zcash_proofs::{default_params_folder, load_parameters, ZcashParameters};
|
||||
|
||||
|
@ -14,6 +16,7 @@ use context::{Context, ZUint256};
|
|||
|
||||
mod address;
|
||||
mod block;
|
||||
mod keys;
|
||||
mod transaction;
|
||||
|
||||
lazy_static! {
|
||||
|
@ -45,7 +48,8 @@ struct CliOptions {
|
|||
|
||||
fn main() {
|
||||
let args = env::args().collect::<Vec<_>>();
|
||||
let opts = CliOptions::parse_args(&args[1..], ParsingStyle::default()).unwrap_or_else(|e| {
|
||||
let mut opts =
|
||||
CliOptions::parse_args(&args[1..], ParsingStyle::default()).unwrap_or_else(|e| {
|
||||
eprintln!("{}: {}", args[0], e);
|
||||
process::exit(2);
|
||||
});
|
||||
|
@ -57,7 +61,12 @@ fn main() {
|
|||
return;
|
||||
}
|
||||
|
||||
if let Ok(bytes) = hex::decode(&opts.data) {
|
||||
let lang = zip339::Language::English;
|
||||
|
||||
if let Ok(mnemonic) = zip339::Mnemonic::from_phrase_in(lang, &opts.data) {
|
||||
opts.data.zeroize();
|
||||
keys::inspect_mnemonic(mnemonic, lang, opts.context);
|
||||
} else if let Ok(bytes) = hex::decode(&opts.data) {
|
||||
inspect_bytes(bytes, opts.context);
|
||||
} else if let Ok(addr) = ZcashAddress::try_from_encoded(&opts.data) {
|
||||
address::inspect(addr);
|
||||
|
|
Loading…
Reference in New Issue