rust: Add `zcash-inspect` binary for inspecting Zcash data

Currently supports Zcash blocks, block headers, and transactions. Some
consensus rules are also checked, and a JSON context object can be
optionally passed to provide any necessary details for extra contextual
consensus checks.
This commit is contained in:
Jack Grigg 2022-03-02 17:56:06 +00:00
parent e9b4a1af09
commit 8d82cee9c8
10 changed files with 1221 additions and 3 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/

24
Cargo.lock generated
View File

@ -959,11 +959,14 @@ dependencies = [
"crossbeam-channel",
"cxx",
"ed25519-zebra",
"equihash",
"group",
"gumdrop",
"hex",
"incrementalmerkletree",
"ipnet",
"jubjub",
"lazy_static",
"libc",
"memuse",
"metrics",
@ -975,6 +978,9 @@ dependencies = [
"rayon",
"secp256k1",
"secrecy",
"serde",
"serde_json",
"sha2",
"subtle",
"thiserror",
"time",
@ -982,6 +988,7 @@ dependencies = [
"tracing-appender",
"tracing-core",
"tracing-subscriber",
"uint",
"zcash_address",
"zcash_encoding",
"zcash_history",
@ -1716,6 +1723,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 +1782,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,23 @@ ipnet = "2"
metrics = "0.19"
metrics-exporter-prometheus = "0.10"
# General tool dependencies
gumdrop = "0.8"
# zcash-inspect tool
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)
@ -135,6 +138,9 @@ cargo-build-lib: $(CARGO_CONFIGURED)
cargo-build-bins: $(CARGO_CONFIGURED)
$(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,408 @@
// 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] {
/// Returns 0 if x == 0, otherwise the smallest power of 2 greater than or equal to x.
/// Algorithm based on <https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2>.
fn next_pow2(mut x: u64) -> u64 {
x -= 1;
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
x |= x >> 32;
x + 1
}
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,216 @@
use std::fmt;
use std::str::FromStr;
use serde::{
de::{Unexpected, Visitor},
Deserialize,
};
use zcash_primitives::{
consensus::Network,
legacy::Script,
transaction::components::{transparent, Amount},
};
#[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)]
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>,
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 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,113 @@
use std::env;
use std::io;
use std::io::Cursor;
use std::process;
use gumdrop::{Options, ParsingStyle};
use lazy_static::lazy_static;
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 block;
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 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;
}
if let Ok(bytes) = hex::decode(opts.data) {
inspect_bytes(bytes, opts.context);
} 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)) {
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.");
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,426 @@
use std::{
collections::HashMap,
convert::{TryFrom, TryInto},
};
use bellman::groth16;
use group::GroupEncoding;
use orchard::note_encryption::OrchardDomain;
use zcash_address::{
unified::{self, Encoding},
ToAddress, ZcashAddress,
};
use zcash_note_encryption::try_output_recovery_with_ovk;
use zcash_primitives::{
consensus::BlockHeight,
legacy::Script,
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,
},
};
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 8 bytes for Zcash heights. These have the format
// `[len(encoding)] || encoding`.
Some(h @ 1..=8) => {
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());
let is_coinbase = is_coinbase(&tx);
let height = if is_coinbase {
eprintln!(" - Coinbase");
extract_height_from_coinbase(&tx)
} else {
None
};
let transparent_coins = if tx.transparent_bundle().map_or(false, |b| !b.vin.is_empty()) {
context.as_ref().and_then(|ctx| ctx.transparent_coins())
} else {
// TODO: Check that `transparentcoins` is not set.
Some(vec![])
};
let common_sighash = transparent_coins.map(|all_prev_outputs| {
let f_transparent = MapTransparent {
auth: TransparentAuth { all_prev_outputs },
};
// 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);
signature_hash(&tx, &SignableInput::Shielded, &txid_parts)
});
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());
// TODO: Check transparent signatures if possible.
}
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