fuel_tx/transaction/types/
upgrade.rs

1use crate::{
2    transaction::{
3        id::PrepareSign,
4        metadata::CommonMetadata,
5        types::chargeable_transaction::{
6            ChargeableMetadata,
7            ChargeableTransaction,
8            UniqueFormatValidityChecks,
9        },
10        Chargeable,
11    },
12    ConsensusParameters,
13    GasCosts,
14    Input,
15    Output,
16    TransactionRepr,
17    ValidityError,
18};
19use educe::Educe;
20use fuel_types::{
21    bytes::WORD_SIZE,
22    canonical::Serialize,
23    Bytes32,
24    ChainId,
25    Word,
26};
27
28use fuel_crypto::Hasher;
29
30#[cfg(feature = "alloc")]
31use alloc::boxed::Box;
32
33pub type Upgrade = ChargeableTransaction<UpgradeBody, UpgradeMetadata>;
34
35#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
36pub enum UpgradeMetadata {
37    /// The metadata for the upgrade transaction that changes the consensus parameters.
38    ConsensusParameters {
39        /// Deserialized consensus parameters from the witness.
40        consensus_parameters: Box<ConsensusParameters>,
41        /// The actual checksum of the serialized consensus parameters.
42        calculated_checksum: Bytes32,
43    },
44    /// Currently there is no metadata for state transition upgrades, so leave it empty.
45    #[default]
46    StateTransition,
47}
48
49impl UpgradeMetadata {
50    pub fn compute(tx: &Upgrade) -> Result<Self, ValidityError> {
51        match &tx.body.purpose {
52            UpgradePurpose::ConsensusParameters {
53                witness_index,
54                checksum,
55            } => {
56                let index = *witness_index as usize;
57                let witness = tx
58                    .witnesses
59                    .get(index)
60                    .ok_or(ValidityError::InputWitnessIndexBounds { index })?;
61
62                let serialized_consensus_parameters = witness.as_vec();
63                let actual_checksum = Hasher::hash(serialized_consensus_parameters);
64
65                if &actual_checksum != checksum {
66                    Err(ValidityError::TransactionUpgradeConsensusParametersChecksumMismatch)?;
67                }
68
69                // The code that creates/verifies the `Upgrade` transaction should always
70                // be able to decode the current consensus parameters
71                // type. The state transition function should always know
72                // how to decode consensus parameters. Otherwise, the next
73                // block will be impossible to produce. If deserialization fails, it is a
74                // sign that the code/state transition function should be updated.
75                let consensus_parameters = postcard::from_bytes::<ConsensusParameters>(
76                    serialized_consensus_parameters,
77                )
78                .map_err(|_| {
79                    ValidityError::TransactionUpgradeConsensusParametersDeserialization
80                })?;
81
82                Ok(Self::ConsensusParameters {
83                    consensus_parameters: Box::new(consensus_parameters),
84                    calculated_checksum: actual_checksum,
85                })
86            }
87            UpgradePurpose::StateTransition { .. } => {
88                // Nothing metadata for state transition upgrades.
89                Ok(Self::StateTransition)
90            }
91        }
92    }
93}
94
95/// The types describe the purpose of the upgrade performed by the [`Upgrade`]
96/// transaction.
97#[derive(
98    Copy, Clone, Educe, strum_macros::EnumCount, serde::Serialize, serde::Deserialize,
99)]
100#[cfg_attr(
101    feature = "da-compression",
102    derive(fuel_compression::Compress, fuel_compression::Decompress)
103)]
104#[derive(fuel_types::canonical::Deserialize, fuel_types::canonical::Serialize)]
105#[educe(Eq, PartialEq, Hash, Debug)]
106pub enum UpgradePurpose {
107    /// The upgrade is performed to change the consensus parameters.
108    ConsensusParameters {
109        /// The index of the witness in the [`Witnesses`] field that contains
110        /// the serialized consensus parameters.
111        witness_index: u16,
112        /// The hash of the serialized consensus parameters.
113        /// Since the serialized consensus parameters live inside witnesses(malleable
114        /// data), any party can override them. The `checksum` is used to verify that the
115        /// data was not modified.
116        checksum: Bytes32,
117    },
118    /// The upgrade is performed to change the state transition function.
119    StateTransition {
120        /// The Merkle root of the new bytecode of the state transition function.
121        /// The bytecode must be present on the blockchain(should be known by the
122        /// network) at the moment of inclusion of this transaction.
123        root: Bytes32,
124    },
125}
126
127/// The body of the [`Upgrade`] transaction.
128#[derive(Clone, Educe, serde::Serialize, serde::Deserialize)]
129#[cfg_attr(
130    feature = "da-compression",
131    derive(fuel_compression::Compress, fuel_compression::Decompress)
132)]
133#[derive(fuel_types::canonical::Deserialize, fuel_types::canonical::Serialize)]
134#[canonical(prefix = TransactionRepr::Upgrade)]
135#[educe(Eq, PartialEq, Hash, Debug)]
136pub struct UpgradeBody {
137    /// The purpose of the upgrade.
138    pub(crate) purpose: UpgradePurpose,
139}
140
141impl Default for UpgradeBody {
142    fn default() -> Self {
143        Self {
144            purpose: UpgradePurpose::StateTransition {
145                root: Default::default(),
146            },
147        }
148    }
149}
150
151impl PrepareSign for UpgradeBody {
152    fn prepare_sign(&mut self) {}
153}
154
155impl Chargeable for Upgrade {
156    #[inline(always)]
157    fn metered_bytes_size(&self) -> usize {
158        Serialize::size(self)
159    }
160
161    #[inline(always)]
162    fn gas_used_by_metadata(&self, gas_cost: &GasCosts) -> Word {
163        let bytes = Serialize::size(self);
164        // Gas required to calculate the `tx_id`.
165        let tx_id_gas = gas_cost.s256().resolve(bytes as u64);
166
167        let purpose_gas = match &self.body.purpose {
168            UpgradePurpose::ConsensusParameters { witness_index, .. } => {
169                let len = self
170                    .witnesses
171                    .get(*witness_index as usize)
172                    .map_or(0, |w| w.as_vec().len());
173                gas_cost.s256().resolve(len as u64)
174            }
175            UpgradePurpose::StateTransition { .. } => {
176                // In the case of the state transition upgrade, we only require the
177                // existence of the bytecode on the blockchain. So we
178                // verify nothing and charge nothing.
179                0
180            }
181        };
182
183        tx_id_gas.saturating_add(purpose_gas)
184    }
185}
186
187impl UniqueFormatValidityChecks for Upgrade {
188    fn check_unique_rules(
189        &self,
190        consensus_params: &ConsensusParameters,
191    ) -> Result<(), ValidityError> {
192        // At least one of inputs must be owned by the privileged address.
193        self.inputs
194            .iter()
195            .find(|input| {
196                if let Some(owner) = input.input_owner() {
197                    owner == consensus_params.privileged_address()
198                } else {
199                    false
200                }
201            })
202            .ok_or(ValidityError::TransactionUpgradeNoPrivilegedAddress)?;
203
204        // We verify validity of the `UpgradePurpose` in the
205        // `UpgradeMetadata::compute`.
206        let calculated_metadata = UpgradeMetadata::compute(self)?;
207
208        if let Some(metadata) = self.metadata.as_ref() {
209            if metadata.body != calculated_metadata {
210                return Err(ValidityError::TransactionMetadataMismatch);
211            }
212        }
213
214        // The upgrade transaction cant touch the contract.
215        self.inputs
216            .iter()
217            .enumerate()
218            .try_for_each(|(index, input)| {
219                if let Some(asset_id) = input.asset_id(consensus_params.base_asset_id()) {
220                    if asset_id != consensus_params.base_asset_id() {
221                        return Err(
222                            ValidityError::TransactionInputContainsNonBaseAssetId {
223                                index,
224                            },
225                        );
226                    }
227                }
228
229                match input {
230                    Input::Contract(_) => {
231                        Err(ValidityError::TransactionInputContainsContract { index })
232                    }
233                    Input::MessageDataSigned(_) | Input::MessageDataPredicate(_) => {
234                        Err(ValidityError::TransactionInputContainsMessageData { index })
235                    }
236                    _ => Ok(()),
237                }
238            })?;
239
240        // The upgrade transaction can't create a contract.
241        self.outputs
242            .iter()
243            .enumerate()
244            .try_for_each(|(index, output)| match output {
245                Output::Contract(_) => {
246                    Err(ValidityError::TransactionOutputContainsContract { index })
247                }
248
249                Output::Variable { .. } => {
250                    Err(ValidityError::TransactionOutputContainsVariable { index })
251                }
252
253                Output::Change { asset_id, .. }
254                    if asset_id != consensus_params.base_asset_id() =>
255                {
256                    Err(ValidityError::TransactionChangeChangeUsesNotBaseAsset { index })
257                }
258
259                Output::ContractCreated { .. } => {
260                    Err(ValidityError::TransactionOutputContainsContractCreated { index })
261                }
262                _ => Ok(()),
263            })?;
264
265        Ok(())
266    }
267}
268
269impl crate::Cacheable for Upgrade {
270    fn is_computed(&self) -> bool {
271        self.metadata.is_some()
272    }
273
274    fn precompute(&mut self, chain_id: &ChainId) -> Result<(), ValidityError> {
275        self.metadata = None;
276        self.metadata = Some(ChargeableMetadata {
277            common: CommonMetadata::compute(self, chain_id)?,
278            body: UpgradeMetadata::compute(self)?,
279        });
280        Ok(())
281    }
282}
283
284mod field {
285    use super::*;
286    use crate::field::{
287        ChargeableBody,
288        UpgradePurpose as UpgradePurposeTrait,
289    };
290
291    impl UpgradePurposeTrait for Upgrade {
292        #[inline(always)]
293        fn upgrade_purpose(&self) -> &UpgradePurpose {
294            &self.body.purpose
295        }
296
297        #[inline(always)]
298        fn upgrade_purpose_mut(&mut self) -> &mut UpgradePurpose {
299            &mut self.body.purpose
300        }
301
302        #[inline(always)]
303        fn upgrade_purpose_offset_static() -> usize {
304            WORD_SIZE // `Transaction` enum discriminant
305        }
306    }
307
308    impl ChargeableBody<UpgradeBody> for Upgrade {
309        fn body(&self) -> &UpgradeBody {
310            &self.body
311        }
312
313        fn body_mut(&mut self) -> &mut UpgradeBody {
314            &mut self.body
315        }
316
317        fn body_offset_end(&self) -> usize {
318            Self::upgrade_purpose_offset_static()
319                .saturating_add(self.body.purpose.size())
320                .saturating_add(
321                    WORD_SIZE  // Policies size
322                    + WORD_SIZE // Inputs size
323                    + WORD_SIZE // Outputs size
324                    + WORD_SIZE, // Witnesses size
325                )
326        }
327    }
328}