consensus: fix same-block utxo lookups

The UTXO query system assumes that a transaction will only request
information about UTXOs created in prior blocks.  But transactions are
allowed to spend UTXOs created by prior transactions in the same block.

This doesn't fit with the existing query model, so instead of trying to
change it, allow the script verifier to take an additional set of known
UTXOs, and propagate this set from the block.
This commit is contained in:
Henry de Valence 2020-11-20 19:47:30 -08:00 committed by teor
parent 719a48ad9e
commit a2a70e1fc1
4 changed files with 64 additions and 25 deletions

4
Cargo.lock generated
View File

@ -2744,7 +2744,7 @@ dependencies = [
[[package]] [[package]]
name = "tower" name = "tower"
version = "0.4.0" version = "0.4.0"
source = "git+https://github.com/tower-rs/tower?rev=5e1e07744820028877654c336a3b9fe057bf46f1#5e1e07744820028877654c336a3b9fe057bf46f1" source = "git+https://github.com/tower-rs/tower?rev=d4d1c67c6a0e4213a52abcc2b9df6cc58276ee39#d4d1c67c6a0e4213a52abcc2b9df6cc58276ee39"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
@ -2789,7 +2789,7 @@ dependencies = [
[[package]] [[package]]
name = "tower-layer" name = "tower-layer"
version = "0.3.0" version = "0.3.0"
source = "git+https://github.com/tower-rs/tower?rev=5e1e07744820028877654c336a3b9fe057bf46f1#5e1e07744820028877654c336a3b9fe057bf46f1" source = "git+https://github.com/tower-rs/tower?rev=d4d1c67c6a0e4213a52abcc2b9df6cc58276ee39#d4d1c67c6a0e4213a52abcc2b9df6cc58276ee39"
[[package]] [[package]]
name = "tower-service" name = "tower-service"

View File

@ -8,6 +8,7 @@
//! verification, where it may be accepted or rejected. //! verification, where it may be accepted or rejected.
use std::{ use std::{
collections::HashMap,
future::Future, future::Future,
pin::Pin, pin::Pin,
sync::Arc, sync::Arc,
@ -25,6 +26,7 @@ use zebra_chain::{
block::{self, Block}, block::{self, Block},
parameters::Network, parameters::Network,
parameters::NetworkUpgrade, parameters::NetworkUpgrade,
transparent,
work::equihash, work::equihash,
}; };
use zebra_state as zs; use zebra_state as zs;
@ -165,13 +167,16 @@ where
let mut async_checks = FuturesUnordered::new(); let mut async_checks = FuturesUnordered::new();
let known_utxos = known_utxos(&block);
for transaction in &block.transactions { for transaction in &block.transactions {
let req = transaction::Request::Block(transaction.clone());
let rsp = transaction_verifier let rsp = transaction_verifier
.ready_and() .ready_and()
.await .await
.expect("transaction verifier is always ready") .expect("transaction verifier is always ready")
.call(req); .call(transaction::Request::Block {
transaction: transaction.clone(),
known_utxos: known_utxos.clone(),
});
async_checks.push(rsp); async_checks.push(rsp);
} }
tracing::trace!(len = async_checks.len(), "built async tx checks"); tracing::trace!(len = async_checks.len(), "built async tx checks");
@ -207,3 +212,16 @@ where
.boxed() .boxed()
} }
} }
fn known_utxos(block: &Block) -> Arc<HashMap<transparent::OutPoint, transparent::Output>> {
let mut map = HashMap::default();
for transaction in &block.transactions {
let hash = transaction.hash();
for (index, output) in transaction.outputs().iter().cloned().enumerate() {
let index = index as u32;
map.insert(transparent::OutPoint { hash, index }, output);
}
}
Arc::new(map)
}

View File

@ -1,4 +1,4 @@
use std::{future::Future, pin::Pin, sync::Arc}; use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc};
use tracing::Instrument; use tracing::Instrument;
@ -40,6 +40,7 @@ impl<ZS> Verifier<ZS> {
pub struct Request { pub struct Request {
pub transaction: Arc<Transaction>, pub transaction: Arc<Transaction>,
pub input_index: usize, pub input_index: usize,
pub known_utxos: Arc<HashMap<transparent::OutPoint, transparent::Output>>,
} }
impl<ZS> tower::Service<Request> for Verifier<ZS> impl<ZS> tower::Service<Request> for Verifier<ZS>
@ -62,32 +63,35 @@ where
fn call(&mut self, req: Request) -> Self::Future { fn call(&mut self, req: Request) -> Self::Future {
use futures_util::FutureExt; use futures_util::FutureExt;
let input = &req.transaction.inputs()[req.input_index]; let Request {
transaction,
input_index,
known_utxos,
} = req;
let input = &transaction.inputs()[input_index];
match input { match input {
transparent::Input::PrevOut { outpoint, .. } => { transparent::Input::PrevOut { outpoint, .. } => {
let outpoint = *outpoint; let outpoint = *outpoint;
let transaction = req.transaction;
let branch_id = self.branch; let branch_id = self.branch;
let input_index = req.input_index;
let span = tracing::trace_span!("script", ?outpoint); let span = tracing::trace_span!("script", ?outpoint);
let output = let query =
span.in_scope(|| self.state.call(zebra_state::Request::AwaitUtxo(outpoint))); span.in_scope(|| self.state.call(zebra_state::Request::AwaitUtxo(outpoint)));
async move { async move {
tracing::trace!("awaiting outpoint lookup"); tracing::trace!("awaiting outpoint lookup");
let previous_output = match output.await? { let output = if let Some(output) = known_utxos.get(&outpoint) {
zebra_state::Response::Utxo(output) => output, tracing::trace!("UXTO in known_utxos, discarding query");
_ => unreachable!("AwaitUtxo always responds with Utxo"), output.clone()
} else if let zebra_state::Response::Utxo(output) = query.await? {
output
} else {
unreachable!("AwaitUtxo always responds with Utxo")
}; };
tracing::trace!(?previous_output, "got UTXO"); tracing::trace!(?output, "got UTXO");
zebra_script::is_valid( zebra_script::is_valid(transaction, branch_id, (input_index as u32, output))?;
transaction,
branch_id,
(input_index as u32, previous_output),
)?;
tracing::trace!("script verification succeeded"); tracing::trace!("script verification succeeded");
Ok(()) Ok(())

View File

@ -1,4 +1,5 @@
use std::{ use std::{
collections::HashMap,
future::Future, future::Future,
pin::Pin, pin::Pin,
sync::Arc, sync::Arc,
@ -15,6 +16,7 @@ use tracing::Instrument;
use zebra_chain::{ use zebra_chain::{
parameters::NetworkUpgrade, parameters::NetworkUpgrade,
transaction::{self, HashType, Transaction}, transaction::{self, HashType, Transaction},
transparent,
}; };
use zebra_state as zs; use zebra_state as zs;
@ -51,9 +53,17 @@ where
#[allow(dead_code)] #[allow(dead_code)]
pub enum Request { pub enum Request {
/// Verify the supplied transaction as part of a block. /// Verify the supplied transaction as part of a block.
Block(Arc<Transaction>), Block {
transaction: Arc<Transaction>,
/// Additional UTXOs which are known at the time of verification.
known_utxos: Arc<HashMap<transparent::OutPoint, transparent::Output>>,
},
/// Verify the supplied transaction as part of the mempool. /// Verify the supplied transaction as part of the mempool.
Mempool(Arc<Transaction>), Mempool {
transaction: Arc<Transaction>,
/// Additional UTXOs which are known at the time of verification.
known_utxos: Arc<HashMap<transparent::OutPoint, transparent::Output>>,
},
} }
impl<ZS> Service<Request> for Verifier<ZS> impl<ZS> Service<Request> for Verifier<ZS>
@ -73,17 +83,23 @@ where
// TODO: break up each chunk into its own method // TODO: break up each chunk into its own method
fn call(&mut self, req: Request) -> Self::Future { fn call(&mut self, req: Request) -> Self::Future {
let is_mempool = match req { let is_mempool = match req {
Request::Block(_) => false, Request::Block { .. } => false,
Request::Mempool(_) => true, Request::Mempool { .. } => true,
}; };
if is_mempool { if is_mempool {
// XXX determine exactly which rules apply to mempool transactions // XXX determine exactly which rules apply to mempool transactions
unimplemented!(); unimplemented!();
} }
let tx = match req { let (tx, known_utxos) = match req {
Request::Block(tx) => tx, Request::Block {
Request::Mempool(tx) => tx, transaction,
known_utxos,
} => (transaction, known_utxos),
Request::Mempool {
transaction,
known_utxos,
} => (transaction, known_utxos),
}; };
let mut redjubjub_verifier = crate::primitives::redjubjub::VERIFIER.clone(); let mut redjubjub_verifier = crate::primitives::redjubjub::VERIFIER.clone();
@ -119,6 +135,7 @@ where
// feed all of the inputs to the script verifier // feed all of the inputs to the script verifier
for input_index in 0..inputs.len() { for input_index in 0..inputs.len() {
let rsp = script_verifier.ready_and().await?.call(script::Request { let rsp = script_verifier.ready_and().await?.call(script::Request {
known_utxos: known_utxos.clone(),
transaction: tx.clone(), transaction: tx.clone(),
input_index, input_index,
}); });