fuel_tx/transaction/
validity.rs

1use crate::{
2    field::{
3        Expiration,
4        Maturity,
5    },
6    input::{
7        coin::{
8            CoinPredicate,
9            CoinSigned,
10        },
11        message::{
12            MessageCoinPredicate,
13            MessageCoinSigned,
14            MessageDataPredicate,
15            MessageDataSigned,
16        },
17    },
18    output,
19    policies::PolicyType,
20    transaction::{
21        consensus_parameters::{
22            PredicateParameters,
23            TxParameters,
24        },
25        field,
26        Executable,
27    },
28    Chargeable,
29    ConsensusParameters,
30    Input,
31    Output,
32    Transaction,
33    Witness,
34};
35use core::hash::Hash;
36use fuel_types::{
37    canonical,
38    canonical::Serialize,
39    Address,
40    BlockHeight,
41    Bytes32,
42    ChainId,
43};
44use hashbrown::HashMap;
45use itertools::Itertools;
46
47mod error;
48
49#[cfg(test)]
50mod tests;
51
52pub use error::ValidityError;
53
54impl Input {
55    #[cfg(any(feature = "typescript", test))]
56    pub fn check(
57        &self,
58        index: usize,
59        txhash: &Bytes32,
60        outputs: &[Output],
61        witnesses: &[Witness],
62        predicate_params: &PredicateParameters,
63        recovery_cache: &mut Option<HashMap<u16, Address>>,
64    ) -> Result<(), ValidityError> {
65        self.check_without_signature(index, outputs, witnesses, predicate_params)?;
66        self.check_signature(index, txhash, witnesses, recovery_cache)?;
67
68        Ok(())
69    }
70
71    pub fn check_signature(
72        &self,
73        index: usize,
74        txhash: &Bytes32,
75        witnesses: &[Witness],
76        recovery_cache: &mut Option<HashMap<u16, Address>>,
77    ) -> Result<(), ValidityError> {
78        match self {
79            Self::CoinSigned(CoinSigned {
80                witness_index,
81                owner,
82                ..
83            })
84            | Self::MessageCoinSigned(MessageCoinSigned {
85                witness_index,
86                recipient: owner,
87                ..
88            })
89            | Self::MessageDataSigned(MessageDataSigned {
90                witness_index,
91                recipient: owner,
92                ..
93            }) => {
94                // Helper function for recovering the address from a witness
95                let recover_address = || -> Result<Address, ValidityError> {
96                    let witness = witnesses
97                        .get(*witness_index as usize)
98                        .ok_or(ValidityError::InputWitnessIndexBounds { index })?;
99
100                    witness.recover_witness(txhash, index)
101                };
102
103                // recover the address associated with a witness, using the cache if
104                // available
105                let recovered_address = if let Some(cache) = recovery_cache {
106                    if let Some(recovered_address) = cache.get(witness_index) {
107                        *recovered_address
108                    } else {
109                        // if this witness hasn't been recovered before,
110                        // cache ecrecover by witness index
111                        let recovered_address = recover_address()?;
112                        cache.insert(*witness_index, recovered_address);
113                        recovered_address
114                    }
115                } else {
116                    recover_address()?
117                };
118
119                if owner != &recovered_address {
120                    return Err(ValidityError::InputInvalidSignature { index });
121                }
122
123                Ok(())
124            }
125
126            Self::CoinPredicate(CoinPredicate {
127                owner, predicate, ..
128            })
129            | Self::MessageCoinPredicate(MessageCoinPredicate {
130                recipient: owner,
131                predicate,
132                ..
133            })
134            | Self::MessageDataPredicate(MessageDataPredicate {
135                recipient: owner,
136                predicate,
137                ..
138            }) if !Input::is_predicate_owner_valid(owner, &**predicate) => {
139                Err(ValidityError::InputPredicateOwner { index })
140            }
141
142            _ => Ok(()),
143        }
144    }
145
146    pub fn check_without_signature(
147        &self,
148        index: usize,
149        outputs: &[Output],
150        witnesses: &[Witness],
151        predicate_params: &PredicateParameters,
152    ) -> Result<(), ValidityError> {
153        match self {
154            Self::CoinPredicate(CoinPredicate { predicate, .. })
155            | Self::MessageCoinPredicate(MessageCoinPredicate { predicate, .. })
156            | Self::MessageDataPredicate(MessageDataPredicate { predicate, .. })
157                if predicate.is_empty() =>
158            {
159                Err(ValidityError::InputPredicateEmpty { index })
160            }
161
162            Self::CoinPredicate(CoinPredicate { predicate, .. })
163            | Self::MessageCoinPredicate(MessageCoinPredicate { predicate, .. })
164            | Self::MessageDataPredicate(MessageDataPredicate { predicate, .. })
165                if predicate.len() as u64 > predicate_params.max_predicate_length() =>
166            {
167                Err(ValidityError::InputPredicateLength { index })
168            }
169
170            Self::CoinPredicate(CoinPredicate { predicate_data, .. })
171            | Self::MessageCoinPredicate(MessageCoinPredicate {
172                predicate_data, ..
173            })
174            | Self::MessageDataPredicate(MessageDataPredicate {
175                predicate_data, ..
176            }) if predicate_data.len() as u64
177                > predicate_params.max_predicate_data_length() =>
178            {
179                Err(ValidityError::InputPredicateDataLength { index })
180            }
181
182            Self::CoinSigned(CoinSigned { witness_index, .. })
183            | Self::MessageCoinSigned(MessageCoinSigned { witness_index, .. })
184            | Self::MessageDataSigned(MessageDataSigned { witness_index, .. })
185                if *witness_index as usize >= witnesses.len() =>
186            {
187                Err(ValidityError::InputWitnessIndexBounds { index })
188            }
189
190            // ∀ inputContract ∃! outputContract : outputContract.inputIndex =
191            // inputContract.index
192            Self::Contract { .. }
193                if 1 != outputs
194                    .iter()
195                    .filter_map(|output| match output {
196                        Output::Contract(output::contract::Contract {
197                            input_index,
198                            ..
199                        }) if *input_index as usize == index => Some(()),
200                        _ => None,
201                    })
202                    .count() =>
203            {
204                Err(ValidityError::InputContractAssociatedOutputContract { index })
205            }
206
207            Self::MessageDataSigned(MessageDataSigned { data, .. })
208            | Self::MessageDataPredicate(MessageDataPredicate { data, .. })
209                if data.is_empty()
210                    || data.len() as u64 > predicate_params.max_message_data_length() =>
211            {
212                Err(ValidityError::InputMessageDataLength { index })
213            }
214
215            // TODO: If h is the block height the UTXO being spent was created,
216            // transaction is  invalid if `blockheight() < h + maturity`.
217            _ => Ok(()),
218        }
219    }
220}
221
222impl Output {
223    /// Validate the output of the transaction.
224    ///
225    /// This function is stateful - meaning it might validate a transaction during VM
226    /// initialization, but this transaction will no longer be valid in post-execution
227    /// because the VM might mutate the message outputs, producing invalid
228    /// transactions.
229    pub fn check(&self, index: usize, inputs: &[Input]) -> Result<(), ValidityError> {
230        match self {
231            Self::Contract(output::contract::Contract { input_index, .. }) => {
232                match inputs.get(*input_index as usize) {
233                    Some(Input::Contract { .. }) => Ok(()),
234                    _ => Err(ValidityError::OutputContractInputIndex { index }),
235                }
236            }
237
238            _ => Ok(()),
239        }
240    }
241}
242
243/// Contains logic for stateless validations that don't result in any reusable metadata
244/// such as spendable input balances or remaining gas. Primarily involves validating that
245/// transaction fields are correctly formatted and signed.
246pub trait FormatValidityChecks {
247    /// Performs all stateless transaction validity checks. This includes the validity
248    /// of fields according to rules in the specification and validity of signatures.
249    fn check(
250        &self,
251        block_height: BlockHeight,
252        consensus_params: &ConsensusParameters,
253    ) -> Result<(), ValidityError> {
254        self.check_without_signatures(block_height, consensus_params)?;
255        self.check_signatures(&consensus_params.chain_id())?;
256
257        Ok(())
258    }
259
260    /// Validates that all required signatures are set in the transaction and that they
261    /// are valid.
262    fn check_signatures(&self, chain_id: &ChainId) -> Result<(), ValidityError>;
263
264    /// Validates the transactions according to rules from the specification:
265    /// <https://github.com/FuelLabs/fuel-specs/blob/master/src/tx-format/transaction.md>
266    fn check_without_signatures(
267        &self,
268        block_height: BlockHeight,
269        consensus_params: &ConsensusParameters,
270    ) -> Result<(), ValidityError>;
271}
272
273impl FormatValidityChecks for Transaction {
274    fn check_signatures(&self, chain_id: &ChainId) -> Result<(), ValidityError> {
275        match self {
276            Self::Script(tx) => tx.check_signatures(chain_id),
277            Self::Create(tx) => tx.check_signatures(chain_id),
278            Self::Mint(tx) => tx.check_signatures(chain_id),
279            Self::Upgrade(tx) => tx.check_signatures(chain_id),
280            Self::Upload(tx) => tx.check_signatures(chain_id),
281            Self::Blob(tx) => tx.check_signatures(chain_id),
282        }
283    }
284
285    fn check_without_signatures(
286        &self,
287        block_height: BlockHeight,
288        consensus_params: &ConsensusParameters,
289    ) -> Result<(), ValidityError> {
290        match self {
291            Self::Script(tx) => {
292                tx.check_without_signatures(block_height, consensus_params)
293            }
294            Self::Create(tx) => {
295                tx.check_without_signatures(block_height, consensus_params)
296            }
297            Self::Mint(tx) => tx.check_without_signatures(block_height, consensus_params),
298            Self::Upgrade(tx) => {
299                tx.check_without_signatures(block_height, consensus_params)
300            }
301            Self::Upload(tx) => {
302                tx.check_without_signatures(block_height, consensus_params)
303            }
304            Self::Blob(tx) => tx.check_without_signatures(block_height, consensus_params),
305        }
306    }
307}
308
309/// Validates the size of the transaction in bytes. Transactions cannot exceed
310/// the total size specified by the transaction parameters. The size of a
311/// transaction is calculated as the sum of the sizes of its static and dynamic
312/// parts.
313pub(crate) fn check_size<T>(tx: &T, tx_params: &TxParameters) -> Result<(), ValidityError>
314where
315    T: canonical::Serialize,
316{
317    if tx.size() as u64 > tx_params.max_size() {
318        Err(ValidityError::TransactionSizeLimitExceeded)?;
319    }
320
321    Ok(())
322}
323
324pub(crate) fn check_common_part<T>(
325    tx: &T,
326    block_height: BlockHeight,
327    consensus_params: &ConsensusParameters,
328) -> Result<(), ValidityError>
329where
330    T: canonical::Serialize + Chargeable + field::Outputs,
331{
332    let tx_params = consensus_params.tx_params();
333    let predicate_params = consensus_params.predicate_params();
334    let base_asset_id = consensus_params.base_asset_id();
335    let gas_costs = consensus_params.gas_costs();
336    let fee_params = consensus_params.fee_params();
337
338    check_size(tx, tx_params)?;
339
340    if !tx.policies().is_valid() {
341        Err(ValidityError::TransactionPoliciesAreInvalid)?
342    }
343
344    if let Some(witness_limit) = tx.policies().get(PolicyType::WitnessLimit) {
345        let witness_size = tx.witnesses().size_dynamic();
346        if witness_size as u64 > witness_limit {
347            Err(ValidityError::TransactionWitnessLimitExceeded)?
348        }
349    }
350
351    let max_gas = tx.max_gas(gas_costs, fee_params);
352    if max_gas > tx_params.max_gas_per_tx() {
353        Err(ValidityError::TransactionMaxGasExceeded)?
354    }
355
356    if !tx.policies().is_set(PolicyType::MaxFee) {
357        Err(ValidityError::TransactionMaxFeeNotSet)?
358    };
359
360    if tx.maturity() > block_height {
361        Err(ValidityError::TransactionMaturity)?;
362    }
363
364    if tx.expiration() < block_height {
365        Err(ValidityError::TransactionExpiration)?;
366    }
367
368    if tx.inputs().len() > tx_params.max_inputs() as usize {
369        Err(ValidityError::TransactionInputsMax)?
370    }
371
372    if tx.outputs().len() > tx_params.max_outputs() as usize {
373        Err(ValidityError::TransactionOutputsMax)?
374    }
375
376    if tx.witnesses().len() > tx_params.max_witnesses() as usize {
377        Err(ValidityError::TransactionWitnessesMax)?
378    }
379
380    let any_spendable_input = tx.inputs().iter().find(|input| match input {
381        Input::CoinSigned(_)
382        | Input::CoinPredicate(_)
383        | Input::MessageCoinSigned(_)
384        | Input::MessageCoinPredicate(_) => true,
385        Input::MessageDataSigned(_)
386        | Input::MessageDataPredicate(_)
387        | Input::Contract(_) => false,
388    });
389
390    if any_spendable_input.is_none() {
391        Err(ValidityError::NoSpendableInput)?
392    }
393
394    tx.input_asset_ids_unique(base_asset_id)
395        .try_for_each(|input_asset_id| {
396            // check for duplicate change outputs
397            if tx
398                .outputs()
399                .iter()
400                .filter_map(|output| match output {
401                    Output::Change { asset_id, .. } if input_asset_id == asset_id => {
402                        Some(())
403                    }
404                    _ => None,
405                })
406                .count()
407                > 1
408            {
409                return Err(ValidityError::TransactionOutputChangeAssetIdDuplicated(
410                    *input_asset_id,
411                ));
412            }
413
414            Ok(())
415        })?;
416
417    // Check for duplicated input utxo id
418    let duplicated_utxo_id = tx
419        .inputs()
420        .iter()
421        .filter_map(|i| i.is_coin().then(|| i.utxo_id()).flatten());
422
423    if let Some(utxo_id) = next_duplicate(duplicated_utxo_id).copied() {
424        return Err(ValidityError::DuplicateInputUtxoId { utxo_id });
425    }
426
427    // Check for duplicated input contract id
428    let duplicated_contract_id = tx.inputs().iter().filter_map(Input::contract_id);
429
430    if let Some(contract_id) = next_duplicate(duplicated_contract_id).copied() {
431        return Err(ValidityError::DuplicateInputContractId { contract_id });
432    }
433
434    // Check for duplicated input message id
435    let duplicated_message_id = tx.inputs().iter().filter_map(Input::message_id);
436    if let Some(message_id) = next_duplicate(duplicated_message_id) {
437        return Err(ValidityError::DuplicateMessageInputId { message_id });
438    }
439
440    // Validate the inputs without checking signature
441    tx.inputs()
442        .iter()
443        .enumerate()
444        .try_for_each(|(index, input)| {
445            input.check_without_signature(
446                index,
447                tx.outputs(),
448                tx.witnesses(),
449                predicate_params,
450            )
451        })?;
452
453    tx.outputs()
454        .iter()
455        .enumerate()
456        .try_for_each(|(index, output)| {
457            output.check(index, tx.inputs())?;
458
459            if let Output::Change { asset_id, .. } = output {
460                if !tx
461                    .input_asset_ids(base_asset_id)
462                    .any(|input_asset_id| input_asset_id == asset_id)
463                {
464                    return Err(ValidityError::TransactionOutputChangeAssetIdNotFound(
465                        *asset_id,
466                    ));
467                }
468            }
469
470            if let Output::Coin { asset_id, .. } = output {
471                if !tx
472                    .input_asset_ids(base_asset_id)
473                    .any(|input_asset_id| input_asset_id == asset_id)
474                {
475                    return Err(ValidityError::TransactionOutputCoinAssetIdNotFound(
476                        *asset_id,
477                    ));
478                }
479            }
480
481            Ok(())
482        })?;
483
484    Ok(())
485}
486
487// TODO https://github.com/FuelLabs/fuel-tx/issues/148
488pub(crate) fn next_duplicate<U>(iter: impl Iterator<Item = U>) -> Option<U>
489where
490    U: PartialEq + Ord + Copy + Hash,
491{
492    #[cfg(not(feature = "std"))]
493    {
494        iter.sorted()
495            .as_slice()
496            .windows(2)
497            .filter_map(|u| (u[0] == u[1]).then(|| u[0]))
498            .next()
499    }
500
501    #[cfg(feature = "std")]
502    {
503        iter.duplicates().next()
504    }
505}
506
507#[cfg(feature = "typescript")]
508mod typescript {
509    use crate::{
510        transaction::consensus_parameters::typescript::PredicateParameters,
511        Witness,
512    };
513    use fuel_types::Bytes32;
514    use wasm_bindgen::JsValue;
515
516    use alloc::{
517        format,
518        vec::Vec,
519    };
520
521    use crate::transaction::{
522        input_ts::Input,
523        output_ts::Output,
524    };
525
526    #[wasm_bindgen::prelude::wasm_bindgen]
527    pub fn check_input(
528        input: &Input,
529        index: usize,
530        txhash: &Bytes32,
531        outputs: Vec<JsValue>,
532        witnesses: Vec<JsValue>,
533        predicate_params: &PredicateParameters,
534    ) -> Result<(), js_sys::Error> {
535        let outputs: Vec<crate::Output> = outputs
536            .into_iter()
537            .map(|v| serde_wasm_bindgen::from_value::<Output>(v).map(|v| *v.0))
538            .collect::<Result<Vec<_>, _>>()
539            .map_err(|e| js_sys::Error::new(&format!("{:?}", e)))?;
540
541        let witnesses: Vec<Witness> = witnesses
542            .into_iter()
543            .map(serde_wasm_bindgen::from_value::<Witness>)
544            .collect::<Result<Vec<_>, _>>()
545            .map_err(|e| js_sys::Error::new(&format!("{:?}", e)))?;
546
547        input
548            .0
549            .check(
550                index,
551                txhash,
552                &outputs,
553                &witnesses,
554                predicate_params.as_ref(),
555                &mut None,
556            )
557            .map_err(|e| js_sys::Error::new(&format!("{:?}", e)))
558    }
559
560    #[wasm_bindgen::prelude::wasm_bindgen]
561    pub fn check_output(
562        output: &Output,
563        index: usize,
564        inputs: Vec<JsValue>,
565    ) -> Result<(), js_sys::Error> {
566        let inputs: Vec<crate::Input> = inputs
567            .into_iter()
568            .map(|v| serde_wasm_bindgen::from_value::<Input>(v).map(|v| *v.0))
569            .collect::<Result<Vec<_>, _>>()
570            .map_err(|e| js_sys::Error::new(&format!("{:?}", e)))?;
571
572        output
573            .0
574            .check(index, &inputs)
575            .map_err(|e| js_sys::Error::new(&format!("{:?}", e)))
576    }
577}