diff --git a/chainstate/tx-verifier/src/lib.rs b/chainstate/tx-verifier/src/lib.rs index 74e4fd9312..8e28f4666a 100644 --- a/chainstate/tx-verifier/src/lib.rs +++ b/chainstate/tx-verifier/src/lib.rs @@ -19,6 +19,7 @@ pub use transaction_verifier::{ check_transaction::{check_transaction, CheckTransactionError}, error, flush::flush_to_storage, + input_check::{BlockVerificationContext, TransactionVerificationContext}, storage::{ TransactionVerifierStorageError, TransactionVerifierStorageMut, TransactionVerifierStorageRef, diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_check.rs b/chainstate/tx-verifier/src/transaction_verifier/input_check.rs new file mode 100644 index 0000000000..fa312e7d6d --- /dev/null +++ b/chainstate/tx-verifier/src/transaction_verifier/input_check.rs @@ -0,0 +1,301 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use chainstate_types::block_index_ancestor_getter; +use common::{ + chain::{ + block::timestamp::BlockTimestamp, + signature::{verify_signature, Signable, Transactable}, + ChainConfig, GenBlock, TxInput, TxOutput, UtxoOutPoint, + }, + primitives::{BlockHeight, Id}, +}; +use itertools::Itertools; + +use crate::TransactionVerifierStorageRef; + +use super::{ + error::ConnectTransactionError, signature_destination_getter::SignatureDestinationGetter, + TransactionSourceForConnect, +}; + +pub struct BlockVerificationContext<'a> { + chain_config: &'a ChainConfig, + destination_getter: SignatureDestinationGetter<'a>, + spending_time: BlockTimestamp, + spending_height: BlockHeight, + tip: Id, +} + +impl<'a> BlockVerificationContext<'a> { + // TODO(PR): Make the timelock-only property compile time checked + pub fn for_timelock_check_only( + chain_config: &'a ChainConfig, + spend_time: BlockTimestamp, + spend_height: BlockHeight, + tip: Id, + ) -> Self { + let dest_getter = SignatureDestinationGetter::new_custom(Box::new(|_| { + panic!("Signature getter called from timelock-only context") + })); + Self::custom(chain_config, dest_getter, spend_time, spend_height, tip) + } + + pub fn from_source( + chain_config: &'a ChainConfig, + destination_getter: SignatureDestinationGetter<'a>, + spending_time: BlockTimestamp, + tx_source: &TransactionSourceForConnect, + ) -> Self { + let tip = match tx_source { + TransactionSourceForConnect::Chain { new_block_index } => { + (*new_block_index.block_id()).into() + } + TransactionSourceForConnect::Mempool { + current_best, + effective_height: _, + } => current_best.block_id().into(), + }; + + Self::custom( + chain_config, + destination_getter, + spending_time, + tx_source.expected_block_height(), + tip, + ) + } + + pub fn custom( + chain_config: &'a ChainConfig, + destination_getter: SignatureDestinationGetter<'a>, + spending_time: BlockTimestamp, + spending_height: BlockHeight, + tip: Id, + ) -> Self { + Self { + chain_config, + destination_getter, + spending_time, + spending_height, + tip, + } + } +} + +struct UtxoInputSpendingInfo { + timestamp: BlockTimestamp, + height: BlockHeight, +} + +enum InputSpendingInfo { + Utxo(UtxoInputSpendingInfo), + Account, + AccountCommand, +} + +impl InputSpendingInfo { + fn as_utxo(&self) -> Option<&UtxoInputSpendingInfo> { + match self { + Self::Utxo(info) => Some(info), + Self::AccountCommand | Self::Account => None, + } + } +} + +pub struct TransactionVerificationContext<'a, T> { + block_ctx: &'a BlockVerificationContext<'a>, + transactable: &'a T, + spent_outputs: Vec>, + spent_infos: Vec, +} + +impl<'a, T: Signable + Transactable> TransactionVerificationContext<'a, T> { + pub fn new( + block_ctx: &'a BlockVerificationContext<'a>, + utxo_view: &U, + transactable: &'a T, + storage: &S, + ) -> Result { + let inputs = transactable.inputs().unwrap_or(&[]); + + let (spent_outputs, spent_infos): (Vec<_>, Vec<_>) = inputs + .iter() + .map(|input| { + let utxo_and_info = match input { + TxInput::Utxo(outpoint) => { + let utxo = + utxo_view.utxo(&outpoint).map_err(|_| utxo::Error::ViewRead)?.ok_or( + ConnectTransactionError::MissingOutputOrSpent(outpoint.clone()), + )?; + + let (height, timestamp) = match utxo.source() { + utxo::UtxoSource::Blockchain(height) => { + let block_index_getter = |db_tx: &S, _cc: &ChainConfig, id: &Id| { + db_tx.get_gen_block_index(id) + }; + + let source_block_index = block_index_ancestor_getter( + block_index_getter, + storage, + block_ctx.chain_config, + (&block_ctx.tip).into(), + *height, + ) + .map_err(|e| { + ConnectTransactionError::InvariantErrorHeaderCouldNotBeLoadedFromHeight( + e, *height, + ) + })?; + + (*height, source_block_index.block_timestamp()) + } + utxo::UtxoSource::Mempool => { + (block_ctx.spending_height, block_ctx.spending_time) + } + }; + + let info = UtxoInputSpendingInfo { timestamp, height }; + (Some(utxo.take_output()), InputSpendingInfo::Utxo(info)) + } + TxInput::Account(..) => (None, InputSpendingInfo::Account), + TxInput::AccountCommand(..) => (None, InputSpendingInfo::AccountCommand), + }; + Ok(utxo_and_info) + }) + .collect::, ConnectTransactionError>>()?.into_iter().unzip(); + + Ok(Self { + block_ctx, + transactable, + spent_outputs, + spent_infos, + }) + } + + pub fn inputs(&self) -> &[TxInput] { + self.transactable.inputs().unwrap_or(&[]) + } + + fn try_for_each_input( + &self, + mut func: impl FnMut(InputVerificationContext) -> Result<(), E>, + ) -> Result<(), E> { + (0..self.spent_outputs.len()) + .try_for_each(|input_index| func(InputVerificationContext::new(self, input_index))) + } + + pub fn verify_inputs(&self) -> Result<(), ConnectTransactionError> { + self.try_for_each_input(|input_ctx| input_ctx.verify_input()) + } + + pub fn verify_input_timelocks(&self) -> Result<(), ConnectTransactionError> { + self.try_for_each_input(|input_ctx| input_ctx.check_timelock()) + } +} + +struct InputVerificationContext<'a, T> { + transaction_ctx: &'a TransactionVerificationContext<'a, T>, + input_index: usize, + info: InputVerificationInfo<'a>, +} + +enum InputVerificationInfo<'a> { + Utxo(UtxoInputVerificationInfo<'a>), + Account, + AccountCommand, +} + +struct UtxoInputVerificationInfo<'a> { + output: &'a TxOutput, + spending_info: &'a UtxoInputSpendingInfo, + outpoint: &'a UtxoOutPoint, +} + +impl<'a, T: Signable + Transactable> InputVerificationContext<'a, T> { + fn new(transaction_ctx: &'a TransactionVerificationContext<'a, T>, input_index: usize) -> Self { + assert!(input_index < transaction_ctx.spent_infos.len()); + + let info = match &transaction_ctx.inputs()[input_index] { + TxInput::Utxo(outpoint) => { + let output = &transaction_ctx.spent_outputs[input_index] + .as_ref() + .expect("Already checked on construction"); + let spending_info = (&transaction_ctx.spent_infos[input_index]) + .as_utxo() + .expect("Already checked on construction"); + let info = UtxoInputVerificationInfo { + output, + spending_info, + outpoint, + }; + InputVerificationInfo::Utxo(info) + } + TxInput::Account(_outpoint) => InputVerificationInfo::Account, + TxInput::AccountCommand(_nonce, _command) => InputVerificationInfo::AccountCommand, + }; + + Self { + transaction_ctx, + input_index, + info, + } + } + + fn input(&self) -> &TxInput { + &self.transaction_ctx.inputs()[self.input_index] + } + + fn verify_input(&self) -> Result<(), ConnectTransactionError> { + self.check_timelock()?; + self.check_signatures()?; + Ok(()) + } + + fn check_timelock(&self) -> Result<(), ConnectTransactionError> { + match &self.info { + InputVerificationInfo::Utxo(info) => { + let timelock = match info.output.timelock() { + Some(timelock) => timelock, + None => return Ok(()), + }; + super::timelock_check::check_timelock( + &info.spending_info.height, + &info.spending_info.timestamp, + timelock, + &self.transaction_ctx.block_ctx.spending_height, + &self.transaction_ctx.block_ctx.spending_time, + &info.outpoint, + ) + } + InputVerificationInfo::Account => Ok(()), + InputVerificationInfo::AccountCommand => Ok(()), + } + } + + fn check_signatures(&self) -> Result<(), ConnectTransactionError> { + let block_ctx = self.transaction_ctx.block_ctx; + let spent_inputs = + self.transaction_ctx.spent_outputs.iter().map(|o| o.as_ref()).collect_vec(); + verify_signature( + &block_ctx.chain_config, + &block_ctx.destination_getter.call(self.input())?, + self.transaction_ctx.transactable, + &spent_inputs, + self.input_index, + ) + .map_err(ConnectTransactionError::SignatureVerificationFailed) + } +} diff --git a/chainstate/tx-verifier/src/transaction_verifier/mod.rs b/chainstate/tx-verifier/src/transaction_verifier/mod.rs index 1438a93c30..051850a174 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/mod.rs @@ -23,6 +23,7 @@ pub mod check_transaction; pub mod error; pub mod flush; pub mod hierarchy; +pub mod input_check; pub mod signature_destination_getter; pub mod storage; pub mod timelock_check; @@ -255,7 +256,7 @@ where }; ensure!( expected_nonce == nonce, - ConnectTransactionError::NonceIsNotIncremental(account, expected_nonce, nonce,) + ConnectTransactionError::NonceIsNotIncremental(account, expected_nonce, nonce) ); // store new nonce self.account_nonce.insert(account, CachedOperation::Write(nonce)); @@ -732,6 +733,28 @@ where &self.utxo_cache, )?; + { + let accounting_adapter = &self.pos_accounting_adapter.accounting_delta(); + let destination_getter = SignatureDestinationGetter::new_for_transaction( + &self.tokens_accounting_cache, + &accounting_adapter, + &self.utxo_cache, + ); + let block_ctx = input_check::BlockVerificationContext::from_source( + self.chain_config.as_ref(), + destination_getter, + *median_time_past, + &tx_source, + ); + input_check::TransactionVerificationContext::new( + &block_ctx, + &self.utxo_cache, + tx, + &self.storage, + )? + .verify_inputs()?; + } + /* // check timelocks of the outputs and make sure there's no premature spending timelock_check::check_timelocks( &self.storage, @@ -753,6 +776,7 @@ where &self.utxo_cache, ), )?; + */ self.connect_pos_accounting_outputs(tx_source, tx.transaction())?; diff --git a/chainstate/tx-verifier/src/transaction_verifier/timelock_check.rs b/chainstate/tx-verifier/src/transaction_verifier/timelock_check.rs index 67f8a8d506..17c883e55d 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/timelock_check.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/timelock_check.rs @@ -13,22 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use chainstate_types::{block_index_ancestor_getter, GenBlockIndex}; use common::{ - chain::{ - block::timestamp::BlockTimestamp, signature::Transactable, timelock::OutputTimeLock, - ChainConfig, GenBlock, TxInput, UtxoOutPoint, - }, - primitives::{BlockCount, BlockDistance, BlockHeight, Id}, + chain::{block::timestamp::BlockTimestamp, timelock::OutputTimeLock, UtxoOutPoint}, + primitives::{BlockCount, BlockDistance, BlockHeight}, }; use thiserror::Error; use utils::ensure; -use utxo::UtxosView; -use super::{ - error::ConnectTransactionError, storage::TransactionVerifierStorageRef, - TransactionSourceForConnect, -}; +use super::error::ConnectTransactionError; #[derive(Error, Debug, PartialEq, Eq, Clone)] pub enum OutputMaturityError { @@ -74,102 +66,6 @@ pub fn check_timelock( Ok(()) } -pub fn check_timelocks( - storage: &S, - chain_config: &C, - utxos_view: &U, - tx: &T, - tx_source: &TransactionSourceForConnect, - spending_time: &BlockTimestamp, -) -> Result<(), ConnectTransactionError> -where - S: TransactionVerifierStorageRef, - C: AsRef, - T: Transactable, - U: UtxosView, -{ - let inputs = match tx.inputs() { - Some(inputs) => inputs, - None => return Ok(()), - }; - - let input_utxos = inputs - .iter() - .map(|input| match input { - TxInput::Utxo(outpoint) => { - let utxo = utxos_view.utxo(outpoint).map_err(|_| utxo::Error::ViewRead)?.ok_or( - ConnectTransactionError::MissingOutputOrSpent(outpoint.clone()), - )?; - Ok(Some((outpoint.clone(), utxo))) - } - TxInput::Account(..) | TxInput::AccountCommand(..) => Ok(None), - }) - .collect::, ConnectTransactionError>>()?; - debug_assert_eq!(inputs.len(), input_utxos.len()); - - let starting_point: GenBlockIndex = match tx_source { - TransactionSourceForConnect::Chain { new_block_index } => { - (*new_block_index).clone().into_gen_block_index() - } - TransactionSourceForConnect::Mempool { - current_best, - effective_height: _, - } => (*current_best).clone(), - }; - - // check if utxos can already be spent - for (outpoint, utxo) in input_utxos.iter().filter_map(|utxo| utxo.as_ref()) { - if let Some(timelock) = utxo.output().timelock() { - let (height, timestamp) = match utxo.source() { - utxo::UtxoSource::Blockchain(height) => { - let block_index_getter = |db_tx: &S, _cc: &ChainConfig, id: &Id| { - db_tx.get_gen_block_index(id) - }; - - let source_block_index = block_index_ancestor_getter( - block_index_getter, - storage, - chain_config.as_ref(), - (&starting_point).into(), - *height, - ) - .map_err(|e| { - ConnectTransactionError::InvariantErrorHeaderCouldNotBeLoadedFromHeight( - e, *height, - ) - })?; - - (*height, source_block_index.block_timestamp()) - } - utxo::UtxoSource::Mempool => match tx_source { - TransactionSourceForConnect::Chain { new_block_index: _ } => { - unreachable!("Mempool utxos can never be reached from storage while connecting local transactions") - } - TransactionSourceForConnect::Mempool { - current_best: _, - effective_height, - } => { - // We're building upon another transaction in mempool. Treat it is as if it - // was included at earliest possible time at earliest possible block. - (*effective_height, *spending_time) - } - }, - }; - - check_timelock( - &height, - ×tamp, - timelock, - &tx_source.expected_block_height(), - spending_time, - outpoint, - )?; - } - } - - Ok(()) -} - pub fn check_output_maturity_setting( timelock: &OutputTimeLock, required: BlockCount, diff --git a/chainstate/tx-verifier/src/transaction_verifier/tx_source.rs b/chainstate/tx-verifier/src/transaction_verifier/tx_source.rs index b8fdd6b7f7..1255d6e9a0 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/tx_source.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/tx_source.rs @@ -15,7 +15,7 @@ use chainstate_types::{BlockIndex, GenBlockIndex}; use common::{ - chain::Block, + chain::{Block, GenBlock}, primitives::{BlockHeight, Id}, }; use utxo::UtxoSource; @@ -112,6 +112,18 @@ impl<'a> TransactionSourceForConnect<'a> { } } + pub fn tip_block_id(&self) -> Id { + match self { + TransactionSourceForConnect::Chain { new_block_index } => { + (*new_block_index.block_id()).into() + } + TransactionSourceForConnect::Mempool { + current_best, + effective_height: _, + } => current_best.block_id(), + } + } + pub fn to_utxo_source(&self) -> UtxoSource { match self { Self::Chain { diff --git a/mempool/src/pool/tx_pool/collect_txs.rs b/mempool/src/pool/tx_pool/collect_txs.rs index ad50da7684..ac6747b800 100644 --- a/mempool/src/pool/tx_pool/collect_txs.rs +++ b/mempool/src/pool/tx_pool/collect_txs.rs @@ -99,6 +99,13 @@ pub fn collect_txs( .expect("best index to exist"); let tx_source = TransactionSourceForConnect::for_mempool(&best_index); + let block_verification_ctx = tx_verifier::BlockVerificationContext::for_timelock_check_only( + chain_config, + unlock_timestamp, + best_index.block_height().next_height(), + mempool_tip, + ); + // Use transactions already in the Accumulator to check for uniqueness and to update the // verifier state to update UTXOs they consume / provide. let accum_ids = tx_accumulator @@ -148,14 +155,13 @@ pub fn collect_txs( // If the transaction with this ID has already been processed, skip it ensure!(processed.insert(tx_id)); let tx = mempool.store.txs_by_id.get(tx_id).expect("already checked").deref(); - let timelock_check = chainstate::tx_verifier::timelock_check::check_timelocks( - &chainstate, - chain_config, + let timelock_check = tx_verifier::TransactionVerificationContext::new( + &block_verification_ctx, &utxo_view, tx.transaction(), - &tx_source, - &unlock_timestamp, - ); + &chainstate, + ) + .and_then(|tx_ctx| tx_ctx.verify_input_timelocks()); ensure!(timelock_check.is_ok()); Some(tx) }) diff --git a/mempool/src/pool/tx_pool/tx_verifier/mod.rs b/mempool/src/pool/tx_pool/tx_verifier/mod.rs index 1cc2f80d00..01325a05fc 100644 --- a/mempool/src/pool/tx_pool/tx_verifier/mod.rs +++ b/mempool/src/pool/tx_pool/tx_verifier/mod.rs @@ -20,7 +20,9 @@ mod utxo_view; use std::sync::Arc; -pub use chainstate::tx_verifier::flush_to_storage; +pub use chainstate::tx_verifier::{ + flush_to_storage, BlockVerificationContext, TransactionVerificationContext, +}; use common::chain::ChainConfig; use utils::shallow_clone::ShallowClone;