Merge pull request #5617 from str4d/zcash-inspect

rust: Add `zcash-inspect` binary for inspecting Zcash data
This commit is contained in:
str4d 2022-08-22 17:24:59 +01:00 committed by GitHub
commit 520b58f632
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1726 additions and 5 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@ src/zcashd
src/zcashd-wallet-tool
src/zcash-cli
src/zcash-gtest
src/zcash-inspect
src/zcash-tx
src/test/test_bitcoin
zcutil/bin/

25
Cargo.lock generated
View File

@ -950,6 +950,7 @@ version = "0.2.0"
dependencies = [
"anyhow",
"backtrace",
"bech32",
"bellman",
"blake2b_simd",
"blake2s_simd",
@ -959,11 +960,14 @@ dependencies = [
"crossbeam-channel",
"cxx",
"ed25519-zebra",
"equihash",
"group",
"gumdrop",
"hex",
"incrementalmerkletree",
"ipnet",
"jubjub",
"lazy_static",
"libc",
"memuse",
"metrics",
@ -975,6 +979,9 @@ dependencies = [
"rayon",
"secp256k1",
"secrecy",
"serde",
"serde_json",
"sha2",
"subtle",
"thiserror",
"time",
@ -982,6 +989,7 @@ dependencies = [
"tracing-appender",
"tracing-core",
"tracing-subscriber",
"uint",
"zcash_address",
"zcash_encoding",
"zcash_history",
@ -1716,6 +1724,12 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6"
[[package]]
name = "ryu"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -1769,6 +1783,17 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha2"
version = "0.9.9"

View File

@ -28,6 +28,10 @@ name = "rustzcash"
path = "src/rust/src/rustzcash.rs"
crate-type = ["staticlib"]
[[bin]]
name = "zcash-inspect"
path = "src/rust/bin/inspect/main.rs"
[[bin]]
name = "zcashd-wallet-tool"
path = "src/rust/bin/wallet_tool.rs"
@ -57,7 +61,7 @@ zcash_encoding = "0.1"
zcash_history = "0.3"
zcash_note_encryption = "0.1"
zcash_primitives = { version = "0.7", features = ["transparent-inputs"] }
zcash_proofs = "0.7.1"
zcash_proofs = { version = "0.7.1", features = ["directories"] }
ed25519-zebra = "3"
zeroize = "1.4.2"
@ -73,12 +77,24 @@ ipnet = "2"
metrics = "0.19"
metrics-exporter-prometheus = "0.10"
# General tool dependencies
gumdrop = "0.8"
# zcash-inspect tool
bech32 = "0.8"
equihash = "0.2"
hex = "0.4"
lazy_static = "1.4"
serde = "1.0"
serde_json = "1.0"
sha2 = "0.9"
uint = "0.9"
# Wallet tool
# (also depends on tracing, and tracing-subscriber with "env-filter" and "fmt" features)
anyhow = "1.0"
backtrace = "0.3"
clearscreen = "1.0"
gumdrop = "0.8"
rand = "0.8"
secrecy = "0.8"
thiserror = "1"

View File

@ -657,6 +657,10 @@ criteria = "safe-to-deploy"
version = "2.1.0"
criteria = "safe-to-deploy"
[[exemptions.ryu]]
version = "1.0.10"
criteria = "safe-to-deploy"
[[exemptions.scopeguard]]
version = "1.1.0"
criteria = "safe-to-deploy"
@ -681,6 +685,10 @@ criteria = "safe-to-deploy"
version = "1.0.136"
criteria = "safe-to-deploy"
[[exemptions.serde_json]]
version = "1.0.81"
criteria = "safe-to-deploy"
[[exemptions.sha2]]
version = "0.9.9"
criteria = "safe-to-deploy"

View File

@ -33,6 +33,9 @@ LIBSECP256K1=secp256k1/libsecp256k1.la
LIBUNIVALUE=univalue/libunivalue.la
LIBZCASH=libzcash.a
INSPECT_TOOL_BIN=zcash-inspect$(EXEEXT)
INSPECT_TOOL_BUILD=$(top_builddir)/target/$(RUST_TARGET)/release/zcash-inspect$(EXEEXT)
WALLET_TOOL_BIN=zcashd-wallet-tool$(EXEEXT)
WALLET_TOOL_BUILD=$(top_builddir)/target/$(RUST_TARGET)/release/zcashd-wallet-tool$(EXEEXT)
@ -90,7 +93,7 @@ $(CXXBRIDGE_H) $(CXXBRIDGE_CPP): $(CXXBRIDGE_RS)
# requires https://github.com/rust-bitcoin/rust-secp256k1/issues/380 to be addressed.
RUST_ENV_VARS = \
RUSTC="$(RUSTC)" \
RUSTFLAGS="--cfg=rust_secp_no_symbol_renaming" \
RUSTFLAGS="--cfg=rust_secp_no_symbol_renaming -L native=$(abs_top_srcdir)/src/secp256k1/.libs" \
CC="$(CC)" \
CFLAGS="$(CFLAGS)" \
CXX="$(CXX)" \
@ -132,9 +135,12 @@ endif
cargo-build-lib: $(CARGO_CONFIGURED)
$(rust_verbose)$(RUST_ENV_VARS) $(CARGO) build --lib $(RUST_BUILD_OPTS) $(cargo_verbose)
cargo-build-bins: $(CARGO_CONFIGURED)
cargo-build-bins: $(CARGO_CONFIGURED) $(LIBSECP256K1)
$(rust_verbose)$(RUST_ENV_VARS) $(CARGO) build --bins $(RUST_BUILD_OPTS) $(cargo_verbose)
$(INSPECT_TOOL_BIN): cargo-build-bins
$(AM_V_at)cp $(INSPECT_TOOL_BUILD) $@
$(WALLET_TOOL_BIN): cargo-build-bins
$(AM_V_at)cp $(WALLET_TOOL_BUILD) $@
@ -174,7 +180,7 @@ endif
if BUILD_BITCOIN_UTILS
bin_PROGRAMS += zcash-cli zcash-tx
bin_SCRIPTS += $(WALLET_TOOL_BIN)
bin_SCRIPTS += $(INSPECT_TOOL_BIN) $(WALLET_TOOL_BIN)
endif
LIBZCASH_H = \

View File

@ -0,0 +1,125 @@
use zcash_address::{
unified::{self, Container, Encoding},
Network, ToAddress, UnsupportedAddress, ZcashAddress,
};
enum AddressKind {
Sprout([u8; 64]),
Sapling([u8; 43]),
Unified(unified::Address),
P2pkh([u8; 20]),
P2sh([u8; 20]),
}
struct Address {
net: Network,
kind: AddressKind,
}
impl zcash_address::FromAddress for Address {
fn from_sprout(net: Network, data: [u8; 64]) -> Result<Self, UnsupportedAddress> {
Ok(Address {
net,
kind: AddressKind::Sprout(data),
})
}
fn from_sapling(net: Network, data: [u8; 43]) -> Result<Self, UnsupportedAddress> {
Ok(Address {
net,
kind: AddressKind::Sapling(data),
})
}
fn from_unified(net: Network, data: unified::Address) -> Result<Self, UnsupportedAddress> {
Ok(Address {
net,
kind: AddressKind::Unified(data),
})
}
fn from_transparent_p2pkh(net: Network, data: [u8; 20]) -> Result<Self, UnsupportedAddress> {
Ok(Address {
net,
kind: AddressKind::P2pkh(data),
})
}
fn from_transparent_p2sh(net: Network, data: [u8; 20]) -> Result<Self, UnsupportedAddress> {
Ok(Address {
net,
kind: AddressKind::P2sh(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",
}
);
if let AddressKind::Unified(ua) = addr.kind {
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));
}
}
}
}
}
}
}

