use std::{sync::Arc, time::Duration};
use orchard::{
builder::{Builder, BundleType},
bundle::{Authorized, BundleVersion},
circuit::OrchardCircuitVersion,
keys::{FullViewingKey, Scope, SpendingKey},
value::NoteValue,
Anchor, Bundle, Proof,
};
use rand::{rngs::StdRng, SeedableRng};
use tower::{service_fn, ServiceExt};
use zcash_protocol::value::ZatBalance;
use zebra_chain::{
block::{Block, Height},
parameters::{
subsidy::block_subsidy,
testnet::{ConfiguredActivationHeights, Parameters},
Network, NetworkUpgrade,
},
serialization::{ZcashDeserialize, ZcashSerialize},
transaction::{Hash, HashType, LockTime, Transaction},
transparent,
};
use crate::{
block::{Request, SemanticBlockVerifier, VerifyBlockError},
error::{BlockError, TransactionError},
transaction::BlockTxVerifier,
BoxError,
};
const ACTIVATION: Height = Height(10);
const EXPIRY: Height = Height(11);
fn network() -> Network {
let params = Parameters::build()
.with_activation_heights(ConfiguredActivationHeights {
canopy: Some(1),
nu5: Some(1),
nu6: Some(1),
nu6_3: Some(2),
nu7: Some(ACTIVATION.0),
..Default::default()
})
.unwrap()
.with_slow_start_interval(Height::MIN)
.with_disable_pow(true)
.clear_funding_streams()
.with_lockbox_disbursements(Vec::new());
let params = params.with_nsm_reissuance_height(Height::MAX);
params.to_network().unwrap()
}
fn outpoint() -> transparent::OutPoint {
transparent::OutPoint {
hash: Hash([1; 32]),
index: 0,
}
}
fn output(value: u64) -> transparent::Output {
transparent::Output {
value: value.try_into().unwrap(),
lock_script: transparent::Script::new(&[0x51]), }
}
fn input() -> transparent::Input {
transparent::Input::PrevOut {
outpoint: outpoint(),
unlock_script: transparent::Script::new(&[]),
sequence: u32::MAX,
}
}
fn block_at(network: &Network, height: Height, transaction: Transaction) -> Arc<Block> {
let mut block =
Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_1_BYTES[..]).unwrap();
let coinbase = Transaction::test_v5(
NetworkUpgrade::current(network, height),
vec![transparent::Input::Coinbase {
height,
data: vec![0],
sequence: u32::MAX,
}],
vec![transparent::Output {
value: block_subsidy(height, network).unwrap(),
lock_script: transparent::Script::new(&[0x51]),
}],
LockTime::unlocked(),
height,
);
block.transactions = vec![Arc::new(coinbase), Arc::new(transaction)];
Arc::make_mut(&mut block.header).merkle_root =
block.transactions.iter().map(|tx| tx.hash()).collect();
Arc::new(block)
}
async fn verify(
network: &Network,
block: Arc<Block>,
) -> Result<zebra_chain::block::Hash, VerifyBlockError> {
let state = service_fn(|request| async move {
Ok::<_, BoxError>(match request {
zebra_state::Request::KnownBlock(_) => zebra_state::Response::KnownBlock(None),
zebra_state::Request::AwaitUtxo(requested) => {
assert_eq!(requested, outpoint());
zebra_state::Response::Utxo(
transparent::OrderedUtxo::new(output(10_000), Height(1), 1).utxo,
)
}
zebra_state::Request::CommitSemanticallyVerifiedBlock(block) => {
zebra_state::Response::Committed(block.hash)
}
other => panic!("unexpected state request: {other:?}"),
})
});
let transactions = BlockTxVerifier::new(network, state);
let transactions = tower::buffer::Buffer::new(transactions, 1);
tokio::time::timeout(
Duration::from_secs(60),
SemanticBlockVerifier::new(network, state, transactions).oneshot(Request::Commit(block)),
)
.await
.expect("semantic block verification must finish within the timeout")
}
#[tokio::test(flavor = "multi_thread")]
async fn nu7_semantic_block_transaction_versions() {
let _init_guard = zebra_test::init();
let network = network();
let before = (ACTIVATION - 1).unwrap();
let v4 = Transaction::test_v4(
vec![input()],
vec![output(10_000)],
LockTime::unlocked(),
EXPIRY,
);
assert_eq!(v4.network_upgrade(), None);
for height in [before, ACTIVATION] {
let block = block_at(&network, height, v4.clone());
assert!(block
.check_transaction_network_upgrade_consistency(&network)
.is_ok());
let result = verify(&network, block.clone()).await;
if height == before {
assert_eq!(result.unwrap(), block.hash());
} else {
assert!(
matches!(
result,
Err(VerifyBlockError::Transaction(
TransactionError::UnsupportedByNetworkUpgrade(4, NetworkUpgrade::Nu7)
))
),
"unexpected result: {result:?}"
);
}
let v5 = Transaction::test_v5(
NetworkUpgrade::current(&network, height),
vec![input()],
vec![output(10_000)],
LockTime::unlocked(),
EXPIRY,
);
let block = block_at(&network, height, v5.clone());
assert_eq!(verify(&network, block.clone()).await.unwrap(), block.hash());
let other_height = if height == before { ACTIVATION } else { before };
let result = verify(&network, block_at(&network, other_height, v5)).await;
assert!(
matches!(
result,
Err(VerifyBlockError::Block {
source: BlockError::WrongTransactionConsensusBranchId
})
),
"unexpected result: {result:?}"
);
}
}
fn ironwood_transaction(nu: NetworkUpgrade, bundle: Bundle<Authorized, ZatBalance>) -> Transaction {
Transaction::test_v6_with_bundles(
nu,
vec![input()],
vec![output(9_000)],
LockTime::unlocked(),
EXPIRY,
None,
Some(bundle),
)
}
#[test]
fn nu7_semantic_block_ironwood_proof_and_signatures() {
let _init_guard = zebra_test::init();
zebra_test::MULTI_THREADED_RUNTIME.block_on(async {
let network = network();
let (before_tx, activation_tx) = tokio::time::timeout(
Duration::from_secs(300),
tokio::task::spawn_blocking(|| {
let mut rng = StdRng::from_seed([0x77; 32]);
let recipient = FullViewingKey::from(&SpendingKey::from_bytes([0; 32]).unwrap())
.address_at(0u32, Scope::External);
let version = BundleVersion::ironwood_v3();
let mut builder = Builder::new(
BundleType::UNPADDED,
version,
version.default_flags(),
Anchor::empty_tree(),
)
.unwrap();
builder
.add_output(None, recipient, NoteValue::from_raw(1_000), [0; 512])
.unwrap();
let proved = builder
.build::<ZatBalance>(&mut rng)
.unwrap()
.unwrap()
.0
.create_proof(
zcash_primitives::transaction::builder::cached_orchard_proving_key(
OrchardCircuitVersion::PostNu6_3,
),
&mut rng,
)
.unwrap();
let mut sign = |nu| {
let provisional = ironwood_transaction(
nu,
proved
.clone()
.apply_signatures(&mut rng, [0; 32], &[])
.unwrap(),
);
let sighash = provisional
.sighasher(nu, Arc::new(vec![output(10_000)]))
.unwrap()
.sighash(HashType::ALL, None);
ironwood_transaction(
nu,
proved
.clone()
.apply_signatures(&mut rng, sighash.0, &[])
.unwrap(),
)
};
(sign(NetworkUpgrade::Nu6_3), sign(NetworkUpgrade::Nu7))
}),
)
.await
.expect("one-action Ironwood proving must finish within the timeout")
.unwrap();
for (height, tx) in [
((ACTIVATION - 1).unwrap(), &before_tx),
(ACTIVATION, &activation_tx),
] {
let bytes = tx.zcash_serialize_to_vec().unwrap();
let tx = Transaction::zcash_deserialize(bytes.as_slice()).unwrap();
let block = block_at(&network, height, tx);
assert_eq!(verify(&network, block.clone()).await.unwrap(), block.hash());
}
let bundle = activation_tx
.sighasher(NetworkUpgrade::Nu7, Arc::new(vec![output(10_000)]))
.unwrap()
.ironwood_bundle()
.unwrap();
let stale_bundle = before_tx
.sighasher(NetworkUpgrade::Nu6_3, Arc::new(vec![output(10_000)]))
.unwrap()
.ironwood_bundle()
.unwrap();
assert_eq!(
stale_bundle.authorization().proof().as_ref(),
bundle.authorization().proof().as_ref(),
"the stale-signature case must retain the known-valid proof"
);
let stale_signatures = ironwood_transaction(NetworkUpgrade::Nu7, stale_bundle);
assert_eq!(stale_signatures.hash(), activation_tx.hash());
assert_ne!(stale_signatures.unmined_id(), activation_tx.unmined_id());
let result = verify(&network, block_at(&network, ACTIVATION, stale_signatures)).await;
assert!(
matches!(
result,
Err(VerifyBlockError::Transaction(
TransactionError::Halo2VerificationFailed
))
),
"unexpected result: {result:?}"
);
let corrupt = bundle.map_authorization(
&mut (),
|_, _, signature| signature,
|_, authorization| {
let mut proof = authorization.proof().as_ref().to_vec();
proof[0] ^= 1;
Authorized::from_parts(Proof::new(proof), authorization.binding_signature().clone())
},
);
let corrupt_tx = ironwood_transaction(NetworkUpgrade::Nu7, corrupt);
assert!(corrupt_tx.ironwood_proof_size_is_canonical());
assert_eq!(corrupt_tx.hash(), activation_tx.hash());
assert_ne!(corrupt_tx.unmined_id(), activation_tx.unmined_id());
let result = verify(&network, block_at(&network, ACTIVATION, corrupt_tx)).await;
assert!(
matches!(
result,
Err(VerifyBlockError::Transaction(
TransactionError::Halo2VerificationFailed
))
),
"unexpected result: {result:?}"
);
});
}