Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate V5 transactions with Sapling shielded data #2437

Merged
44 changes: 32 additions & 12 deletions zebra-consensus/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,17 @@ where
joinsplit_data,
sapling_shielded_data,
)?,
Transaction::V5 { inputs, .. } => {
Self::verify_v5_transaction(req, network, script_verifier, inputs)?
}
Transaction::V5 {
inputs,
sapling_shielded_data,
..
} => Self::verify_v5_transaction(
req,
network,
script_verifier,
inputs,
sapling_shielded_data,
)?,
};

async_checks.check().await?;
Expand Down Expand Up @@ -295,24 +303,32 @@ where
network: Network,
script_verifier: script::Verifier<ZS>,
inputs: &[transparent::Input],
sapling_shielded_data: &Option<sapling::ShieldedData<sapling::SharedAnchor>>,
) -> Result<AsyncChecks, TransactionError> {
Self::verify_v5_transaction_network_upgrade(
&request.transaction(),
request.upgrade(network),
)?;
let transaction = request.transaction();
let upgrade = request.upgrade(network);
let shielded_sighash = transaction.sighash(upgrade, HashType::ALL, None);

Self::verify_v5_transaction_network_upgrade(&transaction, upgrade)?;

let _async_checks = Self::verify_transparent_inputs_and_outputs(
&request,
network,
inputs,
script_verifier,
)?;
)?
.and(Self::verify_sapling_shielded_data(
sapling_shielded_data,
&shielded_sighash,
)?);

// TODO:
// - verify sapling shielded pool (#1981)
// - verify orchard shielded pool (ZIP-224) (#2105)
// - ZIP-216 (#1798)
// - ZIP-244 (#1874)
// - validate bindingSigOrchard (#2103)
// - remaining consensus rules (#2379)
// - remove `should_panic` from tests

unimplemented!("V5 transaction validation is not yet complete");
}
Expand Down Expand Up @@ -416,10 +432,14 @@ where
}

/// Verifies a transaction's Sapling shielded data.
fn verify_sapling_shielded_data(
sapling_shielded_data: &Option<sapling::ShieldedData<sapling::PerSpendAnchor>>,
fn verify_sapling_shielded_data<A>(
sapling_shielded_data: &Option<sapling::ShieldedData<A>>,
shielded_sighash: &blake2b_simd::Hash,
) -> Result<AsyncChecks, TransactionError> {
) -> Result<AsyncChecks, TransactionError>
where
A: sapling::AnchorVariant + Clone,
sapling::Spend<sapling::PerSpendAnchor>: From<(sapling::Spend<A>, A::Shared)>,
{
let mut async_checks = AsyncChecks::new();

if let Some(sapling_shielded_data) = sapling_shielded_data {
Expand Down
45 changes: 44 additions & 1 deletion zebra-consensus/src/transaction/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ fn v5_coinbase_transaction_with_enable_spends_flag_fails_validation() {
}

#[tokio::test]
// TODO: Remove `should_panic` once V5 transaction sighash is implemened by the merge of #2165.
#[should_panic]
async fn v5_transaction_is_rejected_before_nu5_activation() {
const V5_TRANSACTION_VERSION: u32 = 5;

Expand Down Expand Up @@ -751,7 +753,7 @@ fn v4_with_sapling_outputs_and_no_spends() {
transaction.sapling_spends_per_anchor().next().is_none()
&& transaction.sapling_outputs().next().is_some()
})
.expect("No transaction found with Sapling spends");
.expect("No transaction found with Sapling outputs and no Sapling spends");

let expected_hash = transaction.hash();

Expand All @@ -775,6 +777,47 @@ fn v4_with_sapling_outputs_and_no_spends() {
});
}

/// Test if a V5 transaction with Sapling spends is accepted by the verifier.
#[test]
// TODO: Remove `should_panic` once V5 transaction verification is complete.
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
#[should_panic]
fn v5_with_sapling_spends() {
zebra_test::init();
zebra_test::RUNTIME.block_on(async {
let network = Network::Mainnet;

let transaction =
fake_v5_transactions_for_network(network, zebra_test::vectors::MAINNET_BLOCKS.iter())
.rev()
.filter(|transaction| !transaction.is_coinbase() && transaction.inputs().is_empty())
.find(|transaction| transaction.sapling_spends_per_anchor().next().is_some())
.expect("No transaction found with Sapling spends");

let expected_hash = transaction.hash();
let height = transaction
.expiry_height()
.expect("Transaction is missing expiry height");

// Initialize the verifier
let state_service =
service_fn(|_| async { unreachable!("State service should not be called") });
let script_verifier = script::Verifier::new(state_service);
let verifier = Verifier::new(network, script_verifier);

// Test the transaction verifier
let result = verifier
.clone()
.oneshot(Request::Block {
transaction: Arc::new(transaction),
known_utxos: Arc::new(HashMap::new()),
height,
})
.await;

assert_eq!(result, Ok(expected_hash));
});
}

// Utility functions

/// Create a mock transparent transfer to be included in a transaction.
Expand Down