View File

@ -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 crate::{
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.get(0).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");
}
}
}

View File

@ -0,0 +1,287 @@
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use serde::{
de::{Unexpected, Visitor},
Deserialize,
};
use zcash_primitives::{
consensus::Network,
legacy::Script,
transaction::components::{transparent, Amount},
zip32::AccountId,
};
#[derive(Clone, Copy, Debug)]
pub(crate) struct JsonNetwork(Network);
struct JsonNetworkVisitor;
impl<'de> Visitor<'de> 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<'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]);
struct ZUint256Visitor;
impl<'de> Visitor<'de> 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(Amount);
struct ZOutputValueVisitor;
impl<'de> Visitor<'de> 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,
{
Amount::from_u64(v).map(ZOutputValue).map_err(|()| {
serde::de::Error::invalid_type(Unexpected::Unsigned(v), &"a valid 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)
}
}
#[derive(Clone, Debug)]
struct ZScript(Script);
struct ZScriptVisitor;
impl<'de> Visitor<'de> 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)
}
}
#[derive(Clone, Debug, Deserialize)]
struct ZTxOut {
value: ZOutputValue,
script_pubkey: ZScript,
}
#[derive(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()
})
}
}

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

@ -0,0 +1,129 @@
use std::env;
use std::io;
use std::io::Cursor;
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};
mod context;
use context::{Context, ZUint256};
mod address;
mod block;
mod keys;
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, Options)]
struct CliOptions {
#[options(help = "Print this help output")]
help: bool,
#[options(free, required, help = "String or hex-encoded bytes to inspect")]
data: String,
#[options(
free,
help = "JSON object with keys corresponding to requested context information"
)]
context: Option<Context>,
}
fn main() {
let args = env::args().collect::<Vec<_>>();
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]);
println!();
println!("{}", CliOptions::usage());
return;
}
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);
} else {
// Unknown data format.
eprintln!("String does not match known Zcash data formats.");
process::exit(2);
}
}
/// 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
}
})
}
fn inspect_bytes(bytes: Vec<u8>, context: Option<Context>) {
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::Sprout)) {
// TODO: Take the branch ID used above from the context if present.
transaction::inspect(tx, context);
} else {
// It's not a known variable-length format. check fixed-length data formats.
inspect_fixed_length_bytes(bytes);
}
}
fn inspect_fixed_length_bytes(bytes: Vec<u8>) {
match bytes.len() {
32 => {
eprintln!(
"This is most likely a hash of some sort, or maybe a commitment or nullifier."
);
if bytes.iter().take(4).all(|c| c == &0) {
eprintln!("- It could be a mainnet block hash.");
}
}
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);
}
}
}

