#![allow(non_upper_case_globals)]
use fuel_tx::{
Create,
Mint,
Script,
Transaction,
ValidityError,
};
use fuel_types::{
BlockHeight,
ChainId,
};
use alloc::{
boxed::Box,
vec::Vec,
};
use core::{
borrow::Borrow,
fmt::Debug,
future::Future,
};
use fuel_tx::{
field::MaxFeeLimit,
ConsensusParameters,
};
mod balances;
#[cfg(feature = "test-helpers")]
pub mod builder;
pub mod types;
pub use types::*;
use crate::{
error::PredicateVerificationFailed,
interpreter::{
Memory,
MemoryInstance,
},
pool::VmMemoryPool,
prelude::*,
storage::predicate::{
EmptyStorage,
PredicateStorageProvider,
PredicateStorageRequirements,
},
};
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Checks: u32 {
const Basic = 0b00000001;
const Signatures = 0b00000010;
const Predicates = 0b00000100;
}
}
impl core::fmt::Display for Checks {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "{:032b}", self.bits())
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Checked<Tx: IntoChecked> {
transaction: Tx,
metadata: Tx::Metadata,
checks_bitmask: Checks,
}
impl<Tx: IntoChecked> Checked<Tx> {
fn new(transaction: Tx, metadata: Tx::Metadata, checks_bitmask: Checks) -> Self {
Checked {
transaction,
metadata,
checks_bitmask,
}
}
pub(crate) fn basic(transaction: Tx, metadata: Tx::Metadata) -> Self {
Checked::new(transaction, metadata, Checks::Basic)
}
pub fn transaction(&self) -> &Tx {
&self.transaction
}
pub fn metadata(&self) -> &Tx::Metadata {
&self.metadata
}
pub fn checks(&self) -> &Checks {
&self.checks_bitmask
}
pub fn check_signatures(mut self, chain_id: &ChainId) -> Result<Self, CheckError> {
if !self.checks_bitmask.contains(Checks::Signatures) {
self.transaction.check_signatures(chain_id)?;
self.checks_bitmask.insert(Checks::Signatures);
}
Ok(self)
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Ready<Tx: IntoChecked> {
gas_price: Word,
transaction: Tx,
metadata: Tx::Metadata,
checks_bitmask: Checks,
}
impl<Tx: IntoChecked> Ready<Tx> {
pub fn decompose(self) -> (Word, Checked<Tx>) {
let Ready {
gas_price,
transaction,
metadata,
checks_bitmask,
} = self;
let checked = Checked::new(transaction, metadata, checks_bitmask);
(gas_price, checked)
}
pub fn gas_price(&self) -> Word {
self.gas_price
}
}
#[cfg(feature = "test-helpers")]
impl<Tx: IntoChecked> Checked<Tx> {
pub fn test_into_ready(self) -> Ready<Tx> {
let Checked {
transaction,
metadata,
checks_bitmask,
} = self;
Ready {
gas_price: 0,
transaction,
metadata,
checks_bitmask,
}
}
}
impl<Tx: IntoChecked + Chargeable> Checked<Tx> {
pub fn into_ready(
self,
gas_price: Word,
gas_costs: &GasCosts,
fee_parameters: &FeeParameters,
) -> Result<Ready<Tx>, CheckError> {
let Checked {
transaction,
metadata,
checks_bitmask,
} = self;
let fee = TransactionFee::checked_from_tx(
gas_costs,
fee_parameters,
&transaction,
gas_price,
)
.ok_or(CheckError::Validity(ValidityError::BalanceOverflow))?;
let max_fee_from_policies = transaction.max_fee_limit();
let max_fee_from_gas_price = fee.max_fee();
if max_fee_from_gas_price > max_fee_from_policies {
Err(CheckError::InsufficientMaxFee {
max_fee_from_policies,
max_fee_from_gas_price,
})
} else {
Ok(Ready {
gas_price,
transaction,
metadata,
checks_bitmask,
})
}
}
}
impl<Tx: IntoChecked + UniqueIdentifier> Checked<Tx> {
pub fn id(&self) -> TxId {
self.transaction
.cached_id()
.expect("Transaction metadata should be computed for checked transactions")
}
}
#[cfg(feature = "test-helpers")]
impl<Tx: IntoChecked + Default> Default for Checked<Tx>
where
Checked<Tx>: CheckPredicates,
{
fn default() -> Self {
Tx::default()
.into_checked(Default::default(), &ConsensusParameters::standard())
.expect("default tx should produce a valid fully checked transaction")
}
}
impl<Tx: IntoChecked> From<Checked<Tx>> for (Tx, Tx::Metadata) {
fn from(checked: Checked<Tx>) -> Self {
let Checked {
transaction,
metadata,
..
} = checked;
(transaction, metadata)
}
}
impl<Tx: IntoChecked> AsRef<Tx> for Checked<Tx> {
fn as_ref(&self) -> &Tx {
&self.transaction
}
}
#[cfg(feature = "test-helpers")]
impl<Tx: IntoChecked> AsMut<Tx> for Checked<Tx> {
fn as_mut(&mut self) -> &mut Tx {
&mut self.transaction
}
}
impl<Tx: IntoChecked> Borrow<Tx> for Checked<Tx> {
fn borrow(&self) -> &Tx {
self.transaction()
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum CheckError {
Validity(ValidityError),
PredicateVerificationFailed(PredicateVerificationFailed),
InsufficientMaxFee {
max_fee_from_policies: Word,
max_fee_from_gas_price: Word,
},
}
pub trait IntoChecked: FormatValidityChecks + Sized {
type Metadata: Sized;
fn into_checked(
self,
block_height: BlockHeight,
consensus_params: &ConsensusParameters,
) -> Result<Checked<Self>, CheckError>
where
Checked<Self>: CheckPredicates,
{
self.into_checked_reusable_memory(
block_height,
consensus_params,
MemoryInstance::new(),
&EmptyStorage,
)
}
fn into_checked_reusable_memory(
self,
block_height: BlockHeight,
consensus_params: &ConsensusParameters,
memory: impl Memory,
storage: &impl PredicateStorageRequirements,
) -> Result<Checked<Self>, CheckError>
where
Checked<Self>: CheckPredicates,
{
let check_predicate_params = consensus_params.into();
self.into_checked_basic(block_height, consensus_params)?
.check_signatures(&consensus_params.chain_id())?
.check_predicates(&check_predicate_params, memory, storage)
}
fn into_checked_basic(
self,
block_height: BlockHeight,
consensus_params: &ConsensusParameters,
) -> Result<Checked<Self>, CheckError>;
}
#[derive(Debug, Clone)]
pub struct CheckPredicateParams {
pub gas_costs: GasCosts,
pub chain_id: ChainId,
pub max_gas_per_predicate: u64,
pub max_gas_per_tx: u64,
pub max_inputs: u16,
pub contract_max_size: u64,
pub max_message_data_length: u64,
pub tx_offset: usize,
pub fee_params: FeeParameters,
pub base_asset_id: AssetId,
}
#[cfg(feature = "test-helpers")]
impl Default for CheckPredicateParams {
fn default() -> Self {
CheckPredicateParams::from(&ConsensusParameters::standard())
}
}
impl From<ConsensusParameters> for CheckPredicateParams {
fn from(value: ConsensusParameters) -> Self {
CheckPredicateParams::from(&value)
}
}
impl From<&ConsensusParameters> for CheckPredicateParams {
fn from(value: &ConsensusParameters) -> Self {
CheckPredicateParams {
gas_costs: value.gas_costs().clone(),
chain_id: value.chain_id(),
max_gas_per_predicate: value.predicate_params().max_gas_per_predicate(),
max_gas_per_tx: value.tx_params().max_gas_per_tx(),
max_inputs: value.tx_params().max_inputs(),
contract_max_size: value.contract_params().contract_max_size(),
max_message_data_length: value.predicate_params().max_message_data_length(),
tx_offset: value.tx_params().tx_offset(),
fee_params: *(value.fee_params()),
base_asset_id: *value.base_asset_id(),
}
}
}
#[async_trait::async_trait]
pub trait CheckPredicates: Sized {
fn check_predicates(
self,
params: &CheckPredicateParams,
memory: impl Memory,
storage: &impl PredicateStorageRequirements,
) -> Result<Self, CheckError>;
async fn check_predicates_async<E: ParallelExecutor>(
self,
params: &CheckPredicateParams,
pool: &impl VmMemoryPool,
storage: &impl PredicateStorageProvider,
) -> Result<Self, CheckError>;
}
#[async_trait::async_trait]
pub trait EstimatePredicates: Sized {
fn estimate_predicates(
&mut self,
params: &CheckPredicateParams,
memory: impl Memory,
storage: &impl PredicateStorageRequirements,
) -> Result<(), CheckError>;
async fn estimate_predicates_async<E: ParallelExecutor>(
&mut self,
params: &CheckPredicateParams,
pool: &impl VmMemoryPool,
storage: &impl PredicateStorageProvider,
) -> Result<(), CheckError>;
}
#[async_trait::async_trait]
pub trait ParallelExecutor {
type Task: Future + Send + 'static;
fn create_task<F>(func: F) -> Self::Task
where
F: FnOnce() -> Result<(Word, usize), PredicateVerificationFailed>
+ Send
+ 'static;
async fn execute_tasks(
futures: Vec<Self::Task>,
) -> Vec<Result<(Word, usize), PredicateVerificationFailed>>;
}
#[async_trait::async_trait]
impl<Tx> CheckPredicates for Checked<Tx>
where
Tx: ExecutableTransaction + Send + Sync + 'static,
<Tx as IntoChecked>::Metadata: crate::interpreter::CheckedMetadata + Send + Sync,
{
fn check_predicates(
mut self,
params: &CheckPredicateParams,
memory: impl Memory,
storage: &impl PredicateStorageRequirements,
) -> Result<Self, CheckError> {
if !self.checks_bitmask.contains(Checks::Predicates) {
predicates::check_predicates(&self, params, memory, storage)?;
self.checks_bitmask.insert(Checks::Predicates);
}
Ok(self)
}
async fn check_predicates_async<E>(
mut self,
params: &CheckPredicateParams,
pool: &impl VmMemoryPool,
storage: &impl PredicateStorageProvider,
) -> Result<Self, CheckError>
where
E: ParallelExecutor,
{
if !self.checks_bitmask.contains(Checks::Predicates) {
predicates::check_predicates_async::<Tx, E>(&self, params, pool, storage)
.await?;
self.checks_bitmask.insert(Checks::Predicates);
Ok(self)
} else {
Ok(self)
}
}
}
#[async_trait::async_trait]
impl<Tx: ExecutableTransaction + Send + Sync + 'static> EstimatePredicates for Tx {
fn estimate_predicates(
&mut self,
params: &CheckPredicateParams,
memory: impl Memory,
storage: &impl PredicateStorageRequirements,
) -> Result<(), CheckError> {
predicates::estimate_predicates(self, params, memory, storage)?;
Ok(())
}
async fn estimate_predicates_async<E>(
&mut self,
params: &CheckPredicateParams,
pool: &impl VmMemoryPool,
storage: &impl PredicateStorageProvider,
) -> Result<(), CheckError>
where
E: ParallelExecutor,
{
predicates::estimate_predicates_async::<Self, E>(self, params, pool, storage)
.await?;
Ok(())
}
}
#[async_trait::async_trait]
impl EstimatePredicates for Transaction {
fn estimate_predicates(
&mut self,
params: &CheckPredicateParams,
memory: impl Memory,
storage: &impl PredicateStorageRequirements,
) -> Result<(), CheckError> {
match self {
Self::Script(tx) => tx.estimate_predicates(params, memory, storage),
Self::Create(tx) => tx.estimate_predicates(params, memory, storage),
Self::Mint(_) => Ok(()),
Self::Upgrade(tx) => tx.estimate_predicates(params, memory, storage),
Self::Upload(tx) => tx.estimate_predicates(params, memory, storage),
Self::Blob(tx) => tx.estimate_predicates(params, memory, storage),
}
}
async fn estimate_predicates_async<E: ParallelExecutor>(
&mut self,
params: &CheckPredicateParams,
pool: &impl VmMemoryPool,
storage: &impl PredicateStorageProvider,
) -> Result<(), CheckError> {
match self {
Self::Script(tx) => {
tx.estimate_predicates_async::<E>(params, pool, storage)
.await
}
Self::Create(tx) => {
tx.estimate_predicates_async::<E>(params, pool, storage)
.await
}
Self::Mint(_) => Ok(()),
Self::Upgrade(tx) => {
tx.estimate_predicates_async::<E>(params, pool, storage)
.await
}
Self::Upload(tx) => {
tx.estimate_predicates_async::<E>(params, pool, storage)
.await
}
Self::Blob(tx) => {
tx.estimate_predicates_async::<E>(params, pool, storage)
.await
}
}
}
}
#[async_trait::async_trait]
impl CheckPredicates for Checked<Mint> {
fn check_predicates(
mut self,
_params: &CheckPredicateParams,
_memory: impl Memory,
_storage: &impl PredicateStorageRequirements,
) -> Result<Self, CheckError> {
self.checks_bitmask.insert(Checks::Predicates);
Ok(self)
}
async fn check_predicates_async<E: ParallelExecutor>(
mut self,
_params: &CheckPredicateParams,
_pool: &impl VmMemoryPool,
_storage: &impl PredicateStorageProvider,
) -> Result<Self, CheckError> {
self.checks_bitmask.insert(Checks::Predicates);
Ok(self)
}
}
#[async_trait::async_trait]
impl CheckPredicates for Checked<Transaction> {
fn check_predicates(
self,
params: &CheckPredicateParams,
memory: impl Memory,
storage: &impl PredicateStorageRequirements,
) -> Result<Self, CheckError> {
let checked_transaction: CheckedTransaction = self.into();
let checked_transaction: CheckedTransaction = match checked_transaction {
CheckedTransaction::Script(tx) => {
CheckPredicates::check_predicates(tx, params, memory, storage)?.into()
}
CheckedTransaction::Create(tx) => {
CheckPredicates::check_predicates(tx, params, memory, storage)?.into()
}
CheckedTransaction::Mint(tx) => {
CheckPredicates::check_predicates(tx, params, memory, storage)?.into()
}
CheckedTransaction::Upgrade(tx) => {
CheckPredicates::check_predicates(tx, params, memory, storage)?.into()
}
CheckedTransaction::Upload(tx) => {
CheckPredicates::check_predicates(tx, params, memory, storage)?.into()
}
CheckedTransaction::Blob(tx) => {
CheckPredicates::check_predicates(tx, params, memory, storage)?.into()
}
};
Ok(checked_transaction.into())
}
async fn check_predicates_async<E>(
mut self,
params: &CheckPredicateParams,
pool: &impl VmMemoryPool,
storage: &impl PredicateStorageProvider,
) -> Result<Self, CheckError>
where
E: ParallelExecutor,
{
let checked_transaction: CheckedTransaction = self.into();
let checked_transaction: CheckedTransaction = match checked_transaction {
CheckedTransaction::Script(tx) => {
CheckPredicates::check_predicates_async::<E>(tx, params, pool, storage)
.await?
.into()
}
CheckedTransaction::Create(tx) => {
CheckPredicates::check_predicates_async::<E>(tx, params, pool, storage)
.await?
.into()
}
CheckedTransaction::Mint(tx) => {
CheckPredicates::check_predicates_async::<E>(tx, params, pool, storage)
.await?
.into()
}
CheckedTransaction::Upgrade(tx) => {
CheckPredicates::check_predicates_async::<E>(tx, params, pool, storage)
.await?
.into()
}
CheckedTransaction::Upload(tx) => {
CheckPredicates::check_predicates_async::<E>(tx, params, pool, storage)
.await?
.into()
}
CheckedTransaction::Blob(tx) => {
CheckPredicates::check_predicates_async::<E>(tx, params, pool, storage)
.await?
.into()
}
};
Ok(checked_transaction.into())
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[allow(missing_docs)]
pub enum CheckedTransaction {
Script(Checked<Script>),
Create(Checked<Create>),
Mint(Checked<Mint>),
Upgrade(Checked<Upgrade>),
Upload(Checked<Upload>),
Blob(Checked<Blob>),
}
impl From<Checked<Transaction>> for CheckedTransaction {
fn from(checked: Checked<Transaction>) -> Self {
let Checked {
transaction,
metadata,
checks_bitmask,
} = checked;
match (transaction, metadata) {
(Transaction::Script(transaction), CheckedMetadata::Script(metadata)) => {
Self::Script(Checked::new(transaction, metadata, checks_bitmask))
}
(Transaction::Create(transaction), CheckedMetadata::Create(metadata)) => {
Self::Create(Checked::new(transaction, metadata, checks_bitmask))
}
(Transaction::Mint(transaction), CheckedMetadata::Mint(metadata)) => {
Self::Mint(Checked::new(transaction, metadata, checks_bitmask))
}
(Transaction::Upgrade(transaction), CheckedMetadata::Upgrade(metadata)) => {
Self::Upgrade(Checked::new(transaction, metadata, checks_bitmask))
}
(Transaction::Upload(transaction), CheckedMetadata::Upload(metadata)) => {
Self::Upload(Checked::new(transaction, metadata, checks_bitmask))
}
(Transaction::Blob(transaction), CheckedMetadata::Blob(metadata)) => {
Self::Blob(Checked::new(transaction, metadata, checks_bitmask))
}
(Transaction::Script(_), _) => unreachable!(),
(Transaction::Create(_), _) => unreachable!(),
(Transaction::Mint(_), _) => unreachable!(),
(Transaction::Upgrade(_), _) => unreachable!(),
(Transaction::Upload(_), _) => unreachable!(),
(Transaction::Blob(_), _) => unreachable!(),
}
}
}
impl From<Checked<Script>> for CheckedTransaction {
fn from(checked: Checked<Script>) -> Self {
Self::Script(checked)
}
}
impl From<Checked<Create>> for CheckedTransaction {
fn from(checked: Checked<Create>) -> Self {
Self::Create(checked)
}
}
impl From<Checked<Mint>> for CheckedTransaction {
fn from(checked: Checked<Mint>) -> Self {
Self::Mint(checked)
}
}
impl From<Checked<Upgrade>> for CheckedTransaction {
fn from(checked: Checked<Upgrade>) -> Self {
Self::Upgrade(checked)
}
}
impl From<Checked<Upload>> for CheckedTransaction {
fn from(checked: Checked<Upload>) -> Self {
Self::Upload(checked)
}
}
impl From<Checked<Blob>> for CheckedTransaction {
fn from(checked: Checked<Blob>) -> Self {
Self::Blob(checked)
}
}
impl From<CheckedTransaction> for Checked<Transaction> {
fn from(checked: CheckedTransaction) -> Self {
match checked {
CheckedTransaction::Script(Checked {
transaction,
metadata,
checks_bitmask,
}) => Checked::new(transaction.into(), metadata.into(), checks_bitmask),
CheckedTransaction::Create(Checked {
transaction,
metadata,
checks_bitmask,
}) => Checked::new(transaction.into(), metadata.into(), checks_bitmask),
CheckedTransaction::Mint(Checked {
transaction,
metadata,
checks_bitmask,
}) => Checked::new(transaction.into(), metadata.into(), checks_bitmask),
CheckedTransaction::Upgrade(Checked {
transaction,
metadata,
checks_bitmask,
}) => Checked::new(transaction.into(), metadata.into(), checks_bitmask),
CheckedTransaction::Upload(Checked {
transaction,
metadata,
checks_bitmask,
}) => Checked::new(transaction.into(), metadata.into(), checks_bitmask),
CheckedTransaction::Blob(Checked {
transaction,
metadata,
checks_bitmask,
}) => Checked::new(transaction.into(), metadata.into(), checks_bitmask),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[allow(missing_docs)]
pub enum CheckedMetadata {
Script(<Script as IntoChecked>::Metadata),
Create(<Create as IntoChecked>::Metadata),
Mint(<Mint as IntoChecked>::Metadata),
Upgrade(<Upgrade as IntoChecked>::Metadata),
Upload(<Upload as IntoChecked>::Metadata),
Blob(<Blob as IntoChecked>::Metadata),
}
impl From<<Script as IntoChecked>::Metadata> for CheckedMetadata {
fn from(metadata: <Script as IntoChecked>::Metadata) -> Self {
Self::Script(metadata)
}
}
impl From<<Create as IntoChecked>::Metadata> for CheckedMetadata {
fn from(metadata: <Create as IntoChecked>::Metadata) -> Self {
Self::Create(metadata)
}
}
impl From<<Mint as IntoChecked>::Metadata> for CheckedMetadata {
fn from(metadata: <Mint as IntoChecked>::Metadata) -> Self {
Self::Mint(metadata)
}
}
impl From<<Upgrade as IntoChecked>::Metadata> for CheckedMetadata {
fn from(metadata: <Upgrade as IntoChecked>::Metadata) -> Self {
Self::Upgrade(metadata)
}
}
impl From<<Upload as IntoChecked>::Metadata> for CheckedMetadata {
fn from(metadata: <Upload as IntoChecked>::Metadata) -> Self {
Self::Upload(metadata)
}
}
impl From<<Blob as IntoChecked>::Metadata> for CheckedMetadata {
fn from(metadata: <Blob as IntoChecked>::Metadata) -> Self {
Self::Blob(metadata)
}
}
impl IntoChecked for Transaction {
type Metadata = CheckedMetadata;
fn into_checked_basic(
self,
block_height: BlockHeight,
consensus_params: &ConsensusParameters,
) -> Result<Checked<Self>, CheckError> {
match self {
Self::Script(tx) => {
let (transaction, metadata) = tx
.into_checked_basic(block_height, consensus_params)?
.into();
Ok((transaction.into(), metadata.into()))
}
Self::Create(tx) => {
let (transaction, metadata) = tx
.into_checked_basic(block_height, consensus_params)?
.into();
Ok((transaction.into(), metadata.into()))
}
Self::Mint(tx) => {
let (transaction, metadata) = tx
.into_checked_basic(block_height, consensus_params)?
.into();
Ok((transaction.into(), metadata.into()))
}
Self::Upgrade(tx) => {
let (transaction, metadata) = tx
.into_checked_basic(block_height, consensus_params)?
.into();
Ok((transaction.into(), metadata.into()))
}
Self::Upload(tx) => {
let (transaction, metadata) = tx
.into_checked_basic(block_height, consensus_params)?
.into();
Ok((transaction.into(), metadata.into()))
}
Self::Blob(tx) => {
let (transaction, metadata) = tx
.into_checked_basic(block_height, consensus_params)?
.into();
Ok((transaction.into(), metadata.into()))
}
}
.map(|(transaction, metadata)| Checked::basic(transaction, metadata))
}
}
impl From<ValidityError> for CheckError {
fn from(value: ValidityError) -> Self {
CheckError::Validity(value)
}
}
impl From<PredicateVerificationFailed> for CheckError {
fn from(value: PredicateVerificationFailed) -> Self {
CheckError::PredicateVerificationFailed(value)
}
}
#[cfg(feature = "random")]
#[allow(non_snake_case)]
#[allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)]
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
use fuel_asm::op;
use fuel_crypto::SecretKey;
use fuel_tx::{
field::{
ScriptGasLimit,
Tip,
WitnessLimit,
Witnesses,
},
Script,
TransactionBuilder,
ValidityError,
};
use fuel_types::canonical::Serialize;
use quickcheck::TestResult;
use quickcheck_macros::quickcheck;
use rand::{
rngs::StdRng,
Rng,
SeedableRng,
};
fn params(factor: u64) -> ConsensusParameters {
ConsensusParameters::new(
TxParameters::default(),
PredicateParameters::default(),
ScriptParameters::default(),
ContractParameters::default(),
FeeParameters::default().with_gas_price_factor(factor),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
)
}
#[test]
fn into_checked__tx_accepts_valid_tx() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let gas_limit = 1000;
let input_amount = 1000;
let output_amount = 10;
let max_fee_limit = 500;
let tx =
valid_coin_tx(rng, gas_limit, input_amount, output_amount, max_fee_limit);
let checked = tx
.clone()
.into_checked(Default::default(), &ConsensusParameters::standard())
.expect("Expected valid transaction");
assert_eq!(checked.transaction(), &tx);
assert_eq!(
checked.metadata().non_retryable_balances[&AssetId::default()],
input_amount - max_fee_limit - output_amount
);
}
#[test]
fn into_checked__tx_accepts_valid_signed_message_coin_for_fees() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let input_amount = 1000;
let gas_limit = 1000;
let zero_fee_limit = 500;
let tx = signed_message_coin_tx(rng, gas_limit, input_amount, zero_fee_limit);
let checked = tx
.into_checked(Default::default(), &ConsensusParameters::standard())
.expect("Expected valid transaction");
assert_eq!(
checked.metadata().non_retryable_balances[&AssetId::default()],
input_amount - checked.transaction.max_fee_limit()
);
}
#[test]
fn into_checked__tx_excludes_message_output_amount_from_fee() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let input_amount = 100;
let gas_limit = 1000;
let zero_fee_limit = 50;
let tx = signed_message_coin_tx(rng, gas_limit, input_amount, zero_fee_limit);
let checked = tx
.into_checked(Default::default(), &ConsensusParameters::standard())
.expect("Expected valid transaction");
assert_eq!(
checked.metadata().non_retryable_balances[&AssetId::default()],
input_amount - checked.transaction.max_fee_limit()
);
}
#[test]
fn into_checked__message_data_signed_message_is_not_used_to_cover_fees() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let input_amount = 100;
let max_fee = input_amount;
let tx = TransactionBuilder::script(vec![], vec![])
.max_fee_limit(max_fee)
.add_unsigned_message_input(SecretKey::random(rng), rng.gen(), rng.gen(), input_amount, vec![0xff; 10])
.add_unsigned_coin_input(SecretKey::random(rng), rng.gen(), 0, AssetId::BASE, rng.gen())
.finalize();
let err = tx
.into_checked(Default::default(), &ConsensusParameters::standard())
.expect_err("Expected valid transaction");
assert!(matches!(
err,
CheckError::Validity(ValidityError::InsufficientFeeAmount {
expected: _,
provided: 0
})
));
}
#[test]
fn message_data_predicate_message_is_not_used_to_cover_fees() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let gas_limit = 1000;
let input_amount = 100;
let max_fee = input_amount;
let tx = TransactionBuilder::script(vec![], vec![])
.max_fee_limit(max_fee)
.script_gas_limit(gas_limit)
.add_input(Input::message_data_predicate(
rng.gen(),
rng.gen(),
input_amount,
rng.gen(),
Default::default(),
vec![0xff; 10],
vec![0xaa; 10],
vec![0xbb; 10],
))
.add_unsigned_coin_input(SecretKey::random(rng), rng.gen(), 0, AssetId::BASE, rng.gen())
.finalize();
let err = tx
.into_checked(Default::default(), &ConsensusParameters::standard())
.expect_err("Expected valid transaction");
assert!(matches!(
err,
CheckError::Validity(ValidityError::InsufficientFeeAmount {
expected: _,
provided: 0
})
));
}
#[quickcheck]
fn max_fee_coin_input(
gas_price: u64,
gas_limit: u64,
witness_limit: u64,
input_amount: u64,
gas_price_factor: u64,
seed: u64,
) -> TestResult {
if gas_price_factor == 0 {
return TestResult::discard();
}
let rng = &mut StdRng::seed_from_u64(seed);
let gas_costs = GasCosts::default();
let fee_params = FeeParameters::DEFAULT.with_gas_price_factor(gas_price_factor);
let predicate_gas_used = rng.gen();
let tx = predicate_tx(
rng,
gas_limit,
witness_limit,
input_amount,
predicate_gas_used,
);
if let Ok(valid) = is_valid_max_fee(&tx, gas_price, &gas_costs, &fee_params) {
TestResult::from_bool(valid)
} else {
TestResult::discard()
}
}
#[quickcheck]
fn min_fee_coin_input(
gas_price: u64,
gas_limit: u64,
witness_limit: u64,
input_amount: u64,
gas_price_factor: u64,
seed: u64,
) -> TestResult {
if gas_price_factor == 0 {
return TestResult::discard();
}
let rng = &mut StdRng::seed_from_u64(seed);
let gas_costs = GasCosts::default();
let fee_params = FeeParameters::DEFAULT.with_gas_price_factor(gas_price_factor);
let predicate_gas_used = rng.gen();
let tx = predicate_tx(
rng,
gas_limit,
witness_limit,
input_amount,
predicate_gas_used,
);
if let Ok(valid) = is_valid_max_fee(&tx, gas_price, &gas_costs, &fee_params) {
TestResult::from_bool(valid)
} else {
TestResult::discard()
}
}
#[quickcheck]
fn max_fee_message_input(
gas_price: u64,
gas_limit: u64,
input_amount: u64,
gas_price_factor: u64,
tip: u64,
seed: u64,
) -> TestResult {
if gas_price_factor == 0 {
return TestResult::discard();
}
let rng = &mut StdRng::seed_from_u64(seed);
let gas_costs = GasCosts::default();
let fee_params = FeeParameters::DEFAULT.with_gas_price_factor(gas_price_factor);
let tx = predicate_message_coin_tx(rng, gas_limit, input_amount, tip);
if let Ok(valid) = is_valid_max_fee(&tx, gas_price, &gas_costs, &fee_params) {
TestResult::from_bool(valid)
} else {
TestResult::discard()
}
}
#[quickcheck]
fn refund_when_used_gas_is_zero(
gas_price: u64,
gas_limit: u64,
input_amount: u64,
gas_price_factor: u64,
seed: u64,
tip: u64,
) -> TestResult {
if gas_price_factor == 0 {
return TestResult::discard();
}
let rng = &mut StdRng::seed_from_u64(seed);
let gas_costs = GasCosts::default();
let fee_params = FeeParameters::DEFAULT.with_gas_price_factor(gas_price_factor);
let tx = predicate_message_coin_tx(rng, gas_limit, input_amount, tip);
let used_gas = 0;
let refund = tx.refund_fee(&gas_costs, &fee_params, used_gas, gas_price);
let min_fee = tx.min_fee(&gas_costs, &fee_params, gas_price);
let max_fee = tx.max_fee(&gas_costs, &fee_params, gas_price);
if let Some(refund) = refund {
TestResult::from_bool(max_fee - min_fee == refund as u128)
} else {
TestResult::discard()
}
}
#[quickcheck]
fn min_fee_message_input(
gas_limit: u64,
input_amount: u64,
gas_price: u64,
gas_price_factor: u64,
tip: u64,
seed: u64,
) -> TestResult {
if gas_price_factor == 0 {
return TestResult::discard();
}
let rng = &mut StdRng::seed_from_u64(seed);
let gas_costs = GasCosts::default();
let fee_params = FeeParameters::DEFAULT.with_gas_price_factor(gas_price_factor);
let tx = predicate_message_coin_tx(rng, gas_limit, input_amount, tip);
if let Ok(valid) = is_valid_min_fee(&tx, &gas_costs, &fee_params, gas_price) {
TestResult::from_bool(valid)
} else {
TestResult::discard()
}
}
#[test]
fn fee_multiple_signed_inputs() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let gas_price = 100;
let gas_limit = 1000;
let gas_costs = GasCosts::default();
let fee_params = FeeParameters::DEFAULT.with_gas_price_factor(1);
let tx = TransactionBuilder::script(vec![], vec![])
.script_gas_limit(gas_limit)
.add_unsigned_message_input(
SecretKey::random(rng),
rng.gen(),
rng.gen(),
rng.gen::<u32>() as u64,
vec![],
)
.add_unsigned_message_input(
SecretKey::random(rng),
rng.gen(),
rng.gen(),
rng.gen::<u32>() as u64,
vec![],
)
.add_unsigned_message_input(
SecretKey::random(rng),
rng.gen(),
rng.gen(),
rng.gen::<u32>() as u64,
vec![],
)
.finalize();
let fee =
TransactionFee::checked_from_tx(&gas_costs, &fee_params, &tx, gas_price)
.unwrap();
let min_fee = fee.min_fee();
let expected_min_fee = (tx.metered_bytes_size() as u64
* fee_params.gas_per_byte()
+ gas_costs.vm_initialization().resolve(tx.size() as u64)
+ 3 * gas_costs.eck1()
+ gas_costs.s256().resolve(tx.size() as u64))
* gas_price;
assert_eq!(min_fee, expected_min_fee);
let max_fee = fee.max_fee();
let expected_max_fee = expected_min_fee + gas_limit * gas_price;
assert_eq!(max_fee, expected_max_fee);
}
#[test]
fn fee_multiple_signed_inputs_single_owner() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let gas_price = 100;
let gas_limit = 1000;
let gas_costs = GasCosts::default();
let fee_params = FeeParameters::DEFAULT.with_gas_price_factor(1);
let secret = SecretKey::random(rng);
let tx = TransactionBuilder::script(vec![], vec![])
.script_gas_limit(gas_limit)
.add_unsigned_message_input(
secret,
rng.gen(),
rng.gen(),
rng.gen::<u32>() as u64,
vec![],
)
.add_unsigned_message_input(
secret,
rng.gen(),
rng.gen(),
rng.gen::<u32>() as u64,
vec![],
)
.add_unsigned_message_input(
secret,
rng.gen(),
rng.gen(),
rng.gen::<u32>() as u64,
vec![],
)
.finalize();
let fee =
TransactionFee::checked_from_tx(&gas_costs, &fee_params, &tx, gas_price)
.unwrap();
let min_fee = fee.min_fee();
let expected_min_fee = (tx.metered_bytes_size() as u64
* fee_params.gas_per_byte()
+ gas_costs.vm_initialization().resolve(tx.size() as u64)
+ gas_costs.eck1()
+ gas_costs.s256().resolve(tx.size() as u64))
* gas_price;
assert_eq!(min_fee, expected_min_fee);
let max_fee = fee.max_fee();
let expected_max_fee = min_fee + gas_limit * gas_price;
assert_eq!(max_fee, expected_max_fee);
}
fn random_bytes<const N: usize, R: Rng + ?Sized>(rng: &mut R) -> Box<[u8; N]> {
let mut bytes = Box::new([0u8; N]);
for chunk in bytes.chunks_mut(32) {
rng.fill(chunk);
}
bytes
}
#[test]
fn min_fee_multiple_predicate_inputs() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let gas_price = 100;
let gas_limit = 1000;
let gas_costs = GasCosts::default();
let fee_params = FeeParameters::DEFAULT.with_gas_price_factor(1);
let predicate_1 = random_bytes::<1024, _>(rng);
let predicate_2 = random_bytes::<2048, _>(rng);
let predicate_3 = random_bytes::<4096, _>(rng);
let tx = TransactionBuilder::script(vec![], vec![])
.script_gas_limit(gas_limit)
.add_input(Input::message_coin_predicate(
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
50,
predicate_1.to_vec(),
vec![],
))
.add_input(Input::message_coin_predicate(
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
100,
predicate_2.to_vec(),
vec![],
))
.add_input(Input::message_coin_predicate(
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
200,
predicate_3.to_vec(),
vec![],
))
.finalize();
let fee =
TransactionFee::checked_from_tx(&gas_costs, &fee_params, &tx, gas_price)
.unwrap();
let min_fee = fee.min_fee();
let expected_min_fee = (tx.size() as u64 * fee_params.gas_per_byte()
+ gas_costs.vm_initialization().resolve(tx.size() as u64)
+ gas_costs.contract_root().resolve(predicate_1.len() as u64)
+ gas_costs.contract_root().resolve(predicate_2.len() as u64)
+ gas_costs.contract_root().resolve(predicate_3.len() as u64)
+ 3 * gas_costs.vm_initialization().resolve(tx.size() as u64)
+ 50
+ 100
+ 200
+ gas_costs.s256().resolve(tx.size() as u64))
* gas_price;
assert_eq!(min_fee, expected_min_fee);
let max_fee = fee.max_fee();
let expected_max_fee = min_fee + gas_limit * gas_price;
assert_eq!(max_fee, expected_max_fee);
}
#[test]
fn min_fee_multiple_signed_and_predicate_inputs() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let gas_price = 100;
let gas_limit = 1000;
let gas_costs = GasCosts::default();
let fee_params = FeeParameters::DEFAULT.with_gas_price_factor(1);
let predicate_1 = random_bytes::<1024, _>(rng);
let predicate_2 = random_bytes::<2048, _>(rng);
let predicate_3 = random_bytes::<4096, _>(rng);
let tx = TransactionBuilder::script(vec![], vec![])
.script_gas_limit(gas_limit)
.add_unsigned_message_input(
SecretKey::random(rng),
rng.gen(),
rng.gen(),
rng.gen::<u32>() as u64,
vec![],
)
.add_unsigned_message_input(
SecretKey::random(rng),
rng.gen(),
rng.gen(),
rng.gen::<u32>() as u64,
vec![],
)
.add_unsigned_message_input(
SecretKey::random(rng),
rng.gen(),
rng.gen(),
rng.gen::<u32>() as u64,
vec![],
)
.add_input(Input::message_coin_predicate(
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
50,
predicate_1.to_vec(),
vec![],
))
.add_input(Input::message_coin_predicate(
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
100,
predicate_2.to_vec(),
vec![],
))
.add_input(Input::message_coin_predicate(
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
200,
predicate_3.to_vec(),
vec![],
))
.finalize();
let fee =
TransactionFee::checked_from_tx(&gas_costs, &fee_params, &tx, gas_price)
.unwrap();
let min_fee = fee.min_fee();
let expected_min_fee = (tx.metered_bytes_size() as u64
* fee_params.gas_per_byte()
+ 3 * gas_costs.eck1()
+ gas_costs.vm_initialization().resolve(tx.size() as u64)
+ gas_costs.contract_root().resolve(predicate_1.len() as u64)
+ gas_costs.contract_root().resolve(predicate_2.len() as u64)
+ gas_costs.contract_root().resolve(predicate_3.len() as u64)
+ 3 * gas_costs.vm_initialization().resolve(tx.size() as u64)
+ 50
+ 100
+ 200
+ gas_costs.s256().resolve(tx.size() as u64))
* gas_price;
assert_eq!(min_fee, expected_min_fee);
let max_fee = fee.max_fee();
let expected_max_fee = min_fee + gas_limit * gas_price;
assert_eq!(max_fee, expected_max_fee);
}
#[test]
fn fee_create_tx() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let gas_price = 100;
let witness_limit = 1000;
let gas_costs = GasCosts::default();
let fee_params = FeeParameters::DEFAULT.with_gas_price_factor(1);
let gen_storage_slot = || rng.gen::<StorageSlot>();
let storage_slots = core::iter::repeat_with(gen_storage_slot)
.take(100)
.collect::<Vec<_>>();
let storage_slots_len = storage_slots.len();
let bytecode = rng.gen::<Witness>();
let bytecode_len = bytecode.as_ref().len();
let salt = rng.gen::<Salt>();
let tx = TransactionBuilder::create(bytecode.clone(), salt, storage_slots)
.witness_limit(witness_limit)
.finalize();
let fee =
TransactionFee::checked_from_tx(&gas_costs, &fee_params, &tx, gas_price)
.unwrap();
let min_fee = fee.min_fee();
let expected_min_fee = (tx.metered_bytes_size() as u64
* fee_params.gas_per_byte()
+ gas_costs.state_root().resolve(storage_slots_len as Word)
+ gas_costs.contract_root().resolve(bytecode_len as Word)
+ gas_costs.vm_initialization().resolve(tx.size() as u64)
+ gas_costs.s256().resolve(100)
+ gas_costs.s256().resolve(tx.size() as u64))
* gas_price;
assert_eq!(min_fee, expected_min_fee);
let max_fee = fee.max_fee();
let expected_max_fee = min_fee
+ (witness_limit - bytecode.size() as u64)
* fee_params.gas_per_byte()
* gas_price;
assert_eq!(max_fee, expected_max_fee);
}
#[test]
fn fee_create_tx_no_bytecode() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let gas_price = 100;
let witness_limit = 1000;
let gas_costs = GasCosts::default();
let fee_params = FeeParameters::DEFAULT.with_gas_price_factor(1);
let bytecode: Witness = Vec::<u8>::new().into();
let salt = rng.gen::<Salt>();
let tx = TransactionBuilder::create(bytecode.clone(), salt, vec![])
.witness_limit(witness_limit)
.finalize();
let fee =
TransactionFee::checked_from_tx(&gas_costs, &fee_params, &tx, gas_price)
.unwrap();
let min_fee = fee.min_fee();
let expected_min_fee = (tx.metered_bytes_size() as u64
* fee_params.gas_per_byte()
+ gas_costs.state_root().resolve(0)
+ gas_costs.contract_root().resolve(0)
+ gas_costs.vm_initialization().resolve(tx.size() as u64)
+ gas_costs.s256().resolve(100)
+ gas_costs.s256().resolve(tx.size() as u64))
* gas_price;
assert_eq!(min_fee, expected_min_fee);
let max_fee = fee.max_fee();
let expected_max_fee = min_fee
+ (witness_limit - bytecode.size_static() as u64)
* fee_params.gas_per_byte()
* gas_price;
assert_eq!(max_fee, expected_max_fee);
}
#[test]
fn checked_tx_rejects_invalid_tx() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let asset = rng.gen();
let gas_limit = 100;
let input_amount = 1_000;
let tx = TransactionBuilder::script(vec![], vec![])
.script_gas_limit(gas_limit)
.add_input(Input::coin_signed(
rng.gen(),
rng.gen(),
input_amount,
asset,
rng.gen(),
Default::default(),
))
.add_input(Input::contract(
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
))
.add_output(Output::contract(1, rng.gen(), rng.gen()))
.add_output(Output::coin(rng.gen(), 10, asset))
.add_output(Output::change(rng.gen(), 0, asset))
.add_witness(Default::default())
.finalize();
let err = tx
.into_checked(Default::default(), &ConsensusParameters::standard())
.expect_err("Expected invalid transaction");
assert!(matches!(
err,
CheckError::Validity(ValidityError::InputInvalidSignature { .. })
));
}
#[test]
fn into_checked__tx_fails_when_provided_fees_dont_cover_byte_costs() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let arb_input_amount = 1;
let gas_price = 2; let gas_limit = 0; let factor = 1;
let zero_max_fee = 0;
let params = params(factor);
let transaction = base_asset_tx(rng, arb_input_amount, gas_limit, zero_max_fee);
transaction
.clone()
.into_checked(Default::default(), ¶ms)
.unwrap();
let fees = TransactionFee::checked_from_tx(
&GasCosts::default(),
params.fee_params(),
&transaction,
gas_price,
)
.unwrap();
let real_max_fee = fees.max_fee();
let new_input_amount = real_max_fee;
let mut new_transaction =
base_asset_tx(rng, new_input_amount, gas_limit, real_max_fee);
new_transaction
.clone()
.into_checked(Default::default(), ¶ms)
.unwrap()
.into_ready(gas_price, &GasCosts::default(), params.fee_params())
.expect("`new_transaction` should be fully valid");
new_transaction.witnesses_mut().push(rng.gen());
let bigger_checked = new_transaction
.into_checked(Default::default(), ¶ms)
.unwrap();
let err = bigger_checked
.into_ready(gas_price, &GasCosts::default(), params.fee_params())
.expect_err("Expected invalid transaction");
let max_fee_from_policies = match err {
CheckError::InsufficientMaxFee {
max_fee_from_policies,
..
} => max_fee_from_policies,
_ => panic!("expected insufficient max fee; found {err:?}"),
};
assert_eq!(max_fee_from_policies, real_max_fee);
}
#[test]
fn into_checked__tx_fails_when_provided_fees_dont_cover_fee_limit() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let input_amount = 10;
let factor = 1;
let gas_limit = input_amount + 1; let input_amount = 10;
let big_fee_limit = input_amount + 1;
let transaction = base_asset_tx(rng, input_amount, gas_limit, big_fee_limit);
let consensus_params = params(factor);
let err = transaction
.into_checked(Default::default(), &consensus_params)
.expect_err("overflow expected");
let provided = match err {
CheckError::Validity(ValidityError::InsufficientFeeAmount {
provided,
..
}) => provided,
_ => panic!("expected insufficient fee amount; found {err:?}"),
};
assert_eq!(provided, input_amount);
}
#[test]
fn into_ready__bytes_fee_cant_overflow() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let input_amount = 1000;
let max_gas_price = Word::MAX;
let gas_limit = 0; let zero_fee_limit = 0;
let transaction = base_asset_tx(rng, input_amount, gas_limit, zero_fee_limit);
let gas_costs = GasCosts::default();
let consensus_params = params(1);
let fee_params = consensus_params.fee_params();
let err = transaction
.into_checked(Default::default(), &consensus_params)
.unwrap()
.into_ready(max_gas_price, &gas_costs, fee_params)
.expect_err("overflow expected");
assert_eq!(err, CheckError::Validity(ValidityError::BalanceOverflow));
}
#[test]
fn into_ready__fails_if_fee_limit_too_low() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let input_amount = 1000;
let gas_price = 100;
let gas_limit = 0; let gas_costs = GasCosts::default();
let consensus_params = params(1);
let fee_params = consensus_params.fee_params();
let zero_fee_limit = 0;
let transaction = base_asset_tx(rng, input_amount, gas_limit, zero_fee_limit);
let err = transaction
.into_checked(Default::default(), &consensus_params)
.unwrap()
.into_ready(gas_price, &gas_costs, fee_params)
.expect_err("overflow expected");
assert!(matches!(err, CheckError::InsufficientMaxFee { .. }));
}
#[test]
fn into_ready__tx_fails_if_tip_not_covered() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let input_amount = 1;
let gas_limit = 1000;
let params = ConsensusParameters::standard();
let block_height = 1.into();
let gas_costs = GasCosts::default();
let max_fee_limit = input_amount;
let gas_price = 1;
let tx_without_tip =
base_asset_tx_with_tip(rng, input_amount, gas_limit, max_fee_limit, None);
tx_without_tip
.clone()
.into_checked(block_height, ¶ms)
.unwrap()
.into_ready(gas_price, &gas_costs, params.fee_params())
.expect("Should be valid");
let tip = 100;
let tx_without_enough_to_pay_for_tip = base_asset_tx_with_tip(
rng,
input_amount,
gas_limit,
max_fee_limit,
Some(tip),
);
tx_without_enough_to_pay_for_tip
.into_checked(block_height, ¶ms)
.unwrap()
.into_ready(gas_price, &gas_costs, params.fee_params())
.expect_err("Expected invalid transaction");
let new_input_amount = input_amount + tip;
let new_gas_limit = new_input_amount;
let tx = base_asset_tx_with_tip(
rng,
new_input_amount,
gas_limit,
new_gas_limit,
Some(tip),
);
tx.clone()
.into_checked(block_height, ¶ms)
.unwrap()
.into_ready(gas_price, &GasCosts::default(), params.fee_params())
.expect("Should be valid");
}
#[test]
fn into_ready__return_overflow_error_if_gas_price_too_high() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let input_amount = 1000;
let gas_price = Word::MAX;
let gas_limit = 2; let max_fee_limit = 0;
let transaction = base_asset_tx(rng, input_amount, gas_limit, max_fee_limit);
let consensus_params = params(1);
let err = transaction
.into_checked(Default::default(), &consensus_params)
.unwrap()
.into_ready(
gas_price,
&GasCosts::default(),
consensus_params.fee_params(),
)
.expect_err("overflow expected");
assert_eq!(err, CheckError::Validity(ValidityError::BalanceOverflow));
}
#[test]
fn checked_tx_fails_if_asset_is_overspent_by_coin_output() {
let input_amount = 1_000;
let rng = &mut StdRng::seed_from_u64(2322u64);
let secret = SecretKey::random(rng);
let any_asset = rng.gen();
let tx = TransactionBuilder::script(vec![], vec![])
.script_gas_limit(100)
.add_unsigned_coin_input(
secret,
rng.gen(),
input_amount,
AssetId::default(),
rng.gen(),
)
.add_output(Output::change(rng.gen(), 0, AssetId::default()))
.add_unsigned_coin_input(
secret,
rng.gen(),
input_amount,
any_asset,
rng.gen(),
)
.add_output(Output::coin(rng.gen(), input_amount + 1, any_asset))
.add_output(Output::change(rng.gen(), 0, any_asset))
.finalize();
let checked = tx
.into_checked(Default::default(), &ConsensusParameters::standard())
.expect_err("Expected valid transaction");
assert_eq!(
CheckError::Validity(ValidityError::InsufficientInputAmount {
asset: any_asset,
expected: input_amount + 1,
provided: input_amount,
}),
checked
);
}
#[cfg(feature = "std")]
#[test]
fn basic_check_marks_basic_flag() {
let block_height = 1.into();
let tx = Transaction::default_test_tx();
let checked = tx
.into_checked_basic(block_height, &ConsensusParameters::standard())
.unwrap();
assert!(checked.checks().contains(Checks::Basic));
}
#[test]
fn signatures_check_marks_signatures_flag() {
let mut rng = StdRng::seed_from_u64(1);
let block_height = 1.into();
let max_fee_limit = 0;
let tx = valid_coin_tx(&mut rng, 100000, 1000000, 10, max_fee_limit);
let chain_id = ChainId::default();
let checked = tx
.into_checked(
block_height,
&ConsensusParameters::standard_with_id(chain_id),
)
.unwrap()
.check_signatures(&chain_id)
.unwrap();
assert!(checked
.checks()
.contains(Checks::Basic | Checks::Signatures));
}
#[test]
fn predicates_check_marks_predicate_flag() {
let mut rng = StdRng::seed_from_u64(1);
let block_height = 1.into();
let gas_costs = GasCosts::default();
let tx = predicate_tx(&mut rng, 1000000, 1000000, 1000000, gas_costs.ret());
let mut consensus_params = ConsensusParameters::standard();
consensus_params.set_gas_costs(gas_costs);
let check_predicate_params = CheckPredicateParams::from(&consensus_params);
let checked = tx
.into_checked(
block_height,
&consensus_params,
)
.unwrap()
.check_predicates(&check_predicate_params, MemoryInstance::new(), &EmptyStorage)
.unwrap();
assert!(checked
.checks()
.contains(Checks::Basic | Checks::Predicates));
}
fn is_valid_max_fee(
tx: &Script,
gas_price: u64,
gas_costs: &GasCosts,
fee_params: &FeeParameters,
) -> Result<bool, ValidityError> {
fn gas_to_fee(gas: u64, price: u64, factor: u64) -> u128 {
let prices_gas = gas as u128 * price as u128;
let fee = prices_gas / factor as u128;
let fee_remainder = (prices_gas.rem_euclid(factor as u128) > 0) as u128;
fee + fee_remainder
}
let gas_used_by_bytes = fee_params
.gas_per_byte()
.saturating_mul(tx.metered_bytes_size() as u64);
let gas_used_by_inputs = tx.gas_used_by_inputs(gas_costs);
let gas_used_by_metadata = tx.gas_used_by_metadata(gas_costs);
let min_gas = gas_used_by_bytes
.saturating_add(gas_used_by_inputs)
.saturating_add(gas_used_by_metadata)
.saturating_add(
gas_costs
.vm_initialization()
.resolve(tx.metered_bytes_size() as u64),
);
let witness_limit_allowance = tx
.witness_limit()
.saturating_sub(tx.witnesses().size_dynamic() as u64)
.saturating_mul(fee_params.gas_per_byte());
let max_gas = min_gas
.saturating_add(*tx.script_gas_limit())
.saturating_add(witness_limit_allowance);
let max_fee = gas_to_fee(max_gas, gas_price, fee_params.gas_price_factor());
let max_fee_with_tip = max_fee.saturating_add(tx.tip() as u128);
let result = max_fee_with_tip == tx.max_fee(gas_costs, fee_params, gas_price);
Ok(result)
}
fn is_valid_min_fee<Tx>(
tx: &Tx,
gas_costs: &GasCosts,
fee_params: &FeeParameters,
gas_price: u64,
) -> Result<bool, ValidityError>
where
Tx: Chargeable + field::Inputs + field::Outputs,
{
let gas_used_by_bytes = fee_params
.gas_per_byte()
.saturating_mul(tx.metered_bytes_size() as u64);
let gas_used_by_inputs = tx.gas_used_by_inputs(gas_costs);
let gas_used_by_metadata = tx.gas_used_by_metadata(gas_costs);
let gas = gas_used_by_bytes
.saturating_add(gas_used_by_inputs)
.saturating_add(gas_used_by_metadata)
.saturating_add(
gas_costs
.vm_initialization()
.resolve(tx.metered_bytes_size() as u64),
);
let total = gas as u128 * gas_price as u128;
let fee = total / fee_params.gas_price_factor() as u128;
let fee_remainder =
(total.rem_euclid(fee_params.gas_price_factor() as u128) > 0) as u128;
let rounded_fee = fee
.saturating_add(fee_remainder)
.saturating_add(tx.tip() as u128);
let min_fee = rounded_fee;
let calculated_min_fee = tx.min_fee(gas_costs, fee_params, gas_price);
Ok(min_fee == calculated_min_fee)
}
fn valid_coin_tx(
rng: &mut StdRng,
gas_limit: u64,
input_amount: u64,
output_amount: u64,
max_fee_limit: u64,
) -> Script {
let asset = AssetId::default();
TransactionBuilder::script(vec![], vec![])
.script_gas_limit(gas_limit)
.max_fee_limit(max_fee_limit)
.add_unsigned_coin_input(
SecretKey::random(rng),
rng.gen(),
input_amount,
asset,
rng.gen(),
)
.add_input(Input::contract(
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
))
.add_output(Output::contract(1, rng.gen(), rng.gen()))
.add_output(Output::coin(rng.gen(), output_amount, asset))
.add_output(Output::change(rng.gen(), 0, asset))
.finalize()
}
fn predicate_tx(
rng: &mut StdRng,
gas_limit: u64,
witness_limit: u64,
fee_input_amount: u64,
predicate_gas_used: u64,
) -> Script {
let asset = AssetId::default();
let predicate = vec![op::ret(1)].into_iter().collect::<Vec<u8>>();
let owner = Input::predicate_owner(&predicate);
let zero_fee_limit = 0;
TransactionBuilder::script(vec![], vec![])
.max_fee_limit(zero_fee_limit)
.script_gas_limit(gas_limit)
.witness_limit(witness_limit)
.add_input(Input::coin_predicate(
rng.gen(),
owner,
fee_input_amount,
asset,
rng.gen(),
predicate_gas_used,
predicate,
vec![],
))
.add_output(Output::change(rng.gen(), 0, asset))
.finalize()
}
fn signed_message_coin_tx(
rng: &mut StdRng,
gas_limit: u64,
input_amount: u64,
max_fee: u64,
) -> Script {
TransactionBuilder::script(vec![], vec![])
.max_fee_limit(max_fee)
.script_gas_limit(gas_limit)
.add_unsigned_message_input(
SecretKey::random(rng),
rng.gen(),
rng.gen(),
input_amount,
vec![],
)
.finalize()
}
fn predicate_message_coin_tx(
rng: &mut StdRng,
gas_limit: u64,
input_amount: u64,
tip: u64,
) -> Script {
TransactionBuilder::script(vec![], vec![])
.tip(tip)
.script_gas_limit(gas_limit)
.add_input(Input::message_coin_predicate(
rng.gen(),
rng.gen(),
input_amount,
rng.gen(),
Default::default(),
vec![],
vec![],
))
.finalize()
}
fn base_asset_tx(
rng: &mut StdRng,
input_amount: u64,
gas_limit: u64,
max_fee: u64,
) -> Script {
base_asset_tx_with_tip(rng, input_amount, gas_limit, max_fee, None)
}
fn base_asset_tx_with_tip(
rng: &mut StdRng,
input_amount: u64,
gas_limit: u64,
max_fee: u64,
tip: Option<u64>,
) -> Script {
let mut builder = TransactionBuilder::script(vec![], vec![]);
if let Some(tip) = tip {
builder.tip(tip);
}
builder
.max_fee_limit(max_fee)
.script_gas_limit(gas_limit)
.add_unsigned_coin_input(
SecretKey::random(rng),
rng.gen(),
input_amount,
AssetId::default(),
rng.gen(),
)
.add_output(Output::change(rng.gen(), 0, AssetId::default()))
.finalize()
}
}