fuel_tx/transaction/types/
create.rs

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