use crate::database::{
transaction::DatabaseTransaction,
transactions::TransactionIndex,
vm_database::VmDatabase,
Database,
};
use fuel_core_executor::{
refs::ContractRef,
Config,
};
use fuel_core_storage::{
tables::{
Coins,
ContractsLatestUtxo,
FuelBlocks,
Messages,
Receipts,
SpentMessages,
Transactions,
},
transactional::{
StorageTransaction,
Transaction as StorageTransactionTrait,
},
StorageAsMut,
StorageAsRef,
StorageInspect,
};
#[allow(unused_imports)]
use fuel_core_types::{
blockchain::{
block::{
Block,
PartialFuelBlock,
},
header::PartialBlockHeader,
primitives::DaBlockHeight,
},
entities::{
coins::coin::CompressedCoin,
contract::ContractUtxoInfo,
},
fuel_asm::{
RegId,
Word,
},
fuel_tx::{
field::{
Inputs,
Outputs,
TxPointer as TxPointerField,
},
input::{
coin::{
CoinPredicate,
CoinSigned,
},
contract::Contract,
message::{
MessageCoinPredicate,
MessageCoinSigned,
MessageDataPredicate,
MessageDataSigned,
},
},
Address,
AssetId,
Bytes32,
Cacheable,
Input,
Mint,
Output,
Receipt,
Transaction,
TransactionFee,
TxId,
TxPointer,
UniqueIdentifier,
UtxoId,
ValidityError,
},
fuel_types::{
canonical::Serialize,
BlockHeight,
MessageId,
},
fuel_vm::{
checked_transaction::{
CheckError,
CheckPredicateParams,
CheckPredicates,
Checked,
CheckedTransaction,
Checks,
CreateCheckedMetadata,
IntoChecked,
ScriptCheckedMetadata,
},
interpreter::{
CheckedMetadata,
ExecutableTransaction,
InterpreterParams,
},
state::StateTransition,
Backtrace as FuelBacktrace,
Interpreter,
InterpreterError,
},
services::{
block_producer::Components,
executor::{
Error as ExecutorError,
ExecutionKind,
ExecutionResult,
ExecutionType,
ExecutionTypes,
Result as ExecutorResult,
TransactionExecutionResult,
TransactionExecutionStatus,
TransactionValidityError,
UncommittedResult,
},
txpool::TransactionStatus,
},
};
use fuel_core_txpool::types::ContractId;
use fuel_core_types::{
fuel_tx::{
field::{
InputContract,
MintAmount,
MintAssetId,
OutputContract,
},
input,
output,
Chargeable,
},
fuel_vm,
};
use parking_lot::Mutex as ParkingMutex;
use std::{
borrow::Cow,
ops::{
Deref,
DerefMut,
},
sync::Arc,
};
use tracing::{
debug,
warn,
};
mod ports;
pub use ports::{
MaybeCheckedTransaction,
RelayerPort,
TransactionsSource,
};
pub type ExecutionBlockWithSource<TxSource> = ExecutionTypes<Components<TxSource>, Block>;
pub struct OnceTransactionsSource {
transactions: ParkingMutex<Vec<MaybeCheckedTransaction>>,
}
impl OnceTransactionsSource {
pub fn new(transactions: Vec<Transaction>) -> Self {
Self {
transactions: ParkingMutex::new(
transactions
.into_iter()
.map(MaybeCheckedTransaction::Transaction)
.collect(),
),
}
}
}
impl TransactionsSource for OnceTransactionsSource {
fn next(&self, _: u64) -> Vec<MaybeCheckedTransaction> {
let mut lock = self.transactions.lock();
core::mem::take(lock.as_mut())
}
}
#[derive(Clone, Debug)]
pub struct Executor<R>
where
R: RelayerPort + Clone,
{
pub database: Database,
pub relayer: R,
pub config: Arc<Config>,
}
struct ExecutionData {
coinbase: u64,
used_gas: u64,
tx_count: u16,
found_mint: bool,
message_ids: Vec<MessageId>,
tx_status: Vec<TransactionExecutionStatus>,
skipped_transactions: Vec<(TxId, ExecutorError)>,
}
#[derive(Copy, Clone, Default)]
pub struct ExecutionOptions {
pub utxo_validation: bool,
}
impl From<&crate::service::Config> for ExecutionOptions {
fn from(value: &crate::service::Config) -> Self {
Self {
utxo_validation: value.utxo_validation,
}
}
}
impl From<&Config> for ExecutionOptions {
fn from(value: &Config) -> Self {
Self {
utxo_validation: value.utxo_validation_default,
}
}
}
impl<R> Executor<R>
where
R: RelayerPort + Clone,
{
#[cfg(any(test, feature = "test-helpers"))]
pub fn execute_and_commit(
&self,
block: fuel_core_types::services::executor::ExecutionBlock,
options: ExecutionOptions,
) -> ExecutorResult<ExecutionResult> {
let component = match block {
ExecutionTypes::DryRun(_) => {
panic!("It is not possible to commit the dry run result");
}
ExecutionTypes::Production(block) => ExecutionTypes::Production(Components {
header_to_produce: block.header,
transactions_source: OnceTransactionsSource::new(block.transactions),
gas_limit: u64::MAX,
}),
ExecutionTypes::Validation(block) => ExecutionTypes::Validation(block),
};
let (result, db_transaction) =
self.execute_without_commit(component, options)?.into();
db_transaction.commit()?;
Ok(result)
}
}
#[cfg(test)]
impl Executor<Database> {
fn test(database: Database, config: Config) -> Self {
Self {
relayer: database.clone(),
database,
config: Arc::new(config),
}
}
}
impl<R> Executor<R>
where
R: RelayerPort + Clone,
{
pub fn execute_without_commit<TxSource>(
&self,
block: ExecutionBlockWithSource<TxSource>,
options: ExecutionOptions,
) -> ExecutorResult<UncommittedResult<StorageTransaction<Database>>>
where
TxSource: TransactionsSource,
{
self.execute_inner(block, &self.database, options)
}
pub fn dry_run(
&self,
component: Components<Transaction>,
utxo_validation: Option<bool>,
) -> ExecutorResult<Vec<Vec<Receipt>>> {
let utxo_validation =
utxo_validation.unwrap_or(self.config.utxo_validation_default);
let options = ExecutionOptions { utxo_validation };
let component = Components {
header_to_produce: component.header_to_produce,
transactions_source: OnceTransactionsSource::new(vec![
component.transactions_source,
]),
gas_limit: component.gas_limit,
};
let (
ExecutionResult {
block,
skipped_transactions,
..
},
temporary_db,
) = self
.execute_without_commit(ExecutionTypes::DryRun(component), options)?
.into();
if let Some((_, err)) = skipped_transactions.into_iter().next() {
return Err(err)
}
block
.transactions()
.iter()
.map(|tx| {
let id = tx.id(&self.config.consensus_parameters.chain_id);
StorageInspect::<Receipts>::get(temporary_db.as_ref(), &id)
.transpose()
.unwrap_or_else(|| Ok(Default::default()))
.map(|v| v.into_owned())
})
.collect::<Result<Vec<Vec<Receipt>>, _>>()
.map_err(Into::into)
}
}
mod private {
use super::*;
pub struct PartialBlockComponent<'a, TxSource> {
pub empty_block: &'a mut PartialFuelBlock,
pub transactions_source: TxSource,
pub gas_limit: u64,
_marker: core::marker::PhantomData<()>,
}
impl<'a> PartialBlockComponent<'a, OnceTransactionsSource> {
pub fn from_partial_block(block: &'a mut PartialFuelBlock) -> Self {
let transaction = core::mem::take(&mut block.transactions);
Self {
empty_block: block,
transactions_source: OnceTransactionsSource::new(transaction),
gas_limit: u64::MAX,
_marker: Default::default(),
}
}
}
impl<'a, TxSource> PartialBlockComponent<'a, TxSource> {
pub fn from_component(
block: &'a mut PartialFuelBlock,
transactions_source: TxSource,
gas_limit: u64,
) -> Self {
debug_assert!(block.transactions.is_empty());
PartialBlockComponent {
empty_block: block,
transactions_source,
gas_limit,
_marker: Default::default(),
}
}
}
}
use private::*;
impl<R> Executor<R>
where
R: RelayerPort + Clone,
{
#[tracing::instrument(skip_all)]
fn execute_inner<TxSource>(
&self,
block: ExecutionBlockWithSource<TxSource>,
database: &Database,
options: ExecutionOptions,
) -> ExecutorResult<UncommittedResult<StorageTransaction<Database>>>
where
TxSource: TransactionsSource,
{
let pre_exec_block_id = block.id();
let block = block.map_v(PartialFuelBlock::from);
let mut block_db_transaction = database.transaction();
let (block, execution_data) = match block {
ExecutionTypes::DryRun(component) => {
let mut block =
PartialFuelBlock::new(component.header_to_produce, vec![]);
let component = PartialBlockComponent::from_component(
&mut block,
component.transactions_source,
component.gas_limit,
);
let execution_data = self.execute_block(
&mut block_db_transaction,
ExecutionType::DryRun(component),
options,
)?;
(block, execution_data)
}
ExecutionTypes::Production(component) => {
let mut block =
PartialFuelBlock::new(component.header_to_produce, vec![]);
let component = PartialBlockComponent::from_component(
&mut block,
component.transactions_source,
component.gas_limit,
);
let execution_data = self.execute_block(
&mut block_db_transaction,
ExecutionType::Production(component),
options,
)?;
(block, execution_data)
}
ExecutionTypes::Validation(mut block) => {
let component = PartialBlockComponent::from_partial_block(&mut block);
let execution_data = self.execute_block(
&mut block_db_transaction,
ExecutionType::Validation(component),
options,
)?;
(block, execution_data)
}
};
let ExecutionData {
coinbase,
used_gas,
message_ids,
tx_status,
skipped_transactions,
..
} = execution_data;
let block = block.generate(&message_ids[..]);
let finalized_block_id = block.id();
debug!(
"Block {:#x} fees: {} gas: {}",
pre_exec_block_id.unwrap_or(finalized_block_id),
coinbase,
used_gas
);
if let Some(pre_exec_block_id) = pre_exec_block_id {
if pre_exec_block_id != finalized_block_id {
return Err(ExecutorError::InvalidBlockId)
}
}
let result = ExecutionResult {
block,
skipped_transactions,
tx_status,
};
self.persist_transaction_status(&result, block_db_transaction.deref_mut())?;
self.index_tx_owners_for_block(&result.block, &mut block_db_transaction)?;
block_db_transaction
.deref_mut()
.storage::<FuelBlocks>()
.insert(
&finalized_block_id,
&result
.block
.compress(&self.config.consensus_parameters.chain_id),
)?;
Ok(UncommittedResult::new(
result,
StorageTransaction::new(block_db_transaction),
))
}
#[tracing::instrument(skip_all)]
fn execute_block<TxSource>(
&self,
block_db_transaction: &mut DatabaseTransaction,
block: ExecutionType<PartialBlockComponent<TxSource>>,
options: ExecutionOptions,
) -> ExecutorResult<ExecutionData>
where
TxSource: TransactionsSource,
{
let mut data = ExecutionData {
coinbase: 0,
used_gas: 0,
tx_count: 0,
found_mint: false,
message_ids: Vec::new(),
tx_status: Vec::new(),
skipped_transactions: Vec::new(),
};
let execution_data = &mut data;
let (execution_kind, component) = block.split();
let block = component.empty_block;
let source = component.transactions_source;
let mut remaining_gas_limit = component.gas_limit;
let block_height = *block.header.height();
debug_assert!(block.transactions.is_empty());
let mut iter = source.next(remaining_gas_limit).into_iter().peekable();
let mut execute_transaction = |execution_data: &mut ExecutionData,
tx: MaybeCheckedTransaction|
-> ExecutorResult<()> {
let tx_count = execution_data.tx_count;
let tx = {
let mut tx_db_transaction = block_db_transaction.transaction();
let tx_id = tx.id(&self.config.consensus_parameters.chain_id);
let result = self.execute_transaction(
tx,
&tx_id,
&block.header,
execution_data,
execution_kind,
&mut tx_db_transaction,
options,
);
let tx = match result {
Err(err) => {
return match execution_kind {
ExecutionKind::Production => {
execution_data.skipped_transactions.push((tx_id, err));
Ok(())
}
ExecutionKind::DryRun | ExecutionKind::Validation => Err(err),
}
}
Ok(tx) => tx,
};
if let Err(err) = tx_db_transaction.commit() {
return Err(err.into())
}
tx
};
block.transactions.push(tx);
execution_data.tx_count = tx_count
.checked_add(1)
.ok_or(ExecutorError::TooManyTransactions)?;
Ok(())
};
while iter.peek().is_some() {
for transaction in iter {
execute_transaction(&mut *execution_data, transaction)?;
}
remaining_gas_limit =
component.gas_limit.saturating_sub(execution_data.used_gas);
iter = source.next(remaining_gas_limit).into_iter().peekable();
}
if execution_kind == ExecutionKind::Production {
let amount_to_mint = if self.config.coinbase_recipient != ContractId::zeroed()
{
execution_data.coinbase
} else {
0
};
let coinbase_tx = Transaction::mint(
TxPointer::new(block_height, execution_data.tx_count),
input::contract::Contract {
utxo_id: UtxoId::new(Bytes32::zeroed(), 0),
balance_root: Bytes32::zeroed(),
state_root: Bytes32::zeroed(),
tx_pointer: TxPointer::new(BlockHeight::new(0), 0),
contract_id: self.config.coinbase_recipient,
},
output::contract::Contract {
input_index: 0,
balance_root: Bytes32::zeroed(),
state_root: Bytes32::zeroed(),
},
amount_to_mint,
self.config.consensus_parameters.base_asset_id,
);
execute_transaction(
execution_data,
MaybeCheckedTransaction::Transaction(coinbase_tx.into()),
)?;
}
if execution_kind != ExecutionKind::DryRun && !data.found_mint {
return Err(ExecutorError::MintMissing)
}
Ok(data)
}
#[allow(clippy::too_many_arguments)]
fn execute_transaction(
&self,
tx: MaybeCheckedTransaction,
tx_id: &TxId,
header: &PartialBlockHeader,
execution_data: &mut ExecutionData,
execution_kind: ExecutionKind,
tx_db_transaction: &mut DatabaseTransaction,
options: ExecutionOptions,
) -> ExecutorResult<Transaction> {
if execution_data.found_mint {
return Err(ExecutorError::MintIsNotLastTransaction)
}
if tx_db_transaction
.deref_mut()
.storage::<Transactions>()
.contains_key(tx_id)?
{
return Err(ExecutorError::TransactionIdCollision(*tx_id))
}
let block_height = *header.height();
let checked_tx = match tx {
MaybeCheckedTransaction::Transaction(tx) => tx
.into_checked_basic(block_height, &self.config.consensus_parameters)?
.into(),
MaybeCheckedTransaction::CheckedTransaction(checked_tx) => checked_tx,
};
match checked_tx {
CheckedTransaction::Script(script) => self.execute_create_or_script(
script,
header,
execution_data,
tx_db_transaction,
execution_kind,
options,
),
CheckedTransaction::Create(create) => self.execute_create_or_script(
create,
header,
execution_data,
tx_db_transaction,
execution_kind,
options,
),
CheckedTransaction::Mint(mint) => self.execute_mint(
mint,
header,
execution_data,
tx_db_transaction,
execution_kind,
options,
),
}
}
fn execute_mint(
&self,
checked_mint: Checked<Mint>,
header: &PartialBlockHeader,
execution_data: &mut ExecutionData,
block_db_transaction: &mut DatabaseTransaction,
execution_kind: ExecutionKind,
options: ExecutionOptions,
) -> ExecutorResult<Transaction> {
execution_data.found_mint = true;
if checked_mint.transaction().tx_pointer().tx_index() != execution_data.tx_count {
return Err(ExecutorError::MintHasUnexpectedIndex)
}
let coinbase_id = checked_mint.id();
let (mut mint, _) = checked_mint.into();
fn verify_mint_for_empty_contract(mint: &Mint) -> ExecutorResult<()> {
if *mint.mint_amount() != 0 {
return Err(ExecutorError::CoinbaseAmountMismatch)
}
let input = input::contract::Contract {
utxo_id: UtxoId::new(Bytes32::zeroed(), 0),
balance_root: Bytes32::zeroed(),
state_root: Bytes32::zeroed(),
tx_pointer: TxPointer::new(BlockHeight::new(0), 0),
contract_id: ContractId::zeroed(),
};
let output = output::contract::Contract {
input_index: 0,
balance_root: Bytes32::zeroed(),
state_root: Bytes32::zeroed(),
};
if mint.input_contract() != &input || mint.output_contract() != &output {
return Err(ExecutorError::MintMismatch)
}
Ok(())
}
if mint.input_contract().contract_id == ContractId::zeroed() {
verify_mint_for_empty_contract(&mint)?;
} else {
if *mint.mint_amount() != execution_data.coinbase {
return Err(ExecutorError::CoinbaseAmountMismatch)
}
let block_height = *header.height();
let input = mint.input_contract().clone();
let output = *mint.output_contract();
let mut inputs = [Input::Contract(input)];
let mut outputs = [Output::Contract(output)];
if options.utxo_validation {
self.verify_input_state(
block_db_transaction.deref(),
inputs.as_mut_slice(),
block_height,
header.da_height,
)?;
}
self.compute_inputs(
match execution_kind {
ExecutionKind::DryRun => {
ExecutionTypes::DryRun(inputs.as_mut_slice())
}
ExecutionKind::Production => {
ExecutionTypes::Production(inputs.as_mut_slice())
}
ExecutionKind::Validation => {
ExecutionTypes::Validation(inputs.as_slice())
}
},
coinbase_id,
block_db_transaction.deref_mut(),
options,
)?;
let mut sub_block_db_commit = block_db_transaction.transaction();
let sub_db_view = sub_block_db_commit.as_mut();
let mut vm_db = VmDatabase::new(
sub_db_view.clone(),
&header.consensus,
self.config.coinbase_recipient,
);
fuel_vm::interpreter::contract::balance_increase(
&mut vm_db,
&mint.input_contract().contract_id,
mint.mint_asset_id(),
*mint.mint_amount(),
)
.map_err(|e| anyhow::anyhow!(e))
.map_err(ExecutorError::CoinbaseCannotIncreaseBalance)?;
sub_block_db_commit.commit()?;
self.persist_output_utxos(
block_height,
execution_data.tx_count,
&coinbase_id,
block_db_transaction,
inputs.as_slice(),
outputs.as_slice(),
)?;
self.compute_not_utxo_outputs(
match execution_kind {
ExecutionKind::DryRun => ExecutionTypes::DryRun((
outputs.as_mut_slice(),
inputs.as_slice(),
)),
ExecutionKind::Production => ExecutionTypes::Production((
outputs.as_mut_slice(),
inputs.as_slice(),
)),
ExecutionKind::Validation => ExecutionTypes::Validation((
outputs.as_slice(),
inputs.as_slice(),
)),
},
coinbase_id,
block_db_transaction.deref_mut(),
)?;
let Input::Contract(input) = core::mem::take(&mut inputs[0]) else {
unreachable!()
};
let Output::Contract(output) = outputs[0] else {
unreachable!()
};
if execution_kind == ExecutionKind::Validation {
if mint.input_contract() != &input || mint.output_contract() != &output {
return Err(ExecutorError::MintMismatch)
}
} else {
*mint.input_contract_mut() = input;
*mint.output_contract_mut() = output;
}
}
let tx = mint.into();
execution_data.tx_status.push(TransactionExecutionStatus {
id: coinbase_id,
result: TransactionExecutionResult::Success { result: None },
});
if block_db_transaction
.deref_mut()
.storage::<Transactions>()
.insert(&coinbase_id, &tx)?
.is_some()
{
return Err(ExecutorError::TransactionIdCollision(coinbase_id))
}
Ok(tx)
}
#[allow(clippy::too_many_arguments)]
fn execute_create_or_script<Tx>(
&self,
mut checked_tx: Checked<Tx>,
header: &PartialBlockHeader,
execution_data: &mut ExecutionData,
tx_db_transaction: &mut DatabaseTransaction,
execution_kind: ExecutionKind,
options: ExecutionOptions,
) -> ExecutorResult<Transaction>
where
Tx: ExecutableTransaction + PartialEq + Cacheable + Send + Sync + 'static,
<Tx as IntoChecked>::Metadata: Fee + CheckedMetadata + Clone + Send + Sync,
{
let tx_id = checked_tx.id();
let max_fee = checked_tx.metadata().max_fee();
if options.utxo_validation {
checked_tx = checked_tx
.check_predicates(&CheckPredicateParams::from(
&self.config.consensus_parameters,
))
.map_err(|_| {
ExecutorError::TransactionValidity(
TransactionValidityError::InvalidPredicate(tx_id),
)
})?;
debug_assert!(checked_tx.checks().contains(Checks::Predicates));
self.verify_input_state(
tx_db_transaction.deref(),
checked_tx.transaction().inputs(),
*header.height(),
header.da_height,
)?;
checked_tx = checked_tx
.check_signatures(&self.config.consensus_parameters.chain_id)
.map_err(TransactionValidityError::from)?;
debug_assert!(checked_tx.checks().contains(Checks::Signatures));
}
let mut sub_block_db_commit = tx_db_transaction.transaction();
let sub_db_view = sub_block_db_commit.as_mut();
let vm_db = VmDatabase::new(
sub_db_view.clone(),
&header.consensus,
self.config.coinbase_recipient,
);
let mut vm = Interpreter::with_storage(
vm_db,
InterpreterParams::from(&self.config.consensus_parameters),
);
let vm_result: StateTransition<_> = vm
.transact(checked_tx.clone())
.map_err(|error| ExecutorError::VmExecution {
error: InterpreterError::Storage(anyhow::anyhow!(error)),
transaction_id: tx_id,
})?
.into();
let reverted = vm_result.should_revert();
let (state, mut tx, receipts) = vm_result.into_inner();
#[cfg(debug_assertions)]
{
tx.precompute(&self.config.consensus_parameters.chain_id)?;
debug_assert_eq!(tx.id(&self.config.consensus_parameters.chain_id), tx_id);
}
self.compute_inputs(
match execution_kind {
ExecutionKind::DryRun => ExecutionTypes::DryRun(tx.inputs_mut()),
ExecutionKind::Production => ExecutionTypes::Production(tx.inputs_mut()),
ExecutionKind::Validation => ExecutionTypes::Validation(tx.inputs()),
},
tx_id,
tx_db_transaction.deref_mut(),
options,
)?;
if !reverted {
sub_block_db_commit.commit()?;
}
let (used_gas, tx_fee) = self.total_fee_paid(&tx, max_fee, &receipts)?;
match execution_kind {
ExecutionKind::Validation => {
if &tx != checked_tx.transaction() {
return Err(ExecutorError::InvalidTransactionOutcome {
transaction_id: tx_id,
})
}
}
ExecutionKind::DryRun | ExecutionKind::Production => {
}
}
self.spend_input_utxos(tx.inputs(), tx_db_transaction.deref_mut(), reverted)?;
self.persist_output_utxos(
*header.height(),
execution_data.tx_count,
&tx_id,
tx_db_transaction.deref_mut(),
tx.inputs(),
tx.outputs(),
)?;
let mut outputs = tx.outputs().clone();
self.compute_not_utxo_outputs(
match execution_kind {
ExecutionKind::DryRun => {
ExecutionTypes::DryRun((&mut outputs, tx.inputs()))
}
ExecutionKind::Production => {
ExecutionTypes::Production((&mut outputs, tx.inputs()))
}
ExecutionKind::Validation => {
ExecutionTypes::Validation((&outputs, tx.inputs()))
}
},
tx_id,
tx_db_transaction.deref_mut(),
)?;
*tx.outputs_mut() = outputs;
let final_tx = tx.into();
tx_db_transaction
.deref_mut()
.storage::<Transactions>()
.insert(&tx_id, &final_tx)?;
self.persist_receipts(&tx_id, &receipts, tx_db_transaction.deref_mut())?;
let status = if reverted {
self.log_backtrace(&vm, &receipts);
let reason = receipts
.iter()
.find_map(|receipt| match receipt {
Receipt::Revert { ra, .. } => Some(format!("Revert({ra})")),
Receipt::Panic { reason, .. } => Some(format!("{}", reason.reason())),
_ => None,
})
.unwrap_or_else(|| format!("{:?}", &state));
TransactionExecutionResult::Failed {
reason,
result: Some(state),
}
} else {
TransactionExecutionResult::Success {
result: Some(state),
}
};
execution_data.coinbase = execution_data
.coinbase
.checked_add(tx_fee)
.ok_or(ExecutorError::FeeOverflow)?;
execution_data.used_gas = execution_data.used_gas.saturating_add(used_gas);
execution_data.tx_status.push(TransactionExecutionStatus {
id: tx_id,
result: status,
});
execution_data
.message_ids
.extend(receipts.iter().filter_map(|r| r.message_id()));
Ok(final_tx)
}
fn verify_input_state(
&self,
db: &Database,
inputs: &[Input],
block_height: BlockHeight,
block_da_height: DaBlockHeight,
) -> ExecutorResult<()> {
for input in inputs {
match input {
Input::CoinSigned(CoinSigned { utxo_id, .. })
| Input::CoinPredicate(CoinPredicate { utxo_id, .. }) => {
if let Some(coin) = db.storage::<Coins>().get(utxo_id)? {
let coin_mature_height = coin
.tx_pointer
.block_height()
.saturating_add(*coin.maturity)
.into();
if block_height < coin_mature_height {
return Err(TransactionValidityError::CoinHasNotMatured(
*utxo_id,
)
.into())
}
} else {
return Err(
TransactionValidityError::CoinDoesNotExist(*utxo_id).into()
)
}
}
Input::Contract(_) => {
}
Input::MessageCoinSigned(MessageCoinSigned {
sender,
recipient,
amount,
nonce,
..
})
| Input::MessageCoinPredicate(MessageCoinPredicate {
sender,
recipient,
amount,
nonce,
..
})
| Input::MessageDataSigned(MessageDataSigned {
sender,
recipient,
amount,
nonce,
..
})
| Input::MessageDataPredicate(MessageDataPredicate {
sender,
recipient,
amount,
nonce,
..
}) => {
if db.message_is_spent(nonce)? {
return Err(
TransactionValidityError::MessageAlreadySpent(*nonce).into()
)
}
if let Some(message) = self
.relayer
.get_message(nonce, &block_da_height)
.map_err(|e| ExecutorError::RelayerError(e.into()))?
{
if message.da_height > block_da_height {
return Err(TransactionValidityError::MessageSpendTooEarly(
*nonce,
)
.into())
}
if message.sender != *sender {
return Err(TransactionValidityError::MessageSenderMismatch(
*nonce,
)
.into())
}
if message.recipient != *recipient {
return Err(
TransactionValidityError::MessageRecipientMismatch(
*nonce,
)
.into(),
)
}
if message.amount != *amount {
return Err(TransactionValidityError::MessageAmountMismatch(
*nonce,
)
.into())
}
if message.nonce != *nonce {
return Err(TransactionValidityError::MessageNonceMismatch(
*nonce,
)
.into())
}
let expected_data = if message.data.is_empty() {
None
} else {
Some(message.data.as_slice())
};
if expected_data != input.input_data() {
return Err(TransactionValidityError::MessageDataMismatch(
*nonce,
)
.into())
}
} else {
return Err(
TransactionValidityError::MessageDoesNotExist(*nonce).into()
)
}
}
}
}
Ok(())
}
fn spend_input_utxos(
&self,
inputs: &[Input],
db: &mut Database,
reverted: bool,
) -> ExecutorResult<()> {
for input in inputs {
match input {
Input::CoinSigned(CoinSigned { utxo_id, .. })
| Input::CoinPredicate(CoinPredicate { utxo_id, .. }) => {
db.storage::<Coins>().remove(utxo_id)?;
}
Input::MessageDataSigned(_)
| Input::MessageDataPredicate(_)
if reverted => {
continue
}
Input::MessageCoinSigned(MessageCoinSigned { nonce, .. })
| Input::MessageCoinPredicate(MessageCoinPredicate { nonce, .. })
| Input::MessageDataSigned(MessageDataSigned { nonce, .. }) | Input::MessageDataPredicate(MessageDataPredicate { nonce, .. }) => {
let was_already_spent =
db.storage::<SpentMessages>().insert(nonce, &())?;
if was_already_spent.is_some() {
return Err(ExecutorError::MessageAlreadySpent(*nonce))
}
db.storage::<Messages>().remove(nonce)?;
}
_ => {}
}
}
Ok(())
}
fn total_fee_paid<Tx: Chargeable>(
&self,
tx: &Tx,
max_fee: Word,
receipts: &[Receipt],
) -> ExecutorResult<(Word, Word)> {
let mut used_gas = 0;
for r in receipts {
if let Receipt::ScriptResult { gas_used, .. } = r {
used_gas = *gas_used;
break;
}
}
let fee = tx
.refund_fee(
self.config.consensus_parameters.gas_costs(),
self.config.consensus_parameters.fee_params(),
used_gas,
)
.ok_or(ExecutorError::FeeOverflow)?;
Ok((
used_gas,
max_fee
.checked_sub(fee)
.expect("Refunded fee can't be more than `max_fee`."),
))
}
fn compute_inputs(
&self,
inputs: ExecutionTypes<&mut [Input], &[Input]>,
tx_id: TxId,
db: &mut Database,
options: ExecutionOptions,
) -> ExecutorResult<()> {
match inputs {
ExecutionTypes::DryRun(inputs) | ExecutionTypes::Production(inputs) => {
for input in inputs {
match input {
Input::CoinSigned(CoinSigned {
tx_pointer,
utxo_id,
owner,
amount,
asset_id,
maturity,
..
})
| Input::CoinPredicate(CoinPredicate {
tx_pointer,
utxo_id,
owner,
amount,
asset_id,
maturity,
..
}) => {
let coin = self.get_coin_or_default(
db, *utxo_id, *owner, *amount, *asset_id, *maturity,
options,
)?;
*tx_pointer = coin.tx_pointer;
}
Input::Contract(Contract {
ref mut utxo_id,
ref mut balance_root,
ref mut state_root,
ref mut tx_pointer,
ref contract_id,
..
}) => {
let mut contract = ContractRef::new(&mut *db, *contract_id);
let utxo_info =
contract.validated_utxo(options.utxo_validation)?;
*utxo_id = utxo_info.utxo_id;
*tx_pointer = utxo_info.tx_pointer;
*balance_root = contract.balance_root()?;
*state_root = contract.state_root()?;
}
_ => {}
}
}
}
ExecutionTypes::Validation(inputs) => {
for input in inputs {
match input {
Input::CoinSigned(CoinSigned {
tx_pointer,
utxo_id,
owner,
amount,
asset_id,
maturity,
..
})
| Input::CoinPredicate(CoinPredicate {
tx_pointer,
utxo_id,
owner,
amount,
asset_id,
maturity,
..
}) => {
let coin = self.get_coin_or_default(
db, *utxo_id, *owner, *amount, *asset_id, *maturity,
options,
)?;
if tx_pointer != &coin.tx_pointer {
return Err(ExecutorError::InvalidTransactionOutcome {
transaction_id: tx_id,
})
}
}
Input::Contract(Contract {
utxo_id,
balance_root,
state_root,
contract_id,
tx_pointer,
..
}) => {
let mut contract = ContractRef::new(&mut *db, *contract_id);
let provided_info = ContractUtxoInfo {
utxo_id: *utxo_id,
tx_pointer: *tx_pointer,
};
if provided_info
!= contract.validated_utxo(options.utxo_validation)?
{
return Err(ExecutorError::InvalidTransactionOutcome {
transaction_id: tx_id,
})
}
if balance_root != &contract.balance_root()? {
return Err(ExecutorError::InvalidTransactionOutcome {
transaction_id: tx_id,
})
}
if state_root != &contract.state_root()? {
return Err(ExecutorError::InvalidTransactionOutcome {
transaction_id: tx_id,
})
}
}
_ => {}
}
}
}
}
Ok(())
}
#[allow(clippy::type_complexity)]
fn compute_not_utxo_outputs(
&self,
tx: ExecutionTypes<(&mut [Output], &[Input]), (&[Output], &[Input])>,
tx_id: TxId,
db: &mut Database,
) -> ExecutorResult<()> {
match tx {
ExecutionTypes::DryRun(tx) | ExecutionTypes::Production(tx) => {
for output in tx.0.iter_mut() {
if let Output::Contract(contract_output) = output {
let contract_id =
if let Some(Input::Contract(Contract {
contract_id, ..
})) = tx.1.get(contract_output.input_index as usize)
{
contract_id
} else {
return Err(ExecutorError::InvalidTransactionOutcome {
transaction_id: tx_id,
})
};
let mut contract = ContractRef::new(&mut *db, *contract_id);
contract_output.balance_root = contract.balance_root()?;
contract_output.state_root = contract.state_root()?;
}
}
}
ExecutionTypes::Validation(tx) => {
for output in tx.0 {
if let Output::Contract(contract_output) = output {
let contract_id =
if let Some(Input::Contract(Contract {
contract_id, ..
})) = tx.1.get(contract_output.input_index as usize)
{
contract_id
} else {
return Err(ExecutorError::InvalidTransactionOutcome {
transaction_id: tx_id,
})
};
let mut contract = ContractRef::new(&mut *db, *contract_id);
if contract_output.balance_root != contract.balance_root()? {
return Err(ExecutorError::InvalidTransactionOutcome {
transaction_id: tx_id,
})
}
if contract_output.state_root != contract.state_root()? {
return Err(ExecutorError::InvalidTransactionOutcome {
transaction_id: tx_id,
})
}
}
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn get_coin_or_default(
&self,
db: &mut Database,
utxo_id: UtxoId,
owner: Address,
amount: u64,
asset_id: AssetId,
maturity: BlockHeight,
options: ExecutionOptions,
) -> ExecutorResult<CompressedCoin> {
if options.utxo_validation {
db.storage::<Coins>()
.get(&utxo_id)?
.ok_or(ExecutorError::TransactionValidity(
TransactionValidityError::CoinDoesNotExist(utxo_id),
))
.map(Cow::into_owned)
} else {
Ok(CompressedCoin {
owner,
amount,
asset_id,
maturity,
tx_pointer: Default::default(),
})
}
}
fn log_backtrace<Tx>(&self, vm: &Interpreter<VmDatabase, Tx>, receipts: &[Receipt]) {
if self.config.backtrace {
if let Some(backtrace) = receipts
.iter()
.find_map(Receipt::result)
.copied()
.map(|result| FuelBacktrace::from_vm_error(vm, result))
{
let sp = usize::try_from(backtrace.registers()[RegId::SP]).expect(
"The `$sp` register points to the memory of the VM. \
Because the VM's memory is limited by the `usize` of the system, \
it is impossible to lose higher bits during truncation.",
);
warn!(
target = "vm",
"Backtrace on contract: 0x{:x}\nregisters: {:?}\ncall_stack: {:?}\nstack\n: {}",
backtrace.contract(),
backtrace.registers(),
backtrace.call_stack(),
hex::encode(&backtrace.memory()[..sp]), );
}
}
}
fn persist_output_utxos(
&self,
block_height: BlockHeight,
tx_idx: u16,
tx_id: &Bytes32,
db: &mut Database,
inputs: &[Input],
outputs: &[Output],
) -> ExecutorResult<()> {
for (output_index, output) in outputs.iter().enumerate() {
let index = u8::try_from(output_index)
.expect("Transaction can have only up to `u8::MAX` outputs");
let utxo_id = UtxoId::new(*tx_id, index);
match output {
Output::Coin {
amount,
asset_id,
to,
} => Self::insert_coin(
block_height,
tx_idx,
utxo_id,
amount,
asset_id,
to,
db,
)?,
Output::Contract(contract) => {
if let Some(Input::Contract(Contract { contract_id, .. })) =
inputs.get(contract.input_index as usize)
{
db.storage::<ContractsLatestUtxo>().insert(
contract_id,
&ContractUtxoInfo {
utxo_id,
tx_pointer: TxPointer::new(block_height, tx_idx),
},
)?;
} else {
return Err(ExecutorError::TransactionValidity(
TransactionValidityError::InvalidContractInputIndex(utxo_id),
))
}
}
Output::Change {
to,
asset_id,
amount,
} => Self::insert_coin(
block_height,
tx_idx,
utxo_id,
amount,
asset_id,
to,
db,
)?,
Output::Variable {
to,
asset_id,
amount,
} => Self::insert_coin(
block_height,
tx_idx,
utxo_id,
amount,
asset_id,
to,
db,
)?,
Output::ContractCreated { contract_id, .. } => {
db.storage::<ContractsLatestUtxo>().insert(
contract_id,
&ContractUtxoInfo {
utxo_id,
tx_pointer: TxPointer::new(block_height, tx_idx),
},
)?;
}
}
}
Ok(())
}
fn insert_coin(
block_height: BlockHeight,
tx_idx: u16,
utxo_id: UtxoId,
amount: &Word,
asset_id: &AssetId,
to: &Address,
db: &mut Database,
) -> ExecutorResult<()> {
if *amount > Word::MIN {
let coin = CompressedCoin {
owner: *to,
amount: *amount,
asset_id: *asset_id,
maturity: 0u32.into(),
tx_pointer: TxPointer::new(block_height, tx_idx),
};
if db.storage::<Coins>().insert(&utxo_id, &coin)?.is_some() {
return Err(ExecutorError::OutputAlreadyExists)
}
}
Ok(())
}
fn persist_receipts(
&self,
tx_id: &TxId,
receipts: &[Receipt],
db: &mut Database,
) -> ExecutorResult<()> {
if db.storage::<Receipts>().insert(tx_id, receipts)?.is_some() {
return Err(ExecutorError::OutputAlreadyExists)
}
Ok(())
}
fn index_tx_owners_for_block(
&self,
block: &Block,
block_db_transaction: &mut DatabaseTransaction,
) -> ExecutorResult<()> {
for (tx_idx, tx) in block.transactions().iter().enumerate() {
let block_height = *block.header().height();
let inputs;
let outputs;
let tx_idx =
u16::try_from(tx_idx).map_err(|_| ExecutorError::TooManyTransactions)?;
let tx_id = tx.id(&self.config.consensus_parameters.chain_id);
match tx {
Transaction::Script(tx) => {
inputs = tx.inputs().as_slice();
outputs = tx.outputs().as_slice();
}
Transaction::Create(tx) => {
inputs = tx.inputs().as_slice();
outputs = tx.outputs().as_slice();
}
Transaction::Mint(_) => continue,
}
self.persist_owners_index(
block_height,
inputs,
outputs,
&tx_id,
tx_idx,
block_db_transaction.deref_mut(),
)?;
}
Ok(())
}
fn persist_owners_index(
&self,
block_height: BlockHeight,
inputs: &[Input],
outputs: &[Output],
tx_id: &Bytes32,
tx_idx: u16,
db: &mut Database,
) -> ExecutorResult<()> {
let mut owners = vec![];
for input in inputs {
if let Input::CoinSigned(CoinSigned { owner, .. })
| Input::CoinPredicate(CoinPredicate { owner, .. }) = input
{
owners.push(owner);
}
}
for output in outputs {
match output {
Output::Coin { to, .. }
| Output::Change { to, .. }
| Output::Variable { to, .. } => {
owners.push(to);
}
Output::Contract(_) | Output::ContractCreated { .. } => {}
}
}
owners.sort();
owners.dedup();
for owner in owners {
db.record_tx_id_owner(
owner,
block_height,
tx_idx as TransactionIndex,
tx_id,
)?;
}
Ok(())
}
fn persist_transaction_status(
&self,
result: &ExecutionResult,
db: &Database,
) -> ExecutorResult<()> {
let time = result.block.header().time();
let block_id = result.block.id();
for TransactionExecutionStatus { id, result } in result.tx_status.iter() {
match result {
TransactionExecutionResult::Success { result } => {
db.update_tx_status(
id,
TransactionStatus::Success {
block_id,
time,
result: *result,
},
)?;
}
TransactionExecutionResult::Failed { result, reason } => {
db.update_tx_status(
id,
TransactionStatus::Failed {
block_id,
time,
result: *result,
reason: reason.clone(),
},
)?;
}
}
}
Ok(())
}
}
trait Fee {
fn max_fee(&self) -> Word;
fn min_fee(&self) -> Word;
}
impl Fee for ScriptCheckedMetadata {
fn max_fee(&self) -> Word {
self.fee.max_fee()
}
fn min_fee(&self) -> Word {
self.fee.min_fee()
}
}
impl Fee for CreateCheckedMetadata {
fn max_fee(&self) -> Word {
self.fee.max_fee()
}
fn min_fee(&self) -> Word {
self.fee.min_fee()
}
}
#[allow(clippy::arithmetic_side_effects)]
#[allow(clippy::cast_possible_truncation)]
#[cfg(test)]
mod tests {
use super::*;
use fuel_core_storage::tables::Messages;
use fuel_core_types::{
blockchain::header::ConsensusHeader,
entities::message::Message,
fuel_asm::op,
fuel_crypto::SecretKey,
fuel_merkle::sparse,
fuel_tx,
fuel_tx::{
field::{
Inputs,
Outputs,
Script as ScriptField,
},
ConsensusParameters,
Create,
Finalizable,
Script,
Transaction,
TransactionBuilder,
},
fuel_types::{
ChainId,
ContractId,
Salt,
},
fuel_vm::{
script_with_data_offset,
util::test_helpers::TestBuilder as TxBuilder,
Call,
CallFrame,
},
services::executor::ExecutionBlock,
tai64::Tai64,
};
use itertools::Itertools;
use rand::{
prelude::StdRng,
Rng,
SeedableRng,
};
pub(crate) fn setup_executable_script() -> (Create, Script) {
let mut rng = StdRng::seed_from_u64(2322);
let asset_id: AssetId = rng.gen();
let owner: Address = rng.gen();
let input_amount = 1000;
let variable_transfer_amount = 100;
let coin_output_amount = 150;
let (create, contract_id) = create_contract(
vec![
op::addi(0x10, RegId::FP, CallFrame::a_offset().try_into().unwrap()),
op::lw(0x10, 0x10, 0),
op::addi(0x11, RegId::FP, CallFrame::b_offset().try_into().unwrap()),
op::lw(0x11, 0x11, 0),
op::addi(0x12, 0x11, 32),
op::addi(0x13, RegId::ZERO, 0),
op::tro(0x12, 0x13, 0x10, 0x11),
op::ret(RegId::ONE),
]
.into_iter()
.collect::<Vec<u8>>(),
&mut rng,
);
let (script, data_offset) = script_with_data_offset!(
data_offset,
vec![
op::movi(0x10, data_offset + 64),
op::movi(0x11, data_offset),
op::movi(0x12, variable_transfer_amount),
op::call(0x10, 0x12, 0x11, RegId::CGAS),
op::ret(RegId::ONE),
],
fuel_tx::TxParameters::DEFAULT.tx_offset()
);
let script_data: Vec<u8> = [
asset_id.as_ref(),
owner.as_ref(),
Call::new(
contract_id,
variable_transfer_amount as Word,
data_offset as Word,
)
.to_bytes()
.as_ref(),
]
.into_iter()
.flatten()
.copied()
.collect();
let script = TxBuilder::new(2322)
.script_gas_limit(fuel_tx::TxParameters::DEFAULT.max_gas_per_tx >> 1)
.start_script(script, script_data)
.contract_input(contract_id)
.coin_input(asset_id, input_amount)
.variable_output(Default::default())
.coin_output(asset_id, coin_output_amount)
.change_output(asset_id)
.contract_output(&contract_id)
.build()
.transaction()
.clone();
(create, script)
}
pub(crate) fn test_block(num_txs: usize) -> Block {
let transactions = (1..num_txs + 1)
.map(|i| {
TxBuilder::new(2322u64)
.script_gas_limit(10)
.coin_input(AssetId::default(), (i as Word) * 100)
.coin_output(AssetId::default(), (i as Word) * 50)
.change_output(AssetId::default())
.build()
.transaction()
.clone()
.into()
})
.collect_vec();
let mut block = Block::default();
*block.transactions_mut() = transactions;
block
}
pub(crate) fn create_contract<R: Rng>(
contract_code: Vec<u8>,
rng: &mut R,
) -> (Create, ContractId) {
let salt: Salt = rng.gen();
let contract = fuel_tx::Contract::from(contract_code.clone());
let root = contract.root();
let state_root = fuel_tx::Contract::default_state_root();
let contract_id = contract.id(&salt, &root, &state_root);
let tx =
TransactionBuilder::create(contract_code.into(), salt, Default::default())
.add_random_fee_input()
.add_output(Output::contract_created(contract_id, state_root))
.finalize();
(tx, contract_id)
}
#[test]
fn executor_validates_correctly_produced_block() {
let producer = Executor::test(Default::default(), Default::default());
let verifier = Executor::test(Default::default(), Default::default());
let block = test_block(10);
let ExecutionResult {
block,
skipped_transactions,
..
} = producer
.execute_and_commit(
ExecutionTypes::Production(block.into()),
Default::default(),
)
.unwrap();
let validation_result = verifier
.execute_and_commit(ExecutionTypes::Validation(block), Default::default());
assert!(validation_result.is_ok());
assert!(skipped_transactions.is_empty());
}
#[test]
fn executor_commits_transactions_to_block() {
let producer = Executor::test(Default::default(), Default::default());
let block = test_block(10);
let start_block = block.clone();
let ExecutionResult {
block,
skipped_transactions,
..
} = producer
.execute_and_commit(
ExecutionBlock::Production(block.into()),
Default::default(),
)
.unwrap();
assert!(skipped_transactions.is_empty());
assert_ne!(
start_block.header().transactions_root,
block.header().transactions_root
);
assert_eq!(block.transactions().len(), 11);
assert!(block.transactions()[10].as_mint().is_some());
if let Some(mint) = block.transactions()[10].as_mint() {
assert_eq!(
mint.tx_pointer(),
&TxPointer::new(*block.header().height(), 10)
);
assert_eq!(mint.mint_asset_id(), &AssetId::BASE);
assert_eq!(mint.mint_amount(), &0);
assert_eq!(mint.input_contract().contract_id, ContractId::zeroed());
assert_eq!(mint.input_contract().balance_root, Bytes32::zeroed());
assert_eq!(mint.input_contract().state_root, Bytes32::zeroed());
assert_eq!(mint.input_contract().utxo_id, UtxoId::default());
assert_eq!(mint.input_contract().tx_pointer, TxPointer::default());
assert_eq!(mint.output_contract().balance_root, Bytes32::zeroed());
assert_eq!(mint.output_contract().state_root, Bytes32::zeroed());
assert_eq!(mint.output_contract().input_index, 0);
} else {
panic!("Invalid outputs of coinbase");
}
}
mod coinbase {
use super::*;
use fuel_core_storage::tables::ContractsRawCode;
use fuel_core_types::{
fuel_asm::GTFArgs,
fuel_tx::FeeParameters,
};
#[test]
fn executor_commits_transactions_with_non_zero_coinbase_generation() {
let price = 1;
let limit = 0;
let gas_price_factor = 1;
let script = TxBuilder::new(1u64)
.script_gas_limit(limit)
.gas_price(price)
.coin_input(AssetId::BASE, 10000)
.change_output(AssetId::BASE)
.build()
.transaction()
.clone();
let recipient = fuel_tx::Contract::EMPTY_CONTRACT_ID;
let fee_params = FeeParameters {
gas_price_factor,
..Default::default()
};
let config = Config {
coinbase_recipient: recipient,
consensus_parameters: ConsensusParameters {
fee_params,
..Default::default()
},
..Default::default()
};
let database = &mut Database::default();
database
.storage::<ContractsRawCode>()
.insert(&recipient, &[])
.expect("Should insert coinbase contract");
let producer = Executor::test(database.clone(), config);
let expected_fee_amount_1 = TransactionFee::checked_from_tx(
producer.config.consensus_parameters.gas_costs(),
producer.config.consensus_parameters.fee_params(),
&script,
)
.unwrap()
.max_fee();
let invalid_duplicate_tx = script.clone().into();
let mut block = Block::default();
block.header_mut().consensus.height = 1.into();
*block.transactions_mut() = vec![script.into(), invalid_duplicate_tx];
block.header_mut().recalculate_metadata();
let ExecutionResult {
block,
skipped_transactions,
..
} = producer
.execute_and_commit(
ExecutionBlock::Production(block.into()),
Default::default(),
)
.unwrap();
assert_eq!(skipped_transactions.len(), 1);
assert_eq!(block.transactions().len(), 2);
assert!(expected_fee_amount_1 > 0);
let first_mint;
if let Some(mint) = block.transactions()[1].as_mint() {
assert_eq!(
mint.tx_pointer(),
&TxPointer::new(*block.header().height(), 1)
);
assert_eq!(mint.mint_asset_id(), &AssetId::BASE);
assert_eq!(mint.mint_amount(), &expected_fee_amount_1);
assert_eq!(mint.input_contract().contract_id, recipient);
assert_eq!(mint.input_contract().balance_root, Bytes32::zeroed());
assert_eq!(mint.input_contract().state_root, Bytes32::zeroed());
assert_eq!(mint.input_contract().utxo_id, UtxoId::default());
assert_eq!(mint.input_contract().tx_pointer, TxPointer::default());
assert_ne!(mint.output_contract().balance_root, Bytes32::zeroed());
assert_eq!(mint.output_contract().state_root, Bytes32::zeroed());
assert_eq!(mint.output_contract().input_index, 0);
first_mint = mint.clone();
} else {
panic!("Invalid coinbase transaction");
}
let (asset_id, amount) = producer
.database
.contract_balances(recipient, None, None)
.next()
.unwrap()
.unwrap();
assert_eq!(asset_id, AssetId::zeroed());
assert_eq!(amount, expected_fee_amount_1);
let script = TxBuilder::new(2u64)
.script_gas_limit(limit)
.gas_price(price)
.coin_input(AssetId::BASE, 10000)
.change_output(AssetId::BASE)
.build()
.transaction()
.clone();
let expected_fee_amount_2 = TransactionFee::checked_from_tx(
producer.config.consensus_parameters.gas_costs(),
producer.config.consensus_parameters.fee_params(),
&script,
)
.unwrap()
.max_fee();
let mut block = Block::default();
block.header_mut().consensus.height = 2.into();
*block.transactions_mut() = vec![script.into()];
block.header_mut().recalculate_metadata();
let ExecutionResult {
block,
skipped_transactions,
..
} = producer
.execute_and_commit(
ExecutionBlock::Production(block.into()),
Default::default(),
)
.unwrap();
assert_eq!(skipped_transactions.len(), 0);
assert_eq!(block.transactions().len(), 2);
if let Some(second_mint) = block.transactions()[1].as_mint() {
assert_eq!(second_mint.tx_pointer(), &TxPointer::new(2.into(), 1));
assert_eq!(second_mint.mint_asset_id(), &AssetId::BASE);
assert_eq!(second_mint.mint_amount(), &expected_fee_amount_2);
assert_eq!(second_mint.input_contract().contract_id, recipient);
assert_eq!(
second_mint.input_contract().balance_root,
first_mint.output_contract().balance_root
);
assert_eq!(
second_mint.input_contract().state_root,
first_mint.output_contract().state_root
);
assert_eq!(
second_mint.input_contract().utxo_id,
UtxoId::new(first_mint.cached_id().expect("Id exists"), 0)
);
assert_eq!(
second_mint.input_contract().tx_pointer,
TxPointer::new(1.into(), 1)
);
assert_ne!(
second_mint.output_contract().balance_root,
first_mint.output_contract().balance_root
);
assert_eq!(
second_mint.output_contract().state_root,
first_mint.output_contract().state_root
);
assert_eq!(second_mint.output_contract().input_index, 0);
} else {
panic!("Invalid coinbase transaction");
}
let (asset_id, amount) = producer
.database
.contract_balances(recipient, None, None)
.next()
.unwrap()
.unwrap();
assert_eq!(asset_id, AssetId::zeroed());
assert_eq!(amount, expected_fee_amount_1 + expected_fee_amount_2);
}
#[test]
fn skip_coinbase_during_dry_run() {
let price = 1;
let limit = 0;
let gas_price_factor = 1;
let script = TxBuilder::new(2322u64)
.script_gas_limit(limit)
.gas_price(price)
.coin_input(AssetId::BASE, 10000)
.change_output(AssetId::BASE)
.build()
.transaction()
.clone();
let mut config = Config::default();
let recipient = [1u8; 32].into();
config.coinbase_recipient = recipient;
config.consensus_parameters.fee_params.gas_price_factor = gas_price_factor;
let producer = Executor::test(Default::default(), config);
let result = producer
.execute_without_commit(
ExecutionTypes::DryRun(Components {
header_to_produce: Default::default(),
transactions_source: OnceTransactionsSource::new(vec![
script.into()
]),
gas_limit: u64::MAX,
}),
Default::default(),
)
.unwrap();
let ExecutionResult { block, .. } = result.into_result();
assert_eq!(block.transactions().len(), 1);
}
#[test]
fn executor_commits_transactions_with_non_zero_coinbase_validation() {
let price = 1;
let limit = 0;
let gas_price_factor = 1;
let script = TxBuilder::new(2322u64)
.script_gas_limit(limit)
.gas_price(price)
.coin_input(AssetId::BASE, 10000)
.change_output(AssetId::BASE)
.build()
.transaction()
.clone();
let recipient = fuel_tx::Contract::EMPTY_CONTRACT_ID;
let fee_params = FeeParameters {
gas_price_factor,
..Default::default()
};
let config = Config {
coinbase_recipient: recipient,
consensus_parameters: ConsensusParameters {
fee_params,
..Default::default()
},
..Default::default()
};
let database = &mut Database::default();
database
.storage::<ContractsRawCode>()
.insert(&recipient, &[])
.expect("Should insert coinbase contract");
let producer = Executor::test(database.clone(), config);
let mut block = Block::default();
*block.transactions_mut() = vec![script.into()];
let ExecutionResult {
block: produced_block,
skipped_transactions,
..
} = producer
.execute_and_commit(
ExecutionBlock::Production(block.into()),
Default::default(),
)
.unwrap();
assert!(skipped_transactions.is_empty());
let produced_txs = produced_block.transactions().to_vec();
let validator = Executor::test(
Default::default(),
producer.config.as_ref().clone(),
);
let ExecutionResult {
block: validated_block,
..
} = validator
.execute_and_commit(
ExecutionBlock::Validation(produced_block),
Default::default(),
)
.unwrap();
assert_eq!(validated_block.transactions(), produced_txs);
let (asset_id, amount) = validator
.database
.contract_balances(recipient, None, None)
.next()
.unwrap()
.unwrap();
assert_eq!(asset_id, AssetId::zeroed());
assert_ne!(amount, 0);
}
#[test]
fn execute_cb_command() {
fn compare_coinbase_addresses(
config_coinbase: ContractId,
expected_in_tx_coinbase: ContractId,
) -> bool {
let script = TxBuilder::new(2322u64)
.script_gas_limit(100000)
.gas_price(0)
.start_script(vec![
op::movi(0x11, Address::LEN.try_into().unwrap()),
op::aloc(0x11),
op::move_(0x10, RegId::HP),
op::cb(0x10),
op::gtf_args(0x12, 0x00, GTFArgs::ScriptData),
op::meq(0x13, 0x10, 0x12, 0x11),
op::ret(0x13)
], expected_in_tx_coinbase.to_vec() )
.coin_input(AssetId::BASE, 1000)
.variable_output(Default::default())
.coin_output(AssetId::BASE, 1000)
.change_output(AssetId::BASE)
.build()
.transaction()
.clone();
let config = Config {
coinbase_recipient: config_coinbase,
..Default::default()
};
let producer = Executor::test(Default::default(), config);
let mut block = Block::default();
*block.transactions_mut() = vec![script.clone().into()];
assert!(producer
.execute_and_commit(
ExecutionBlock::Production(block.into()),
Default::default()
)
.is_ok());
let receipts = producer
.database
.storage::<Receipts>()
.get(&script.id(&producer.config.consensus_parameters.chain_id))
.unwrap()
.unwrap();
if let Some(Receipt::Return { val, .. }) = receipts.get(0) {
*val == 1
} else {
panic!("Execution of the `CB` script failed failed")
}
}
assert!(compare_coinbase_addresses(
ContractId::from([1u8; 32]),
ContractId::from([1u8; 32])
));
assert!(!compare_coinbase_addresses(
ContractId::from([9u8; 32]),
ContractId::from([1u8; 32])
));
assert!(!compare_coinbase_addresses(
ContractId::from([1u8; 32]),
ContractId::from([9u8; 32])
));
assert!(compare_coinbase_addresses(
ContractId::from([9u8; 32]),
ContractId::from([9u8; 32])
));
}
#[test]
fn invalidate_unexpected_index() {
let mint = Transaction::mint(
TxPointer::new(Default::default(), 1),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
);
let mut block = Block::default();
*block.transactions_mut() = vec![mint.into()];
block.header_mut().recalculate_metadata();
let validator = Executor::test(
Default::default(),
Config {
utxo_validation_default: false,
..Default::default()
},
);
let validation_err = validator
.execute_and_commit(ExecutionBlock::Validation(block), Default::default())
.expect_err("Expected error because coinbase if invalid");
assert!(matches!(
validation_err,
ExecutorError::MintHasUnexpectedIndex
));
}
#[test]
fn invalidate_is_not_last() {
let mint = Transaction::mint(
TxPointer::new(Default::default(), 0),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
);
let tx = Transaction::default_test_tx();
let mut block = Block::default();
*block.transactions_mut() = vec![mint.into(), tx];
block.header_mut().recalculate_metadata();
let validator = Executor::test(Default::default(), Default::default());
let validation_err = validator
.execute_and_commit(ExecutionBlock::Validation(block), Default::default())
.expect_err("Expected error because coinbase if invalid");
assert!(matches!(
validation_err,
ExecutorError::MintIsNotLastTransaction
));
}
#[test]
fn invalidate_block_missed_coinbase() {
let block = Block::default();
let validator = Executor::test(Default::default(), Default::default());
let validation_err = validator
.execute_and_commit(ExecutionBlock::Validation(block), Default::default())
.expect_err("Expected error because coinbase is missing");
assert!(matches!(validation_err, ExecutorError::MintMissing));
}
#[test]
fn invalidate_block_height() {
let mint = Transaction::mint(
TxPointer::new(1.into(), Default::default()),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
);
let mut block = Block::default();
*block.transactions_mut() = vec![mint.into()];
block.header_mut().recalculate_metadata();
let validator = Executor::test(Default::default(), Default::default());
let validation_err = validator
.execute_and_commit(ExecutionBlock::Validation(block), Default::default())
.expect_err("Expected error because coinbase if invalid");
assert!(matches!(
validation_err,
ExecutorError::InvalidTransaction(CheckError::Validity(
ValidityError::TransactionMintIncorrectBlockHeight
))
));
}
#[test]
fn invalidate_invalid_base_asset() {
let mint = Transaction::mint(
TxPointer::new(Default::default(), Default::default()),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
);
let mut block = Block::default();
*block.transactions_mut() = vec![mint.into()];
block.header_mut().recalculate_metadata();
let mut config = Config::default();
config.consensus_parameters.base_asset_id = [1u8; 32].into();
let validator = Executor::test(Default::default(), config);
let validation_err = validator
.execute_and_commit(ExecutionBlock::Validation(block), Default::default())
.expect_err("Expected error because coinbase if invalid");
assert!(matches!(
validation_err,
ExecutorError::InvalidTransaction(CheckError::Validity(
ValidityError::TransactionMintNonBaseAsset
))
));
}
#[test]
fn invalidate_mismatch_amount() {
let mint = Transaction::mint(
TxPointer::new(Default::default(), Default::default()),
Default::default(),
Default::default(),
123,
Default::default(),
);
let mut block = Block::default();
*block.transactions_mut() = vec![mint.into()];
block.header_mut().recalculate_metadata();
let validator = Executor::test(Default::default(), Default::default());
let validation_err = validator
.execute_and_commit(ExecutionBlock::Validation(block), Default::default())
.expect_err("Expected error because coinbase if invalid");
assert!(matches!(
validation_err,
ExecutorError::CoinbaseAmountMismatch
));
}
}
#[test]
fn executor_invalidates_missing_gas_input() {
let mut rng = StdRng::seed_from_u64(2322u64);
let producer = Executor::test(Default::default(), Default::default());
let consensus_parameters = &producer.config.consensus_parameters;
let verifier = Executor::test(Default::default(), Default::default());
let gas_limit = 100;
let gas_price = 1;
let script = TransactionBuilder::script(vec![], vec![])
.add_unsigned_coin_input(
SecretKey::random(&mut rng),
rng.gen(),
rng.gen(),
rng.gen(),
Default::default(),
Default::default(),
)
.script_gas_limit(gas_limit)
.gas_price(gas_price)
.finalize();
let max_fee: u64 = script
.max_fee(
consensus_parameters.gas_costs(),
consensus_parameters.fee_params(),
)
.try_into()
.unwrap();
let tx: Transaction = script.into();
let mut block = PartialFuelBlock {
header: Default::default(),
transactions: vec![tx.clone()],
};
let mut block_db_transaction = producer.database.transaction();
let ExecutionData {
skipped_transactions,
..
} = producer
.execute_block(
&mut block_db_transaction,
ExecutionType::Production(PartialBlockComponent::from_partial_block(
&mut block,
)),
Default::default(),
)
.unwrap();
let produce_result = &skipped_transactions[0].1;
assert!(matches!(
produce_result,
&ExecutorError::InvalidTransaction(
CheckError::Validity(
ValidityError::InsufficientFeeAmount { expected, .. }
)
) if expected == max_fee
));
let mut block_db_transaction = verifier.database.transaction();
verifier
.execute_block(
&mut block_db_transaction,
ExecutionType::Validation(PartialBlockComponent::from_partial_block(
&mut block,
)),
Default::default(),
)
.unwrap();
block.transactions.insert(block.transactions.len() - 1, tx);
let mut block_db_transaction = verifier.database.transaction();
let verify_result = verifier.execute_block(
&mut block_db_transaction,
ExecutionType::Validation(PartialBlockComponent::from_partial_block(
&mut block,
)),
Default::default(),
);
assert!(matches!(
verify_result,
Err(ExecutorError::InvalidTransaction(
CheckError::Validity(
ValidityError::InsufficientFeeAmount { expected, .. }
)
)) if expected == max_fee
))
}
#[test]
fn executor_invalidates_duplicate_tx_id() {
let producer = Executor::test(Default::default(), Default::default());
let verifier = Executor::test(Default::default(), Default::default());
let mut block = PartialFuelBlock {
header: Default::default(),
transactions: vec![
Transaction::default_test_tx(),
Transaction::default_test_tx(),
],
};
let mut block_db_transaction = producer.database.transaction();
let ExecutionData {
skipped_transactions,
..
} = producer
.execute_block(
&mut block_db_transaction,
ExecutionType::Production(PartialBlockComponent::from_partial_block(
&mut block,
)),
Default::default(),
)
.unwrap();
let produce_result = &skipped_transactions[0].1;
assert!(matches!(
produce_result,
&ExecutorError::TransactionIdCollision(_)
));
let mut block_db_transaction = verifier.database.transaction();
verifier
.execute_block(
&mut block_db_transaction,
ExecutionType::Validation(PartialBlockComponent::from_partial_block(
&mut block,
)),
Default::default(),
)
.unwrap();
block
.transactions
.insert(block.transactions.len() - 1, Transaction::default_test_tx());
let mut block_db_transaction = verifier.database.transaction();
let verify_result = verifier.execute_block(
&mut block_db_transaction,
ExecutionType::Validation(PartialBlockComponent::from_partial_block(
&mut block,
)),
Default::default(),
);
assert!(matches!(
verify_result,
Err(ExecutorError::TransactionIdCollision(_))
));
}
#[test]
fn executor_invalidates_missing_inputs() {
let mut rng = StdRng::seed_from_u64(2322u64);
let tx = TransactionBuilder::script(
vec![op::ret(RegId::ONE)].into_iter().collect(),
vec![],
)
.add_unsigned_coin_input(
SecretKey::random(&mut rng),
rng.gen(),
10,
Default::default(),
Default::default(),
Default::default(),
)
.add_output(Output::Change {
to: Default::default(),
amount: 0,
asset_id: Default::default(),
})
.finalize_as_transaction();
let config = Config {
utxo_validation_default: true,
..Default::default()
};
let producer = Executor::test(Database::default(), config.clone());
let verifier = Executor::test(Default::default(), config);
let mut block = PartialFuelBlock {
header: Default::default(),
transactions: vec![tx.clone()],
};
let mut block_db_transaction = producer.database.transaction();
let ExecutionData {
skipped_transactions,
..
} = producer
.execute_block(
&mut block_db_transaction,
ExecutionType::Production(PartialBlockComponent::from_partial_block(
&mut block,
)),
ExecutionOptions {
utxo_validation: true,
},
)
.unwrap();
let produce_result = &skipped_transactions[0].1;
assert!(matches!(
produce_result,
&ExecutorError::TransactionValidity(
TransactionValidityError::CoinDoesNotExist(_)
)
));
let mut block_db_transaction = verifier.database.transaction();
verifier
.execute_block(
&mut block_db_transaction,
ExecutionType::Validation(PartialBlockComponent::from_partial_block(
&mut block,
)),
ExecutionOptions {
utxo_validation: true,
},
)
.unwrap();
block.transactions.insert(block.transactions.len() - 1, tx);
let mut block_db_transaction = verifier.database.transaction();
let verify_result = verifier.execute_block(
&mut block_db_transaction,
ExecutionType::Validation(PartialBlockComponent::from_partial_block(
&mut block,
)),
ExecutionOptions {
utxo_validation: true,
},
);
assert!(matches!(
verify_result,
Err(ExecutorError::TransactionValidity(
TransactionValidityError::CoinDoesNotExist(_)
))
));
}
#[test]
fn executor_invalidates_blocks_with_diverging_tx_outputs() {
let input_amount = 10;
let fake_output_amount = 100;
let tx: Transaction = TxBuilder::new(2322u64)
.script_gas_limit(1)
.coin_input(Default::default(), input_amount)
.change_output(Default::default())
.build()
.transaction()
.clone()
.into();
let tx_id = tx.id(&ChainId::default());
let producer = Executor::test(Default::default(), Default::default());
let verifier = Executor::test(Default::default(), Default::default());
let mut block = Block::default();
*block.transactions_mut() = vec![tx];
let ExecutionResult { mut block, .. } = producer
.execute_and_commit(
ExecutionBlock::Production(block.into()),
Default::default(),
)
.unwrap();
if let Transaction::Script(script) = &mut block.transactions_mut()[0] {
if let Output::Change { amount, .. } = &mut script.outputs_mut()[0] {
*amount = fake_output_amount
}
}
let verify_result = verifier
.execute_and_commit(ExecutionBlock::Validation(block), Default::default());
assert!(matches!(
verify_result,
Err(ExecutorError::InvalidTransactionOutcome { transaction_id }) if transaction_id == tx_id
));
}
#[test]
fn executor_invalidates_blocks_with_diverging_tx_commitment() {
let mut rng = StdRng::seed_from_u64(2322u64);
let tx: Transaction = TxBuilder::new(2322u64)
.script_gas_limit(1)
.coin_input(Default::default(), 10)
.change_output(Default::default())
.build()
.transaction()
.clone()
.into();
let producer = Executor::test(Default::default(), Default::default());
let verifier = Executor::test(Default::default(), Default::default());
let mut block = Block::default();
*block.transactions_mut() = vec![tx];
let ExecutionResult { mut block, .. } = producer
.execute_and_commit(
ExecutionBlock::Production(block.into()),
Default::default(),
)
.unwrap();
block.header_mut().application.generated.transactions_root = rng.gen();
block.header_mut().recalculate_metadata();
let verify_result = verifier
.execute_and_commit(ExecutionBlock::Validation(block), Default::default());
assert!(matches!(verify_result, Err(ExecutorError::InvalidBlockId)))
}
#[test]
fn executor_invalidates_missing_coin_input() {
let tx: Transaction = Transaction::default();
let executor = Executor::test(
Database::default(),
Config {
utxo_validation_default: true,
..Default::default()
},
);
let block = PartialFuelBlock {
header: Default::default(),
transactions: vec![tx],
};
let ExecutionResult {
skipped_transactions,
..
} = executor
.execute_and_commit(
ExecutionBlock::Production(block),
ExecutionOptions {
utxo_validation: true,
},
)
.unwrap();
let err = &skipped_transactions[0].1;
assert!(matches!(
err,
&ExecutorError::InvalidTransaction(CheckError::Validity(
ValidityError::NoSpendableInput
))
));
}
#[test]
fn skipped_tx_not_changed_spent_status() {
let tx1 = TxBuilder::new(2322u64)
.coin_input(AssetId::default(), 100)
.change_output(AssetId::default())
.build()
.transaction()
.clone();
let tx2 = TxBuilder::new(2322u64)
.coin_input(AssetId::default(), 100)
.coin_input(AssetId::default(), 100)
.change_output(AssetId::default())
.build()
.transaction()
.clone();
let first_input = tx2.inputs()[0].clone();
let second_input = tx2.inputs()[1].clone();
let db = &mut Database::default();
db.storage::<Coins>()
.insert(
&first_input.utxo_id().unwrap().clone(),
&CompressedCoin {
owner: *first_input.input_owner().unwrap(),
amount: 100,
asset_id: AssetId::default(),
maturity: Default::default(),
tx_pointer: Default::default(),
},
)
.unwrap();
db.storage::<Coins>()
.insert(
&second_input.utxo_id().unwrap().clone(),
&CompressedCoin {
owner: *second_input.input_owner().unwrap(),
amount: 100,
asset_id: AssetId::default(),
maturity: Default::default(),
tx_pointer: Default::default(),
},
)
.unwrap();
let executor = Executor::test(
db.clone(),
Config {
utxo_validation_default: true,
..Default::default()
},
);
let block = PartialFuelBlock {
header: Default::default(),
transactions: vec![tx1.into(), tx2.clone().into()],
};
db.storage::<Coins>()
.get(first_input.utxo_id().unwrap())
.unwrap()
.expect("coin should be unspent");
db.storage::<Coins>()
.get(second_input.utxo_id().unwrap())
.unwrap()
.expect("coin should be unspent");
let ExecutionResult {
block,
skipped_transactions,
..
} = executor
.execute_and_commit(
ExecutionBlock::Production(block),
ExecutionOptions {
utxo_validation: true,
},
)
.unwrap();
assert_eq!(block.transactions().len(), 2 );
assert_eq!(skipped_transactions.len(), 1);
assert_eq!(skipped_transactions[0].0, tx2.id(&ChainId::default()));
let coin = db
.storage::<Coins>()
.get(first_input.utxo_id().unwrap())
.unwrap();
assert!(coin.is_none());
db.storage::<Coins>()
.get(second_input.utxo_id().unwrap())
.unwrap()
.expect("coin should be unspent");
}
#[test]
fn skipped_txs_not_affect_order() {
let tx1 = TransactionBuilder::script(vec![], vec![])
.add_random_fee_input()
.script_gas_limit(1000000)
.gas_price(1000000)
.finalize_as_transaction();
let (tx2, tx3) = setup_executable_script();
let executor = Executor::test(Default::default(), Default::default());
let block = PartialFuelBlock {
header: Default::default(),
transactions: vec![tx1.clone(), tx2.clone().into(), tx3.clone().into()],
};
let ExecutionResult {
block,
skipped_transactions,
..
} = executor
.execute_and_commit(ExecutionBlock::Production(block), Default::default())
.unwrap();
assert_eq!(
block.transactions().len(),
3 );
assert_eq!(
block.transactions()[0].id(&ChainId::default()),
tx2.id(&ChainId::default())
);
assert_eq!(
block.transactions()[1].id(&ChainId::default()),
tx3.id(&ChainId::default())
);
assert_eq!(skipped_transactions.len(), 1);
assert_eq!(&skipped_transactions[0].0, &tx1.id(&ChainId::default()));
let tx2_index_in_the_block =
block.transactions()[1].as_script().unwrap().inputs()[0]
.tx_pointer()
.unwrap()
.tx_index();
assert_eq!(tx2_index_in_the_block, 0);
}
#[test]
fn input_coins_are_marked_as_spent() {
let tx: Transaction = TxBuilder::new(2322u64)
.coin_input(AssetId::default(), 100)
.change_output(AssetId::default())
.build()
.transaction()
.clone()
.into();
let db = &Database::default();
let executor = Executor::test(db.clone(), Default::default());
let block = PartialFuelBlock {
header: Default::default(),
transactions: vec![tx],
};
let ExecutionResult { block, .. } = executor
.execute_and_commit(ExecutionBlock::Production(block), Default::default())
.unwrap();
let coin = db
.storage::<Coins>()
.get(
block.transactions()[0].as_script().unwrap().inputs()[0]
.utxo_id()
.unwrap(),
)
.unwrap();
assert!(coin.is_none());
}
#[test]
fn contracts_balance_and_state_roots_no_modifications_updated() {
let mut rng = StdRng::seed_from_u64(2322u64);
let (create, contract_id) = create_contract(vec![], &mut rng);
let non_modify_state_tx: Transaction = TxBuilder::new(2322)
.script_gas_limit(10000)
.coin_input(AssetId::zeroed(), 10000)
.start_script(vec![op::ret(1)], vec![])
.contract_input(contract_id)
.fee_input()
.contract_output(&contract_id)
.build()
.transaction()
.clone()
.into();
let db = &mut Database::default();
let executor = Executor::test(
db.clone(),
Config {
utxo_validation_default: false,
..Default::default()
},
);
let block = PartialFuelBlock {
header: PartialBlockHeader {
consensus: ConsensusHeader {
height: 1.into(),
..Default::default()
},
..Default::default()
},
transactions: vec![create.into(), non_modify_state_tx],
};
let ExecutionResult {
block, tx_status, ..
} = executor
.execute_and_commit(ExecutionBlock::Production(block), Default::default())
.unwrap();
let empty_state = (*sparse::empty_sum()).into();
let executed_tx = block.transactions()[1].as_script().unwrap();
assert!(matches!(
tx_status[2].result,
TransactionExecutionResult::Success { .. }
));
assert_eq!(executed_tx.inputs()[0].state_root(), Some(&empty_state));
assert_eq!(executed_tx.inputs()[0].balance_root(), Some(&empty_state));
assert_eq!(executed_tx.outputs()[0].state_root(), Some(&empty_state));
assert_eq!(executed_tx.outputs()[0].balance_root(), Some(&empty_state));
let expected_tx = block.transactions()[1].clone();
let storage_tx = executor
.database
.storage::<Transactions>()
.get(&executed_tx.id(&ChainId::default()))
.unwrap()
.unwrap()
.into_owned();
assert_eq!(storage_tx, expected_tx);
}
#[test]
fn contracts_balance_and_state_roots_updated_no_modifications_on_fail() {
let mut rng = StdRng::seed_from_u64(2322u64);
let (create, contract_id) = create_contract(vec![], &mut rng);
let non_modify_state_tx: Transaction = TxBuilder::new(2322)
.start_script(vec![op::add(RegId::PC, RegId::PC, RegId::PC)], vec![])
.contract_input(contract_id)
.fee_input()
.contract_output(&contract_id)
.build()
.transaction()
.clone()
.into();
let db = &mut Database::default();
let executor = Executor::test(
db.clone(),
Config {
utxo_validation_default: false,
..Default::default()
},
);
let block = PartialFuelBlock {
header: PartialBlockHeader {
consensus: ConsensusHeader {
height: 1.into(),
..Default::default()
},
..Default::default()
},
transactions: vec![create.into(), non_modify_state_tx],
};
let ExecutionResult {
block, tx_status, ..
} = executor
.execute_and_commit(ExecutionBlock::Production(block), Default::default())
.unwrap();
let empty_state = (*sparse::empty_sum()).into();
let executed_tx = block.transactions()[1].as_script().unwrap();
assert!(matches!(
tx_status[1].result,
TransactionExecutionResult::Failed { .. }
));
assert_eq!(
executed_tx.inputs()[0].state_root(),
executed_tx.outputs()[0].state_root()
);
assert_eq!(
executed_tx.inputs()[0].balance_root(),
executed_tx.outputs()[0].balance_root()
);
assert_eq!(executed_tx.inputs()[0].state_root(), Some(&empty_state));
assert_eq!(executed_tx.inputs()[0].balance_root(), Some(&empty_state));
let expected_tx = block.transactions()[1].clone();
let storage_tx = executor
.database
.storage::<Transactions>()
.get(&expected_tx.id(&ChainId::default()))
.unwrap()
.unwrap()
.into_owned();
assert_eq!(storage_tx, expected_tx);
}
#[test]
fn contracts_balance_and_state_roots_updated_modifications_updated() {
let mut rng = StdRng::seed_from_u64(2322u64);
let (create, contract_id) = create_contract(
vec![
op::sww(0x1, 0x29, RegId::PC),
op::ret(1),
]
.into_iter()
.collect::<Vec<u8>>(),
&mut rng,
);
let transfer_amount = 100 as Word;
let asset_id = AssetId::from([2; 32]);
let (script, data_offset) = script_with_data_offset!(
data_offset,
vec![
op::movi(0x10, data_offset + AssetId::LEN as u32),
op::movi(0x11, data_offset),
op::movi(0x12, transfer_amount as u32),
op::call(0x10, 0x12, 0x11, RegId::CGAS),
op::ret(RegId::ONE),
],
fuel_tx::TxParameters::DEFAULT.tx_offset()
);
let script_data: Vec<u8> = [
asset_id.as_ref(),
Call::new(contract_id, transfer_amount, data_offset as Word)
.to_bytes()
.as_ref(),
]
.into_iter()
.flatten()
.copied()
.collect();
let modify_balance_and_state_tx = TxBuilder::new(2322)
.script_gas_limit(10000)
.coin_input(AssetId::zeroed(), 10000)
.start_script(script, script_data)
.contract_input(contract_id)
.coin_input(asset_id, transfer_amount)
.fee_input()
.contract_output(&contract_id)
.build()
.transaction()
.clone();
let db = &mut Database::default();
let executor = Executor::test(
db.clone(),
Config {
utxo_validation_default: false,
..Default::default()
},
);
let block = PartialFuelBlock {
header: PartialBlockHeader {
consensus: ConsensusHeader {
height: 1.into(),
..Default::default()
},
..Default::default()
},
transactions: vec![create.into(), modify_balance_and_state_tx.into()],
};
let ExecutionResult {
block, tx_status, ..
} = executor
.execute_and_commit(ExecutionBlock::Production(block), Default::default())
.unwrap();
let empty_state = (*sparse::empty_sum()).into();
let executed_tx = block.transactions()[1].as_script().unwrap();
assert!(matches!(
tx_status[2].result,
TransactionExecutionResult::Success { .. }
));
assert_eq!(executed_tx.inputs()[0].state_root(), Some(&empty_state));
assert_eq!(executed_tx.inputs()[0].balance_root(), Some(&empty_state));
assert_ne!(
executed_tx.inputs()[0].state_root(),
executed_tx.outputs()[0].state_root()
);
assert_ne!(
executed_tx.inputs()[0].balance_root(),
executed_tx.outputs()[0].balance_root()
);
let expected_tx = block.transactions()[1].clone();
let storage_tx = executor
.database
.storage::<Transactions>()
.get(&expected_tx.id(&ChainId::default()))
.unwrap()
.unwrap()
.into_owned();
assert_eq!(storage_tx, expected_tx);
}
#[test]
fn contracts_balance_and_state_roots_in_inputs_updated() {
let mut rng = StdRng::seed_from_u64(2322u64);
let (create, contract_id) = create_contract(
vec![
op::sww(0x1, 0x29, RegId::PC),
op::ret(1),
]
.into_iter()
.collect::<Vec<u8>>(),
&mut rng,
);
let transfer_amount = 100 as Word;
let asset_id = AssetId::from([2; 32]);
let (script, data_offset) = script_with_data_offset!(
data_offset,
vec![
op::movi(0x10, data_offset + AssetId::LEN as u32),
op::movi(0x11, data_offset),
op::movi(0x12, transfer_amount as u32),
op::call(0x10, 0x12, 0x11, RegId::CGAS),
op::ret(RegId::ONE),
],
fuel_tx::TxParameters::DEFAULT.tx_offset()
);
let script_data: Vec<u8> = [
asset_id.as_ref(),
Call::new(contract_id, transfer_amount, data_offset as Word)
.to_bytes()
.as_ref(),
]
.into_iter()
.flatten()
.copied()
.collect();
let modify_balance_and_state_tx = TxBuilder::new(2322)
.script_gas_limit(10000)
.coin_input(AssetId::zeroed(), 10000)
.start_script(script, script_data)
.contract_input(contract_id)
.coin_input(asset_id, transfer_amount)
.fee_input()
.contract_output(&contract_id)
.build()
.transaction()
.clone();
let db = &mut Database::default();
let executor = Executor::test(
db.clone(),
Config {
utxo_validation_default: false,
..Default::default()
},
);
let block = PartialFuelBlock {
header: PartialBlockHeader {
consensus: ConsensusHeader {
height: 1.into(),
..Default::default()
},
..Default::default()
},
transactions: vec![create.into(), modify_balance_and_state_tx.into()],
};
let ExecutionResult { block, .. } = executor
.execute_and_commit(ExecutionBlock::Production(block), Default::default())
.unwrap();
let executed_tx = block.transactions()[1].as_script().unwrap();
let state_root = executed_tx.outputs()[0].state_root();
let balance_root = executed_tx.outputs()[0].balance_root();
let mut new_tx = executed_tx.clone();
*new_tx.script_mut() = vec![];
new_tx
.precompute(&executor.config.consensus_parameters.chain_id)
.unwrap();
let block = PartialFuelBlock {
header: PartialBlockHeader {
consensus: ConsensusHeader {
height: 2.into(),
..Default::default()
},
..Default::default()
},
transactions: vec![new_tx.into()],
};
let ExecutionResult {
block, tx_status, ..
} = executor
.execute_and_commit(ExecutionBlock::Production(block), Default::default())
.unwrap();
assert!(matches!(
tx_status[1].result,
TransactionExecutionResult::Success { .. }
));
let tx = block.transactions()[0].as_script().unwrap();
assert_eq!(tx.inputs()[0].balance_root(), balance_root);
assert_eq!(tx.inputs()[0].state_root(), state_root);
}
#[test]
fn foreign_transfer_should_not_affect_balance_root() {
let mut rng = StdRng::seed_from_u64(2322u64);
let (create, contract_id) = create_contract(vec![], &mut rng);
let transfer_amount = 100 as Word;
let asset_id = AssetId::from([2; 32]);
let mut foreign_transfer = TxBuilder::new(2322)
.script_gas_limit(10000)
.coin_input(AssetId::zeroed(), 10000)
.start_script(vec![op::ret(1)], vec![])
.coin_input(asset_id, transfer_amount)
.coin_output(asset_id, transfer_amount)
.build()
.transaction()
.clone();
if let Some(Output::Coin { to, .. }) = foreign_transfer
.as_script_mut()
.unwrap()
.outputs_mut()
.last_mut()
{
*to = Address::try_from(contract_id.as_ref()).unwrap();
} else {
panic!("Last outputs should be a coin for the contract");
}
let db = &mut Database::default();
let executor = Executor::test(db.clone(), Default::default());
let block = PartialFuelBlock {
header: PartialBlockHeader {
consensus: ConsensusHeader {
height: 1.into(),
..Default::default()
},
..Default::default()
},
transactions: vec![create.into(), foreign_transfer.into()],
};
let _ = executor
.execute_and_commit(ExecutionBlock::Production(block), Default::default())
.unwrap();
let empty_state = (*sparse::empty_sum()).into();
assert_eq!(
ContractRef::new(db, contract_id).balance_root().unwrap(),
empty_state
);
}
#[test]
fn input_coins_are_marked_as_spent_with_utxo_validation_enabled() {
let mut rng = StdRng::seed_from_u64(2322u64);
let starting_block = BlockHeight::from(5);
let starting_block_tx_idx = Default::default();
let tx = TransactionBuilder::script(
vec![op::ret(RegId::ONE)].into_iter().collect(),
vec![],
)
.add_unsigned_coin_input(
SecretKey::random(&mut rng),
rng.gen(),
100,
Default::default(),
Default::default(),
Default::default(),
)
.add_output(Output::Change {
to: Default::default(),
amount: 0,
asset_id: Default::default(),
})
.finalize();
let db = &mut Database::default();
if let Input::CoinSigned(CoinSigned {
utxo_id,
owner,
amount,
asset_id,
..
}) = tx.inputs()[0]
{
db.storage::<Coins>()
.insert(
&utxo_id,
&CompressedCoin {
owner,
amount,
asset_id,
maturity: Default::default(),
tx_pointer: TxPointer::new(starting_block, starting_block_tx_idx),
},
)
.unwrap();
}
let executor = Executor::test(
db.clone(),
Config {
utxo_validation_default: true,
..Default::default()
},
);
let block = PartialFuelBlock {
header: PartialBlockHeader {
consensus: ConsensusHeader {
height: 6.into(),
..Default::default()
},
..Default::default()
},
transactions: vec![tx.into()],
};
let ExecutionResult { block, .. } = executor
.execute_and_commit(
ExecutionBlock::Production(block),
ExecutionOptions {
utxo_validation: true,
},
)
.unwrap();
let coin = db
.storage::<Coins>()
.get(
block.transactions()[0].as_script().unwrap().inputs()[0]
.utxo_id()
.unwrap(),
)
.unwrap();
assert!(coin.is_none());
}
#[test]
fn validation_succeeds_when_input_contract_utxo_id_uses_expected_value() {
let mut rng = StdRng::seed_from_u64(2322);
let (tx, contract_id) = create_contract(vec![], &mut rng);
let first_block = PartialFuelBlock {
header: Default::default(),
transactions: vec![tx.into()],
};
let tx2: Transaction = TxBuilder::new(2322)
.start_script(vec![op::ret(1)], vec![])
.contract_input(contract_id)
.fee_input()
.contract_output(&contract_id)
.build()
.transaction()
.clone()
.into();
let second_block = PartialFuelBlock {
header: PartialBlockHeader {
consensus: ConsensusHeader {
height: 2.into(),
..Default::default()
},
..Default::default()
},
transactions: vec![tx2],
};
let db = Database::default();
let setup = Executor::test(db.clone(), Default::default());
setup
.execute_and_commit(
ExecutionBlock::Production(first_block),
Default::default(),
)
.unwrap();
let producer_view = db.transaction().deref_mut().clone();
let producer = Executor::test(producer_view, Default::default());
let ExecutionResult {
block: second_block,
..
} = producer
.execute_and_commit(
ExecutionBlock::Production(second_block),
Default::default(),
)
.unwrap();
let verifier = Executor::test(db, Default::default());
let verify_result = verifier.execute_and_commit(
ExecutionBlock::Validation(second_block),
Default::default(),
);
assert!(verify_result.is_ok());
}
#[test]
fn invalidates_if_input_contract_utxo_id_is_divergent() {
let mut rng = StdRng::seed_from_u64(2322);
let (tx, contract_id) = create_contract(vec![], &mut rng);
let tx2: Transaction = TxBuilder::new(2322)
.start_script(vec![op::addi(0x10, RegId::ZERO, 0), op::ret(1)], vec![])
.contract_input(contract_id)
.fee_input()
.contract_output(&contract_id)
.build()
.transaction()
.clone()
.into();
let first_block = PartialFuelBlock {
header: Default::default(),
transactions: vec![tx.into(), tx2],
};
let tx3: Transaction = TxBuilder::new(2322)
.start_script(vec![op::addi(0x10, RegId::ZERO, 1), op::ret(1)], vec![])
.contract_input(contract_id)
.fee_input()
.contract_output(&contract_id)
.build()
.transaction()
.clone()
.into();
let tx_id = tx3.id(&ChainId::default());
let second_block = PartialFuelBlock {
header: PartialBlockHeader {
consensus: ConsensusHeader {
height: 2.into(),
..Default::default()
},
..Default::default()
},
transactions: vec![tx3],
};
let db = Database::default();
let setup = Executor::test(db.clone(), Default::default());
setup
.execute_and_commit(
ExecutionBlock::Production(first_block),
Default::default(),
)
.unwrap();
let producer_view = db.transaction().deref_mut().clone();
let producer = Executor::test(producer_view, Default::default());
let ExecutionResult {
block: mut second_block,
..
} = producer
.execute_and_commit(
ExecutionBlock::Production(second_block),
Default::default(),
)
.unwrap();
if let Transaction::Script(script) = &mut second_block.transactions_mut()[0] {
if let Input::Contract(Contract { utxo_id, .. }) = &mut script.inputs_mut()[0]
{
*utxo_id = UtxoId::new(tx_id, 0);
}
}
let verifier = Executor::test(db, Default::default());
let verify_result = verifier.execute_and_commit(
ExecutionBlock::Validation(second_block),
Default::default(),
);
assert!(matches!(
verify_result,
Err(ExecutorError::InvalidTransactionOutcome {
transaction_id
}) if transaction_id == tx_id
));
}
#[test]
fn outputs_with_amount_are_included_utxo_set() {
let (deploy, script) = setup_executable_script();
let script_id = script.id(&ChainId::default());
let database = &Database::default();
let executor = Executor::test(database.clone(), Default::default());
let block = PartialFuelBlock {
header: Default::default(),
transactions: vec![deploy.into(), script.into()],
};
let ExecutionResult { block, .. } = executor
.execute_and_commit(ExecutionBlock::Production(block), Default::default())
.unwrap();
for (idx, output) in block.transactions()[1]
.as_script()
.unwrap()
.outputs()
.iter()
.enumerate()
{
let id = fuel_tx::UtxoId::new(script_id, idx as u8);
match output {
Output::Change { .. } | Output::Variable { .. } | Output::Coin { .. } => {
let maybe_utxo = database.storage::<Coins>().get(&id).unwrap();
assert!(maybe_utxo.is_some());
let utxo = maybe_utxo.unwrap();
assert!(utxo.amount > 0)
}
_ => (),
}
}
}
#[test]
fn outputs_with_no_value_are_excluded_from_utxo_set() {
let mut rng = StdRng::seed_from_u64(2322);
let asset_id: AssetId = rng.gen();
let input_amount = 0;
let coin_output_amount = 0;
let tx: Transaction = TxBuilder::new(2322)
.coin_input(asset_id, input_amount)
.variable_output(Default::default())
.coin_output(asset_id, coin_output_amount)
.change_output(asset_id)
.build()
.transaction()
.clone()
.into();
let tx_id = tx.id(&ChainId::default());
let database = &Database::default();
let executor = Executor::test(database.clone(), Default::default());
let block = PartialFuelBlock {
header: Default::default(),
transactions: vec![tx],
};
executor
.execute_and_commit(ExecutionBlock::Production(block), Default::default())
.unwrap();
for idx in 0..2 {
let id = UtxoId::new(tx_id, idx);
let maybe_utxo = database.storage::<Coins>().get(&id).unwrap();
assert!(maybe_utxo.is_none());
}
}
fn message_from_input(input: &Input, da_height: u64) -> Message {
Message {
sender: *input.sender().unwrap(),
recipient: *input.recipient().unwrap(),
nonce: *input.nonce().unwrap(),
amount: input.amount().unwrap(),
data: input
.input_data()
.map(|data| data.to_vec())
.unwrap_or_default(),
da_height: DaBlockHeight(da_height),
}
}
fn make_tx_and_message(rng: &mut StdRng, da_height: u64) -> (Transaction, Message) {
let tx = TransactionBuilder::script(vec![], vec![])
.add_unsigned_message_input(
SecretKey::random(rng),
rng.gen(),
rng.gen(),
1000,
vec![],
)
.finalize();
let message = message_from_input(&tx.inputs()[0], da_height);
(tx.into(), message)
}
fn make_executor(messages: &[&Message]) -> Executor<Database> {
let mut database = Database::default();
let database_ref = &mut database;
for message in messages {
database_ref
.storage::<Messages>()
.insert(message.id(), message)
.unwrap();
}
Executor::test(
database,
Config {
utxo_validation_default: true,
..Default::default()
},
)
}
#[test]
fn unspent_message_succeeds_when_msg_da_height_lt_block_da_height() {
let mut rng = StdRng::seed_from_u64(2322);
let (tx, message) = make_tx_and_message(&mut rng, 0);
let block = PartialFuelBlock {
header: Default::default(),
transactions: vec![tx],
};
let ExecutionResult { block, .. } = make_executor(&[&message])
.execute_and_commit(
ExecutionBlock::Production(block),
ExecutionOptions {
utxo_validation: true,
},
)
.expect("block execution failed unexpectedly");
make_executor(&[&message])
.execute_and_commit(
ExecutionBlock::Validation(block),
ExecutionOptions {
utxo_validation: true,
},
)
.expect("block validation failed unexpectedly");
}
#[test]
fn successful_execution_consume_all_messages() {
let mut rng = StdRng::seed_from_u64(2322);
let to: Address = rng.gen();
let amount = 500;
let tx = TransactionBuilder::script(vec![], vec![])
.add_unsigned_message_input(SecretKey::random(&mut rng), rng.gen(), rng.gen(), amount, vec![])
.add_unsigned_message_input(SecretKey::random(&mut rng), rng.gen(), rng.gen(), amount, vec![0xff; 10])
.add_output(Output::change(to, amount + amount, AssetId::BASE))
.finalize();
let tx_id = tx.id(&ChainId::default());
let message_coin = message_from_input(&tx.inputs()[0], 0);
let message_data = message_from_input(&tx.inputs()[1], 0);
let messages = vec![&message_coin, &message_data];
let mut block = PartialFuelBlock {
header: Default::default(),
transactions: vec![tx.into()],
};
let exec = make_executor(&messages);
let mut block_db_transaction = exec.database.transaction();
assert_eq!(block_db_transaction.all_messages(None, None).count(), 2);
let ExecutionData {
skipped_transactions,
..
} = exec
.execute_block(
&mut block_db_transaction,
ExecutionType::Production(PartialBlockComponent::from_partial_block(
&mut block,
)),
ExecutionOptions {
utxo_validation: true,
},
)
.unwrap();
assert_eq!(skipped_transactions.len(), 0);
assert_eq!(block_db_transaction.all_messages(None, None).count(), 0);
assert!(block_db_transaction
.message_is_spent(&message_coin.nonce)
.unwrap());
assert!(block_db_transaction
.message_is_spent(&message_data.nonce)
.unwrap());
assert_eq!(
block_db_transaction
.coin(&UtxoId::new(tx_id, 0))
.unwrap()
.amount,
amount + amount
);
}
#[test]
fn reverted_execution_consume_only_message_coins() {
let mut rng = StdRng::seed_from_u64(2322);
let to: Address = rng.gen();
let amount = 500;
let script = vec![op::ret(1)].into_iter().collect();
let tx = TransactionBuilder::script(script, vec![])
.add_unsigned_message_input(SecretKey::random(&mut rng), rng.gen(), rng.gen(), amount, vec![])
.add_unsigned_message_input(SecretKey::random(&mut rng), rng.gen(), rng.gen(), amount, vec![0xff; 10])
.add_output(Output::change(to, amount + amount, AssetId::BASE))
.finalize();
let tx_id = tx.id(&ChainId::default());
let message_coin = message_from_input(&tx.inputs()[0], 0);
let message_data = message_from_input(&tx.inputs()[1], 0);
let messages = vec![&message_coin, &message_data];
let mut block = PartialFuelBlock {
header: Default::default(),
transactions: vec![tx.into()],
};
let exec = make_executor(&messages);
let mut block_db_transaction = exec.database.transaction();
assert_eq!(block_db_transaction.all_messages(None, None).count(), 2);
let ExecutionData {
skipped_transactions,
..
} = exec
.execute_block(
&mut block_db_transaction,
ExecutionType::Production(PartialBlockComponent::from_partial_block(
&mut block,
)),
ExecutionOptions {
utxo_validation: true,
},
)
.unwrap();
assert_eq!(skipped_transactions.len(), 0);
assert_eq!(block_db_transaction.all_messages(None, None).count(), 1);
assert!(block_db_transaction
.message_is_spent(&message_coin.nonce)
.unwrap());
assert!(!block_db_transaction
.message_is_spent(&message_data.nonce)
.unwrap());
assert_eq!(
block_db_transaction
.coin(&UtxoId::new(tx_id, 0))
.unwrap()
.amount,
amount
);
}
#[test]
fn message_fails_when_spending_nonexistent_message_id() {
let mut rng = StdRng::seed_from_u64(2322);
let (tx, _message) = make_tx_and_message(&mut rng, 0);
let mut block = Block::default();
*block.transactions_mut() = vec![tx.clone()];
let ExecutionResult {
skipped_transactions,
mut block,
..
} = make_executor(&[]) .execute_and_commit(
ExecutionBlock::Production(block.clone().into()),
ExecutionOptions {
utxo_validation: true,
},
)
.unwrap();
let err = &skipped_transactions[0].1;
assert!(matches!(
err,
&ExecutorError::TransactionValidity(
TransactionValidityError::MessageDoesNotExist(_)
)
));
make_executor(&[]) .execute_and_commit(
ExecutionBlock::Validation(block.clone()),
ExecutionOptions {
utxo_validation: true,
},
)
.unwrap();
let index = block.transactions().len() - 1;
block.transactions_mut().insert(index, tx);
let res = make_executor(&[]) .execute_and_commit(
ExecutionBlock::Validation(block),
ExecutionOptions {
utxo_validation: true,
},
);
assert!(matches!(
res,
Err(ExecutorError::TransactionValidity(
TransactionValidityError::MessageDoesNotExist(_)
))
));
}
#[test]
fn message_fails_when_spending_da_height_gt_block_da_height() {
let mut rng = StdRng::seed_from_u64(2322);
let (tx, message) = make_tx_and_message(&mut rng, 1);
let mut block = Block::default();
*block.transactions_mut() = vec![tx.clone()];
let ExecutionResult {
skipped_transactions,
mut block,
..
} = make_executor(&[&message])
.execute_and_commit(
ExecutionBlock::Production(block.clone().into()),
ExecutionOptions {
utxo_validation: true,
},
)
.unwrap();
let err = &skipped_transactions[0].1;
assert!(matches!(
err,
&ExecutorError::TransactionValidity(
TransactionValidityError::MessageSpendTooEarly(_)
)
));
make_executor(&[&message])
.execute_and_commit(
ExecutionBlock::Validation(block.clone()),
ExecutionOptions {
utxo_validation: true,
},
)
.unwrap();
let index = block.transactions().len() - 1;
block.transactions_mut().insert(index, tx);
let res = make_executor(&[&message]).execute_and_commit(
ExecutionBlock::Validation(block),
ExecutionOptions {
utxo_validation: true,
},
);
assert!(matches!(
res,
Err(ExecutorError::TransactionValidity(
TransactionValidityError::MessageSpendTooEarly(_)
))
));
}
#[test]
fn message_fails_when_spending_already_spent_message_id() {
let mut rng = StdRng::seed_from_u64(2322);
let (tx1, message) = make_tx_and_message(&mut rng, 0);
let (mut tx2, _) = make_tx_and_message(&mut rng, 0);
tx2.as_script_mut().unwrap().inputs_mut()[0] =
tx1.as_script().unwrap().inputs()[0].clone();
let mut block = PartialFuelBlock {
header: Default::default(),
transactions: vec![tx1, tx2.clone()],
};
let exec = make_executor(&[&message]);
let mut block_db_transaction = exec.database.transaction();
let ExecutionData {
skipped_transactions,
..
} = exec
.execute_block(
&mut block_db_transaction,
ExecutionType::Production(PartialBlockComponent::from_partial_block(
&mut block,
)),
ExecutionOptions {
utxo_validation: true,
},
)
.unwrap();
assert_eq!(skipped_transactions.len(), 1);
let err = &skipped_transactions[0].1;
assert!(matches!(
err,
&ExecutorError::TransactionValidity(
TransactionValidityError::MessageAlreadySpent(_)
)
));
let exec = make_executor(&[&message]);
let mut block_db_transaction = exec.database.transaction();
exec.execute_block(
&mut block_db_transaction,
ExecutionType::Validation(PartialBlockComponent::from_partial_block(
&mut block,
)),
ExecutionOptions {
utxo_validation: true,
},
)
.unwrap();
block.transactions.insert(block.transactions.len() - 1, tx2);
let exec = make_executor(&[&message]);
let mut block_db_transaction = exec.database.transaction();
let res = exec.execute_block(
&mut block_db_transaction,
ExecutionType::Validation(PartialBlockComponent::from_partial_block(
&mut block,
)),
ExecutionOptions {
utxo_validation: true,
},
);
assert!(matches!(
res,
Err(ExecutorError::TransactionValidity(
TransactionValidityError::MessageAlreadySpent(_)
))
));
}
#[test]
fn get_block_height_returns_current_executing_block() {
let mut rng = StdRng::seed_from_u64(1234);
let base_asset_id = rng.gen();
let script = vec![op::bhei(0x10), op::ret(0x10)];
let tx = TransactionBuilder::script(script.into_iter().collect(), vec![])
.script_gas_limit(10000)
.add_unsigned_coin_input(
SecretKey::random(&mut rng),
rng.gen(),
1000,
base_asset_id,
Default::default(),
Default::default(),
)
.finalize();
let block_height = rng.gen_range(5u32..1000u32);
let block_tx_idx = rng.gen();
let block = PartialFuelBlock {
header: PartialBlockHeader {
consensus: ConsensusHeader {
height: block_height.into(),
..Default::default()
},
..Default::default()
},
transactions: vec![tx.clone().into()],
};
let database = &mut &mut Database::default();
let coin_input = &tx.inputs()[0];
database
.storage::<Coins>()
.insert(
coin_input.utxo_id().unwrap(),
&CompressedCoin {
owner: *coin_input.input_owner().unwrap(),
amount: coin_input.amount().unwrap(),
asset_id: *coin_input.asset_id(&base_asset_id).unwrap(),
maturity: coin_input.maturity().unwrap(),
tx_pointer: TxPointer::new(Default::default(), block_tx_idx),
},
)
.unwrap();
let executor = Executor::test(
database.clone(),
Config {
utxo_validation_default: true,
..Default::default()
},
);
executor
.execute_and_commit(
ExecutionBlock::Production(block),
ExecutionOptions {
utxo_validation: true,
},
)
.unwrap();
let receipts = database
.storage::<Receipts>()
.get(&tx.id(&ChainId::default()))
.unwrap()
.unwrap();
assert_eq!(block_height as u64, receipts[0].val().unwrap());
}
#[test]
fn get_time_returns_current_executing_block_time() {
let mut rng = StdRng::seed_from_u64(1234);
let base_asset_id = rng.gen();
let script = vec![op::bhei(0x10), op::time(0x11, 0x10), op::ret(0x11)];
let tx = TransactionBuilder::script(script.into_iter().collect(), vec![])
.script_gas_limit(10000)
.add_unsigned_coin_input(
SecretKey::random(&mut rng),
rng.gen(),
1000,
base_asset_id,
Default::default(),
Default::default(),
)
.finalize();
let block_height = rng.gen_range(5u32..1000u32);
let time = Tai64(rng.gen_range(1u32..u32::MAX) as u64);
let block = PartialFuelBlock {
header: PartialBlockHeader {
consensus: ConsensusHeader {
height: block_height.into(),
time,
..Default::default()
},
..Default::default()
},
transactions: vec![tx.clone().into()],
};
let database = &mut &mut Database::default();
let coin_input = &tx.inputs()[0];
database
.storage::<Coins>()
.insert(
coin_input.utxo_id().unwrap(),
&CompressedCoin {
owner: *coin_input.input_owner().unwrap(),
amount: coin_input.amount().unwrap(),
asset_id: *coin_input.asset_id(&base_asset_id).unwrap(),
maturity: coin_input.maturity().unwrap(),
tx_pointer: TxPointer::default(),
},
)
.unwrap();
let executor = Executor::test(
database.clone(),
Config {
utxo_validation_default: true,
..Default::default()
},
);
executor
.execute_and_commit(
ExecutionBlock::Production(block),
ExecutionOptions {
utxo_validation: true,
},
)
.unwrap();
let receipts = database
.storage::<Receipts>()
.get(&tx.id(&ChainId::default()))
.unwrap()
.unwrap();
assert_eq!(time.0, receipts[0].val().unwrap());
}
}