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