rust: Add P2PKH signature checking to `zcash-inspect`

Co-authored-by: ying tong <yingtong@z.cash>
This commit is contained in:
Jack Grigg 2022-06-29 15:32:11 +00:00
parent 8d82cee9c8
commit a125180a50
3 changed files with 132 additions and 13 deletions

View File

@ -93,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)" \
@ -135,7 +135,7 @@ 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

View File

@ -86,6 +86,7 @@ fn inspect_bytes(bytes: Vec<u8>, context: Option<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.

View File

@ -6,21 +6,23 @@ use std::{
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::Script,
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,
Authorization, Transaction, TransactionData, TxVersion,
},
};
use zcash_proofs::sapling::SaplingVerificationContext;
@ -184,6 +186,13 @@ 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 {
@ -193,16 +202,23 @@ pub(crate) fn inspect(tx: Transaction, context: Option<Context>) {
None
};
let transparent_coins = if tx.transparent_bundle().map_or(false, |b| !b.vin.is_empty()) {
context.as_ref().and_then(|ctx| ctx.transparent_coins())
} else {
// TODO: Check that `transparentcoins` is not set.
Some(vec![])
let 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 common_sighash = transparent_coins.map(|all_prev_outputs| {
let sighash_params = transparent_coins.as_ref().map(|coins| {
let f_transparent = MapTransparent {
auth: TransparentAuth { all_prev_outputs },
auth: TransparentAuth {
all_prev_outputs: coins.clone(),
},
};
// We don't have tx.clone()
@ -214,15 +230,117 @@ pub(crate) fn inspect(tx: Transaction, context: Option<Context>) {
tx.into_data()
.map_authorization(f_transparent, IdentityMap, IdentityMap);
let txid_parts = tx.digest(TxIdDigester);
signature_hash(&tx, &SignableInput::Shielded, &txid_parts)
(tx, txid_parts)
});
let common_sighash = sighash_params
.as_ref()
.map(|(tx, txid_parts)| signature_hash(tx, &SignableInput::Shielded, txid_parts));
if let Some(bundle) = tx.transparent_bundle() {
assert!(!(bundle.vin.is_empty() && bundle.vout.is_empty()));
if !(bundle.vin.is_empty() || is_coinbase) {
eprintln!(" - {} transparent input(s)", bundle.vin.len());
if let Some(coins) = &transparent_coins {
if bundle.vin.len() != coins.len() {
eprintln!(" ⚠️ \"transparentcoins\" has length {}", coins.len());
}
// TODO: Check transparent signatures if possible.
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());