iota_sdk/types/block/
semantic.rs

1// Copyright 2022 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use alloc::collections::BTreeMap;
5use core::{convert::Infallible, fmt};
6
7use hashbrown::{HashMap, HashSet};
8use primitive_types::U256;
9
10use crate::types::block::{
11    address::Address,
12    output::{ChainId, FoundryId, InputsCommitment, NativeTokens, Output, OutputId, TokenId},
13    payload::transaction::{RegularTransactionEssence, TransactionEssence, TransactionId},
14    unlock::Unlocks,
15    Error,
16};
17
18/// Errors related to ledger types.
19#[derive(Debug)]
20pub enum ConflictError {
21    /// Invalid conflict byte.
22    InvalidConflict(u8),
23}
24
25impl fmt::Display for ConflictError {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match self {
28            Self::InvalidConflict(byte) => write!(f, "invalid conflict byte {byte}"),
29        }
30    }
31}
32
33impl From<Infallible> for ConflictError {
34    fn from(err: Infallible) -> Self {
35        match err {}
36    }
37}
38
39#[cfg(feature = "std")]
40impl std::error::Error for ConflictError {}
41
42/// Represents the different reasons why a transaction can conflict with the ledger state.
43#[repr(u8)]
44#[derive(Debug, Copy, Clone, Eq, PartialEq, packable::Packable)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46#[packable(unpack_error = ConflictError)]
47#[packable(tag_type = u8, with_error = ConflictError::InvalidConflict)]
48pub enum ConflictReason {
49    /// The block has no conflict.
50    None = 0,
51    /// The referenced Utxo was already spent.
52    InputUtxoAlreadySpent = 1,
53    /// The referenced Utxo was already spent while confirming this milestone.
54    InputUtxoAlreadySpentInThisMilestone = 2,
55    /// The referenced Utxo cannot be found.
56    InputUtxoNotFound = 3,
57    /// The created amount does not match the consumed amount.
58    CreatedConsumedAmountMismatch = 4,
59    /// The unlock signature is invalid.
60    InvalidSignature = 5,
61    /// The configured timelock is not yet expired.
62    TimelockNotExpired = 6,
63    /// The given native tokens are invalid.
64    InvalidNativeTokens = 7,
65    /// Storage deposit return mismatch.
66    StorageDepositReturnUnfulfilled = 8,
67    /// An invalid unlock was used.
68    InvalidUnlock = 9,
69    /// The inputs commitments do not match.
70    InputsCommitmentsMismatch = 10,
71    /// The sender was not verified.
72    UnverifiedSender = 11,
73    /// The chain state transition is invalid.
74    InvalidChainStateTransition = 12,
75    /// The semantic validation failed for a reason not covered by the previous variants.
76    SemanticValidationFailed = 255,
77}
78
79impl fmt::Display for ConflictReason {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            Self::None => write!(f, "The block has no conflict"),
83            Self::InputUtxoAlreadySpent => write!(f, "The referenced UTXO was already spent"),
84            Self::InputUtxoAlreadySpentInThisMilestone => write!(
85                f,
86                "The referenced UTXO was already spent while confirming this milestone"
87            ),
88            Self::InputUtxoNotFound => write!(f, "The referenced UTXO cannot be found"),
89            Self::CreatedConsumedAmountMismatch => {
90                write!(f, "The sum of the inputs and output values does not match")
91            }
92            Self::InvalidSignature => write!(f, "The unlock block signature is invalid"),
93            Self::TimelockNotExpired => write!(f, "The configured timelock is not yet expired"),
94            Self::InvalidNativeTokens => write!(f, "The native tokens are invalid"),
95            Self::StorageDepositReturnUnfulfilled => write!(
96                f,
97                "The return amount in a transaction is not fulfilled by the output side"
98            ),
99            Self::InvalidUnlock => write!(f, "The input unlock is invalid"),
100            Self::InputsCommitmentsMismatch => write!(f, "The inputs commitment is invalid"),
101            Self::UnverifiedSender => write!(
102                f,
103                " The output contains a Sender with an ident (address) which is not unlocked"
104            ),
105            Self::InvalidChainStateTransition => write!(f, "The chain state transition is invalid"),
106            Self::SemanticValidationFailed => write!(f, "The semantic validation failed"),
107        }
108    }
109}
110
111impl TryFrom<u8> for ConflictReason {
112    type Error = ConflictError;
113
114    fn try_from(c: u8) -> Result<Self, Self::Error> {
115        Ok(match c {
116            0 => Self::None,
117            1 => Self::InputUtxoAlreadySpent,
118            2 => Self::InputUtxoAlreadySpentInThisMilestone,
119            3 => Self::InputUtxoNotFound,
120            4 => Self::CreatedConsumedAmountMismatch,
121            5 => Self::InvalidSignature,
122            6 => Self::TimelockNotExpired,
123            7 => Self::InvalidNativeTokens,
124            8 => Self::StorageDepositReturnUnfulfilled,
125            9 => Self::InvalidUnlock,
126            10 => Self::InputsCommitmentsMismatch,
127            11 => Self::UnverifiedSender,
128            12 => Self::InvalidChainStateTransition,
129            255 => Self::SemanticValidationFailed,
130            x => return Err(Self::Error::InvalidConflict(x)),
131        })
132    }
133}
134
135impl Default for ConflictReason {
136    fn default() -> Self {
137        Self::None
138    }
139}
140
141///
142pub struct ValidationContext<'a> {
143    ///
144    pub essence: &'a RegularTransactionEssence,
145    ///
146    pub essence_hash: [u8; 32],
147    ///
148    pub inputs_commitment: InputsCommitment,
149    ///
150    pub unlocks: &'a Unlocks,
151    ///
152    pub milestone_timestamp: u32,
153    ///
154    pub input_amount: u64,
155    ///
156    pub input_native_tokens: BTreeMap<TokenId, U256>,
157    ///
158    pub input_chains: HashMap<ChainId, &'a Output>,
159    ///
160    pub output_amount: u64,
161    ///
162    pub output_native_tokens: BTreeMap<TokenId, U256>,
163    ///
164    pub output_chains: HashMap<ChainId, &'a Output>,
165    ///
166    pub unlocked_addresses: HashSet<Address>,
167    ///
168    pub storage_deposit_returns: HashMap<Address, u64>,
169    ///
170    pub simple_deposits: HashMap<Address, u64>,
171}
172
173impl<'a> ValidationContext<'a> {
174    ///
175    pub fn new(
176        transaction_id: &TransactionId,
177        essence: &'a RegularTransactionEssence,
178        inputs: impl Iterator<Item = (&'a OutputId, &'a Output)> + Clone,
179        unlocks: &'a Unlocks,
180        milestone_timestamp: u32,
181    ) -> Self {
182        Self {
183            essence,
184            unlocks,
185            essence_hash: TransactionEssence::from(essence.clone()).hash(),
186            inputs_commitment: InputsCommitment::new(inputs.clone().map(|(_, output)| output)),
187            milestone_timestamp,
188            input_amount: 0,
189            input_native_tokens: BTreeMap::<TokenId, U256>::new(),
190            input_chains: inputs
191                .filter_map(|(output_id, input)| {
192                    input
193                        .chain_id()
194                        .map(|chain_id| (chain_id.or_from_output_id(output_id), input))
195                })
196                .collect(),
197            output_amount: 0,
198            output_native_tokens: BTreeMap::<TokenId, U256>::new(),
199            output_chains: essence
200                .outputs()
201                .iter()
202                .enumerate()
203                .filter_map(|(index, output)| {
204                    output.chain_id().map(|chain_id| {
205                        (
206                            chain_id.or_from_output_id(&OutputId::new(*transaction_id, index as u16).unwrap()),
207                            output,
208                        )
209                    })
210                })
211                .collect(),
212            unlocked_addresses: HashSet::new(),
213            storage_deposit_returns: HashMap::new(),
214            simple_deposits: HashMap::new(),
215        }
216    }
217}
218
219///
220pub fn semantic_validation(
221    mut context: ValidationContext<'_>,
222    inputs: &[(&OutputId, &Output)],
223    unlocks: &Unlocks,
224) -> Result<ConflictReason, Error> {
225    // Validation of the inputs commitment.
226    if context.essence.inputs_commitment() != &context.inputs_commitment {
227        return Ok(ConflictReason::InputsCommitmentsMismatch);
228    }
229
230    // Validation of inputs.
231    for ((output_id, consumed_output), unlock) in inputs.iter().zip(unlocks.iter()) {
232        let (conflict, amount, consumed_native_tokens, unlock_conditions) = match consumed_output {
233            Output::Basic(output) => (
234                output.unlock(output_id, unlock, inputs, &mut context),
235                output.amount(),
236                output.native_tokens(),
237                output.unlock_conditions(),
238            ),
239            Output::Alias(output) => (
240                output.unlock(output_id, unlock, inputs, &mut context),
241                output.amount(),
242                output.native_tokens(),
243                output.unlock_conditions(),
244            ),
245            Output::Foundry(output) => (
246                output.unlock(output_id, unlock, inputs, &mut context),
247                output.amount(),
248                output.native_tokens(),
249                output.unlock_conditions(),
250            ),
251            Output::Nft(output) => (
252                output.unlock(output_id, unlock, inputs, &mut context),
253                output.amount(),
254                output.native_tokens(),
255                output.unlock_conditions(),
256            ),
257            _ => return Err(Error::UnsupportedOutputKind(consumed_output.kind())),
258        };
259
260        if let Err(conflict) = conflict {
261            return Ok(conflict);
262        }
263
264        if unlock_conditions.is_time_locked(context.milestone_timestamp) {
265            return Ok(ConflictReason::TimelockNotExpired);
266        }
267
268        if !unlock_conditions.is_expired(context.milestone_timestamp) {
269            if let Some(storage_deposit_return) = unlock_conditions.storage_deposit_return() {
270                let amount = context
271                    .storage_deposit_returns
272                    .entry(*storage_deposit_return.return_address())
273                    .or_default();
274
275                *amount = amount
276                    .checked_add(storage_deposit_return.amount())
277                    .ok_or(Error::StorageDepositReturnOverflow)?;
278            }
279        }
280
281        context.input_amount = context
282            .input_amount
283            .checked_add(amount)
284            .ok_or(Error::ConsumedAmountOverflow)?;
285
286        for native_token in consumed_native_tokens.iter() {
287            let native_token_amount = context.input_native_tokens.entry(*native_token.token_id()).or_default();
288
289            *native_token_amount = native_token_amount
290                .checked_add(native_token.amount())
291                .ok_or(Error::ConsumedNativeTokensAmountOverflow)?;
292        }
293    }
294
295    // Validation of outputs.
296    for created_output in context.essence.outputs() {
297        let (amount, created_native_tokens, features) = match created_output {
298            Output::Basic(output) => {
299                if let Some(address) = output.simple_deposit_address() {
300                    let amount = context.simple_deposits.entry(*address).or_default();
301
302                    *amount = amount
303                        .checked_add(output.amount())
304                        .ok_or(Error::CreatedAmountOverflow)?;
305                }
306
307                (output.amount(), output.native_tokens(), output.features())
308            }
309            Output::Alias(output) => (output.amount(), output.native_tokens(), output.features()),
310            Output::Foundry(output) => (output.amount(), output.native_tokens(), output.features()),
311            Output::Nft(output) => (output.amount(), output.native_tokens(), output.features()),
312            _ => return Err(Error::UnsupportedOutputKind(created_output.kind())),
313        };
314
315        if let Some(sender) = features.sender() {
316            if !context.unlocked_addresses.contains(sender.address()) {
317                return Ok(ConflictReason::UnverifiedSender);
318            }
319        }
320
321        context.output_amount = context
322            .output_amount
323            .checked_add(amount)
324            .ok_or(Error::CreatedAmountOverflow)?;
325
326        for native_token in created_native_tokens.iter() {
327            let native_token_amount = context
328                .output_native_tokens
329                .entry(*native_token.token_id())
330                .or_default();
331
332            *native_token_amount = native_token_amount
333                .checked_add(native_token.amount())
334                .ok_or(Error::CreatedNativeTokensAmountOverflow)?;
335        }
336    }
337
338    // Validation of storage deposit returns.
339    for (return_address, return_amount) in context.storage_deposit_returns.iter() {
340        if let Some(deposit_amount) = context.simple_deposits.get(return_address) {
341            if deposit_amount < return_amount {
342                return Ok(ConflictReason::StorageDepositReturnUnfulfilled);
343            }
344        } else {
345            return Ok(ConflictReason::StorageDepositReturnUnfulfilled);
346        }
347    }
348
349    // Validation of amounts.
350    if context.input_amount != context.output_amount {
351        return Ok(ConflictReason::CreatedConsumedAmountMismatch);
352    }
353
354    let mut native_token_ids = HashSet::new();
355
356    // Validation of input native tokens.
357    for (token_id, _input_amount) in context.input_native_tokens.iter() {
358        native_token_ids.insert(token_id);
359    }
360
361    // Validation of output native tokens.
362    for (token_id, output_amount) in context.output_native_tokens.iter() {
363        let input_amount = context.input_native_tokens.get(token_id).copied().unwrap_or_default();
364
365        if output_amount > &input_amount
366            && !context
367                .output_chains
368                .contains_key(&ChainId::from(FoundryId::from(*token_id)))
369        {
370            return Ok(ConflictReason::InvalidNativeTokens);
371        }
372
373        native_token_ids.insert(token_id);
374    }
375
376    if native_token_ids.len() > NativeTokens::COUNT_MAX as usize {
377        return Ok(ConflictReason::InvalidNativeTokens);
378    }
379
380    // Validation of state transitions and destructions.
381    for (chain_id, current_state) in context.input_chains.iter() {
382        if Output::verify_state_transition(
383            Some(current_state),
384            context.output_chains.get(chain_id).map(core::ops::Deref::deref),
385            &context,
386        )
387        .is_err()
388        {
389            return Ok(ConflictReason::InvalidChainStateTransition);
390        }
391    }
392
393    // Validation of state creations.
394    for (chain_id, next_state) in context.output_chains.iter() {
395        if context.input_chains.get(chain_id).is_none()
396            && Output::verify_state_transition(None, Some(next_state), &context).is_err()
397        {
398            return Ok(ConflictReason::InvalidChainStateTransition);
399        }
400    }
401
402    Ok(ConflictReason::None)
403}