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 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 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 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 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 _ => Ok(()),
218 }
219 }
220}
221
222impl Output {
223 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
243pub trait FormatValidityChecks {
247 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 fn check_signatures(&self, chain_id: &ChainId) -> Result<(), ValidityError>;
263
264 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
309pub(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 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 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 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 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 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
487pub(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}