View File

@ -0,0 +1,555 @@
use std::{
collections::HashMap,
convert::{TryFrom, TryInto},
};
use bellman::groth16;
use group::GroupEncoding;
use orchard::note_encryption::OrchardDomain;
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},
sapling::note_encryption::SaplingDomain,
transaction::{
components::{orchard as orchard_serialization, sapling, transparent, Amount},
sighash::{signature_hash, SignableInput, TransparentAuthorizingContext},
txid::TxIdDigester,
Authorization, Transaction, TransactionData, TxVersion,
},
};
use zcash_proofs::sapling::SaplingVerificationContext;
use crate::{context::Context, 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.get(0))
.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<Amount> {
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()
}
}
// TODO: Move these trait impls into `zcash_primitives` so they can be on `()`.
struct IdentityMap;
impl sapling::MapAuth<sapling::Authorized, sapling::Authorized> for IdentityMap {
fn map_proof(
&self,
p: <sapling::Authorized as sapling::Authorization>::Proof,
) -> <sapling::Authorized as sapling::Authorization>::Proof {
p
}
fn map_auth_sig(
&self,
s: <sapling::Authorized as sapling::Authorization>::AuthSig,
) -> <sapling::Authorized as sapling::Authorization>::AuthSig {
s
}
fn map_authorization(&self, a: sapling::Authorized) -> sapling::Authorized {
a
}
}
impl orchard_serialization::MapAuth<orchard::bundle::Authorized, orchard::bundle::Authorized>
for IdentityMap
{
fn map_spend_auth(
&self,
s: <orchard::bundle::Authorized as orchard::bundle::Authorization>::SpendAuth,
) -> <orchard::bundle::Authorized as orchard::bundle::Authorization>::SpendAuth {
s
}
fn map_authorization(&self, a: orchard::bundle::Authorized) -> orchard::bundle::Authorized {
a
}
}
pub(crate) struct PrecomputedAuth;
impl Authorization for PrecomputedAuth {
type TransparentAuth = TransparentAuth;
type SaplingAuth = sapling::Authorized;
type OrchardAuth = orchard::bundle::Authorized;
}
pub(crate) fn inspect(tx: Transaction, context: Option<Context>) {
eprintln!("Zcash transaction");
eprintln!(" - ID: {}", tx.txid());
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, IdentityMap, IdentityMap);
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() {
match coin.script_pubkey.address() {
Some(addr @ TransparentAddress::PublicKey(_)) => {
// 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 = txin.script_sig.0[1 + sig_len];
let pubkey = secp256k1::PublicKey::from_slice(
&txin.script_sig.0[1 + sig_len + 2..],
);
if let Err(e) = sig {
eprintln!(
" ⚠️ Txin {} has invalid signature encoding: {}",
i, e
);
}
if let Err(e) = pubkey {
eprintln!(" ⚠️ Txin {} has invalid pubkey encoding: {}", i, e);
}
if let (Ok(sig), Ok(pubkey)) = (sig, 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 {
hash_type,
index: i,
// For P2PKH these are the same.
script_code: &coin.script_pubkey,
script_pubkey: &coin.script_pubkey,
value: 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);
}
}
}
}
// TODO: Check P2SH structure.
Some(TransparentAddress::Script(_)) => {
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"
);
}
}
if !bundle.vout.is_empty() {
eprintln!(" - {} transparent output(s)", bundle.vout.len());
}
}
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(true);
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.clone(),
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::for_height(params, height.unwrap()),
&zcash_primitives::keys::OutgoingViewingKey([0; 32]),
output,
&output.cv,
&output.out_ciphertext,
) {
if note.value == 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));
}
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);
}
}
}

View File

@ -83,6 +83,7 @@ clean_exe src/zcash-cli
clean_exe src/zcashd
clean_exe src/zcashd-wallet-tool
clean_exe src/zcash-gtest
clean_exe src/zcash-inspect
clean_exe src/zcash-tx
clean_exe src/test/test_bitcoin