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 transparent inputs and outputs in V5 transactions #2302

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 137 additions & 43 deletions zebra-consensus/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ mod check;
#[cfg(test)]
mod tests;

/// An alias for a set of asynchronous checks that should succeed.
type AsyncChecks = FuturesUnordered<Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send>>>;
jvff marked this conversation as resolved.
Show resolved Hide resolved

/// Asynchronous transaction verification.
///
/// # Correctness
Expand Down Expand Up @@ -96,20 +99,25 @@ pub enum Request {
}

impl Request {
/// The transaction to verify that's in this request.
pub fn transaction(&self) -> Arc<Transaction> {
match self {
Request::Block { transaction, .. } => transaction.clone(),
Request::Mempool { transaction, .. } => transaction.clone(),
}
}

/// The set of additional known unspent transaction outputs that's in this request.
pub fn known_utxos(&self) -> Arc<HashMap<transparent::OutPoint, zs::Utxo>> {
match self {
Request::Block { known_utxos, .. } => known_utxos.clone(),
Request::Mempool { known_utxos, .. } => known_utxos.clone(),
}
}

/// The network upgrade to consider for the verification.
///
/// This is based on the block height from the request, and the supplied `network`.
pub fn upgrade(&self, network: Network) -> NetworkUpgrade {
match self {
Request::Block { height, .. } => NetworkUpgrade::current(network, *height),
Expand Down Expand Up @@ -151,10 +159,14 @@ where

async move {
tracing::trace!(?tx);
match tx.as_ref() {

// Do basic checks first
check::has_inputs_and_outputs(&tx)?;

let async_checks = match tx.as_ref() {
Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => {
tracing::debug!(?tx, "got transaction with wrong version");
Err(TransactionError::WrongVersion)
return Err(TransactionError::WrongVersion);
}
Transaction::V4 {
inputs,
Expand All @@ -173,10 +185,16 @@ where
joinsplit_data,
sapling_shielded_data,
)
.await
.await?
}
Transaction::V5 { .. } => Self::verify_v5_transaction(req, network).await,
}
Transaction::V5 { inputs, .. } => {
Self::verify_v5_transaction(req, network, script_verifier, inputs).await?
}
};

Self::wait_for_checks(async_checks).await?;

Ok(tx.hash())
}
.instrument(span)
.boxed()
Expand All @@ -188,14 +206,32 @@ where
ZS: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
ZS::Future: Send + 'static,
{
/// Verify a V4 transaction.
///
/// Returns a set of asynchronous checks that must all succeed for the transaction to be
/// considered valid. These checks include:
///
/// - transparent transfers
/// - sprout shielded data
/// - sapling shielded data
///
/// The parameters of this method are:
///
/// - the `request` to verify (that contains the transaction and other metadata, see [`Request`]
/// for more information)
/// - the `network` to consider when verifying
/// - the `script_verifier` to use for verifying the transparent transfers
/// - the transparent `inputs` in the transaction
/// - the Sprout `joinsplit_data` shielded data in the transaction
/// - the `sapling_shielded_data` in the transaction
async fn verify_v4_transaction(
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
request: Request,
network: Network,
mut script_verifier: script::Verifier<ZS>,
script_verifier: script::Verifier<ZS>,
inputs: &[transparent::Input],
joinsplit_data: &Option<transaction::JoinSplitData<Groth16Proof>>,
sapling_shielded_data: &Option<sapling::ShieldedData<sapling::PerSpendAnchor>>,
) -> Result<transaction::Hash, TransactionError> {
) -> Result<AsyncChecks, TransactionError> {
let mut spend_verifier = primitives::groth16::SPEND_VERIFIER.clone();
let mut output_verifier = primitives::groth16::OUTPUT_VERIFIER.clone();

Expand All @@ -204,33 +240,18 @@ where

// A set of asynchronous checks which must all succeed.
// We finish by waiting on these below.
let mut async_checks = FuturesUnordered::new();
let mut async_checks = AsyncChecks::new();

let tx = request.transaction();
let upgrade = request.upgrade(network);

// Do basic checks first
check::has_inputs_and_outputs(&tx)?;

// Handle transparent inputs and outputs.
if tx.is_coinbase() {
check::coinbase_tx_no_prevout_joinsplit_spend(&tx)?;
} else {
// feed all of the inputs to the script and shielded verifiers
// the script_verifier also checks transparent sighashes, using its own implementation
let cached_ffi_transaction = Arc::new(CachedFfiTransaction::new(tx.clone()));

for input_index in 0..inputs.len() {
let rsp = script_verifier.ready_and().await?.call(script::Request {
upgrade,
known_utxos: request.known_utxos(),
cached_ffi_transaction: cached_ffi_transaction.clone(),
input_index,
});

async_checks.push(rsp);
}
}
// Add asynchronous checks of the transparent inputs and outputs
async_checks.extend(Self::verify_transparent_inputs_and_outputs(
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
&request,
network,
inputs,
script_verifier,
)?);

let shielded_sighash = tx.sighash(upgrade, HashType::ALL, None);

Expand Down Expand Up @@ -359,27 +380,45 @@ where
// async_checks.push(rsp);
}

// Finally, wait for all asynchronous checks to complete
// successfully, or fail verification if they error.
while let Some(check) = async_checks.next().await {
tracing::trace!(?check, remaining = async_checks.len());
check?;
}

Ok(tx.hash())
Ok(async_checks)
}

/// Verify a V5 transaction.
///
/// Returns a set of asynchronous checks that must all succeed for the transaction to be
/// considered valid. These checks include:
///
/// - transaction support by the considered network upgrade (see [`Request::upgrade`])
/// - transparent transfers
/// - sapling shielded data (TODO)
/// - orchard shielded data (TODO)
///
/// The parameters of this method are:
///
/// - the `request` to verify (that contains the transaction and other metadata, see [`Request`]
/// for more information)
/// - the `network` to consider when verifying
/// - the `script_verifier` to use for verifying the transparent transfers
/// - the transparent `inputs` in the transaction
async fn verify_v5_transaction(
request: Request,
network: Network,
) -> Result<transaction::Hash, TransactionError> {
script_verifier: script::Verifier<ZS>,
inputs: &[transparent::Input],
) -> Result<AsyncChecks, TransactionError> {
Self::verify_v5_transaction_network_upgrade(
&request.transaction(),
request.upgrade(network),
)?;

let _async_checks = Self::verify_transparent_inputs_and_outputs(
&request,
network,
inputs,
script_verifier,
)?;

// TODO:
// - verify transparent pool (#1981)
// - verify sapling shielded pool (#1981)
// - verify orchard shielded pool (ZIP-224) (#2105)
// - ZIP-216 (#1798)
Expand All @@ -388,11 +427,12 @@ where
unimplemented!("V5 transaction validation is not yet complete");
}

/// Verifies if a V5 `transaction` is supported by `network_upgrade`.
fn verify_v5_transaction_network_upgrade(
transaction: &Transaction,
upgrade: NetworkUpgrade,
network_upgrade: NetworkUpgrade,
) -> Result<(), TransactionError> {
match upgrade {
match network_upgrade {
// Supports V5 transactions
NetworkUpgrade::Nu5 => Ok(()),

Expand All @@ -405,8 +445,62 @@ where
| NetworkUpgrade::Heartwood
| NetworkUpgrade::Canopy => Err(TransactionError::UnsupportedByNetworkUpgrade(
transaction.version(),
upgrade,
network_upgrade,
)),
}
}

/// Verifies if a transaction's transparent `inputs` are valid using the provided
/// `script_verifier`.
fn verify_transparent_inputs_and_outputs(
request: &Request,
network: Network,
inputs: &[transparent::Input],
script_verifier: script::Verifier<ZS>,
) -> Result<AsyncChecks, TransactionError> {
let transaction = request.transaction();

if transaction.is_coinbase() {
check::coinbase_tx_no_prevout_joinsplit_spend(&transaction)?;

Ok(AsyncChecks::new())
} else {
// feed all of the inputs to the script and shielded verifiers
// the script_verifier also checks transparent sighashes, using its own implementation
let cached_ffi_transaction = Arc::new(CachedFfiTransaction::new(transaction));
let known_utxos = request.known_utxos();
let upgrade = request.upgrade(network);

let script_checks = (0..inputs.len())
.into_iter()
.map(move |input_index| {
let request = script::Request {
upgrade,
known_utxos: known_utxos.clone(),
cached_ffi_transaction: cached_ffi_transaction.clone(),
input_index,
};

script_verifier.clone().oneshot(request).boxed()
})
.collect();

Ok(script_checks)
}
}

/// Await a set of checks that should all succeed.
///
/// If any of the checks fail, this method immediately returns the error and cancels all other
/// checks by dropping them.
async fn wait_for_checks(mut checks: AsyncChecks) -> Result<(), TransactionError> {
// Wait for all asynchronous checks to complete
// successfully, or fail verification if they error.
while let Some(check) = checks.next().await {
tracing::trace!(?check, remaining = checks.len());
check?;
}

Ok(())
}
}
Loading