From 8951f4caa93d8689d6872ab0dc467423ee343d61 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 13 Dec 2023 09:35:16 +0100 Subject: [PATCH] Move unlocks to semantic module (#1772) * Semantic module * Move transitions to semantic * Error module * SemanticValidationContext::address_unlock * Remove some from * Use id_non_null * SemanticValidationContext::output_unlock --- sdk/src/types/block/address/mod.rs | 92 +-------- sdk/src/types/block/output/account.rs | 34 +--- sdk/src/types/block/output/anchor.rs | 16 +- sdk/src/types/block/output/basic.rs | 29 +-- sdk/src/types/block/output/delegation.rs | 22 +- sdk/src/types/block/output/foundry.rs | 17 +- sdk/src/types/block/output/nft.rs | 40 +--- sdk/src/types/block/semantic/error.rs | 177 ++++++++++++++++ sdk/src/types/block/semantic/mod.rs | 191 +----------------- sdk/src/types/block/semantic/unlock.rs | 177 ++++++++++++++++ .../client/input_selection/basic_outputs.rs | 6 +- 11 files changed, 377 insertions(+), 424 deletions(-) create mode 100644 sdk/src/types/block/semantic/error.rs create mode 100644 sdk/src/types/block/semantic/unlock.rs diff --git a/sdk/src/types/block/address/mod.rs b/sdk/src/types/block/address/mod.rs index d7eb35cac2..c90a7738c2 100644 --- a/sdk/src/types/block/address/mod.rs +++ b/sdk/src/types/block/address/mod.rs @@ -28,10 +28,7 @@ pub use self::{ }; use crate::{ types::block::{ - output::{Output, StorageScore, StorageScoreParameters}, - semantic::{SemanticValidationContext, TransactionFailureReason}, - signature::Signature, - unlock::Unlock, + output::{StorageScore, StorageScoreParameters}, Error, }, utils::ConvertTo, @@ -126,93 +123,6 @@ impl Address { pub fn is_valid_bech32(address: &str) -> bool { Self::try_from_bech32(address).is_ok() } - - /// - pub fn unlock( - &self, - unlock: &Unlock, - context: &mut SemanticValidationContext<'_>, - ) -> Result<(), TransactionFailureReason> { - match (self, unlock) { - (Self::Ed25519(ed25519_address), Unlock::Signature(unlock)) => { - if context.unlocked_addresses.contains(self) { - return Err(TransactionFailureReason::InvalidInputUnlock); - } - - let Signature::Ed25519(signature) = unlock.signature(); - - if signature - .is_valid(context.transaction_signing_hash.as_ref(), ed25519_address) - .is_err() - { - return Err(TransactionFailureReason::InvalidUnlockBlockSignature); - } - - context.unlocked_addresses.insert(self.clone()); - } - (Self::Ed25519(_), Unlock::Reference(_)) => { - // TODO actually check that it was unlocked by the same signature. - if !context.unlocked_addresses.contains(self) { - return Err(TransactionFailureReason::InvalidInputUnlock); - } - } - (Self::Account(account_address), Unlock::Account(unlock)) => { - // PANIC: indexing is fine as it is already syntactically verified that indexes reference below. - if let (output_id, Output::Account(account_output)) = context.inputs[unlock.index() as usize] { - if &account_output.account_id_non_null(output_id) != account_address.account_id() { - return Err(TransactionFailureReason::InvalidInputUnlock); - } - if !context.unlocked_addresses.contains(self) { - return Err(TransactionFailureReason::InvalidInputUnlock); - } - } else { - return Err(TransactionFailureReason::InvalidInputUnlock); - } - } - (Self::Nft(nft_address), Unlock::Nft(unlock)) => { - // PANIC: indexing is fine as it is already syntactically verified that indexes reference below. - if let (output_id, Output::Nft(nft_output)) = context.inputs[unlock.index() as usize] { - if &nft_output.nft_id_non_null(output_id) != nft_address.nft_id() { - return Err(TransactionFailureReason::InvalidInputUnlock); - } - if !context.unlocked_addresses.contains(self) { - return Err(TransactionFailureReason::InvalidInputUnlock); - } - } else { - return Err(TransactionFailureReason::InvalidInputUnlock); - } - } - // TODO maybe shouldn't be a semantic error but this function currently returns a TransactionFailureReason. - (Self::Anchor(_), _) => return Err(TransactionFailureReason::SemanticValidationFailed), - (Self::ImplicitAccountCreation(implicit_account_creation_address), _) => { - return Self::from(*implicit_account_creation_address.ed25519_address()).unlock(unlock, context); - } - (Self::Multi(multi_address), Unlock::Multi(unlock)) => { - if multi_address.len() != unlock.len() { - return Err(TransactionFailureReason::InvalidInputUnlock); - } - - let mut cumulative_unlocked_weight = 0u16; - - for (address, unlock) in multi_address.addresses().iter().zip(unlock.unlocks()) { - if !unlock.is_empty() { - address.unlock(unlock, context)?; - cumulative_unlocked_weight += address.weight() as u16; - } - } - - if cumulative_unlocked_weight < multi_address.threshold() { - return Err(TransactionFailureReason::InvalidInputUnlock); - } - } - (Self::Restricted(restricted_address), _) => { - return restricted_address.address().unlock(unlock, context); - } - _ => return Err(TransactionFailureReason::InvalidInputUnlock), - } - - Ok(()) - } } impl StorageScore for Address { diff --git a/sdk/src/types/block/output/account.rs b/sdk/src/types/block/output/account.rs index 977d432973..942693651c 100644 --- a/sdk/src/types/block/output/account.rs +++ b/sdk/src/types/block/output/account.rs @@ -19,8 +19,7 @@ use crate::types::block::{ ChainId, MinimumOutputAmount, Output, OutputBuilderAmount, OutputId, StorageScore, StorageScoreParameters, }, protocol::{ProtocolParameters, WorkScore, WorkScoreParameters}, - semantic::{SemanticValidationContext, StateTransitionError, TransactionFailureReason}, - unlock::Unlock, + semantic::StateTransitionError, Error, }; @@ -375,37 +374,6 @@ impl AccountOutput { AccountAddress::new(self.account_id_non_null(output_id)) } - /// - pub fn unlock( - &self, - output_id: &OutputId, - unlock: &Unlock, - context: &mut SemanticValidationContext<'_>, - ) -> Result<(), TransactionFailureReason> { - self.unlock_conditions() - .locked_address( - self.address(), - None, - context.protocol_parameters.committable_age_range(), - ) - // Safe to unwrap, AccountOutput can't have an expiration unlock condition. - .unwrap() - .unwrap() - .unlock(unlock, context)?; - - let account_id = if self.account_id().is_null() { - AccountId::from(output_id) - } else { - *self.account_id() - }; - - context - .unlocked_addresses - .insert(Address::from(AccountAddress::from(account_id))); - - Ok(()) - } - // Transition, just without full SemanticValidationContext pub(crate) fn transition_inner( current_state: &Self, diff --git a/sdk/src/types/block/output/anchor.rs b/sdk/src/types/block/output/anchor.rs index dc1270983b..a2d2bcef20 100644 --- a/sdk/src/types/block/output/anchor.rs +++ b/sdk/src/types/block/output/anchor.rs @@ -427,27 +427,21 @@ impl AnchorOutput { unlock: &Unlock, context: &mut SemanticValidationContext<'_>, ) -> Result<(), TransactionFailureReason> { - let anchor_id = if self.anchor_id().is_null() { - AnchorId::from(output_id) - } else { - *self.anchor_id() - }; + let anchor_id = self.anchor_id_non_null(output_id); let next_state = context.output_chains.get(&ChainId::from(anchor_id)); match next_state { Some(Output::Anchor(next_state)) => { if self.state_index() == next_state.state_index() { - self.governor_address().unlock(unlock, context)?; + context.address_unlock(self.governor_address(), unlock)?; } else { - self.state_controller_address().unlock(unlock, context)?; + context.address_unlock(self.state_controller_address(), unlock)?; // Only a state transition can be used to consider the anchor address for output unlocks and // sender/issuer validations. - context - .unlocked_addresses - .insert(Address::from(AnchorAddress::from(anchor_id))); + context.unlocked_addresses.insert(Address::from(anchor_id)); } } - None => self.governor_address().unlock(unlock, context)?, + None => context.address_unlock(self.governor_address(), unlock)?, // The next state can only be an anchor output since it is identified by an anchor chain identifier. Some(_) => unreachable!(), }; diff --git a/sdk/src/types/block/output/basic.rs b/sdk/src/types/block/output/basic.rs index 060f2ec022..6b07bd0f21 100644 --- a/sdk/src/types/block/output/basic.rs +++ b/sdk/src/types/block/output/basic.rs @@ -13,11 +13,9 @@ use crate::types::block::{ verify_allowed_unlock_conditions, AddressUnlockCondition, StorageDepositReturnUnlockCondition, UnlockCondition, UnlockConditionFlags, UnlockConditions, }, - MinimumOutputAmount, NativeToken, Output, OutputBuilderAmount, OutputId, StorageScore, StorageScoreParameters, + MinimumOutputAmount, NativeToken, Output, OutputBuilderAmount, StorageScore, StorageScoreParameters, }, protocol::{ProtocolParameters, WorkScore, WorkScoreParameters}, - semantic::{SemanticValidationContext, TransactionFailureReason}, - unlock::Unlock, Error, }; @@ -311,31 +309,6 @@ impl BasicOutput { .unwrap() } - /// - pub fn unlock( - &self, - _output_id: &OutputId, - unlock: &Unlock, - context: &mut SemanticValidationContext<'_>, - ) -> Result<(), TransactionFailureReason> { - let slot_index = context - .transaction - .context_inputs() - .iter() - .find_map(|c| c.as_commitment_opt().map(|c| c.slot_index())); - - self.unlock_conditions() - .locked_address( - self.address(), - slot_index, - context.protocol_parameters.committable_age_range(), - ) - .map_err(|_| TransactionFailureReason::InvalidCommitmentContextInput)? - // because of expiration the input can't be unlocked at this time - .ok_or(TransactionFailureReason::SemanticValidationFailed)? - .unlock(unlock, context) - } - /// Returns the address of the unlock conditions if the output is a simple deposit. /// Simple deposit outputs are basic outputs with only an address unlock condition, no native tokens and no /// features. They are used to return storage deposits. diff --git a/sdk/src/types/block/output/delegation.rs b/sdk/src/types/block/output/delegation.rs index fa64224726..18c32126f3 100644 --- a/sdk/src/types/block/output/delegation.rs +++ b/sdk/src/types/block/output/delegation.rs @@ -13,9 +13,8 @@ use crate::types::block::{ MinimumOutputAmount, Output, OutputBuilderAmount, OutputId, StorageScore, StorageScoreParameters, }, protocol::{ProtocolParameters, WorkScore, WorkScoreParameters}, - semantic::{SemanticValidationContext, StateTransitionError, TransactionFailureReason}, + semantic::StateTransitionError, slot::EpochIndex, - unlock::Unlock, Error, }; @@ -315,25 +314,6 @@ impl DelegationOutput { ChainId::Delegation(self.delegation_id) } - /// Tries to unlock the [`DelegationOutput`]. - pub fn unlock( - &self, - _output_id: &OutputId, - unlock: &Unlock, - context: &mut SemanticValidationContext<'_>, - ) -> Result<(), TransactionFailureReason> { - self.unlock_conditions() - .locked_address( - self.address(), - None, - context.protocol_parameters.committable_age_range(), - ) - // Safe to unwrap, DelegationOutput can't have an expiration unlock condition. - .unwrap() - .unwrap() - .unlock(unlock, context) - } - // Transition, just without full SemanticValidationContext. pub(crate) fn transition_inner(current_state: &Self, next_state: &Self) -> Result<(), StateTransitionError> { #[allow(clippy::nonminimal_bool)] diff --git a/sdk/src/types/block/output/foundry.rs b/sdk/src/types/block/output/foundry.rs index 20bf4656fe..ccc61cdbcc 100644 --- a/sdk/src/types/block/output/foundry.rs +++ b/sdk/src/types/block/output/foundry.rs @@ -18,13 +18,12 @@ use crate::types::block::{ account::AccountId, feature::{verify_allowed_features, Feature, FeatureFlags, Features, NativeTokenFeature}, unlock_condition::{verify_allowed_unlock_conditions, UnlockCondition, UnlockConditionFlags, UnlockConditions}, - ChainId, MinimumOutputAmount, NativeToken, Output, OutputBuilderAmount, OutputId, StorageScore, - StorageScoreParameters, TokenId, TokenScheme, + ChainId, MinimumOutputAmount, NativeToken, Output, OutputBuilderAmount, StorageScore, StorageScoreParameters, + TokenId, TokenScheme, }, payload::signed_transaction::{TransactionCapabilities, TransactionCapabilityFlag}, protocol::{ProtocolParameters, WorkScore, WorkScoreParameters}, - semantic::{SemanticValidationContext, StateTransitionError, TransactionFailureReason}, - unlock::Unlock, + semantic::{StateTransitionError, TransactionFailureReason}, Error, }; @@ -402,16 +401,6 @@ impl FoundryOutput { ChainId::Foundry(self.id()) } - /// - pub fn unlock( - &self, - _output_id: &OutputId, - unlock: &Unlock, - context: &mut SemanticValidationContext<'_>, - ) -> Result<(), TransactionFailureReason> { - Address::from(*self.account_address()).unlock(unlock, context) - } - // Transition, just without full SemanticValidationContext pub(crate) fn transition_inner( current_state: &Self, diff --git a/sdk/src/types/block/output/nft.rs b/sdk/src/types/block/output/nft.rs index 3398b5a6af..0e5930b880 100644 --- a/sdk/src/types/block/output/nft.rs +++ b/sdk/src/types/block/output/nft.rs @@ -22,8 +22,7 @@ use crate::types::block::{ StorageScoreParameters, }, protocol::{ProtocolParameters, WorkScore, WorkScoreParameters}, - semantic::{SemanticValidationContext, StateTransitionError, TransactionFailureReason}, - unlock::Unlock, + semantic::StateTransitionError, Error, }; @@ -406,43 +405,6 @@ impl NftOutput { NftAddress::new(self.nft_id_non_null(output_id)) } - /// - pub fn unlock( - &self, - output_id: &OutputId, - unlock: &Unlock, - context: &mut SemanticValidationContext<'_>, - ) -> Result<(), TransactionFailureReason> { - let slot_index = context - .transaction - .context_inputs() - .iter() - .find_map(|c| c.as_commitment_opt().map(|c| c.slot_index())); - - self.unlock_conditions() - .locked_address( - self.address(), - slot_index, - context.protocol_parameters.committable_age_range(), - ) - .map_err(|_| TransactionFailureReason::InvalidCommitmentContextInput)? - // because of expiration the input can't be unlocked at this time - .ok_or(TransactionFailureReason::SemanticValidationFailed)? - .unlock(unlock, context)?; - - let nft_id = if self.nft_id().is_null() { - NftId::from(output_id) - } else { - *self.nft_id() - }; - - context - .unlocked_addresses - .insert(Address::from(NftAddress::from(nft_id))); - - Ok(()) - } - // Transition, just without full SemanticValidationContext pub(crate) fn transition_inner(current_state: &Self, next_state: &Self) -> Result<(), StateTransitionError> { if current_state.immutable_features != next_state.immutable_features { diff --git a/sdk/src/types/block/semantic/error.rs b/sdk/src/types/block/semantic/error.rs new file mode 100644 index 0000000000..3fbb99968d --- /dev/null +++ b/sdk/src/types/block/semantic/error.rs @@ -0,0 +1,177 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use core::fmt; + +use crate::types::block::Error; + +/// Describes the reason of a transaction failure. +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, packable::Packable)] +#[cfg_attr(feature = "serde", derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr))] +#[packable(unpack_error = Error)] +#[packable(tag_type = u8, with_error = Error::InvalidTransactionFailureReason)] +#[non_exhaustive] +pub enum TransactionFailureReason { + /// The referenced UTXO was already spent. + InputUtxoAlreadySpent = 1, + /// The transaction is conflicting with another transaction. Conflicting specifically means a double spend + /// situation that both transaction pass all validation rules, eventually losing one(s) should have this reason. + ConflictingWithAnotherTx = 2, + /// The referenced UTXO is invalid. + InvalidReferencedUtxo = 3, + /// The transaction is invalid. + InvalidTransaction = 4, + /// The sum of the inputs and output base token amount does not match. + SumInputsOutputsAmountMismatch = 5, + /// The unlock block signature is invalid. + InvalidUnlockBlockSignature = 6, + /// The configured timelock is not yet expired. + TimelockNotExpired = 7, + /// The given native tokens are invalid. + InvalidNativeTokens = 8, + /// The return amount in a transaction is not fulfilled by the output side. + StorageDepositReturnUnfulfilled = 9, + /// An input unlock was invalid. + InvalidInputUnlock = 10, + /// The output contains a Sender with an ident (address) which is not unlocked. + SenderNotUnlocked = 11, + /// The chain state transition is invalid. + InvalidChainStateTransition = 12, + /// The referenced input is created after transaction issuing time. + InvalidTransactionIssuingTime = 13, + /// The mana amount is invalid. + InvalidManaAmount = 14, + /// The Block Issuance Credits amount is invalid. + InvalidBlockIssuanceCreditsAmount = 15, + /// Reward Context Input is invalid. + InvalidRewardContextInput = 16, + /// Commitment Context Input is invalid. + InvalidCommitmentContextInput = 17, + /// Staking Feature is not provided in account output when claiming rewards. + MissingStakingFeature = 18, + /// Failed to claim staking reward. + FailedToClaimStakingReward = 19, + /// Failed to claim delegation reward. + FailedToClaimDelegationReward = 20, + /// Burning of native tokens is not allowed in the transaction capabilities. + TransactionCapabilityNativeTokenBurningNotAllowed = 21, + /// Burning of mana is not allowed in the transaction capabilities. + TransactionCapabilityManaBurningNotAllowed = 22, + /// Destruction of accounts is not allowed in the transaction capabilities. + TransactionCapabilityAccountDestructionNotAllowed = 23, + /// Destruction of anchors is not allowed in the transaction capabilities. + TransactionCapabilityAnchorDestructionNotAllowed = 24, + /// Destruction of foundries is not allowed in the transaction capabilities. + TransactionCapabilityFoundryDestructionNotAllowed = 25, + /// Destruction of nfts is not allowed in the transaction capabilities. + TransactionCapabilityNftDestructionNotAllowed = 26, + /// The semantic validation failed for a reason not covered by the previous variants. + SemanticValidationFailed = 255, +} + +impl fmt::Display for TransactionFailureReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InputUtxoAlreadySpent => write!(f, "The referenced UTXO was already spent."), + Self::ConflictingWithAnotherTx => write!( + f, + "The transaction is conflicting with another transaction. Conflicting specifically means a double spend situation that both transactions pass all validation rules, eventually losing one(s) should have this reason." + ), + Self::InvalidReferencedUtxo => write!(f, "The referenced UTXO is invalid."), + Self::InvalidTransaction => write!(f, "The transaction is invalid."), + Self::SumInputsOutputsAmountMismatch => { + write!(f, "The sum of the inputs and output base token amount does not match.") + } + Self::InvalidUnlockBlockSignature => write!(f, "The unlock block signature is invalid."), + Self::TimelockNotExpired => write!(f, "The configured timelock is not yet expired."), + Self::InvalidNativeTokens => write!(f, "The given native tokens are invalid."), + Self::StorageDepositReturnUnfulfilled => write!( + f, + "The return amount in a transaction is not fulfilled by the output side." + ), + Self::InvalidInputUnlock => write!(f, "An input unlock was invalid."), + Self::SenderNotUnlocked => write!( + f, + "The output contains a Sender with an ident (address) which is not unlocked." + ), + Self::InvalidChainStateTransition => write!(f, "The chain state transition is invalid."), + Self::InvalidTransactionIssuingTime => { + write!(f, "The referenced input is created after transaction issuing time.") + } + Self::InvalidManaAmount => write!(f, "The mana amount is invalid."), + Self::InvalidBlockIssuanceCreditsAmount => write!(f, "The Block Issuance Credits amount is invalid."), + Self::InvalidRewardContextInput => write!(f, "Reward Context Input is invalid."), + Self::InvalidCommitmentContextInput => write!(f, "Commitment Context Input is invalid."), + Self::MissingStakingFeature => write!( + f, + "Staking Feature is not provided in account output when claiming rewards." + ), + Self::FailedToClaimStakingReward => write!(f, "Failed to claim staking reward."), + Self::FailedToClaimDelegationReward => write!(f, "Failed to claim delegation reward."), + Self::TransactionCapabilityNativeTokenBurningNotAllowed => write!( + f, + "Burning of native tokens is not allowed in the transaction capabilities." + ), + Self::TransactionCapabilityManaBurningNotAllowed => { + write!(f, "Burning of mana is not allowed in the transaction capabilities.") + } + Self::TransactionCapabilityAccountDestructionNotAllowed => write!( + f, + "Destruction of accounts is not allowed in the transaction capabilities." + ), + Self::TransactionCapabilityAnchorDestructionNotAllowed => write!( + f, + "Destruction of anchors is not allowed in the transaction capabilities." + ), + Self::TransactionCapabilityFoundryDestructionNotAllowed => write!( + f, + "Destruction of foundries is not allowed in the transaction capabilities." + ), + Self::TransactionCapabilityNftDestructionNotAllowed => { + write!(f, "Destruction of nfts is not allowed in the transaction capabilities.") + } + Self::SemanticValidationFailed => write!( + f, + "The semantic validation failed for a reason not covered by the previous variants." + ), + } + } +} + +impl TryFrom for TransactionFailureReason { + type Error = Error; + + fn try_from(c: u8) -> Result { + Ok(match c { + 1 => Self::InputUtxoAlreadySpent, + 2 => Self::ConflictingWithAnotherTx, + 3 => Self::InvalidReferencedUtxo, + 4 => Self::InvalidTransaction, + 5 => Self::SumInputsOutputsAmountMismatch, + 6 => Self::InvalidUnlockBlockSignature, + 7 => Self::TimelockNotExpired, + 8 => Self::InvalidNativeTokens, + 9 => Self::StorageDepositReturnUnfulfilled, + 10 => Self::InvalidInputUnlock, + 11 => Self::SenderNotUnlocked, + 12 => Self::InvalidChainStateTransition, + 13 => Self::InvalidTransactionIssuingTime, + 14 => Self::InvalidManaAmount, + 15 => Self::InvalidBlockIssuanceCreditsAmount, + 16 => Self::InvalidRewardContextInput, + 17 => Self::InvalidCommitmentContextInput, + 18 => Self::MissingStakingFeature, + 19 => Self::FailedToClaimStakingReward, + 20 => Self::FailedToClaimDelegationReward, + 21 => Self::TransactionCapabilityNativeTokenBurningNotAllowed, + 22 => Self::TransactionCapabilityManaBurningNotAllowed, + 23 => Self::TransactionCapabilityAccountDestructionNotAllowed, + 24 => Self::TransactionCapabilityAnchorDestructionNotAllowed, + 25 => Self::TransactionCapabilityFoundryDestructionNotAllowed, + 26 => Self::TransactionCapabilityNftDestructionNotAllowed, + 255 => Self::SemanticValidationFailed, + x => return Err(Self::Error::InvalidTransactionFailureReason(x)), + }) + } +} diff --git a/sdk/src/types/block/semantic/mod.rs b/sdk/src/types/block/semantic/mod.rs index beea9713a6..b979334e62 100644 --- a/sdk/src/types/block/semantic/mod.rs +++ b/sdk/src/types/block/semantic/mod.rs @@ -1,15 +1,19 @@ // Copyright 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod error; mod state_transition; +mod unlock; use alloc::collections::BTreeMap; -use core::fmt; use hashbrown::{HashMap, HashSet}; use primitive_types::U256; -pub use self::state_transition::{StateTransitionError, StateTransitionVerifier}; +pub use self::{ + error::TransactionFailureReason, + state_transition::{StateTransitionError, StateTransitionVerifier}, +}; use crate::types::block::{ address::{Address, AddressCapabilityFlag}, output::{AccountId, AnchorOutput, ChainId, FoundryId, NativeTokens, Output, OutputId, TokenId, UnlockCondition}, @@ -19,177 +23,6 @@ use crate::types::block::{ Error, }; -/// Describes the reason of a transaction failure. -#[repr(u8)] -#[derive(Debug, Copy, Clone, Eq, PartialEq, packable::Packable)] -#[cfg_attr(feature = "serde", derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr))] -#[packable(unpack_error = Error)] -#[packable(tag_type = u8, with_error = Error::InvalidTransactionFailureReason)] -#[non_exhaustive] -pub enum TransactionFailureReason { - /// The referenced UTXO was already spent. - InputUtxoAlreadySpent = 1, - /// The transaction is conflicting with another transaction. Conflicting specifically means a double spend - /// situation that both transaction pass all validation rules, eventually losing one(s) should have this reason. - ConflictingWithAnotherTx = 2, - /// The referenced UTXO is invalid. - InvalidReferencedUtxo = 3, - /// The transaction is invalid. - InvalidTransaction = 4, - /// The sum of the inputs and output base token amount does not match. - SumInputsOutputsAmountMismatch = 5, - /// The unlock block signature is invalid. - InvalidUnlockBlockSignature = 6, - /// The configured timelock is not yet expired. - TimelockNotExpired = 7, - /// The given native tokens are invalid. - InvalidNativeTokens = 8, - /// The return amount in a transaction is not fulfilled by the output side. - StorageDepositReturnUnfulfilled = 9, - /// An input unlock was invalid. - InvalidInputUnlock = 10, - /// The output contains a Sender with an ident (address) which is not unlocked. - SenderNotUnlocked = 11, - /// The chain state transition is invalid. - InvalidChainStateTransition = 12, - /// The referenced input is created after transaction issuing time. - InvalidTransactionIssuingTime = 13, - /// The mana amount is invalid. - InvalidManaAmount = 14, - /// The Block Issuance Credits amount is invalid. - InvalidBlockIssuanceCreditsAmount = 15, - /// Reward Context Input is invalid. - InvalidRewardContextInput = 16, - /// Commitment Context Input is invalid. - InvalidCommitmentContextInput = 17, - /// Staking Feature is not provided in account output when claiming rewards. - MissingStakingFeature = 18, - /// Failed to claim staking reward. - FailedToClaimStakingReward = 19, - /// Failed to claim delegation reward. - FailedToClaimDelegationReward = 20, - /// Burning of native tokens is not allowed in the transaction capabilities. - TransactionCapabilityNativeTokenBurningNotAllowed = 21, - /// Burning of mana is not allowed in the transaction capabilities. - TransactionCapabilityManaBurningNotAllowed = 22, - /// Destruction of accounts is not allowed in the transaction capabilities. - TransactionCapabilityAccountDestructionNotAllowed = 23, - /// Destruction of anchors is not allowed in the transaction capabilities. - TransactionCapabilityAnchorDestructionNotAllowed = 24, - /// Destruction of foundries is not allowed in the transaction capabilities. - TransactionCapabilityFoundryDestructionNotAllowed = 25, - /// Destruction of nfts is not allowed in the transaction capabilities. - TransactionCapabilityNftDestructionNotAllowed = 26, - /// The semantic validation failed for a reason not covered by the previous variants. - SemanticValidationFailed = 255, -} - -impl fmt::Display for TransactionFailureReason { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::InputUtxoAlreadySpent => write!(f, "The referenced UTXO was already spent."), - Self::ConflictingWithAnotherTx => write!( - f, - "The transaction is conflicting with another transaction. Conflicting specifically means a double spend situation that both transactions pass all validation rules, eventually losing one(s) should have this reason." - ), - Self::InvalidReferencedUtxo => write!(f, "The referenced UTXO is invalid."), - Self::InvalidTransaction => write!(f, "The transaction is invalid."), - Self::SumInputsOutputsAmountMismatch => { - write!(f, "The sum of the inputs and output base token amount does not match.") - } - Self::InvalidUnlockBlockSignature => write!(f, "The unlock block signature is invalid."), - Self::TimelockNotExpired => write!(f, "The configured timelock is not yet expired."), - Self::InvalidNativeTokens => write!(f, "The given native tokens are invalid."), - Self::StorageDepositReturnUnfulfilled => write!( - f, - "The return amount in a transaction is not fulfilled by the output side." - ), - Self::InvalidInputUnlock => write!(f, "An input unlock was invalid."), - Self::SenderNotUnlocked => write!( - f, - "The output contains a Sender with an ident (address) which is not unlocked." - ), - Self::InvalidChainStateTransition => write!(f, "The chain state transition is invalid."), - Self::InvalidTransactionIssuingTime => { - write!(f, "The referenced input is created after transaction issuing time.") - } - Self::InvalidManaAmount => write!(f, "The mana amount is invalid."), - Self::InvalidBlockIssuanceCreditsAmount => write!(f, "The Block Issuance Credits amount is invalid."), - Self::InvalidRewardContextInput => write!(f, "Reward Context Input is invalid."), - Self::InvalidCommitmentContextInput => write!(f, "Commitment Context Input is invalid."), - Self::MissingStakingFeature => write!( - f, - "Staking Feature is not provided in account output when claiming rewards." - ), - Self::FailedToClaimStakingReward => write!(f, "Failed to claim staking reward."), - Self::FailedToClaimDelegationReward => write!(f, "Failed to claim delegation reward."), - Self::TransactionCapabilityNativeTokenBurningNotAllowed => write!( - f, - "Burning of native tokens is not allowed in the transaction capabilities." - ), - Self::TransactionCapabilityManaBurningNotAllowed => { - write!(f, "Burning of mana is not allowed in the transaction capabilities.") - } - Self::TransactionCapabilityAccountDestructionNotAllowed => write!( - f, - "Destruction of accounts is not allowed in the transaction capabilities." - ), - Self::TransactionCapabilityAnchorDestructionNotAllowed => write!( - f, - "Destruction of anchors is not allowed in the transaction capabilities." - ), - Self::TransactionCapabilityFoundryDestructionNotAllowed => write!( - f, - "Destruction of foundries is not allowed in the transaction capabilities." - ), - Self::TransactionCapabilityNftDestructionNotAllowed => { - write!(f, "Destruction of nfts is not allowed in the transaction capabilities.") - } - Self::SemanticValidationFailed => write!( - f, - "The semantic validation failed for a reason not covered by the previous variants." - ), - } - } -} - -impl TryFrom for TransactionFailureReason { - type Error = Error; - - fn try_from(c: u8) -> Result { - Ok(match c { - 1 => Self::InputUtxoAlreadySpent, - 2 => Self::ConflictingWithAnotherTx, - 3 => Self::InvalidReferencedUtxo, - 4 => Self::InvalidTransaction, - 5 => Self::SumInputsOutputsAmountMismatch, - 6 => Self::InvalidUnlockBlockSignature, - 7 => Self::TimelockNotExpired, - 8 => Self::InvalidNativeTokens, - 9 => Self::StorageDepositReturnUnfulfilled, - 10 => Self::InvalidInputUnlock, - 11 => Self::SenderNotUnlocked, - 12 => Self::InvalidChainStateTransition, - 13 => Self::InvalidTransactionIssuingTime, - 14 => Self::InvalidManaAmount, - 15 => Self::InvalidBlockIssuanceCreditsAmount, - 16 => Self::InvalidRewardContextInput, - 17 => Self::InvalidCommitmentContextInput, - 18 => Self::MissingStakingFeature, - 19 => Self::FailedToClaimStakingReward, - 20 => Self::FailedToClaimDelegationReward, - 21 => Self::TransactionCapabilityNativeTokenBurningNotAllowed, - 22 => Self::TransactionCapabilityManaBurningNotAllowed, - 23 => Self::TransactionCapabilityAccountDestructionNotAllowed, - 24 => Self::TransactionCapabilityAnchorDestructionNotAllowed, - 25 => Self::TransactionCapabilityFoundryDestructionNotAllowed, - 26 => Self::TransactionCapabilityNftDestructionNotAllowed, - 255 => Self::SemanticValidationFailed, - x => return Err(Self::Error::InvalidTransactionFailureReason(x)), - }) - } -} - /// pub struct SemanticValidationContext<'a> { pub(crate) transaction: &'a Transaction, @@ -345,17 +178,7 @@ impl<'a> SemanticValidationContext<'a> { return Ok(Some(TransactionFailureReason::InvalidInputUnlock)); } - let unlock = &unlocks[index]; - let conflict = match consumed_output { - Output::Basic(output) => output.unlock(output_id, unlock, &mut self), - Output::Account(output) => output.unlock(output_id, unlock, &mut self), - Output::Anchor(_) => return Err(Error::UnsupportedOutputKind(AnchorOutput::KIND)), - Output::Foundry(output) => output.unlock(output_id, unlock, &mut self), - Output::Nft(output) => output.unlock(output_id, unlock, &mut self), - Output::Delegation(output) => output.unlock(output_id, unlock, &mut self), - }; - - if let Err(conflict) = conflict { + if let Err(conflict) = self.output_unlock(&consumed_output, &output_id, &unlocks[index]) { return Ok(Some(conflict)); } } diff --git a/sdk/src/types/block/semantic/unlock.rs b/sdk/src/types/block/semantic/unlock.rs new file mode 100644 index 0000000000..814848e448 --- /dev/null +++ b/sdk/src/types/block/semantic/unlock.rs @@ -0,0 +1,177 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::block::{ + address::Address, + output::{Output, OutputId}, + semantic::{SemanticValidationContext, TransactionFailureReason}, + signature::Signature, + unlock::Unlock, +}; + +impl SemanticValidationContext<'_> { + /// + pub fn address_unlock(&mut self, address: &Address, unlock: &Unlock) -> Result<(), TransactionFailureReason> { + match (address, unlock) { + (Address::Ed25519(ed25519_address), Unlock::Signature(unlock)) => { + if self.unlocked_addresses.contains(address) { + return Err(TransactionFailureReason::InvalidInputUnlock); + } + + let Signature::Ed25519(signature) = unlock.signature(); + + if signature + .is_valid(self.transaction_signing_hash.as_ref(), ed25519_address) + .is_err() + { + return Err(TransactionFailureReason::InvalidUnlockBlockSignature); + } + + self.unlocked_addresses.insert(address.clone()); + } + (Address::Ed25519(_), Unlock::Reference(_)) => { + // TODO actually check that it was unlocked by the same signature. + if !self.unlocked_addresses.contains(address) { + return Err(TransactionFailureReason::InvalidInputUnlock); + } + } + (Address::Account(account_address), Unlock::Account(unlock)) => { + // PANIC: indexing is fine as it is already syntactically verified that indexes reference below. + if let (output_id, Output::Account(account_output)) = self.inputs[unlock.index() as usize] { + if &account_output.account_id_non_null(output_id) != account_address.account_id() { + return Err(TransactionFailureReason::InvalidInputUnlock); + } + if !self.unlocked_addresses.contains(address) { + return Err(TransactionFailureReason::InvalidInputUnlock); + } + } else { + return Err(TransactionFailureReason::InvalidInputUnlock); + } + } + (Address::Nft(nft_address), Unlock::Nft(unlock)) => { + // PANIC: indexing is fine as it is already syntactically verified that indexes reference below. + if let (output_id, Output::Nft(nft_output)) = self.inputs[unlock.index() as usize] { + if &nft_output.nft_id_non_null(output_id) != nft_address.nft_id() { + return Err(TransactionFailureReason::InvalidInputUnlock); + } + if !self.unlocked_addresses.contains(address) { + return Err(TransactionFailureReason::InvalidInputUnlock); + } + } else { + return Err(TransactionFailureReason::InvalidInputUnlock); + } + } + // TODO maybe shouldn't be a semantic error but this function currently returns a TransactionFailureReason. + (Address::Anchor(_), _) => return Err(TransactionFailureReason::SemanticValidationFailed), + (Address::ImplicitAccountCreation(implicit_account_creation_address), _) => { + return self.address_unlock( + &Address::from(*implicit_account_creation_address.ed25519_address()), + unlock, + ); + } + (Address::Multi(multi_address), Unlock::Multi(unlock)) => { + if multi_address.len() != unlock.len() { + return Err(TransactionFailureReason::InvalidInputUnlock); + } + + let mut cumulative_unlocked_weight = 0u16; + + for (address, unlock) in multi_address.addresses().iter().zip(unlock.unlocks()) { + if !unlock.is_empty() { + self.address_unlock(address, unlock)?; + cumulative_unlocked_weight += address.weight() as u16; + } + } + + if cumulative_unlocked_weight < multi_address.threshold() { + return Err(TransactionFailureReason::InvalidInputUnlock); + } + } + (Address::Restricted(restricted_address), _) => { + return self.address_unlock(restricted_address.address(), unlock); + } + _ => return Err(TransactionFailureReason::InvalidInputUnlock), + } + + Ok(()) + } + + pub fn output_unlock( + &mut self, + output: &Output, + output_id: &OutputId, + unlock: &Unlock, + ) -> Result<(), TransactionFailureReason> { + match output { + Output::Basic(output) => { + let slot_index = self + .transaction + .context_inputs() + .iter() + .find_map(|c| c.as_commitment_opt().map(|c| c.slot_index())); + let locked_address = output + .unlock_conditions() + .locked_address( + output.address(), + slot_index, + self.protocol_parameters.committable_age_range(), + ) + .map_err(|_| TransactionFailureReason::InvalidCommitmentContextInput)? + // because of expiration the input can't be unlocked at this time + .ok_or(TransactionFailureReason::SemanticValidationFailed)?; + + self.address_unlock(locked_address, unlock)?; + } + Output::Account(output) => { + let locked_address = output + .unlock_conditions() + .locked_address(output.address(), None, self.protocol_parameters.committable_age_range()) + // Safe to unwrap, AccountOutput can't have an expiration unlock condition. + .unwrap() + .unwrap(); + + self.address_unlock(locked_address, unlock)?; + + self.unlocked_addresses + .insert(Address::from(output.account_id_non_null(output_id))); + } + Output::Anchor(_) => panic!(), + // Output::Anchor(_) => return Err(Error::UnsupportedOutputKind(AnchorOutput::KIND)), + Output::Foundry(output) => self.address_unlock(&Address::from(*output.account_address()), unlock)?, + Output::Nft(output) => { + let slot_index = self + .transaction + .context_inputs() + .iter() + .find_map(|c| c.as_commitment_opt().map(|c| c.slot_index())); + let locked_address = output + .unlock_conditions() + .locked_address( + output.address(), + slot_index, + self.protocol_parameters.committable_age_range(), + ) + .map_err(|_| TransactionFailureReason::InvalidCommitmentContextInput)? + // because of expiration the input can't be unlocked at this time + .ok_or(TransactionFailureReason::SemanticValidationFailed)?; + + self.address_unlock(locked_address, unlock)?; + + self.unlocked_addresses + .insert(Address::from(output.nft_id_non_null(output_id))); + } + Output::Delegation(output) => { + let locked_address: &Address = output + .unlock_conditions() + .locked_address(output.address(), None, self.protocol_parameters.committable_age_range()) + // Safe to unwrap, DelegationOutput can't have an expiration unlock condition. + .unwrap() + .unwrap(); + + self.address_unlock(locked_address, unlock)?; + } + } + + Ok(()) + } +} diff --git a/sdk/tests/client/input_selection/basic_outputs.rs b/sdk/tests/client/input_selection/basic_outputs.rs index 4fac367de0..fd1a6440e9 100644 --- a/sdk/tests/client/input_selection/basic_outputs.rs +++ b/sdk/tests/client/input_selection/basic_outputs.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use iota_sdk::{ client::api::input_selection::{Error, InputSelection, Requirement}, types::block::{ - address::{AccountAddress, Address, MultiAddress, NftAddress, RestrictedAddress, WeightedAddress}, + address::{Address, MultiAddress, RestrictedAddress, WeightedAddress}, output::{AccountId, NftId}, protocol::protocol_parameters, }, @@ -823,7 +823,7 @@ fn account_sender_zero_id() { 2_000_000, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), None, - Some(Address::from(AccountAddress::from(account_id))), + Some(Address::from(account_id)), None, None, None, @@ -1011,7 +1011,7 @@ fn nft_sender_zero_id() { 2_000_000, Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), None, - Some(Address::from(NftAddress::from(nft_id))), + Some(Address::from(nft_id)), None, None, None,