bee_block/
semantic.rs

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