fuel_tx/transaction/types/
create.rs

1use crate::{
2    transaction::{
3        field::{
4            BytecodeWitnessIndex,
5            Salt as SaltField,
6            StorageSlots,
7        },
8        metadata::CommonMetadata,
9        types::chargeable_transaction::{
10            ChargeableMetadata,
11            ChargeableTransaction,
12            UniqueFormatValidityChecks,
13        },
14    },
15    Chargeable,
16    ConsensusParameters,
17    Contract,
18    GasCosts,
19    Input,
20    Output,
21    PrepareSign,
22    StorageSlot,
23    TransactionRepr,
24    ValidityError,
25};
26use educe::Educe;
27use fuel_types::{
28    bytes::WORD_SIZE,
29    canonical,
30    Bytes32,
31    Bytes4,
32    ChainId,
33    ContractId,
34    Salt,
35    Word,
36};
37
38#[cfg(feature = "alloc")]
39use alloc::vec::Vec;
40
41#[cfg(all(test, feature = "std"))]
42mod ser_de_tests;
43
44pub type Create = ChargeableTransaction<CreateBody, CreateMetadata>;
45
46#[derive(Default, Debug, Clone, Educe)]
47#[educe(Eq, PartialEq, Hash)]
48pub struct CreateMetadata {
49    pub contract_id: ContractId,
50    pub contract_root: Bytes32,
51    pub state_root: Bytes32,
52}
53
54impl CreateMetadata {
55    /// Computes the `Metadata` for the `tx` transaction.
56    pub fn compute(tx: &Create) -> Result<Self, ValidityError> {
57        let salt = tx.salt();
58        let storage_slots = tx.storage_slots();
59        let contract = Contract::try_from(tx)?;
60        let contract_root = contract.root();
61        let state_root = Contract::initial_state_root(storage_slots.iter());
62        let contract_id = contract.id(salt, &contract_root, &state_root);
63
64        Ok(Self {
65            contract_id,
66            contract_root,
67            state_root,
68        })
69    }
70}
71
72#[derive(Default, Debug, Clone, Educe, serde::Serialize, serde::Deserialize)]
73#[cfg_attr(
74    feature = "da-compression",
75    derive(fuel_compression::Compress, fuel_compression::Decompress)
76)]
77#[derive(fuel_types::canonical::Deserialize, fuel_types::canonical::Serialize)]
78#[canonical(prefix = TransactionRepr::Create)]
79#[educe(Eq, PartialEq, Hash)]
80pub struct CreateBody {
81    pub(crate) bytecode_witness_index: u16,
82    pub(crate) salt: Salt,
83    pub(crate) storage_slots: Vec<StorageSlot>,
84}
85
86impl PrepareSign for CreateBody {
87    fn prepare_sign(&mut self) {}
88}
89
90impl Chargeable for Create {
91    #[inline(always)]
92    fn metered_bytes_size(&self) -> usize {
93        canonical::Serialize::size(self)
94    }
95
96    fn gas_used_by_metadata(&self, gas_costs: &GasCosts) -> Word {
97        let Create {
98            body:
99                CreateBody {
100                    bytecode_witness_index,
101                    storage_slots,
102                    ..
103                },
104            witnesses,
105            ..
106        } = self;
107
108        let contract_len = witnesses
109            .get(*bytecode_witness_index as usize)
110            .map(|c| c.as_ref().len())
111            .unwrap_or(0);
112
113        let contract_root_gas = gas_costs.contract_root().resolve(contract_len as Word);
114        let state_root_length = storage_slots.len() as Word;
115        let state_root_gas = gas_costs.state_root().resolve(state_root_length);
116
117        // See https://github.com/FuelLabs/fuel-specs/blob/master/src/identifiers/contract-id.md
118        let contract_id_input_length =
119            Bytes4::LEN + Salt::LEN + Bytes32::LEN + Bytes32::LEN;
120        let contract_id_gas = gas_costs.s256().resolve(contract_id_input_length as Word);
121        let bytes = canonical::Serialize::size(self);
122        // Gas required to calculate the `tx_id`.
123        let tx_id_gas = gas_costs.s256().resolve(bytes as u64);
124
125        contract_root_gas
126            .saturating_add(state_root_gas)
127            .saturating_add(contract_id_gas)
128            .saturating_add(tx_id_gas)
129    }
130}
131
132impl UniqueFormatValidityChecks for Create {
133    fn check_unique_rules(
134        &self,
135        consensus_params: &ConsensusParameters,
136    ) -> Result<(), ValidityError> {
137        let contract_params = consensus_params.contract_params();
138        let base_asset_id = consensus_params.base_asset_id();
139
140        let bytecode_witness_len = self
141            .witnesses
142            .get(self.body.bytecode_witness_index as usize)
143            .map(|w| w.as_ref().len() as Word)
144            .ok_or(ValidityError::TransactionCreateBytecodeWitnessIndex)?;
145
146        if bytecode_witness_len > contract_params.contract_max_size() {
147            return Err(ValidityError::TransactionCreateBytecodeLen);
148        }
149
150        // Restrict to subset of u16::MAX, allowing this to be increased in the future
151        // in a non-breaking way.
152        if self.body.storage_slots.len() as u64 > contract_params.max_storage_slots() {
153            return Err(ValidityError::TransactionCreateStorageSlotMax);
154        }
155
156        // Verify storage slots are sorted
157        if !self
158            .body
159            .storage_slots
160            .as_slice()
161            .windows(2)
162            .all(|s| s[0] < s[1])
163        {
164            return Err(ValidityError::TransactionCreateStorageSlotOrder);
165        }
166
167        self.inputs
168            .iter()
169            .enumerate()
170            .try_for_each(|(index, input)| {
171                if let Some(asset_id) = input.asset_id(consensus_params.base_asset_id()) {
172                    if asset_id != consensus_params.base_asset_id() {
173                        return Err(
174                            ValidityError::TransactionInputContainsNonBaseAssetId {
175                                index,
176                            },
177                        );
178                    }
179                }
180
181                match input {
182                    Input::Contract(_) => {
183                        Err(ValidityError::TransactionInputContainsContract { index })
184                    }
185                    Input::MessageDataSigned(_) | Input::MessageDataPredicate(_) => {
186                        Err(ValidityError::TransactionInputContainsMessageData { index })
187                    }
188                    _ => Ok(()),
189                }
190            })?;
191
192        debug_assert!(
193            self.metadata.is_some(),
194            "`check_without_signatures` is called without cached metadata"
195        );
196        let (state_root_calculated, contract_id_calculated) =
197            if let Some(metadata) = &self.metadata {
198                (metadata.body.state_root, metadata.body.contract_id)
199            } else {
200                let metadata = CreateMetadata::compute(self)?;
201                (metadata.state_root, metadata.contract_id)
202            };
203
204        let mut contract_created = false;
205        self.outputs
206            .iter()
207            .enumerate()
208            .try_for_each(|(index, output)| match output {
209                Output::Contract(_) => {
210                    Err(ValidityError::TransactionOutputContainsContract { index })
211                }
212
213                Output::Variable { .. } => {
214                    Err(ValidityError::TransactionOutputContainsVariable { index })
215                }
216
217                Output::Change { asset_id, .. } if asset_id != base_asset_id => {
218                    Err(ValidityError::TransactionChangeChangeUsesNotBaseAsset { index })
219                }
220
221                Output::ContractCreated {
222                    contract_id,
223                    state_root,
224                } if contract_id != &contract_id_calculated
225                    || state_root != &state_root_calculated =>
226                    {
227                        Err(
228                            ValidityError::TransactionCreateOutputContractCreatedDoesntMatch {
229                                index,
230                            },
231                        )
232                    }
233
234                // TODO: Output::ContractCreated { contract_id, state_root } if
235                // contract_id == &id && state_root == &storage_root
236                //  maybe move from `fuel-vm` to here
237                Output::ContractCreated { .. } if contract_created => {
238                    Err(ValidityError::TransactionCreateOutputContractCreatedMultiple {
239                        index,
240                    })
241                }
242
243                Output::ContractCreated { .. } => {
244                    contract_created = true;
245
246                    Ok(())
247                }
248
249                _ => Ok(()),
250            })?;
251
252        if !contract_created {
253            return Err(ValidityError::TransactionOutputDoesntContainContractCreated);
254        }
255
256        Ok(())
257    }
258}
259
260impl crate::Cacheable for Create {
261    fn is_computed(&self) -> bool {
262        self.metadata.is_some()
263    }
264
265    fn precompute(&mut self, chain_id: &ChainId) -> Result<(), ValidityError> {
266        self.metadata = None;
267        self.metadata = Some(ChargeableMetadata {
268            common: CommonMetadata::compute(self, chain_id)?,
269            body: CreateMetadata::compute(self)?,
270        });
271        Ok(())
272    }
273}
274
275mod field {
276    use super::*;
277    use crate::field::{
278        ChargeableBody,
279        StorageSlotRef,
280    };
281
282    impl BytecodeWitnessIndex for Create {
283        #[inline(always)]
284        fn bytecode_witness_index(&self) -> &u16 {
285            &self.body.bytecode_witness_index
286        }
287
288        #[inline(always)]
289        fn bytecode_witness_index_mut(&mut self) -> &mut u16 {
290            &mut self.body.bytecode_witness_index
291        }
292
293        #[inline(always)]
294        fn bytecode_witness_index_offset_static() -> usize {
295            WORD_SIZE // `Transaction` enum discriminant
296        }
297    }
298
299    impl SaltField for Create {
300        #[inline(always)]
301        fn salt(&self) -> &Salt {
302            &self.body.salt
303        }
304
305        #[inline(always)]
306        fn salt_mut(&mut self) -> &mut Salt {
307            &mut self.body.salt
308        }
309
310        #[inline(always)]
311        fn salt_offset_static() -> usize {
312            Self::bytecode_witness_index_offset_static().saturating_add(WORD_SIZE)
313        }
314    }
315
316    impl StorageSlots for Create {
317        #[inline(always)]
318        fn storage_slots(&self) -> &Vec<StorageSlot> {
319            &self.body.storage_slots
320        }
321
322        #[inline(always)]
323        fn storage_slots_mut(&mut self) -> StorageSlotRef {
324            StorageSlotRef {
325                storage_slots: &mut self.body.storage_slots,
326            }
327        }
328
329        #[inline(always)]
330        fn storage_slots_offset_static() -> usize {
331            Self::salt_offset_static().saturating_add(
332                Salt::LEN
333                + WORD_SIZE // Storage slots size
334                + WORD_SIZE // Policies size
335                + WORD_SIZE // Inputs size
336                + WORD_SIZE // Outputs size
337                + WORD_SIZE, // Witnesses size
338            )
339        }
340
341        fn storage_slots_offset_at(&self, idx: usize) -> Option<usize> {
342            if idx < self.body.storage_slots.len() {
343                Some(
344                    Self::storage_slots_offset_static()
345                        .checked_add(idx.checked_mul(StorageSlot::SLOT_SIZE)?)?,
346                )
347            } else {
348                None
349            }
350        }
351    }
352
353    impl ChargeableBody<CreateBody> for Create {
354        fn body(&self) -> &CreateBody {
355            &self.body
356        }
357
358        fn body_mut(&mut self) -> &mut CreateBody {
359            &mut self.body
360        }
361
362        fn body_offset_end(&self) -> usize {
363            Self::storage_slots_offset_static().saturating_add(
364                self.body
365                    .storage_slots
366                    .len()
367                    .saturating_mul(StorageSlot::SLOT_SIZE),
368            )
369        }
370    }
371}
372
373impl TryFrom<&Create> for Contract {
374    type Error = ValidityError;
375
376    fn try_from(tx: &Create) -> Result<Self, Self::Error> {
377        let Create {
378            body:
379                CreateBody {
380                    bytecode_witness_index,
381                    ..
382                },
383            witnesses,
384            ..
385        } = tx;
386
387        witnesses
388            .get(*bytecode_witness_index as usize)
389            .map(|c| c.as_ref().into())
390            .ok_or(ValidityError::TransactionCreateBytecodeWitnessIndex)
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397    use crate::{
398        builder::Finalizable,
399        transaction::validity::FormatValidityChecks,
400    };
401    use fuel_types::Bytes32;
402
403    #[test]
404    fn storage_slots_sorting() {
405        // Test that storage slots must be sorted correctly
406        let mut slot_data = [0u8; 64];
407
408        let storage_slots = (0..10u64)
409            .map(|i| {
410                slot_data[..8].copy_from_slice(&i.to_be_bytes());
411                StorageSlot::from(&slot_data.into())
412            })
413            .collect::<Vec<StorageSlot>>();
414
415        let mut tx = crate::TransactionBuilder::create(
416            vec![].into(),
417            Salt::zeroed(),
418            storage_slots,
419        )
420        .add_fee_input()
421        .finalize();
422        tx.body.storage_slots.reverse();
423
424        let err = tx
425            .check(0.into(), &ConsensusParameters::standard())
426            .expect_err("Expected erroneous transaction");
427
428        assert_eq!(ValidityError::TransactionCreateStorageSlotOrder, err);
429    }
430
431    #[test]
432    fn storage_slots_no_duplicates() {
433        let storage_slots = vec![
434            StorageSlot::new(Bytes32::zeroed(), Bytes32::zeroed()),
435            StorageSlot::new(Bytes32::zeroed(), Bytes32::zeroed()),
436        ];
437
438        let err = crate::TransactionBuilder::create(
439            vec![].into(),
440            Salt::zeroed(),
441            storage_slots,
442        )
443        .add_fee_input()
444        .finalize()
445        .check(0.into(), &ConsensusParameters::standard())
446        .expect_err("Expected erroneous transaction");
447
448        assert_eq!(ValidityError::TransactionCreateStorageSlotOrder, err);
449    }
450}