Merge pull request #74 from zcash/inspect
Import `zcash-inspect` as `zcash-devtool inspect`
This commit is contained in:
commit
33541fe694
|
@ -1758,6 +1758,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
|
"serde",
|
||||||
"signature",
|
"signature",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1777,6 +1778,22 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ed25519-zebra"
|
||||||
|
version = "4.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d9ce6874da5d4415896cd45ffbc4d1cfc0c4f9c079427bd870742c30f2f65a9"
|
||||||
|
dependencies = [
|
||||||
|
"curve25519-dalek",
|
||||||
|
"ed25519",
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
"hex",
|
||||||
|
"rand_core",
|
||||||
|
"serde",
|
||||||
|
"sha2",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "educe"
|
name = "educe"
|
||||||
version = "0.4.23"
|
version = "0.4.23"
|
||||||
|
@ -2448,6 +2465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.11",
|
"ahash 0.8.11",
|
||||||
|
"allocator-api2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -7501,16 +7519,23 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"age",
|
"age",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"bech32 0.11.0",
|
||||||
|
"bellman",
|
||||||
"bip0039",
|
"bip0039",
|
||||||
"bip32",
|
"bip32",
|
||||||
|
"blake2b_simd",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
"ed25519-zebra",
|
||||||
|
"equihash",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"group",
|
||||||
"hex",
|
"hex",
|
||||||
"image",
|
"image",
|
||||||
"iso_currency",
|
"iso_currency",
|
||||||
"jubjub",
|
"jubjub",
|
||||||
|
"lazy_static",
|
||||||
"minicbor",
|
"minicbor",
|
||||||
"nokhwa",
|
"nokhwa",
|
||||||
"orchard",
|
"orchard",
|
||||||
|
@ -7525,8 +7550,11 @@ dependencies = [
|
||||||
"rust_decimal",
|
"rust_decimal",
|
||||||
"sapling-crypto",
|
"sapling-crypto",
|
||||||
"schemerz",
|
"schemerz",
|
||||||
|
"secp256k1",
|
||||||
"secrecy 0.8.0",
|
"secrecy 0.8.0",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
@ -7535,12 +7563,15 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"tui-logger",
|
"tui-logger",
|
||||||
|
"uint",
|
||||||
"ur",
|
"ur",
|
||||||
"uuid",
|
"uuid",
|
||||||
"zcash_address",
|
"zcash_address",
|
||||||
"zcash_client_backend",
|
"zcash_client_backend",
|
||||||
"zcash_client_sqlite",
|
"zcash_client_sqlite",
|
||||||
|
"zcash_encoding",
|
||||||
"zcash_keys",
|
"zcash_keys",
|
||||||
|
"zcash_note_encryption",
|
||||||
"zcash_primitives",
|
"zcash_primitives",
|
||||||
"zcash_proofs",
|
"zcash_proofs",
|
||||||
"zcash_protocol",
|
"zcash_protocol",
|
||||||
|
|
19
Cargo.toml
19
Cargo.toml
|
@ -37,7 +37,7 @@ zcash_client_sqlite = { version = "0.14", features = ["unstable", "orchard", "se
|
||||||
zcash_keys = { version = "0.6", features = ["unstable", "orchard"] }
|
zcash_keys = { version = "0.6", features = ["unstable", "orchard"] }
|
||||||
zcash_primitives = "0.21"
|
zcash_primitives = "0.21"
|
||||||
zcash_proofs = "0.21"
|
zcash_proofs = "0.21"
|
||||||
zcash_protocol = "0.4"
|
zcash_protocol = { version = "0.4", features = ["local-consensus"] }
|
||||||
zip32 = "0.1"
|
zip32 = "0.1"
|
||||||
zip321 = "0.2"
|
zip321 = "0.2"
|
||||||
|
|
||||||
|
@ -52,6 +52,21 @@ chrono = "0.4"
|
||||||
iso_currency = { version = "0.5", features = ["with-serde"] }
|
iso_currency = { version = "0.5", features = ["with-serde"] }
|
||||||
rust_decimal = "1"
|
rust_decimal = "1"
|
||||||
|
|
||||||
|
# Inspect
|
||||||
|
bech32 = "0.11"
|
||||||
|
bellman = "0.14"
|
||||||
|
blake2b_simd = "1"
|
||||||
|
ed25519-zebra = "4"
|
||||||
|
equihash = "0.2"
|
||||||
|
group = "0.13"
|
||||||
|
lazy_static = "1"
|
||||||
|
secp256k1 = "0.27"
|
||||||
|
serde_json = "1"
|
||||||
|
sha2 = "0.10"
|
||||||
|
uint = "0.9"
|
||||||
|
zcash_encoding = "0.2"
|
||||||
|
zcash_note_encryption = "0.4"
|
||||||
|
|
||||||
# PCZT QR codes
|
# PCZT QR codes
|
||||||
image = { version = "0.25", optional = true }
|
image = { version = "0.25", optional = true }
|
||||||
minicbor = { version = "0.19", optional = true }
|
minicbor = { version = "0.19", optional = true }
|
||||||
|
@ -82,6 +97,7 @@ tui = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
equihash = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
||||||
orchard = { git = "https://github.com/zcash/orchard.git", rev = "4fa6d3b549f8803016a309281404eab095d04de8" }
|
orchard = { git = "https://github.com/zcash/orchard.git", rev = "4fa6d3b549f8803016a309281404eab095d04de8" }
|
||||||
pczt = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
pczt = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
||||||
sapling = { package = "sapling-crypto", git = "https://github.com/zcash/sapling-crypto.git", rev = "3c2235747553da642fb142d1eeb9b1afa8391987" }
|
sapling = { package = "sapling-crypto", git = "https://github.com/zcash/sapling-crypto.git", rev = "3c2235747553da642fb142d1eeb9b1afa8391987" }
|
||||||
|
@ -89,6 +105,7 @@ transparent = { package = "zcash_transparent", git = "https://github.com/zcash/l
|
||||||
zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
||||||
zcash_client_backend = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
zcash_client_backend = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
||||||
zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
||||||
|
zcash_encoding = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
||||||
zcash_keys = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
zcash_keys = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
||||||
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
||||||
zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "9c6d1b958bd015f3fc3f8d5e5815b2bfc54e484b" }
|
||||||
|
|
|
@ -4,6 +4,7 @@ use uuid::Uuid;
|
||||||
use zcash_client_backend::data_api::WalletRead;
|
use zcash_client_backend::data_api::WalletRead;
|
||||||
use zcash_client_sqlite::AccountUuid;
|
use zcash_client_sqlite::AccountUuid;
|
||||||
|
|
||||||
|
pub(crate) mod inspect;
|
||||||
pub(crate) mod pczt;
|
pub(crate) mod pczt;
|
||||||
pub(crate) mod wallet;
|
pub(crate) mod wallet;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
use std::io;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
use bech32::primitives::decode::CheckedHrpstring;
|
||||||
|
use bech32::Bech32;
|
||||||
|
use clap::Args;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use secrecy::Zeroize;
|
||||||
|
use zcash_address::{
|
||||||
|
unified::{self, Encoding},
|
||||||
|
ZcashAddress,
|
||||||
|
};
|
||||||
|
use zcash_primitives::{block::BlockHeader, consensus::BranchId, transaction::Transaction};
|
||||||
|
use zcash_proofs::{default_params_folder, load_parameters, ZcashParameters};
|
||||||
|
use zcash_protocol::consensus::NetworkType;
|
||||||
|
|
||||||
|
mod context;
|
||||||
|
use context::{Context, ZUint256};
|
||||||
|
use zcash_protocol::constants;
|
||||||
|
|
||||||
|
mod address;
|
||||||
|
mod block;
|
||||||
|
mod keys;
|
||||||
|
mod lookup;
|
||||||
|
mod transaction;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref GROTH16_PARAMS: ZcashParameters = {
|
||||||
|
let folder = default_params_folder().unwrap();
|
||||||
|
load_parameters(
|
||||||
|
&folder.join("sapling-spend.params"),
|
||||||
|
&folder.join("sapling-output.params"),
|
||||||
|
Some(&folder.join("sprout-groth16.params")),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
static ref ORCHARD_VK: orchard::circuit::VerifyingKey = orchard::circuit::VerifyingKey::build();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub(crate) struct Command {
|
||||||
|
/// Query information from the chain to help determine what the data is
|
||||||
|
#[arg(short, long)]
|
||||||
|
lookup: bool,
|
||||||
|
|
||||||
|
/// String or hex-encoded bytes to inspect
|
||||||
|
data: String,
|
||||||
|
|
||||||
|
/// JSON object with keys corresponding to requested context information
|
||||||
|
context: Option<Context>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
pub(crate) async fn run(self) -> Result<(), anyhow::Error> {
|
||||||
|
let mut opts = self;
|
||||||
|
|
||||||
|
if let Ok(mnemonic) = bip0039::Mnemonic::from_phrase(&opts.data) {
|
||||||
|
opts.data.zeroize();
|
||||||
|
keys::inspect_mnemonic(mnemonic, opts.context);
|
||||||
|
} else if let Ok(bytes) = hex::decode(&opts.data) {
|
||||||
|
inspect_bytes(bytes, opts.context, opts.lookup).await;
|
||||||
|
} else if let Ok(addr) = ZcashAddress::try_from_encoded(&opts.data) {
|
||||||
|
address::inspect(addr);
|
||||||
|
} else if let Ok((network, uivk)) = unified::Uivk::decode(&opts.data) {
|
||||||
|
keys::view::inspect_uivk(uivk, network);
|
||||||
|
} else if let Ok((network, ufvk)) = unified::Ufvk::decode(&opts.data) {
|
||||||
|
keys::view::inspect_ufvk(ufvk, network);
|
||||||
|
} else if let Ok(parsed) = CheckedHrpstring::new::<Bech32>(&opts.data) {
|
||||||
|
let data = parsed.byte_iter().collect::<Vec<_>>();
|
||||||
|
match parsed.hrp().as_str() {
|
||||||
|
constants::mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY => {
|
||||||
|
keys::view::inspect_sapling_extfvk(data, NetworkType::Main);
|
||||||
|
}
|
||||||
|
constants::testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY => {
|
||||||
|
keys::view::inspect_sapling_extfvk(data, NetworkType::Test);
|
||||||
|
}
|
||||||
|
constants::regtest::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY => {
|
||||||
|
keys::view::inspect_sapling_extfvk(data, NetworkType::Regtest);
|
||||||
|
}
|
||||||
|
constants::mainnet::HRP_SAPLING_EXTENDED_SPENDING_KEY => {
|
||||||
|
keys::inspect_sapling_extsk(data, NetworkType::Main);
|
||||||
|
}
|
||||||
|
constants::testnet::HRP_SAPLING_EXTENDED_SPENDING_KEY => {
|
||||||
|
keys::inspect_sapling_extsk(data, NetworkType::Test);
|
||||||
|
}
|
||||||
|
constants::regtest::HRP_SAPLING_EXTENDED_SPENDING_KEY => {
|
||||||
|
keys::inspect_sapling_extsk(data, NetworkType::Regtest);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Unknown data format.
|
||||||
|
eprintln!("String does not match known Zcash data formats.");
|
||||||
|
process::exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unknown data format.
|
||||||
|
eprintln!("String does not match known Zcash data formats.");
|
||||||
|
process::exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensures that the given reader completely consumes the given bytes.
|
||||||
|
fn complete<F, T>(bytes: &[u8], f: F) -> Option<T>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Cursor<&[u8]>) -> io::Result<T>,
|
||||||
|
{
|
||||||
|
let mut cursor = Cursor::new(bytes);
|
||||||
|
let res = f(&mut cursor);
|
||||||
|
res.ok().and_then(|t| {
|
||||||
|
if cursor.position() >= bytes.len() as u64 {
|
||||||
|
Some(t)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inspect_bytes(bytes: Vec<u8>, context: Option<Context>, lookup: bool) {
|
||||||
|
if let Some(block) = complete(&bytes, |r| block::Block::read(r)) {
|
||||||
|
block::inspect(&block, context);
|
||||||
|
} else if let Some(header) = complete(&bytes, |r| BlockHeader::read(r)) {
|
||||||
|
block::inspect_header(&header, context);
|
||||||
|
} else if let Some(tx) = complete(&bytes, |r| Transaction::read(r, BranchId::Nu5)) {
|
||||||
|
// TODO: Take the branch ID used above from the context if present.
|
||||||
|
// https://github.com/zcash/zcash/issues/6831
|
||||||
|
transaction::inspect(tx, context, None);
|
||||||
|
} else {
|
||||||
|
// It's not a known variable-length format. check fixed-length data formats.
|
||||||
|
match bytes.len() {
|
||||||
|
32 => inspect_possible_hash(bytes.try_into().unwrap(), context, lookup).await,
|
||||||
|
64 => {
|
||||||
|
// Could be a signature
|
||||||
|
eprintln!("This is most likely a signature.");
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
eprintln!("Binary data does not match known Zcash data formats.");
|
||||||
|
process::exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inspect_possible_hash(bytes: [u8; 32], context: Option<Context>, lookup: bool) {
|
||||||
|
let maybe_mainnet_block_hash = bytes.iter().take(4).all(|c| c == &0);
|
||||||
|
|
||||||
|
if lookup {
|
||||||
|
// Block hashes and txids are byte-reversed; we didn't do this when parsing the
|
||||||
|
// original hex because other hex byte encodings are not byte-reversed.
|
||||||
|
let mut candidate = bytes;
|
||||||
|
candidate.reverse();
|
||||||
|
|
||||||
|
let found = async {
|
||||||
|
match lookup::Lightwalletd::mainnet().await {
|
||||||
|
Err(e) => eprintln!("Error: Failed to connect to mainnet lightwalletd: {:?}", e),
|
||||||
|
Ok(mut mainnet) => {
|
||||||
|
if let Some((tx, mined_height)) = mainnet.lookup_txid(candidate).await {
|
||||||
|
transaction::inspect(tx, context, mined_height);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match lookup::Lightwalletd::testnet().await {
|
||||||
|
Err(e) => eprintln!("Error: Failed to connect to testnet lightwalletd: {:?}", e),
|
||||||
|
Ok(mut testnet) => {
|
||||||
|
if let Some((tx, mined_height)) = testnet.lookup_txid(candidate).await {
|
||||||
|
transaction::inspect(tx, context, mined_height);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if found {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("This is most likely a hash of some sort, or maybe a commitment or nullifier.");
|
||||||
|
if maybe_mainnet_block_hash {
|
||||||
|
eprintln!("- It could be a mainnet block hash.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
use zcash_address::{
|
||||||
|
unified::{self, Container, Encoding},
|
||||||
|
ConversionError, Network, ToAddress, ZcashAddress,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
enum AddressKind {
|
||||||
|
Sprout([u8; 64]),
|
||||||
|
Sapling([u8; 43]),
|
||||||
|
Unified(unified::Address),
|
||||||
|
P2pkh([u8; 20]),
|
||||||
|
P2sh([u8; 20]),
|
||||||
|
Tex([u8; 20]),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Address {
|
||||||
|
net: Network,
|
||||||
|
kind: AddressKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl zcash_address::TryFromAddress for Address {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from_sprout(net: Network, data: [u8; 64]) -> Result<Self, ConversionError<Self::Error>> {
|
||||||
|
Ok(Address {
|
||||||
|
net,
|
||||||
|
kind: AddressKind::Sprout(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_from_sapling(
|
||||||
|
net: Network,
|
||||||
|
data: [u8; 43],
|
||||||
|
) -> Result<Self, ConversionError<Self::Error>> {
|
||||||
|
Ok(Address {
|
||||||
|
net,
|
||||||
|
kind: AddressKind::Sapling(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_from_unified(
|
||||||
|
net: Network,
|
||||||
|
data: unified::Address,
|
||||||
|
) -> Result<Self, ConversionError<Self::Error>> {
|
||||||
|
Ok(Address {
|
||||||
|
net,
|
||||||
|
kind: AddressKind::Unified(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_from_transparent_p2pkh(
|
||||||
|
net: Network,
|
||||||
|
data: [u8; 20],
|
||||||
|
) -> Result<Self, ConversionError<Self::Error>> {
|
||||||
|
Ok(Address {
|
||||||
|
net,
|
||||||
|
kind: AddressKind::P2pkh(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_from_transparent_p2sh(
|
||||||
|
net: Network,
|
||||||
|
data: [u8; 20],
|
||||||
|
) -> Result<Self, ConversionError<Self::Error>> {
|
||||||
|
Ok(Address {
|
||||||
|
net,
|
||||||
|
kind: AddressKind::P2sh(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_from_tex(net: Network, data: [u8; 20]) -> Result<Self, ConversionError<Self::Error>> {
|
||||||
|
Ok(Address {
|
||||||
|
net,
|
||||||
|
kind: AddressKind::Tex(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inspect(addr: ZcashAddress) {
|
||||||
|
eprintln!("Zcash address");
|
||||||
|
|
||||||
|
match addr.convert::<Address>() {
|
||||||
|
// TODO: Check for valid internals once we have migrated to a newer zcash_address
|
||||||
|
// version with custom errors.
|
||||||
|
Err(_) => unreachable!(),
|
||||||
|
Ok(addr) => {
|
||||||
|
eprintln!(
|
||||||
|
" - Network: {}",
|
||||||
|
match addr.net {
|
||||||
|
Network::Main => "main",
|
||||||
|
Network::Test => "testnet",
|
||||||
|
Network::Regtest => "regtest",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
" - Kind: {}",
|
||||||
|
match addr.kind {
|
||||||
|
AddressKind::Sprout(_) => "Sprout",
|
||||||
|
AddressKind::Sapling(_) => "Sapling",
|
||||||
|
AddressKind::Unified(_) => "Unified Address",
|
||||||
|
AddressKind::P2pkh(_) => "Transparent P2PKH",
|
||||||
|
AddressKind::P2sh(_) => "Transparent P2SH",
|
||||||
|
AddressKind::Tex(_) => "TEX (ZIP 320)",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
match addr.kind {
|
||||||
|
AddressKind::Unified(ua) => {
|
||||||
|
eprintln!(" - Receivers:");
|
||||||
|
for receiver in ua.items() {
|
||||||
|
match receiver {
|
||||||
|
unified::Receiver::Orchard(data) => {
|
||||||
|
eprintln!(
|
||||||
|
" - Orchard ({})",
|
||||||
|
unified::Address::try_from_items(vec![
|
||||||
|
unified::Receiver::Orchard(data)
|
||||||
|
])
|
||||||
|
.unwrap()
|
||||||
|
.encode(&addr.net)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
unified::Receiver::Sapling(data) => {
|
||||||
|
eprintln!(
|
||||||
|
" - Sapling ({})",
|
||||||
|
ZcashAddress::from_sapling(addr.net, data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
unified::Receiver::P2pkh(data) => {
|
||||||
|
eprintln!(
|
||||||
|
" - Transparent P2PKH ({})",
|
||||||
|
ZcashAddress::from_transparent_p2pkh(addr.net, data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
unified::Receiver::P2sh(data) => {
|
||||||
|
eprintln!(
|
||||||
|
" - Transparent P2SH ({})",
|
||||||
|
ZcashAddress::from_transparent_p2sh(addr.net, data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
unified::Receiver::Unknown { typecode, data } => {
|
||||||
|
eprintln!(" - Unknown");
|
||||||
|
eprintln!(" - Typecode: {}", typecode);
|
||||||
|
eprintln!(" - Payload: {}", hex::encode(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AddressKind::P2pkh(data) => {
|
||||||
|
eprintln!(
|
||||||
|
" - Corresponding TEX: {}",
|
||||||
|
ZcashAddress::from_tex(addr.net, data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
AddressKind::Tex(data) => {
|
||||||
|
eprintln!(
|
||||||
|
" - Corresponding P2PKH: {}",
|
||||||
|
ZcashAddress::from_transparent_p2pkh(addr.net, data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,401 @@
|
||||||
|
// To silence lints in the `uint::construct_uint!` macro.
|
||||||
|
#![allow(clippy::assign_op_pattern)]
|
||||||
|
#![allow(clippy::ptr_offset_with_cast)]
|
||||||
|
|
||||||
|
use std::cmp;
|
||||||
|
use std::convert::{TryFrom, TryInto};
|
||||||
|
use std::io::{self, Read};
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use zcash_encoding::Vector;
|
||||||
|
use zcash_primitives::{
|
||||||
|
block::BlockHeader,
|
||||||
|
consensus::{BlockHeight, BranchId, Network, NetworkUpgrade, Parameters},
|
||||||
|
transaction::Transaction,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
transaction::{extract_height_from_coinbase, is_coinbase},
|
||||||
|
Context, ZUint256,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MIN_BLOCK_VERSION: i32 = 4;
|
||||||
|
|
||||||
|
uint::construct_uint! {
|
||||||
|
pub(crate) struct U256(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl U256 {
|
||||||
|
fn from_compact(compact: u32) -> (Self, bool, bool) {
|
||||||
|
let size = compact >> 24;
|
||||||
|
let word = compact & 0x007fffff;
|
||||||
|
let result = if size <= 3 {
|
||||||
|
U256::from(word >> (8 * (3 - size)))
|
||||||
|
} else {
|
||||||
|
U256::from(word) << (8 * (size - 3))
|
||||||
|
};
|
||||||
|
(
|
||||||
|
result,
|
||||||
|
word != 0 && (compact & 0x00800000) != 0,
|
||||||
|
word != 0
|
||||||
|
&& ((size > 34) || (word > 0xff && size > 33) || (word > 0xffff && size > 32)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait BlockParams: Parameters {
|
||||||
|
fn equihash_n(&self) -> u32;
|
||||||
|
fn equihash_k(&self) -> u32;
|
||||||
|
fn pow_limit(&self) -> U256;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlockParams for Network {
|
||||||
|
fn equihash_n(&self) -> u32 {
|
||||||
|
match self {
|
||||||
|
Self::MainNetwork | Self::TestNetwork => 200,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn equihash_k(&self) -> u32 {
|
||||||
|
match self {
|
||||||
|
Self::MainNetwork | Self::TestNetwork => 9,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pow_limit(&self) -> U256 {
|
||||||
|
match self {
|
||||||
|
Self::MainNetwork => U256::from_big_endian(
|
||||||
|
&hex::decode("0007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
Self::TestNetwork => U256::from_big_endian(
|
||||||
|
&hex::decode("07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn guess_params(header: &BlockHeader) -> Option<Network> {
|
||||||
|
// If the block target falls between the testnet and mainnet powLimit, assume testnet.
|
||||||
|
let (target, is_negative, did_overflow) = U256::from_compact(header.bits);
|
||||||
|
if !(is_negative || did_overflow)
|
||||||
|
&& target > Network::MainNetwork.pow_limit()
|
||||||
|
&& target <= Network::TestNetwork.pow_limit()
|
||||||
|
{
|
||||||
|
return Some(Network::TestNetwork);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_equihash_solution(header: &BlockHeader, params: Network) -> Result<(), equihash::Error> {
|
||||||
|
let eh_input = {
|
||||||
|
let mut eh_input = vec![];
|
||||||
|
header.write(&mut eh_input).unwrap();
|
||||||
|
eh_input.truncate(4 + 32 + 32 + 32 + 4 + 4);
|
||||||
|
eh_input
|
||||||
|
};
|
||||||
|
equihash::is_valid_solution(
|
||||||
|
params.equihash_n(),
|
||||||
|
params.equihash_k(),
|
||||||
|
&eh_input,
|
||||||
|
&header.nonce,
|
||||||
|
&header.solution,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_proof_of_work(header: &BlockHeader, params: Network) -> Result<(), &str> {
|
||||||
|
let (target, is_negative, did_overflow) = U256::from_compact(header.bits);
|
||||||
|
let hash = U256::from_little_endian(&header.hash().0);
|
||||||
|
|
||||||
|
if is_negative {
|
||||||
|
Err("nBits is negative")
|
||||||
|
} else if target.is_zero() {
|
||||||
|
Err("target is zero")
|
||||||
|
} else if did_overflow {
|
||||||
|
Err("nBits overflowed")
|
||||||
|
} else if target > params.pow_limit() {
|
||||||
|
Err("target is larger than powLimit")
|
||||||
|
} else if hash > target {
|
||||||
|
Err("block hash larger than target")
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_block_commitments_hash(
|
||||||
|
chain_history_root: [u8; 32],
|
||||||
|
auth_data_root: [u8; 32],
|
||||||
|
) -> [u8; 32] {
|
||||||
|
blake2b_simd::Params::new()
|
||||||
|
.hash_length(32)
|
||||||
|
.personal(b"ZcashBlockCommit")
|
||||||
|
.to_state()
|
||||||
|
.update(&chain_history_root)
|
||||||
|
.update(&auth_data_root)
|
||||||
|
.update(&[0; 32])
|
||||||
|
.finalize()
|
||||||
|
.as_bytes()
|
||||||
|
.try_into()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Block {
|
||||||
|
header: BlockHeader,
|
||||||
|
txs: Vec<Transaction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Block {
|
||||||
|
pub(crate) fn read<R: Read>(mut reader: R) -> io::Result<Self> {
|
||||||
|
let header = BlockHeader::read(&mut reader)?;
|
||||||
|
let txs = Vector::read(reader, |r| Transaction::read(r, BranchId::Sprout))?;
|
||||||
|
|
||||||
|
Ok(Block { header, txs })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn guess_params(&self) -> Option<Network> {
|
||||||
|
guess_params(&self.header)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_height(&self) -> Option<BlockHeight> {
|
||||||
|
self.txs.first().and_then(extract_height_from_coinbase)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the Merkle tree for this block and returns its root.
|
||||||
|
///
|
||||||
|
/// The returned `bool` indicates whether mutation was detected in the Merkle tree (a
|
||||||
|
/// duplication of transactions in the block leading to an identical Merkle root).
|
||||||
|
fn build_merkle_root(&self) -> ([u8; 32], bool) {
|
||||||
|
// Safe upper bound for the number of total nodes.
|
||||||
|
let mut merkle_tree = Vec::with_capacity(self.txs.len() * 2 + 16);
|
||||||
|
for tx in &self.txs {
|
||||||
|
merkle_tree.push(sha2::digest::generic_array::GenericArray::from(
|
||||||
|
*tx.txid().as_ref(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut size = self.txs.len();
|
||||||
|
let mut j = 0;
|
||||||
|
let mut mutated = false;
|
||||||
|
while size > 1 {
|
||||||
|
let mut i = 0;
|
||||||
|
while i < size {
|
||||||
|
let i2 = cmp::min(i + 1, size - 1);
|
||||||
|
if i2 == i + 1 && i2 + 1 == size && merkle_tree[j + i] == merkle_tree[j + i2] {
|
||||||
|
// Two identical hashes at the end of the list at a particular level.
|
||||||
|
mutated = true;
|
||||||
|
}
|
||||||
|
let mut inner_hasher = Sha256::new();
|
||||||
|
inner_hasher.update(merkle_tree[j + i]);
|
||||||
|
inner_hasher.update(merkle_tree[j + i2]);
|
||||||
|
merkle_tree.push(Sha256::digest(inner_hasher.finalize()));
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
j += size;
|
||||||
|
size = (size + 1) / 2;
|
||||||
|
}
|
||||||
|
(
|
||||||
|
merkle_tree
|
||||||
|
.last()
|
||||||
|
.copied()
|
||||||
|
.map(|root| root.into())
|
||||||
|
.unwrap_or([0; 32]),
|
||||||
|
mutated,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_auth_data_root(&self) -> [u8; 32] {
|
||||||
|
fn next_pow2(x: u64) -> u64 {
|
||||||
|
// Fails if `x` is greater than `1u64 << 63`, but this can't occur because a
|
||||||
|
// block can't feasibly contain that many transactions.
|
||||||
|
1u64 << (64 - x.saturating_sub(1).leading_zeros())
|
||||||
|
}
|
||||||
|
|
||||||
|
let perfect_size = next_pow2(self.txs.len() as u64) as usize;
|
||||||
|
assert_eq!((perfect_size & (perfect_size - 1)), 0);
|
||||||
|
let expected_size = cmp::max(perfect_size * 2, 1) - 1; // The total number of nodes.
|
||||||
|
let mut tree = Vec::with_capacity(expected_size);
|
||||||
|
|
||||||
|
// Add the leaves to the tree. v1-v4 transactions will append empty leaves.
|
||||||
|
for tx in &self.txs {
|
||||||
|
tree.push(<[u8; 32]>::try_from(tx.auth_commitment().as_bytes()).unwrap());
|
||||||
|
}
|
||||||
|
// Append empty leaves until we get a perfect tree.
|
||||||
|
tree.resize(perfect_size, [0; 32]);
|
||||||
|
|
||||||
|
let mut j = 0;
|
||||||
|
let mut layer_width = perfect_size;
|
||||||
|
while layer_width > 1 {
|
||||||
|
let mut i = 0;
|
||||||
|
while i < layer_width {
|
||||||
|
tree.push(
|
||||||
|
blake2b_simd::Params::new()
|
||||||
|
.hash_length(32)
|
||||||
|
.personal(b"ZcashAuthDatHash")
|
||||||
|
.to_state()
|
||||||
|
.update(&tree[j + i])
|
||||||
|
.update(&tree[j + i + 1])
|
||||||
|
.finalize()
|
||||||
|
.as_bytes()
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to the next layer.
|
||||||
|
j += layer_width;
|
||||||
|
layer_width /= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(tree.len(), expected_size);
|
||||||
|
tree.last().copied().unwrap_or([0; 32])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inspect_header(header: &BlockHeader, context: Option<Context>) {
|
||||||
|
eprintln!("Zcash block header");
|
||||||
|
inspect_header_inner(
|
||||||
|
header,
|
||||||
|
guess_params(header).or_else(|| context.and_then(|c| c.network())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inspect_header_inner(header: &BlockHeader, params: Option<Network>) {
|
||||||
|
eprintln!(" - Hash: {}", header.hash());
|
||||||
|
eprintln!(" - Version: {}", header.version);
|
||||||
|
if header.version < MIN_BLOCK_VERSION {
|
||||||
|
// zcashd: version-too-low
|
||||||
|
eprintln!("⚠️ Version too low",);
|
||||||
|
}
|
||||||
|
if let Some(params) = params {
|
||||||
|
if let Err(e) = check_equihash_solution(header, params) {
|
||||||
|
// zcashd: invalid-solution
|
||||||
|
eprintln!("⚠️ Invalid Equihash solution: {}", e);
|
||||||
|
}
|
||||||
|
if let Err(e) = check_proof_of_work(header, params) {
|
||||||
|
// zcashd: high-hash
|
||||||
|
eprintln!("⚠️ Invalid Proof-of-Work: {}", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("🔎 To check contextual rules, add \"network\" to context (either \"main\" or \"test\")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inspect(block: &Block, context: Option<Context>) {
|
||||||
|
eprintln!("Zcash block");
|
||||||
|
let params = block
|
||||||
|
.guess_params()
|
||||||
|
.or_else(|| context.as_ref().and_then(|c| c.network()));
|
||||||
|
inspect_header_inner(&block.header, params);
|
||||||
|
|
||||||
|
let height = match block.txs.len() {
|
||||||
|
0 => {
|
||||||
|
// zcashd: bad-cb-missing
|
||||||
|
eprintln!("⚠️ Missing coinbase transaction");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
txs => {
|
||||||
|
eprintln!(" - {} transaction(s) including coinbase", txs);
|
||||||
|
|
||||||
|
if !is_coinbase(&block.txs[0]) {
|
||||||
|
// zcashd: bad-cb-missing
|
||||||
|
eprintln!("⚠️ vtx[0] is not a coinbase transaction");
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let height = block.extract_height();
|
||||||
|
match height {
|
||||||
|
Some(h) => eprintln!(" - Height: {}", h),
|
||||||
|
// zcashd: bad-cb-height
|
||||||
|
None => eprintln!("⚠️ No height in coinbase transaction"),
|
||||||
|
}
|
||||||
|
height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (i, tx) in block.txs.iter().enumerate().skip(1) {
|
||||||
|
if is_coinbase(tx) {
|
||||||
|
// zcashd: bad-cb-multiple
|
||||||
|
eprintln!("⚠️ vtx[{}] is a coinbase transaction", i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (merkle_root, merkle_root_mutated) = block.build_merkle_root();
|
||||||
|
if merkle_root != block.header.merkle_root {
|
||||||
|
// zcashd: bad-txnmrklroot
|
||||||
|
eprintln!("⚠️ header.merkleroot doesn't match transaction Merkle tree root");
|
||||||
|
eprintln!(" - merkleroot (calc): {}", ZUint256(merkle_root));
|
||||||
|
eprintln!(
|
||||||
|
" - header.merkleroot: {}",
|
||||||
|
ZUint256(block.header.merkle_root)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if merkle_root_mutated {
|
||||||
|
// zcashd: bad-txns-duplicate
|
||||||
|
eprintln!("⚠️ Transaction Merkle tree is malleable");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rest of the checks require network parameters and a block height.
|
||||||
|
let (params, height) = match (params, height) {
|
||||||
|
(Some(params), Some(height)) => (params, height),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
if params.is_nu_active(NetworkUpgrade::Nu5, height) {
|
||||||
|
if block.txs[0].expiry_height() != height {
|
||||||
|
// zcashd: bad-cb-height
|
||||||
|
eprintln!(
|
||||||
|
"⚠️ [NU5] coinbase expiry height ({}) doesn't match coinbase scriptSig height ({})",
|
||||||
|
block.txs[0].expiry_height(),
|
||||||
|
height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(chain_history_root) = context.and_then(|c| c.chainhistoryroot) {
|
||||||
|
let auth_data_root = block.build_auth_data_root();
|
||||||
|
let block_commitments_hash =
|
||||||
|
derive_block_commitments_hash(chain_history_root.0, auth_data_root);
|
||||||
|
|
||||||
|
if block_commitments_hash != block.header.final_sapling_root {
|
||||||
|
// zcashd: bad-block-commitments-hash
|
||||||
|
eprintln!(
|
||||||
|
"⚠️ [NU5] header.blockcommitments doesn't match ZIP 244 block commitment"
|
||||||
|
);
|
||||||
|
eprintln!(" - chainhistoryroot: {}", chain_history_root);
|
||||||
|
eprintln!(" - authdataroot: {}", ZUint256(auth_data_root));
|
||||||
|
eprintln!(
|
||||||
|
" - blockcommitments (calc): {}",
|
||||||
|
ZUint256(block_commitments_hash)
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
" - header.blockcommitments: {}",
|
||||||
|
ZUint256(block.header.final_sapling_root)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("🔎 To check header.blockcommitments, add \"chainhistoryroot\" to context");
|
||||||
|
}
|
||||||
|
} else if Some(height) == params.activation_height(NetworkUpgrade::Heartwood) {
|
||||||
|
if block.header.final_sapling_root != [0; 32] {
|
||||||
|
// zcashd: bad-heartwood-root-in-block
|
||||||
|
eprintln!("⚠️ This is the block that activates Heartwood but header.blockcommitments is not null");
|
||||||
|
}
|
||||||
|
} else if params.is_nu_active(NetworkUpgrade::Heartwood, height) {
|
||||||
|
if let Some(chain_history_root) = context.and_then(|c| c.chainhistoryroot) {
|
||||||
|
if chain_history_root.0 != block.header.final_sapling_root {
|
||||||
|
// zcashd: bad-heartwood-root-in-block
|
||||||
|
eprintln!(
|
||||||
|
"⚠️ [Heartwood] header.blockcommitments doesn't match provided chain history root"
|
||||||
|
);
|
||||||
|
eprintln!(" - chainhistoryroot: {}", chain_history_root);
|
||||||
|
eprintln!(
|
||||||
|
" - header.blockcommitments: {}",
|
||||||
|
ZUint256(block.header.final_sapling_root)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("🔎 To check header.blockcommitments, add \"chainhistoryroot\" to context");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,325 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use serde::{
|
||||||
|
de::{Unexpected, Visitor},
|
||||||
|
Deserialize, Serialize, Serializer,
|
||||||
|
};
|
||||||
|
use zcash_primitives::{
|
||||||
|
consensus::Network,
|
||||||
|
legacy::Script,
|
||||||
|
transaction::components::{amount::NonNegativeAmount, transparent, TxOut},
|
||||||
|
zip32::AccountId,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub(crate) struct JsonNetwork(Network);
|
||||||
|
|
||||||
|
struct JsonNetworkVisitor;
|
||||||
|
|
||||||
|
impl Visitor<'_> for JsonNetworkVisitor {
|
||||||
|
type Value = JsonNetwork;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("either \"main\" or \"test\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
match v {
|
||||||
|
"main" => Ok(JsonNetwork(Network::MainNetwork)),
|
||||||
|
"test" => Ok(JsonNetwork(Network::TestNetwork)),
|
||||||
|
_ => Err(serde::de::Error::invalid_value(
|
||||||
|
Unexpected::Str(v),
|
||||||
|
&"either \"main\" or \"test\"",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for JsonNetwork {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
deserializer.deserialize_str(JsonNetworkVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct JsonAccountId(AccountId);
|
||||||
|
|
||||||
|
struct JsonAccountIdVisitor;
|
||||||
|
|
||||||
|
impl Visitor<'_> 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)))
|
||||||
|
.and_then(|a| {
|
||||||
|
AccountId::try_from(a).map_err(|e| E::custom(format!("AccountId invalid: {}", e)))
|
||||||
|
})
|
||||||
|
.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)))
|
||||||
|
.and_then(|a| {
|
||||||
|
AccountId::try_from(a).map_err(|e| E::custom(format!("AccountId invalid: {}", e)))
|
||||||
|
})
|
||||||
|
.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)))
|
||||||
|
.and_then(|a| {
|
||||||
|
AccountId::try_from(a).map_err(|e| E::custom(format!("AccountId invalid: {}", e)))
|
||||||
|
})
|
||||||
|
.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)))
|
||||||
|
.and_then(|a| {
|
||||||
|
AccountId::try_from(a).map_err(|e| E::custom(format!("AccountId invalid: {}", e)))
|
||||||
|
})
|
||||||
|
.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]);
|
||||||
|
|
||||||
|
struct ZUint256Visitor;
|
||||||
|
|
||||||
|
impl Visitor<'_> for ZUint256Visitor {
|
||||||
|
type Value = ZUint256;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("a hex-encoded 32-byte array")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
let mut data = [0; 32];
|
||||||
|
hex::decode_to_slice(v, &mut data).map_err(|e| match e {
|
||||||
|
hex::FromHexError::InvalidHexCharacter { c, .. } => {
|
||||||
|
serde::de::Error::invalid_value(Unexpected::Char(c), &"valid hex character")
|
||||||
|
}
|
||||||
|
hex::FromHexError::OddLength => {
|
||||||
|
serde::de::Error::invalid_length(v.len(), &"an even-length string")
|
||||||
|
}
|
||||||
|
hex::FromHexError::InvalidStringLength => {
|
||||||
|
serde::de::Error::invalid_length(v.len(), &"a 64-character string")
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
data.reverse();
|
||||||
|
Ok(ZUint256(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for ZUint256 {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
deserializer.deserialize_str(ZUint256Visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ZUint256 {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let mut data = self.0;
|
||||||
|
data.reverse();
|
||||||
|
formatter.write_str(&hex::encode(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct ZOutputValue(NonNegativeAmount);
|
||||||
|
|
||||||
|
struct ZOutputValueVisitor;
|
||||||
|
|
||||||
|
impl Visitor<'_> for ZOutputValueVisitor {
|
||||||
|
type Value = ZOutputValue;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("a non-negative integer number of zatoshis")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
NonNegativeAmount::from_u64(v)
|
||||||
|
.map(ZOutputValue)
|
||||||
|
.map_err(|e| match e {
|
||||||
|
zcash_protocol::value::BalanceError::Overflow => serde::de::Error::invalid_type(
|
||||||
|
Unexpected::Unsigned(v),
|
||||||
|
&"a valid zatoshi amount",
|
||||||
|
),
|
||||||
|
zcash_protocol::value::BalanceError::Underflow => serde::de::Error::invalid_type(
|
||||||
|
Unexpected::Unsigned(v),
|
||||||
|
&"a non-negative zatoshi amount",
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for ZOutputValue {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
deserializer.deserialize_u64(ZOutputValueVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for ZOutputValue {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
serializer.serialize_u64(u64::from(self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct ZScript(Script);
|
||||||
|
|
||||||
|
struct ZScriptVisitor;
|
||||||
|
|
||||||
|
impl Visitor<'_> for ZScriptVisitor {
|
||||||
|
type Value = ZScript;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("a hex-encoded Script")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
let data = hex::decode(v).map_err(|e| match e {
|
||||||
|
hex::FromHexError::InvalidHexCharacter { c, .. } => {
|
||||||
|
serde::de::Error::invalid_value(Unexpected::Char(c), &"valid hex character")
|
||||||
|
}
|
||||||
|
hex::FromHexError::OddLength => {
|
||||||
|
serde::de::Error::invalid_length(v.len(), &"an even-length string")
|
||||||
|
}
|
||||||
|
hex::FromHexError::InvalidStringLength => {
|
||||||
|
serde::de::Error::invalid_length(v.len(), &"a 64-character string")
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
Ok(ZScript(Script(data)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for ZScript {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
deserializer.deserialize_str(ZScriptVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for ZScript {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
serializer.serialize_str(&hex::encode(&self.0 .0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub(crate) struct ZTxOut {
|
||||||
|
value: ZOutputValue,
|
||||||
|
script_pubkey: ZScript,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TxOut> for ZTxOut {
|
||||||
|
fn from(out: TxOut) -> Self {
|
||||||
|
ZTxOut {
|
||||||
|
value: ZOutputValue(out.value),
|
||||||
|
script_pubkey: ZScript(out.script_pubkey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub(crate) struct Context {
|
||||||
|
network: Option<JsonNetwork>,
|
||||||
|
accounts: Option<Vec<JsonAccountId>>,
|
||||||
|
pub(crate) chainhistoryroot: Option<ZUint256>,
|
||||||
|
transparentcoins: Option<Vec<ZTxOut>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Context {
|
||||||
|
type Err = serde_json::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
serde_json::from_str(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub(crate) fn network(&self) -> Option<Network> {
|
||||||
|
self.network.map(|n| n.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn addr_network(&self) -> Option<zcash_address::Network> {
|
||||||
|
self.network().map(|params| match params {
|
||||||
|
Network::MainNetwork => zcash_address::Network::Main,
|
||||||
|
Network::TestNetwork => zcash_address::Network::Test,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|coin| transparent::TxOut {
|
||||||
|
value: coin.value.0,
|
||||||
|
script_pubkey: coin.script_pubkey.0,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,227 @@
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::iter;
|
||||||
|
|
||||||
|
use bech32::{Bech32, Hrp};
|
||||||
|
use secrecy::Zeroize;
|
||||||
|
use zcash_address::{
|
||||||
|
unified::{self, Encoding},
|
||||||
|
ToAddress, ZcashAddress,
|
||||||
|
};
|
||||||
|
use zcash_keys::keys::UnifiedFullViewingKey;
|
||||||
|
use zcash_primitives::{
|
||||||
|
legacy::{
|
||||||
|
keys::{AccountPrivKey, IncomingViewingKey},
|
||||||
|
TransparentAddress,
|
||||||
|
},
|
||||||
|
zip32,
|
||||||
|
};
|
||||||
|
use zcash_protocol::{
|
||||||
|
consensus::{Network, NetworkConstants, NetworkType},
|
||||||
|
local_consensus::LocalNetwork,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::Context;
|
||||||
|
|
||||||
|
pub(crate) mod view;
|
||||||
|
|
||||||
|
pub(crate) fn inspect_mnemonic(mnemonic: bip0039::Mnemonic, context: Option<Context>) {
|
||||||
|
eprintln!("Mnemonic phrase");
|
||||||
|
eprintln!(" - Language: English");
|
||||||
|
|
||||||
|
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,
|
||||||
|
) {
|
||||||
|
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 = sapling::zip32::ExtendedSpendingKey::master(&seed);
|
||||||
|
let sapling_extsk = sapling::zip32::ExtendedSpendingKey::from_path(
|
||||||
|
&sapling_master,
|
||||||
|
&[
|
||||||
|
zip32::ChildIndex::hardened(32),
|
||||||
|
zip32::ChildIndex::hardened(network.coin_type()),
|
||||||
|
account.into(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
#[allow(deprecated)]
|
||||||
|
let sapling_extfvk = sapling_extsk.to_extended_full_viewing_key();
|
||||||
|
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::<Bech32>(
|
||||||
|
Hrp::parse_unchecked(network.hrp_sapling_extended_spending_key()),
|
||||||
|
&sapling_extsk_bytes,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut sapling_extfvk_bytes = vec![];
|
||||||
|
sapling_extfvk.write(&mut sapling_extfvk_bytes).unwrap();
|
||||||
|
eprintln!(
|
||||||
|
" - ExtFVK: {}",
|
||||||
|
bech32::encode::<Bech32>(
|
||||||
|
Hrp::parse_unchecked(network.hrp_sapling_extended_full_viewing_key()),
|
||||||
|
&sapling_extfvk_bytes
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let sapling_addr_bytes = sapling_default_addr.1.to_bytes();
|
||||||
|
eprintln!(
|
||||||
|
" - Default address: {}",
|
||||||
|
bech32::encode::<Bech32>(
|
||||||
|
Hrp::parse_unchecked(network.hrp_sapling_payment_address()),
|
||||||
|
&sapling_addr_bytes,
|
||||||
|
)
|
||||||
|
.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::PublicKeyHash(data) => ZcashAddress::from_transparent_p2pkh(addr_net, data),
|
||||||
|
TransparentAddress::ScriptHash(_) => 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."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inspect_sapling_extsk(data: Vec<u8>, network: NetworkType) {
|
||||||
|
match sapling::zip32::ExtendedSpendingKey::read(&data[..]).map_err(|_| ()) {
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("Invalid encoding that claims to be a Sapling extended spending key");
|
||||||
|
}
|
||||||
|
Ok(extsk) => {
|
||||||
|
eprintln!("Sapling extended spending key");
|
||||||
|
|
||||||
|
let default_addr_bytes = extsk.default_address().1.to_bytes();
|
||||||
|
eprintln!(
|
||||||
|
"- Default address: {}",
|
||||||
|
bech32::encode::<Bech32>(
|
||||||
|
Hrp::parse_unchecked(network.hrp_sapling_payment_address()),
|
||||||
|
&default_addr_bytes,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
if let Ok(ufvk) = UnifiedFullViewingKey::from_sapling_extended_full_viewing_key(
|
||||||
|
extsk.to_extended_full_viewing_key(),
|
||||||
|
) {
|
||||||
|
let encoded_ufvk = match network {
|
||||||
|
NetworkType::Main => ufvk.encode(&Network::MainNetwork),
|
||||||
|
NetworkType::Test => ufvk.encode(&Network::TestNetwork),
|
||||||
|
NetworkType::Regtest => ufvk.encode(&LocalNetwork {
|
||||||
|
overwinter: None,
|
||||||
|
sapling: None,
|
||||||
|
blossom: None,
|
||||||
|
heartwood: None,
|
||||||
|
canopy: None,
|
||||||
|
nu5: None,
|
||||||
|
nu6: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
eprintln!("- UFVK: {encoded_ufvk}");
|
||||||
|
|
||||||
|
let (default_ua, _) = ufvk.default_address(None).expect("should exist");
|
||||||
|
let encoded_ua = match network {
|
||||||
|
NetworkType::Main => default_ua.encode(&Network::MainNetwork),
|
||||||
|
NetworkType::Test => default_ua.encode(&Network::TestNetwork),
|
||||||
|
NetworkType::Regtest => default_ua.encode(&LocalNetwork {
|
||||||
|
overwinter: None,
|
||||||
|
sapling: None,
|
||||||
|
blossom: None,
|
||||||
|
heartwood: None,
|
||||||
|
canopy: None,
|
||||||
|
nu5: None,
|
||||||
|
nu6: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
eprintln!(" - Default address: {encoded_ua}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("WARNING: This spending key is now likely cached in your terminal's history buffer.");
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
use bech32::{Bech32, Hrp};
|
||||||
|
use zcash_address::unified::{self, Container, Encoding};
|
||||||
|
use zcash_keys::keys::UnifiedFullViewingKey;
|
||||||
|
use zcash_protocol::{
|
||||||
|
consensus::{Network, NetworkConstants, NetworkType},
|
||||||
|
local_consensus::LocalNetwork,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn inspect_ufvk(ufvk: unified::Ufvk, network: NetworkType) {
|
||||||
|
eprintln!("Unified full viewing key");
|
||||||
|
eprintln!(
|
||||||
|
" - Network: {}",
|
||||||
|
match network {
|
||||||
|
NetworkType::Main => "main",
|
||||||
|
NetworkType::Test => "testnet",
|
||||||
|
NetworkType::Regtest => "regtest",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
eprintln!(" - Items:");
|
||||||
|
for item in ufvk.items() {
|
||||||
|
match item {
|
||||||
|
unified::Fvk::Orchard(data) => {
|
||||||
|
eprintln!(
|
||||||
|
" - Orchard ({})",
|
||||||
|
unified::Ufvk::try_from_items(vec![unified::Fvk::Orchard(data)])
|
||||||
|
.unwrap()
|
||||||
|
.encode(&network)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
unified::Fvk::Sapling(data) => {
|
||||||
|
eprintln!(
|
||||||
|
" - Sapling ({})",
|
||||||
|
bech32::encode::<Bech32>(
|
||||||
|
Hrp::parse_unchecked(network.hrp_sapling_extended_full_viewing_key()),
|
||||||
|
&data
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
unified::Fvk::P2pkh(data) => {
|
||||||
|
eprintln!(" - Transparent P2PKH");
|
||||||
|
eprintln!(" - Payload: {}", hex::encode(data));
|
||||||
|
}
|
||||||
|
unified::Fvk::Unknown { typecode, data } => {
|
||||||
|
eprintln!(" - Unknown");
|
||||||
|
eprintln!(" - Typecode: {}", typecode);
|
||||||
|
eprintln!(" - Payload: {}", hex::encode(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inspect_uivk(uivk: unified::Uivk, network: NetworkType) {
|
||||||
|
eprintln!("Unified incoming viewing key");
|
||||||
|
eprintln!(
|
||||||
|
" - Network: {}",
|
||||||
|
match network {
|
||||||
|
NetworkType::Main => "main",
|
||||||
|
NetworkType::Test => "testnet",
|
||||||
|
NetworkType::Regtest => "regtest",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
eprintln!(" - Items:");
|
||||||
|
for item in uivk.items() {
|
||||||
|
match item {
|
||||||
|
unified::Ivk::Orchard(data) => {
|
||||||
|
eprintln!(
|
||||||
|
" - Orchard ({})",
|
||||||
|
unified::Uivk::try_from_items(vec![unified::Ivk::Orchard(data)])
|
||||||
|
.unwrap()
|
||||||
|
.encode(&network)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
unified::Ivk::Sapling(data) => {
|
||||||
|
eprintln!(" - Sapling");
|
||||||
|
eprintln!(" - Payload: {}", hex::encode(data));
|
||||||
|
}
|
||||||
|
unified::Ivk::P2pkh(data) => {
|
||||||
|
eprintln!(" - Transparent P2PKH");
|
||||||
|
eprintln!(" - Payload: {}", hex::encode(data));
|
||||||
|
}
|
||||||
|
unified::Ivk::Unknown { typecode, data } => {
|
||||||
|
eprintln!(" - Unknown");
|
||||||
|
eprintln!(" - Typecode: {}", typecode);
|
||||||
|
eprintln!(" - Payload: {}", hex::encode(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inspect_sapling_extfvk(data: Vec<u8>, network: NetworkType) {
|
||||||
|
match sapling::zip32::ExtendedFullViewingKey::read(&data[..]).map_err(|_| ()) {
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("Invalid encoding that claims to be a Sapling extended full viewing key");
|
||||||
|
}
|
||||||
|
Ok(extfvk) => {
|
||||||
|
eprintln!("Sapling extended full viewing key");
|
||||||
|
|
||||||
|
let default_addr_bytes = extfvk.default_address().1.to_bytes();
|
||||||
|
eprintln!(
|
||||||
|
"- Default address: {}",
|
||||||
|
bech32::encode::<Bech32>(
|
||||||
|
Hrp::parse_unchecked(network.hrp_sapling_payment_address()),
|
||||||
|
&default_addr_bytes,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Ok(ufvk) = UnifiedFullViewingKey::from_sapling_extended_full_viewing_key(extfvk)
|
||||||
|
{
|
||||||
|
let encoded_ufvk = match network {
|
||||||
|
NetworkType::Main => ufvk.encode(&Network::MainNetwork),
|
||||||
|
NetworkType::Test => ufvk.encode(&Network::TestNetwork),
|
||||||
|
NetworkType::Regtest => ufvk.encode(&LocalNetwork {
|
||||||
|
overwinter: None,
|
||||||
|
sapling: None,
|
||||||
|
blossom: None,
|
||||||
|
heartwood: None,
|
||||||
|
canopy: None,
|
||||||
|
nu5: None,
|
||||||
|
nu6: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
eprintln!("- Equivalent UFVK: {encoded_ufvk}");
|
||||||
|
|
||||||
|
let (default_ua, _) = ufvk.default_address(None).expect("should exist");
|
||||||
|
let encoded_ua = match network {
|
||||||
|
NetworkType::Main => default_ua.encode(&Network::MainNetwork),
|
||||||
|
NetworkType::Test => default_ua.encode(&Network::TestNetwork),
|
||||||
|
NetworkType::Regtest => default_ua.encode(&LocalNetwork {
|
||||||
|
overwinter: None,
|
||||||
|
sapling: None,
|
||||||
|
blossom: None,
|
||||||
|
heartwood: None,
|
||||||
|
canopy: None,
|
||||||
|
nu5: None,
|
||||||
|
nu6: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
eprintln!(" - Default address: {encoded_ua}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
use tonic::transport::{Channel, ClientTlsConfig};
|
||||||
|
use zcash_client_backend::proto::service::{
|
||||||
|
compact_tx_streamer_client::CompactTxStreamerClient, TxFilter,
|
||||||
|
};
|
||||||
|
use zcash_primitives::transaction::Transaction;
|
||||||
|
use zcash_protocol::consensus::{BlockHeight, BranchId, Network};
|
||||||
|
|
||||||
|
const MAINNET: Server = Server {
|
||||||
|
host: "zec.rocks",
|
||||||
|
port: 443,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TESTNET: Server = Server {
|
||||||
|
host: "testnet.zec.rocks",
|
||||||
|
port: 443,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Server {
|
||||||
|
host: &'static str,
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
fn endpoint(&self) -> String {
|
||||||
|
format!("https://{}:{}", self.host, self.port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect(server: &Server) -> anyhow::Result<CompactTxStreamerClient<Channel>> {
|
||||||
|
let channel = Channel::from_shared(server.endpoint())?;
|
||||||
|
|
||||||
|
let tls = ClientTlsConfig::new()
|
||||||
|
.domain_name(server.host.to_string())
|
||||||
|
.with_webpki_roots();
|
||||||
|
let channel = channel.tls_config(tls)?;
|
||||||
|
|
||||||
|
Ok(CompactTxStreamerClient::new(channel.connect().await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct Lightwalletd {
|
||||||
|
inner: CompactTxStreamerClient<Channel>,
|
||||||
|
parameters: Network,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Lightwalletd {
|
||||||
|
pub(crate) async fn mainnet() -> anyhow::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: connect(&MAINNET).await?,
|
||||||
|
parameters: Network::MainNetwork,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn testnet() -> anyhow::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: connect(&TESTNET).await?,
|
||||||
|
parameters: Network::TestNetwork,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn lookup_txid(
|
||||||
|
&mut self,
|
||||||
|
candidate: [u8; 32],
|
||||||
|
) -> Option<(Transaction, Option<BlockHeight>)> {
|
||||||
|
let request = TxFilter {
|
||||||
|
hash: candidate.into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let response = self.inner.get_transaction(request).await.ok()?.into_inner();
|
||||||
|
|
||||||
|
// `RawTransaction.height` has type u64 in the protobuf format, but is documented
|
||||||
|
// as using -1 for the "not mined" sentinel. Given that we only support u32 block
|
||||||
|
// heights, -1 in two's complement will fall outside that range.
|
||||||
|
let mined_height = response.height.try_into().ok();
|
||||||
|
|
||||||
|
Transaction::read(
|
||||||
|
&response.data[..],
|
||||||
|
mined_height
|
||||||
|
.map(|height| BranchId::for_height(&self.parameters, height))
|
||||||
|
.unwrap_or(BranchId::Nu5),
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.map(|tx| (tx, mined_height))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,563 @@
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
convert::{TryFrom, TryInto},
|
||||||
|
};
|
||||||
|
|
||||||
|
use ::transparent::sighash::SighashType;
|
||||||
|
use bellman::groth16;
|
||||||
|
use group::GroupEncoding;
|
||||||
|
use orchard::note_encryption::OrchardDomain;
|
||||||
|
use sapling::{note_encryption::SaplingDomain, SaplingVerificationContext};
|
||||||
|
use secp256k1::{Secp256k1, VerifyOnly};
|
||||||
|
use zcash_address::{
|
||||||
|
unified::{self, Encoding},
|
||||||
|
ToAddress, ZcashAddress,
|
||||||
|
};
|
||||||
|
use zcash_note_encryption::try_output_recovery_with_ovk;
|
||||||
|
#[allow(deprecated)]
|
||||||
|
use zcash_primitives::{
|
||||||
|
consensus::BlockHeight,
|
||||||
|
legacy::{keys::pubkey_to_address, Script, TransparentAddress},
|
||||||
|
memo::{Memo, MemoBytes},
|
||||||
|
transaction::{
|
||||||
|
components::{amount::NonNegativeAmount, sapling as sapling_serialization, transparent},
|
||||||
|
sighash::{signature_hash, SignableInput, TransparentAuthorizingContext},
|
||||||
|
txid::TxIdDigester,
|
||||||
|
Authorization, Transaction, TransactionData, TxId, TxVersion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
context::{Context, ZTxOut},
|
||||||
|
GROTH16_PARAMS, ORCHARD_VK,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn is_coinbase(tx: &Transaction) -> bool {
|
||||||
|
tx.transparent_bundle()
|
||||||
|
.map(|b| b.is_coinbase())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_height_from_coinbase(tx: &Transaction) -> Option<BlockHeight> {
|
||||||
|
const OP_0: u8 = 0x00;
|
||||||
|
const OP_1NEGATE: u8 = 0x4f;
|
||||||
|
const OP_1: u8 = 0x51;
|
||||||
|
const OP_16: u8 = 0x60;
|
||||||
|
|
||||||
|
tx.transparent_bundle()
|
||||||
|
.and_then(|bundle| bundle.vin.first())
|
||||||
|
.and_then(|input| match input.script_sig.0.first().copied() {
|
||||||
|
// {0, -1} will never occur as the first byte of a coinbase scriptSig.
|
||||||
|
Some(OP_0 | OP_1NEGATE) => None,
|
||||||
|
// Blocks 1 to 16.
|
||||||
|
Some(h @ OP_1..=OP_16) => Some(BlockHeight::from_u32((h - OP_1 + 1).into())),
|
||||||
|
// All other heights use CScriptNum encoding, which will never be longer
|
||||||
|
// than 5 bytes for Zcash heights. These have the format
|
||||||
|
// `[len(encoding)] || encoding`.
|
||||||
|
Some(h @ 1..=5) => {
|
||||||
|
let rest = &input.script_sig.0[1..];
|
||||||
|
let encoding_len = h as usize;
|
||||||
|
if rest.len() < encoding_len {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// Parse the encoding.
|
||||||
|
let encoding = &rest[..encoding_len];
|
||||||
|
if encoding.last().unwrap() & 0x80 != 0 {
|
||||||
|
// Height is never negative.
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let mut height: u64 = 0;
|
||||||
|
for (i, b) in encoding.iter().enumerate() {
|
||||||
|
height |= (*b as u64) << (8 * i);
|
||||||
|
}
|
||||||
|
height.try_into().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Anything else is an invalid height encoding.
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_value(value: u64) -> String {
|
||||||
|
format!(
|
||||||
|
"{} zatoshis ({} ZEC)",
|
||||||
|
value,
|
||||||
|
(value as f64) / 1_0000_0000f64
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_memo(memo_bytes: MemoBytes) -> String {
|
||||||
|
match Memo::try_from(memo_bytes) {
|
||||||
|
Ok(Memo::Empty) => "No memo".to_string(),
|
||||||
|
Ok(Memo::Text(memo)) => format!("Text memo: '{}'", String::from(memo)),
|
||||||
|
Ok(memo) => format!("{:?}", memo),
|
||||||
|
Err(e) => format!("Invalid memo: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct TransparentAuth {
|
||||||
|
all_prev_outputs: Vec<transparent::TxOut>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl transparent::Authorization for TransparentAuth {
|
||||||
|
type ScriptSig = Script;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransparentAuthorizingContext for TransparentAuth {
|
||||||
|
fn input_amounts(&self) -> Vec<NonNegativeAmount> {
|
||||||
|
self.all_prev_outputs
|
||||||
|
.iter()
|
||||||
|
.map(|prevout| prevout.value)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_scriptpubkeys(&self) -> Vec<Script> {
|
||||||
|
self.all_prev_outputs
|
||||||
|
.iter()
|
||||||
|
.map(|prevout| prevout.script_pubkey.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MapTransparent {
|
||||||
|
auth: TransparentAuth,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl transparent::MapAuth<transparent::Authorized, TransparentAuth> for MapTransparent {
|
||||||
|
fn map_script_sig(
|
||||||
|
&self,
|
||||||
|
s: <transparent::Authorized as transparent::Authorization>::ScriptSig,
|
||||||
|
) -> <TransparentAuth as transparent::Authorization>::ScriptSig {
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_authorization(&self, _: transparent::Authorized) -> TransparentAuth {
|
||||||
|
// TODO: This map should consume self, so we can move self.auth
|
||||||
|
self.auth.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct PrecomputedAuth;
|
||||||
|
|
||||||
|
impl Authorization for PrecomputedAuth {
|
||||||
|
type TransparentAuth = TransparentAuth;
|
||||||
|
type SaplingAuth = sapling::bundle::Authorized;
|
||||||
|
type OrchardAuth = orchard::bundle::Authorized;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inspect(
|
||||||
|
tx: Transaction,
|
||||||
|
context: Option<Context>,
|
||||||
|
mined_height: Option<BlockHeight>,
|
||||||
|
) {
|
||||||
|
eprintln!("Zcash transaction");
|
||||||
|
eprintln!(" - ID: {}", tx.txid());
|
||||||
|
if let Some(height) = mined_height {
|
||||||
|
eprintln!(" - Mined in block {}", height);
|
||||||
|
}
|
||||||
|
eprintln!(" - Version: {:?}", tx.version());
|
||||||
|
match tx.version() {
|
||||||
|
// TODO: If pre-v5 and no branch ID provided in context, disable signature checks.
|
||||||
|
TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => (),
|
||||||
|
TxVersion::Zip225 => {
|
||||||
|
eprintln!(" - Consensus branch ID: {:?}", tx.consensus_branch_id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_coinbase = is_coinbase(&tx);
|
||||||
|
let height = if is_coinbase {
|
||||||
|
eprintln!(" - Coinbase");
|
||||||
|
extract_height_from_coinbase(&tx)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let transparent_coins = match (
|
||||||
|
tx.transparent_bundle().map_or(false, |b| !b.vin.is_empty()),
|
||||||
|
context.as_ref().and_then(|ctx| ctx.transparent_coins()),
|
||||||
|
) {
|
||||||
|
(true, coins) => coins,
|
||||||
|
(false, Some(_)) => {
|
||||||
|
eprintln!("⚠️ Context was given \"transparentcoins\" but this transaction has no transparent inputs");
|
||||||
|
Some(vec![])
|
||||||
|
}
|
||||||
|
(false, None) => Some(vec![]),
|
||||||
|
};
|
||||||
|
|
||||||
|
let sighash_params = transparent_coins.as_ref().map(|coins| {
|
||||||
|
let f_transparent = MapTransparent {
|
||||||
|
auth: TransparentAuth {
|
||||||
|
all_prev_outputs: coins.clone(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// We don't have tx.clone()
|
||||||
|
let mut buf = vec![];
|
||||||
|
tx.write(&mut buf).unwrap();
|
||||||
|
let tx = Transaction::read(&buf[..], tx.consensus_branch_id()).unwrap();
|
||||||
|
|
||||||
|
let tx: TransactionData<PrecomputedAuth> =
|
||||||
|
tx.into_data().map_authorization(f_transparent, (), ());
|
||||||
|
let txid_parts = tx.digest(TxIdDigester);
|
||||||
|
(tx, txid_parts)
|
||||||
|
});
|
||||||
|
|
||||||
|
let common_sighash = sighash_params
|
||||||
|
.as_ref()
|
||||||
|
.map(|(tx, txid_parts)| signature_hash(tx, &SignableInput::Shielded, txid_parts));
|
||||||
|
if let Some(sighash) = &common_sighash {
|
||||||
|
if tx.sprout_bundle().is_some()
|
||||||
|
|| tx.sapling_bundle().is_some()
|
||||||
|
|| tx.orchard_bundle().is_some()
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
" - Sighash for shielded signatures: {}",
|
||||||
|
hex::encode(sighash.as_ref()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bundle) = tx.transparent_bundle() {
|
||||||
|
assert!(!(bundle.vin.is_empty() && bundle.vout.is_empty()));
|
||||||
|
if !(bundle.vin.is_empty() || is_coinbase) {
|
||||||
|
eprintln!(" - {} transparent input(s)", bundle.vin.len());
|
||||||
|
if let Some(coins) = &transparent_coins {
|
||||||
|
if bundle.vin.len() != coins.len() {
|
||||||
|
eprintln!(" ⚠️ \"transparentcoins\" has length {}", coins.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, txid_parts) = sighash_params.as_ref().unwrap();
|
||||||
|
let ctx = Secp256k1::<VerifyOnly>::gen_new();
|
||||||
|
|
||||||
|
for (i, (txin, coin)) in bundle.vin.iter().zip(coins).enumerate() {
|
||||||
|
eprintln!(
|
||||||
|
" - prevout: txid {}, index {}",
|
||||||
|
TxId::from_bytes(*txin.prevout.hash()),
|
||||||
|
txin.prevout.n()
|
||||||
|
);
|
||||||
|
match coin.recipient_address() {
|
||||||
|
Some(addr @ TransparentAddress::PublicKeyHash(_)) => {
|
||||||
|
// Format is [sig_and_type_len] || sig || [hash_type] || [pubkey_len] || pubkey
|
||||||
|
// where [x] encodes a single byte.
|
||||||
|
let sig_and_type_len = txin.script_sig.0.first().map(|l| *l as usize);
|
||||||
|
let pubkey_len = sig_and_type_len
|
||||||
|
.and_then(|sig_len| txin.script_sig.0.get(1 + sig_len))
|
||||||
|
.map(|l| *l as usize);
|
||||||
|
let script_len = sig_and_type_len.zip(pubkey_len).map(
|
||||||
|
|(sig_and_type_len, pubkey_len)| {
|
||||||
|
1 + sig_and_type_len + 1 + pubkey_len
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if Some(txin.script_sig.0.len()) != script_len {
|
||||||
|
eprintln!(
|
||||||
|
" ⚠️ \"transparentcoins\" {} is P2PKH; txin {} scriptSig has length {} but data {}",
|
||||||
|
i,
|
||||||
|
i,
|
||||||
|
txin.script_sig.0.len(),
|
||||||
|
if let Some(l) = script_len {
|
||||||
|
format!("implies length {}.", l)
|
||||||
|
} else {
|
||||||
|
"would cause an out-of-bounds read.".to_owned()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let sig_len = sig_and_type_len.unwrap() - 1;
|
||||||
|
|
||||||
|
let sig = secp256k1::ecdsa::Signature::from_der(
|
||||||
|
&txin.script_sig.0[1..1 + sig_len],
|
||||||
|
);
|
||||||
|
let hash_type = SighashType::parse(txin.script_sig.0[1 + sig_len]);
|
||||||
|
let pubkey_bytes = &txin.script_sig.0[1 + sig_len + 2..];
|
||||||
|
let pubkey = secp256k1::PublicKey::from_slice(pubkey_bytes);
|
||||||
|
|
||||||
|
if let Err(e) = sig {
|
||||||
|
eprintln!(
|
||||||
|
" ⚠️ Txin {} has invalid signature encoding: {}",
|
||||||
|
i, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if hash_type.is_none() {
|
||||||
|
eprintln!(" ⚠️ Txin {} has invalid sighash type", i);
|
||||||
|
}
|
||||||
|
if let Err(e) = pubkey {
|
||||||
|
eprintln!(
|
||||||
|
" ⚠️ Txin {} has invalid pubkey encoding: {}",
|
||||||
|
i, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let (Ok(sig), Some(hash_type), Ok(pubkey)) =
|
||||||
|
(sig, hash_type, pubkey)
|
||||||
|
{
|
||||||
|
#[allow(deprecated)]
|
||||||
|
if pubkey_to_address(&pubkey) != addr {
|
||||||
|
eprintln!(" ⚠️ Txin {} pubkey does not match coin's script_pubkey", i);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sighash = signature_hash(
|
||||||
|
tx,
|
||||||
|
&SignableInput::Transparent(
|
||||||
|
::transparent::sighash::SignableInput::from_parts(
|
||||||
|
hash_type,
|
||||||
|
i,
|
||||||
|
// For P2PKH these are the same.
|
||||||
|
&coin.script_pubkey,
|
||||||
|
&coin.script_pubkey,
|
||||||
|
coin.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
txid_parts,
|
||||||
|
);
|
||||||
|
let msg = secp256k1::Message::from_slice(sighash.as_ref())
|
||||||
|
.expect("signature_hash() returns correct length");
|
||||||
|
|
||||||
|
if let Err(e) = ctx.verify_ecdsa(&msg, &sig, &pubkey) {
|
||||||
|
eprintln!(" ⚠️ Spend {} is invalid: {}", i, e);
|
||||||
|
eprintln!(
|
||||||
|
" - sighash is {}",
|
||||||
|
hex::encode(sighash.as_ref())
|
||||||
|
);
|
||||||
|
eprintln!(" - pubkey is {}", hex::encode(pubkey_bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Check P2SH structure.
|
||||||
|
Some(TransparentAddress::ScriptHash(_)) => {
|
||||||
|
eprintln!(" 🔎 \"transparentcoins\"[{}] is a P2SH coin.", i);
|
||||||
|
}
|
||||||
|
// TODO: Check arbitrary scripts.
|
||||||
|
None => {
|
||||||
|
eprintln!(
|
||||||
|
" 🔎 \"transparentcoins\"[{}] has a script we can't check yet.",
|
||||||
|
i
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
" 🔎 To check transparent inputs, add \"transparentcoins\" array to context."
|
||||||
|
);
|
||||||
|
eprintln!(" The following transparent inputs are required: ");
|
||||||
|
for txin in &bundle.vin {
|
||||||
|
eprintln!(
|
||||||
|
" - txid {}, index {}",
|
||||||
|
TxId::from_bytes(*txin.prevout.hash()),
|
||||||
|
txin.prevout.n()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !bundle.vout.is_empty() {
|
||||||
|
eprintln!(" - {} transparent output(s)", bundle.vout.len());
|
||||||
|
for txout in &bundle.vout {
|
||||||
|
eprintln!(
|
||||||
|
" - {}",
|
||||||
|
serde_json::to_string(&ZTxOut::from(txout.clone())).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bundle) = tx.sprout_bundle() {
|
||||||
|
eprintln!(" - {} Sprout JoinSplit(s)", bundle.joinsplits.len());
|
||||||
|
|
||||||
|
// TODO: Verify Sprout proofs once we can access the Sprout bundle parts.
|
||||||
|
|
||||||
|
match ed25519_zebra::VerificationKey::try_from(bundle.joinsplit_pubkey) {
|
||||||
|
Err(e) => eprintln!(" ⚠️ joinsplitPubkey is invalid: {}", e),
|
||||||
|
Ok(vk) => {
|
||||||
|
if let Some(sighash) = &common_sighash {
|
||||||
|
if let Err(e) = vk.verify(
|
||||||
|
&ed25519_zebra::Signature::from(bundle.joinsplit_sig),
|
||||||
|
sighash.as_ref(),
|
||||||
|
) {
|
||||||
|
eprintln!(" ⚠️ joinsplitSig is invalid: {}", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
" 🔎 To check Sprout JoinSplit(s), add \"transparentcoins\" array to context"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bundle) = tx.sapling_bundle() {
|
||||||
|
assert!(!(bundle.shielded_spends().is_empty() && bundle.shielded_outputs().is_empty()));
|
||||||
|
|
||||||
|
// TODO: Separate into checking proofs, signatures, and other structural details.
|
||||||
|
let mut ctx = SaplingVerificationContext::new();
|
||||||
|
|
||||||
|
if !bundle.shielded_spends().is_empty() {
|
||||||
|
eprintln!(" - {} Sapling Spend(s)", bundle.shielded_spends().len());
|
||||||
|
if let Some(sighash) = &common_sighash {
|
||||||
|
for (i, spend) in bundle.shielded_spends().iter().enumerate() {
|
||||||
|
if !ctx.check_spend(
|
||||||
|
spend.cv(),
|
||||||
|
*spend.anchor(),
|
||||||
|
&spend.nullifier().0,
|
||||||
|
*spend.rk(),
|
||||||
|
sighash.as_ref(),
|
||||||
|
*spend.spend_auth_sig(),
|
||||||
|
groth16::Proof::read(&spend.zkproof()[..]).unwrap(),
|
||||||
|
&GROTH16_PARAMS.spend_vk,
|
||||||
|
) {
|
||||||
|
eprintln!(" ⚠️ Spend {} is invalid", i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
" 🔎 To check Sapling Spend(s), add \"transparentcoins\" array to context"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bundle.shielded_outputs().is_empty() {
|
||||||
|
eprintln!(" - {} Sapling Output(s)", bundle.shielded_outputs().len());
|
||||||
|
for (i, output) in bundle.shielded_outputs().iter().enumerate() {
|
||||||
|
if is_coinbase {
|
||||||
|
if let Some((params, addr_net)) = context
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|ctx| ctx.network().zip(ctx.addr_network()))
|
||||||
|
{
|
||||||
|
if let Some((note, addr, memo)) = try_output_recovery_with_ovk(
|
||||||
|
&SaplingDomain::new(sapling_serialization::zip212_enforcement(
|
||||||
|
¶ms,
|
||||||
|
height.unwrap(),
|
||||||
|
)),
|
||||||
|
&sapling::keys::OutgoingViewingKey([0; 32]),
|
||||||
|
output,
|
||||||
|
output.cv(),
|
||||||
|
output.out_ciphertext(),
|
||||||
|
) {
|
||||||
|
if note.value().inner() == 0 {
|
||||||
|
eprintln!(" - Output {} (dummy output):", i);
|
||||||
|
} else {
|
||||||
|
let zaddr = ZcashAddress::from_sapling(addr_net, addr.to_bytes());
|
||||||
|
|
||||||
|
eprintln!(" - Output {}:", i);
|
||||||
|
eprintln!(" - {}", zaddr);
|
||||||
|
eprintln!(" - {}", render_value(note.value().inner()));
|
||||||
|
}
|
||||||
|
let memo = MemoBytes::from_bytes(&memo).expect("correct length");
|
||||||
|
eprintln!(" - {}", render_memo(memo));
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
" ⚠️ Output {} is not recoverable with the all-zeros OVK",
|
||||||
|
i
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!(" 🔎 To check Sapling coinbase rules, add \"network\" to context (either \"main\" or \"test\")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.check_output(
|
||||||
|
output.cv(),
|
||||||
|
*output.cmu(),
|
||||||
|
jubjub::ExtendedPoint::from_bytes(&output.ephemeral_key().0).unwrap(),
|
||||||
|
groth16::Proof::read(&output.zkproof()[..]).unwrap(),
|
||||||
|
&GROTH16_PARAMS.output_vk,
|
||||||
|
) {
|
||||||
|
eprintln!(" ⚠️ Output {} is invalid", i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(sighash) = &common_sighash {
|
||||||
|
if !ctx.final_check(
|
||||||
|
*bundle.value_balance(),
|
||||||
|
sighash.as_ref(),
|
||||||
|
bundle.authorization().binding_sig,
|
||||||
|
) {
|
||||||
|
eprintln!("⚠️ Sapling bindingSig is invalid");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("🔎 To check Sapling bindingSig, add \"transparentcoins\" array to context");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bundle) = tx.orchard_bundle() {
|
||||||
|
eprintln!(" - {} Orchard Action(s)", bundle.actions().len());
|
||||||
|
|
||||||
|
// Orchard nullifiers must not be duplicated within a transaction.
|
||||||
|
let mut nullifiers = HashMap::<[u8; 32], Vec<usize>>::default();
|
||||||
|
for (i, action) in bundle.actions().iter().enumerate() {
|
||||||
|
nullifiers
|
||||||
|
.entry(action.nullifier().to_bytes())
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(i);
|
||||||
|
}
|
||||||
|
for (_, indices) in nullifiers {
|
||||||
|
if indices.len() > 1 {
|
||||||
|
eprintln!("⚠️ Nullifier is duplicated between actions {:?}", indices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_coinbase {
|
||||||
|
// All coinbase outputs must be decryptable with the all-zeroes OVK.
|
||||||
|
for (i, action) in bundle.actions().iter().enumerate() {
|
||||||
|
let ovk = orchard::keys::OutgoingViewingKey::from([0u8; 32]);
|
||||||
|
if let Some((note, addr, memo)) = try_output_recovery_with_ovk(
|
||||||
|
&OrchardDomain::for_action(action),
|
||||||
|
&ovk,
|
||||||
|
action,
|
||||||
|
action.cv_net(),
|
||||||
|
&action.encrypted_note().out_ciphertext,
|
||||||
|
) {
|
||||||
|
if note.value().inner() == 0 {
|
||||||
|
eprintln!(" - Output {} (dummy output):", i);
|
||||||
|
} else {
|
||||||
|
eprintln!(" - Output {}:", i);
|
||||||
|
|
||||||
|
if let Some(net) = context.as_ref().and_then(|ctx| ctx.addr_network()) {
|
||||||
|
assert_eq!(note.recipient(), addr);
|
||||||
|
// Construct a single-receiver UA.
|
||||||
|
let zaddr = ZcashAddress::from_unified(
|
||||||
|
net,
|
||||||
|
unified::Address::try_from_items(vec![unified::Receiver::Orchard(
|
||||||
|
addr.to_raw_address_bytes(),
|
||||||
|
)])
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
eprintln!(" - {}", zaddr);
|
||||||
|
} else {
|
||||||
|
eprintln!(" 🔎 To show recipient address, add \"network\" to context (either \"main\" or \"test\")");
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!(" - {}", render_value(note.value().inner()));
|
||||||
|
}
|
||||||
|
eprintln!(
|
||||||
|
" - {}",
|
||||||
|
render_memo(MemoBytes::from_bytes(&memo).unwrap())
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
" ⚠️ Output {} is not recoverable with the all-zeros OVK",
|
||||||
|
i
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(sighash) = &common_sighash {
|
||||||
|
for (i, action) in bundle.actions().iter().enumerate() {
|
||||||
|
if let Err(e) = action.rk().verify(sighash.as_ref(), action.authorization()) {
|
||||||
|
eprintln!(" ⚠️ Action {} spendAuthSig is invalid: {}", i, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"🔎 To check Orchard Action signatures, add \"transparentcoins\" array to context"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = bundle.verify_proof(&ORCHARD_VK) {
|
||||||
|
eprintln!("⚠️ Orchard proof is invalid: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,6 +40,9 @@ pub(crate) struct MyOptions {
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub(crate) enum Command {
|
pub(crate) enum Command {
|
||||||
|
/// Inspect Zcash-related data
|
||||||
|
Inspect(commands::inspect::Command),
|
||||||
|
|
||||||
/// Manipulate a local wallet backed by `zcash_client_sqlite`
|
/// Manipulate a local wallet backed by `zcash_client_sqlite`
|
||||||
Wallet(commands::Wallet),
|
Wallet(commands::Wallet),
|
||||||
|
|
||||||
|
@ -114,6 +117,7 @@ fn main() -> Result<(), anyhow::Error> {
|
||||||
let shutdown = ShutdownListener::new();
|
let shutdown = ShutdownListener::new();
|
||||||
|
|
||||||
match opts.command {
|
match opts.command {
|
||||||
|
Some(Command::Inspect(command)) => command.run().await,
|
||||||
Some(Command::Wallet(commands::Wallet {
|
Some(Command::Wallet(commands::Wallet {
|
||||||
wallet_dir,
|
wallet_dir,
|
||||||
command,
|
command,
|
||||||
|
|
Loading…
Reference in New Issue