rust: Add mnemonic phrase inspection to `zcash-inspect`

This commit is contained in:
Jack Grigg 2022-08-15 21:51:39 +00:00
parent b0bf639760
commit fd7cb4bb4a
5 changed files with 254 additions and 5 deletions

1
Cargo.lock generated
View File

@ -950,6 +950,7 @@ version = "0.2.0"
dependencies = [
"anyhow",
"backtrace",
"bech32",
"bellman",
"blake2b_simd",
"blake2s_simd",

View File

@ -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"

View File

@ -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

View File

@ -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."
);
}

View File

@ -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,10 +48,11 @@ struct CliOptions {
fn main() {
let args = env::args().collect::<Vec<_>>();
let opts = CliOptions::parse_args(&args[1..], ParsingStyle::default()).unwrap_or_else(|e| {
eprintln!("{}: {}", args[0], e);
process::exit(2);
});
let mut opts =
CliOptions::parse_args(&args[1..], ParsingStyle::default()).unwrap_or_else(|e| {
eprintln!("{}: {}", args[0], e);
process::exit(2);
});
if opts.help_requested() {
println!("Usage: {} data [context]", args[0]);
@ -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);