From b1762a30ed39be7ea3ed6d3718cb28fbc2af1ecb Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Thu, 9 Nov 2023 15:48:09 +0100 Subject: [PATCH] Add MultiUnlock (#1590) * Add MultiUnlock * Add EmptyUnlock * Make WeightedAddress public * Fix packable compilation issue * Nits * Nit * return errors * Remove variable * Derive deref * Typo * More deref * UnlocksCount = WeightedAddressCount * nit * Fix verify_unlocks * no_std * Update sdk/src/types/block/unlock/multi.rs Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> * Update sdk/src/types/block/unlock/empty.rs Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> * Fmt * Comment and rename * Move import to dto * Order * review --------- Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Co-authored-by: DaughterOfMars --- sdk/src/types/block/address/mod.rs | 24 ++++++- sdk/src/types/block/address/multi.rs | 8 ++- sdk/src/types/block/error.rs | 6 +- sdk/src/types/block/unlock/empty.rs | 43 +++++++++++++ sdk/src/types/block/unlock/mod.rs | 95 +++++++++++++++++++--------- sdk/src/types/block/unlock/multi.rs | 79 +++++++++++++++++++++++ 6 files changed, 218 insertions(+), 37 deletions(-) create mode 100644 sdk/src/types/block/unlock/empty.rs create mode 100644 sdk/src/types/block/unlock/multi.rs diff --git a/sdk/src/types/block/address/mod.rs b/sdk/src/types/block/address/mod.rs index 96f2bda0ca..4857bf4093 100644 --- a/sdk/src/types/block/address/mod.rs +++ b/sdk/src/types/block/address/mod.rs @@ -22,7 +22,7 @@ pub use self::{ bech32::{Bech32Address, Hrp}, ed25519::Ed25519Address, implicit_account_creation::ImplicitAccountCreationAddress, - multi::MultiAddress, + multi::{MultiAddress, WeightedAddress}, nft::NftAddress, restricted::{AddressCapabilities, AddressCapabilityFlag, RestrictedAddress}, }; @@ -168,8 +168,26 @@ impl Address { } // TODO maybe shouldn't be a semantic error but this function currently returns a TransactionFailureReason. (Self::Anchor(_), _) => return Err(TransactionFailureReason::SemanticValidationFailed), - (Self::ImplicitAccountCreation(address), _) => { - return Self::from(*address.ed25519_address()).unlock(unlock, context); + (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); + } } _ => return Err(TransactionFailureReason::InvalidInputUnlock), } diff --git a/sdk/src/types/block/address/multi.rs b/sdk/src/types/block/address/multi.rs index 4402dce12c..36fdc297a0 100644 --- a/sdk/src/types/block/address/multi.rs +++ b/sdk/src/types/block/address/multi.rs @@ -4,7 +4,7 @@ use alloc::{boxed::Box, string::ToString, vec::Vec}; use core::{fmt, ops::RangeInclusive}; -use derive_more::{AsRef, Display, From}; +use derive_more::{AsRef, Deref, Display, From}; use iterator_sorted::is_unique_sorted; use packable::{ bounded::BoundedU8, @@ -21,11 +21,12 @@ pub(crate) type WeightedAddressCount = BoundedU8<{ *MultiAddress::ADDRESSES_COUNT.start() }, { *MultiAddress::ADDRESSES_COUNT.end() }>; /// An address with an assigned weight. -#[derive(Clone, Debug, Display, Eq, PartialEq, Ord, PartialOrd, Hash, From, AsRef, Packable)] +#[derive(Clone, Debug, Display, Eq, PartialEq, Ord, PartialOrd, Hash, From, AsRef, Deref, Packable)] #[display(fmt = "{address}")] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct WeightedAddress { /// The unlocked address. + #[deref] #[packable(verify_with = verify_address)] address: Address, /// The weight of the unlocked address. @@ -76,9 +77,10 @@ fn verify_weight(weight: &u8, _visitor: &()) -> Result<(), E /// An address that consists of addresses with weights and a threshold value. /// The Multi Address can be unlocked if the cumulative weight of all unlocked addresses is equal to or exceeds the /// threshold. -#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Clone, Debug, Deref, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct MultiAddress { /// The weighted unlocked addresses. + #[deref] addresses: BoxedSlicePrefix, /// The threshold that needs to be reached by the unlocked addresses in order to unlock the multi address. threshold: u16, diff --git a/sdk/src/types/block/error.rs b/sdk/src/types/block/error.rs index 17a6023d38..7fb39b3728 100644 --- a/sdk/src/types/block/error.rs +++ b/sdk/src/types/block/error.rs @@ -23,7 +23,7 @@ use crate::types::block::{ }, payload::{ContextInputCount, InputCount, OutputCount, TagLength, TaggedDataLength}, protocol::ProtocolParametersHash, - unlock::{UnlockCount, UnlockIndex}, + unlock::{UnlockCount, UnlockIndex, UnlocksCount}, }; /// Error occurring when creating/parsing/validating blocks. @@ -78,6 +78,8 @@ pub enum Error { threshold: u16, }, InvalidWeightedAddressCount(>::Error), + InvalidMultiUnlockCount(>::Error), + MultiUnlockRecursion, WeightedAddressesNotUniqueSorted, InvalidContextInputKind(u8), InvalidContextInputCount(>::Error), @@ -282,6 +284,8 @@ impl fmt::Display for Error { ) } Self::InvalidWeightedAddressCount(count) => write!(f, "invalid weighted address count: {count}"), + Self::InvalidMultiUnlockCount(count) => write!(f, "invalid multi unlock count: {count}"), + Self::MultiUnlockRecursion => write!(f, "multi unlock recursion"), Self::WeightedAddressesNotUniqueSorted => { write!(f, "weighted addresses are not unique and/or sorted") } diff --git a/sdk/src/types/block/unlock/empty.rs b/sdk/src/types/block/unlock/empty.rs new file mode 100644 index 0000000000..a4b89ff15a --- /dev/null +++ b/sdk/src/types/block/unlock/empty.rs @@ -0,0 +1,43 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/// Used to maintain correct index relationship between addresses and signatures when unlocking a +/// [`MultiAddress`](crate::types::block::address::MultiAddress) where not all addresses are unlocked. +#[derive(Clone, Debug, Eq, PartialEq, Hash, packable::Packable)] +pub struct EmptyUnlock; + +impl EmptyUnlock { + /// The [`Unlock`](crate::types::block::unlock::Unlock) kind of an [`EmptyUnlock`]. + pub const KIND: u8 = 6; +} + +mod dto { + use serde::{Deserialize, Serialize}; + + use super::*; + use crate::types::block::Error; + + #[derive(Serialize, Deserialize)] + struct EmptyUnlockDto { + #[serde(rename = "type")] + kind: u8, + } + + impl From<&EmptyUnlock> for EmptyUnlockDto { + fn from(_: &EmptyUnlock) -> Self { + Self { + kind: EmptyUnlock::KIND, + } + } + } + + impl TryFrom for EmptyUnlock { + type Error = Error; + + fn try_from(_: EmptyUnlockDto) -> Result { + Ok(Self) + } + } + + crate::impl_serde_typed_dto!(EmptyUnlock, EmptyUnlockDto, "empty unlock"); +} diff --git a/sdk/src/types/block/unlock/mod.rs b/sdk/src/types/block/unlock/mod.rs index 07e8c36273..fce07f8150 100644 --- a/sdk/src/types/block/unlock/mod.rs +++ b/sdk/src/types/block/unlock/mod.rs @@ -3,6 +3,8 @@ mod account; mod anchor; +mod empty; +mod multi; mod nft; mod reference; mod signature; @@ -14,9 +16,10 @@ use derive_more::{Deref, From}; use hashbrown::HashSet; use packable::{bounded::BoundedU16, prefix::BoxedSlicePrefix, Packable}; +pub(crate) use self::multi::UnlocksCount; pub use self::{ - account::AccountUnlock, anchor::AnchorUnlock, nft::NftUnlock, reference::ReferenceUnlock, - signature::SignatureUnlock, + account::AccountUnlock, anchor::AnchorUnlock, empty::EmptyUnlock, multi::MultiUnlock, nft::NftUnlock, + reference::ReferenceUnlock, signature::SignatureUnlock, }; use crate::types::block::{ input::{INPUT_COUNT_MAX, INPUT_COUNT_RANGE, INPUT_INDEX_MAX}, @@ -50,12 +53,18 @@ pub enum Unlock { /// An account unlock. #[packable(tag = AccountUnlock::KIND)] Account(AccountUnlock), - /// An Anchor unlock. + /// An anchor unlock. #[packable(tag = AnchorUnlock::KIND)] Anchor(AnchorUnlock), /// An NFT unlock. #[packable(tag = NftUnlock::KIND)] Nft(NftUnlock), + /// A multi unlock. + #[packable(tag = MultiUnlock::KIND)] + Multi(MultiUnlock), + /// An empty unlock. + #[packable(tag = EmptyUnlock::KIND)] + Empty(EmptyUnlock), } impl From for Unlock { @@ -72,6 +81,8 @@ impl core::fmt::Debug for Unlock { Self::Account(unlock) => unlock.fmt(f), Self::Anchor(unlock) => unlock.fmt(f), Self::Nft(unlock) => unlock.fmt(f), + Self::Multi(unlock) => unlock.fmt(f), + Self::Empty(unlock) => unlock.fmt(f), } } } @@ -85,10 +96,12 @@ impl Unlock { Self::Account(_) => AccountUnlock::KIND, Self::Anchor(_) => AnchorUnlock::KIND, Self::Nft(_) => NftUnlock::KIND, + Self::Multi(_) => MultiUnlock::KIND, + Self::Empty(_) => EmptyUnlock::KIND, } } - crate::def_is_as_opt!(Unlock: Signature, Reference, Account, Nft); + crate::def_is_as_opt!(Unlock: Signature, Reference, Account, Anchor, Nft, Multi, Empty); } pub(crate) type UnlockCount = BoundedU16<{ *UNLOCK_COUNT_RANGE.start() }, { *UNLOCK_COUNT_RANGE.end() }>; @@ -120,40 +133,62 @@ impl Unlocks { } } +/// Verifies the consistency of non-multi unlocks. +/// Will error on multi unlocks as they can't be nested. +fn verify_non_multi_unlock<'a>( + unlocks: &'a [Unlock], + unlock: &'a Unlock, + index: u16, + seen_signatures: &mut HashSet<&'a SignatureUnlock>, +) -> Result<(), Error> { + match unlock { + Unlock::Signature(signature) => { + if !seen_signatures.insert(signature.as_ref()) { + return Err(Error::DuplicateSignatureUnlock(index)); + } + } + Unlock::Reference(reference) => { + if index == 0 + || reference.index() >= index + || !matches!(unlocks[reference.index() as usize], Unlock::Signature(_)) + { + return Err(Error::InvalidUnlockReference(index)); + } + } + Unlock::Account(account) => { + if index == 0 || account.index() >= index { + return Err(Error::InvalidUnlockAccount(index)); + } + } + Unlock::Anchor(anchor) => { + if index == 0 || anchor.index() >= index { + return Err(Error::InvalidUnlockAnchor(index)); + } + } + Unlock::Nft(nft) => { + if index == 0 || nft.index() >= index { + return Err(Error::InvalidUnlockNft(index)); + } + } + Unlock::Multi(_) => return Err(Error::MultiUnlockRecursion), + Unlock::Empty(_) => {} + } + + Ok(()) +} + fn verify_unlocks(unlocks: &[Unlock], _: &()) -> Result<(), Error> { if VERIFY { let mut seen_signatures = HashSet::new(); for (index, unlock) in (0u16..).zip(unlocks.iter()) { match unlock { - Unlock::Signature(signature) => { - if !seen_signatures.insert(signature) { - return Err(Error::DuplicateSignatureUnlock(index)); - } - } - Unlock::Reference(reference) => { - if index == 0 - || reference.index() >= index - || !matches!(unlocks[reference.index() as usize], Unlock::Signature(_)) - { - return Err(Error::InvalidUnlockReference(index)); - } - } - Unlock::Account(account) => { - if index == 0 || account.index() >= index { - return Err(Error::InvalidUnlockAccount(index)); - } - } - Unlock::Anchor(anchor) => { - if index == 0 || anchor.index() >= index { - return Err(Error::InvalidUnlockAnchor(index)); - } - } - Unlock::Nft(nft) => { - if index == 0 || nft.index() >= index { - return Err(Error::InvalidUnlockNft(index)); + Unlock::Multi(multi) => { + for unlock in multi.unlocks() { + verify_non_multi_unlock(unlocks, unlock, index, &mut seen_signatures)? } } + _ => verify_non_multi_unlock(unlocks, unlock, index, &mut seen_signatures)?, } } } diff --git a/sdk/src/types/block/unlock/multi.rs b/sdk/src/types/block/unlock/multi.rs new file mode 100644 index 0000000000..024a9d8c88 --- /dev/null +++ b/sdk/src/types/block/unlock/multi.rs @@ -0,0 +1,79 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use alloc::{boxed::Box, vec::Vec}; + +use derive_more::Deref; +use packable::{prefix::BoxedSlicePrefix, Packable}; + +use crate::types::block::{address::WeightedAddressCount, unlock::Unlock, Error}; + +pub(crate) type UnlocksCount = WeightedAddressCount; + +/// Unlocks a [`MultiAddress`](crate::types::block::address::MultiAddress) with a list of other unlocks. +#[derive(Clone, Debug, Deref, Eq, PartialEq, Hash, Packable)] +#[packable(unpack_error = Error, with = |e| e.unwrap_item_err_or_else(|p| Error::InvalidMultiUnlockCount(p.into())))] +pub struct MultiUnlock(#[packable(verify_with = verify_unlocks)] BoxedSlicePrefix); + +impl MultiUnlock { + /// The [`Unlock`](crate::types::block::unlock::Unlock) kind of an [`MultiUnlock`]. + pub const KIND: u8 = 5; + + /// Creates a new [`MultiUnlock`]. + #[inline(always)] + pub fn new(unlocks: impl IntoIterator) -> Result { + let unlocks = unlocks.into_iter().collect::>(); + + verify_unlocks::(&unlocks, &())?; + + Ok(Self( + BoxedSlicePrefix::::try_from(unlocks).map_err(Error::InvalidMultiUnlockCount)?, + )) + } + + /// Return the inner unlocks of an [`MultiUnlock`]. + #[inline(always)] + pub fn unlocks(&self) -> &[Unlock] { + &self.0 + } +} + +fn verify_unlocks(unlocks: &[Unlock], _visitor: &()) -> Result<(), Error> { + if VERIFY && unlocks.iter().any(Unlock::is_multi) { + return Err(Error::MultiUnlockRecursion); + } else { + Ok(()) + } +} + +mod dto { + use serde::{Deserialize, Serialize}; + + use super::*; + + #[derive(Serialize, Deserialize)] + struct MultiUnlockDto { + #[serde(rename = "type")] + kind: u8, + unlocks: Vec, + } + + impl From<&MultiUnlock> for MultiUnlockDto { + fn from(value: &MultiUnlock) -> Self { + Self { + kind: MultiUnlock::KIND, + unlocks: value.0.to_vec(), + } + } + } + + impl TryFrom for MultiUnlock { + type Error = Error; + + fn try_from(value: MultiUnlockDto) -> Result { + Self::new(value.unlocks) + } + } + + crate::impl_serde_typed_dto!(MultiUnlock, MultiUnlockDto, "multi unlock"); +}