fuel_tx/transaction/
validity.rs

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