Merge pull request #5617 from str4d/zcash-inspect
rust: Add `zcash-inspect` binary for inspecting Zcash data
This commit is contained in:
commit
520b58f632
|
@ -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/
|
||||
|
|
|
@ -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"
|
||||
|
|
20
Cargo.toml
20
Cargo.toml
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 = \
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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."
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue