Skip to content

Commit

Permalink
Validate transaction lock times (#3060)
Browse files Browse the repository at this point in the history
* Create a `LockTime::unlocked` helper constructor

Returns a `LockTime` that is unlocked at the genesis block.

* Return `Option<LockTime>` from `lock_time` method

Prepare to return `None` for when a transaction has its lock time
disabled.

* Return `None` instead of zero `LockTime`

Because a zero lock time means that the transaction was unlocked at the
genesis block, so it was never actually locked.

* Rephrase zero lock time check comment

Clarify that the check is not redundant, and is necessary for the
genesis transaction.

Co-authored-by: teor <[email protected]>

* Add a `transparent::Input::sequence` getter method

Retrieve a transparent input's sequence number.

* Check if lock time is enabled by a sequence number

Validate the consensus rule that the lock time is only enabled if at
least one transparent input has a value different from `u32::MAX` as its
sequence number.

* Add more Zcash specific details to comment

Explain the Zcash specific lock time behaviors.

Co-authored-by: teor <[email protected]>

* Add `time` field to `Request::Block` variant

The block time to use to check if the transaction was unlocked and
allowed to be included in the block.

* Add `Request::block_time` getter

Returns the block time for the block that owns the transaction being
validated or the current time plus a tolerance for mempool transactions.

* Validate transaction lock times

If they are enabled by a transaction's transparent input sequence
numbers, make sure that they are in the past.

* Add comments with consensus rule parts

Make it easier to map what part of the consensus rule each match arm is
responsible for.

Co-authored-by: teor <[email protected]>
  • Loading branch information
jvff and teor2345 authored Nov 23, 2021
1 parent dbd49a3 commit ec2c980
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 14 deletions.
50 changes: 43 additions & 7 deletions zebra-chain/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,13 +318,49 @@ impl Transaction {
}

/// Get this transaction's lock time.
pub fn lock_time(&self) -> LockTime {
match self {
Transaction::V1 { lock_time, .. } => *lock_time,
Transaction::V2 { lock_time, .. } => *lock_time,
Transaction::V3 { lock_time, .. } => *lock_time,
Transaction::V4 { lock_time, .. } => *lock_time,
Transaction::V5 { lock_time, .. } => *lock_time,
pub fn lock_time(&self) -> Option<LockTime> {
let lock_time = match self {
Transaction::V1 { lock_time, .. }
| Transaction::V2 { lock_time, .. }
| Transaction::V3 { lock_time, .. }
| Transaction::V4 { lock_time, .. }
| Transaction::V5 { lock_time, .. } => *lock_time,
};

// `zcashd` checks that the block height is greater than the lock height.
// This check allows the genesis block transaction, which would otherwise be invalid.
// (Or have to use a lock time.)
//
// It matches the `zcashd` check here:
// https://github.com/zcash/zcash/blob/1a7c2a3b04bcad6549be6d571bfdff8af9a2c814/src/main.cpp#L720
if lock_time == LockTime::unlocked() {
return None;
}

// Consensus rule:
//
// > The transaction must be finalized: either its locktime must be in the past (or less
// > than or equal to the current block height), or all of its sequence numbers must be
// > 0xffffffff.
//
// In `zcashd`, this rule applies to both coinbase and prevout input sequence numbers.
//
// Unlike Bitcoin, Zcash allows transactions with no transparent inputs. These transactions
// only have shielded inputs. Surprisingly, the `zcashd` implementation ignores the lock
// time in these transactions. `zcashd` only checks the lock time when it finds a
// transparent input sequence number that is not `u32::MAX`.
//
// https://developer.bitcoin.org/devguide/transactions.html#non-standard-transactions
let has_sequence_number_enabling_lock_time = self
.inputs()
.iter()
.map(transparent::Input::sequence)
.any(|sequence_number| sequence_number != u32::MAX);

if has_sequence_number_enabling_lock_time {
Some(lock_time)
} else {
None
}
}

Expand Down
7 changes: 7 additions & 0 deletions zebra-chain/src/transaction/lock_time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ impl LockTime {
/// LockTime is u32 in the spec, so times are limited to u32::MAX.
pub const MAX_TIMESTAMP: i64 = u32::MAX as i64;

/// Returns a [`LockTime`] that is always unlocked.
///
/// The lock time is set to the block height of the genesis block.
pub fn unlocked() -> Self {
LockTime::Height(block::Height(0))
}

/// Returns the minimum LockTime::Time, as a LockTime.
///
/// Users should not construct lock times less than `min_lock_timestamp`.
Expand Down
7 changes: 7 additions & 0 deletions zebra-chain/src/transparent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ impl fmt::Display for Input {
}

impl Input {
/// Returns the input's sequence number.
pub fn sequence(&self) -> u32 {
match self {
Input::PrevOut { sequence, .. } | Input::Coinbase { sequence, .. } => *sequence,
}
}

/// If this is a `PrevOut` input, returns this input's outpoint.
/// Otherwise, returns `None`.
pub fn outpoint(&self) -> Option<OutPoint> {
Expand Down
1 change: 1 addition & 0 deletions zebra-consensus/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ where
transaction: transaction.clone(),
known_utxos: known_utxos.clone(),
height,
time: block.header.time,
});
async_checks.push(rsp);
}
Expand Down
6 changes: 3 additions & 3 deletions zebra-consensus/src/block/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use zebra_chain::{
},
parameters::{Network, NetworkUpgrade},
serialization::{ZcashDeserialize, ZcashDeserializeInto},
transaction::{arbitrary::transaction_to_fake_v5, Transaction},
transaction::{arbitrary::transaction_to_fake_v5, LockTime, Transaction},
work::difficulty::{ExpandedDifficulty, INVALID_COMPACT_DIFFICULTY},
};
use zebra_script::CachedFfiTransaction;
Expand Down Expand Up @@ -396,7 +396,7 @@ fn founders_reward_validation_failure() -> Result<(), Report> {
.map(|transaction| Transaction::V3 {
inputs: transaction.inputs().to_vec(),
outputs: vec![transaction.outputs()[0].clone()],
lock_time: transaction.lock_time(),
lock_time: transaction.lock_time().unwrap_or_else(LockTime::unlocked),
expiry_height: Height(0),
joinsplit_data: None,
})
Expand Down Expand Up @@ -468,7 +468,7 @@ fn funding_stream_validation_failure() -> Result<(), Report> {
.map(|transaction| Transaction::V4 {
inputs: transaction.inputs().to_vec(),
outputs: vec![transaction.outputs()[0].clone()],
lock_time: transaction.lock_time(),
lock_time: transaction.lock_time().unwrap_or_else(LockTime::unlocked),
expiry_height: Height(0),
joinsplit_data: None,
sapling_shielded_data: None,
Expand Down
10 changes: 9 additions & 1 deletion zebra-consensus/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
//! implement, and ensures that we don't reject blocks or transactions
//! for a non-enumerated reason.
use chrono::{DateTime, Utc};
use thiserror::Error;

use zebra_chain::{orchard, sapling, sprout, transparent};
use zebra_chain::{block, orchard, sapling, sprout, transparent};

use crate::{block::MAX_BLOCK_SIGOPS, BoxError};

Expand Down Expand Up @@ -56,6 +57,13 @@ pub enum TransactionError {
#[error("coinbase inputs MUST NOT exist in mempool")]
CoinbaseInMempool,

#[error("transaction is locked until after block height {}", _0.0)]
LockedUntilAfterBlockHeight(block::Height),

#[error("transaction is locked until after block time {0}")]
#[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
LockedUntilAfterBlockTime(DateTime<Utc>),

#[error("coinbase expiration height is invalid")]
CoinbaseExpiration,

Expand Down
15 changes: 15 additions & 0 deletions zebra-consensus/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::{
task::{Context, Poll},
};

use chrono::{DateTime, Utc};
use futures::{
stream::{FuturesUnordered, StreamExt},
FutureExt, TryFutureExt,
Expand Down Expand Up @@ -80,6 +81,8 @@ pub enum Request {
known_utxos: Arc<HashMap<transparent::OutPoint, transparent::OrderedUtxo>>,
/// The height of the block containing this transaction.
height: block::Height,
/// The time that the block was mined.
time: DateTime<Utc>,
},
/// Verify the supplied transaction as part of the mempool.
///
Expand Down Expand Up @@ -185,6 +188,14 @@ impl Request {
}
}

/// The block time used for lock time consensus rules validation.
pub fn block_time(&self) -> Option<DateTime<Utc>> {
match self {
Request::Block { time, .. } => Some(*time),
Request::Mempool { .. } => None,
}
}

/// The network upgrade to consider for the verification.
///
/// This is based on the block height from the request, and the supplied `network`.
Expand Down Expand Up @@ -282,6 +293,10 @@ where
let (utxo_sender, mut utxo_receiver) = mpsc::unbounded_channel();

// Do basic checks first
if let Some(block_time) = req.block_time() {
check::lock_time_has_passed(&tx, req.height(), block_time)?;
}

check::has_inputs_and_outputs(&tx)?;
check::has_enough_orchard_flags(&tx)?;

Expand Down
45 changes: 44 additions & 1 deletion zebra-consensus/src/transaction/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,61 @@
use std::{borrow::Cow, collections::HashSet, convert::TryFrom, hash::Hash};

use chrono::{DateTime, Utc};

use zebra_chain::{
amount::{Amount, NonNegative},
block::Height,
orchard::Flags,
parameters::{Network, NetworkUpgrade},
primitives::zcash_note_encryption,
sapling::{Output, PerSpendAnchor, Spend},
transaction::Transaction,
transaction::{LockTime, Transaction},
};

use crate::error::TransactionError;

/// Checks if the transaction's lock time allows this transaction to be included in a block.
///
/// Consensus rule:
///
/// > The transaction must be finalized: either its locktime must be in the past (or less
/// > than or equal to the current block height), or all of its sequence numbers must be
/// > 0xffffffff.
///
/// [`Transaction::lock_time`] validates the transparent input sequence numbers, returning [`None`]
/// if they indicate that the transaction is finalized by them. Otherwise, this function validates
/// if the lock time is in the past.
pub fn lock_time_has_passed(
tx: &Transaction,
block_height: Height,
block_time: DateTime<Utc>,
) -> Result<(), TransactionError> {
match tx.lock_time() {
Some(LockTime::Height(unlock_height)) => {
// > The transaction can be added to any block which has a greater height.
// The Bitcoin documentation is wrong or outdated here,
// so this code is based on the `zcashd` implementation at:
// https://github.com/zcash/zcash/blob/1a7c2a3b04bcad6549be6d571bfdff8af9a2c814/src/main.cpp#L722
if block_height > unlock_height {
Ok(())
} else {
Err(TransactionError::LockedUntilAfterBlockHeight(unlock_height))
}
}
Some(LockTime::Time(unlock_time)) => {
// > The transaction can be added to any block whose block time is greater than the locktime.
// https://developer.bitcoin.org/devguide/transactions.html#locktime-and-sequence-number
if block_time > unlock_time {
Ok(())
} else {
Err(TransactionError::LockedUntilAfterBlockTime(unlock_time))
}
}
None => Ok(()),
}
}

/// Checks that the transaction has inputs and outputs.
///
/// For `Transaction::V4`:
Expand Down
Loading

0 comments on commit ec2c980

Please sign in to comment.