use crate::{
ConsensusParameters,
GasCosts,
Input,
Output,
TransactionRepr,
ValidityError,
transaction::{
Chargeable,
id::PrepareSign,
metadata::CommonMetadata,
types::chargeable_transaction::{
ChargeableMetadata,
ChargeableTransaction,
UniqueFormatValidityChecks,
},
},
};
use educe::Educe;
use fuel_types::{
Bytes32,
ChainId,
Word,
bytes::WORD_SIZE,
canonical::Serialize,
};
use fuel_crypto::Hasher;
#[cfg(feature = "alloc")]
use alloc::boxed::Box;
pub type Upgrade = ChargeableTransaction<UpgradeBody, UpgradeMetadata>;
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
pub enum UpgradeMetadata {
ConsensusParameters {
consensus_parameters: Box<ConsensusParameters>,
calculated_checksum: Bytes32,
},
#[default]
StateTransition,
}
impl UpgradeMetadata {
pub fn compute(tx: &Upgrade) -> Result<Self, ValidityError> {
match &tx.body.purpose {
UpgradePurpose::ConsensusParameters {
witness_index,
checksum,
} => {
let index = *witness_index as usize;
let witness = tx
.witnesses
.get(index)
.ok_or(ValidityError::InputWitnessIndexBounds { index })?;
let serialized_consensus_parameters = witness.as_vec();
let actual_checksum = Hasher::hash(serialized_consensus_parameters);
if &actual_checksum != checksum {
Err(ValidityError::TransactionUpgradeConsensusParametersChecksumMismatch)?;
}
let consensus_parameters = postcard::from_bytes::<ConsensusParameters>(
serialized_consensus_parameters,
)
.map_err(|_| {
ValidityError::TransactionUpgradeConsensusParametersDeserialization
})?;
Ok(Self::ConsensusParameters {
consensus_parameters: Box::new(consensus_parameters),
calculated_checksum: actual_checksum,
})
}
UpgradePurpose::StateTransition { .. } => {
Ok(Self::StateTransition)
}
}
}
}
#[derive(
Copy, Clone, Educe, strum_macros::EnumCount, serde::Serialize, serde::Deserialize,
)]
#[cfg_attr(
feature = "da-compression",
derive(fuel_compression::Compress, fuel_compression::Decompress)
)]
#[derive(fuel_types::canonical::Deserialize, fuel_types::canonical::Serialize)]
#[educe(Eq, PartialEq, Hash, Debug)]
pub enum UpgradePurpose {
ConsensusParameters {
witness_index: u16,
checksum: Bytes32,
},
StateTransition {
root: Bytes32,
},
}
#[derive(Clone, Educe, serde::Serialize, serde::Deserialize)]
#[cfg_attr(
feature = "da-compression",
derive(fuel_compression::Compress, fuel_compression::Decompress)
)]
#[derive(fuel_types::canonical::Deserialize, fuel_types::canonical::Serialize)]
#[canonical(prefix = TransactionRepr::Upgrade)]
#[educe(Eq, PartialEq, Hash, Debug)]
pub struct UpgradeBody {
pub(crate) purpose: UpgradePurpose,
}
impl Default for UpgradeBody {
fn default() -> Self {
Self {
purpose: UpgradePurpose::StateTransition {
root: Default::default(),
},
}
}
}
impl PrepareSign for UpgradeBody {
fn prepare_sign(&mut self) {}
}
impl Chargeable for Upgrade {
#[inline(always)]
fn metered_bytes_size(&self) -> usize {
Serialize::size(self)
}
#[inline(always)]
fn gas_used_by_metadata(&self, gas_cost: &GasCosts) -> Word {
let bytes = Serialize::size(self);
let tx_id_gas = gas_cost.s256().resolve(bytes as u64);
let purpose_gas = match &self.body.purpose {
UpgradePurpose::ConsensusParameters { witness_index, .. } => {
let len = self
.witnesses
.get(*witness_index as usize)
.map_or(0, |w| w.as_vec().len());
gas_cost.s256().resolve(len as u64)
}
UpgradePurpose::StateTransition { .. } => {
0
}
};
tx_id_gas.saturating_add(purpose_gas)
}
}
impl UniqueFormatValidityChecks for Upgrade {
fn check_unique_rules(
&self,
consensus_params: &ConsensusParameters,
) -> Result<(), ValidityError> {
self.inputs
.iter()
.find(|input| {
if let Some(owner) = input.input_owner() {
owner == consensus_params.privileged_address()
} else {
false
}
})
.ok_or(ValidityError::TransactionUpgradeNoPrivilegedAddress)?;
let calculated_metadata = UpgradeMetadata::compute(self)?;
if let Some(metadata) = self.metadata.as_ref()
&& metadata.body != calculated_metadata
{
return Err(ValidityError::TransactionMetadataMismatch);
}
self.inputs
.iter()
.enumerate()
.try_for_each(|(index, input)| {
if let Some(asset_id) = input.asset_id(consensus_params.base_asset_id())
&& asset_id != consensus_params.base_asset_id()
{
return Err(ValidityError::TransactionInputContainsNonBaseAssetId {
index,
});
}
match input {
Input::Contract(_) => {
Err(ValidityError::TransactionInputContainsContract { index })
}
Input::MessageDataSigned(_) | Input::MessageDataPredicate(_) => {
Err(ValidityError::TransactionInputContainsMessageData { index })
}
_ => Ok(()),
}
})?;
self.outputs
.iter()
.enumerate()
.try_for_each(|(index, output)| match output {
Output::Contract(_) => {
Err(ValidityError::TransactionOutputContainsContract { index })
}
Output::Variable { .. } => {
Err(ValidityError::TransactionOutputContainsVariable { index })
}
Output::Change { asset_id, .. }
if asset_id != consensus_params.base_asset_id() =>
{
Err(ValidityError::TransactionChangeChangeUsesNotBaseAsset { index })
}
Output::ContractCreated { .. } => {
Err(ValidityError::TransactionOutputContainsContractCreated { index })
}
_ => Ok(()),
})?;
Ok(())
}
}
impl crate::Cacheable for Upgrade {
fn is_computed(&self) -> bool {
self.metadata.is_some()
}
fn precompute(&mut self, chain_id: &ChainId) -> Result<(), ValidityError> {
self.metadata = None;
self.metadata = Some(ChargeableMetadata {
common: CommonMetadata::compute(self, chain_id)?,
body: UpgradeMetadata::compute(self)?,
});
Ok(())
}
}
mod field {
use super::*;
use crate::field::{
ChargeableBody,
UpgradePurpose as UpgradePurposeTrait,
};
impl UpgradePurposeTrait for Upgrade {
#[inline(always)]
fn upgrade_purpose(&self) -> &UpgradePurpose {
&self.body.purpose
}
#[inline(always)]
fn upgrade_purpose_mut(&mut self) -> &mut UpgradePurpose {
&mut self.body.purpose
}
#[inline(always)]
fn upgrade_purpose_offset_static() -> usize {
WORD_SIZE }
}
impl ChargeableBody<UpgradeBody> for Upgrade {
fn body(&self) -> &UpgradeBody {
&self.body
}
fn body_mut(&mut self) -> &mut UpgradeBody {
&mut self.body
}
fn body_offset_end(&self) -> usize {
Self::upgrade_purpose_offset_static()
.saturating_add(self.body.purpose.size())
.saturating_add(
WORD_SIZE + WORD_SIZE + WORD_SIZE + WORD_SIZE, )
}
}
}