Skip to content

Commit

Permalink
Add MultiUnlock (#1590)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Update sdk/src/types/block/unlock/empty.rs

Co-authored-by: Thoralf-M <[email protected]>

* Fmt

* Comment and rename

* Move import to dto

* Order

* review

---------

Co-authored-by: Thoralf-M <[email protected]>
Co-authored-by: DaughterOfMars <[email protected]>
  • Loading branch information
3 people authored Nov 9, 2023
1 parent b0660db commit b1762a3
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 37 deletions.
24 changes: 21 additions & 3 deletions sdk/src/types/block/address/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand Down Expand Up @@ -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),
}
Expand Down
8 changes: 5 additions & 3 deletions sdk/src/types/block/address/multi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -76,9 +77,10 @@ fn verify_weight<const VERIFY: bool>(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<WeightedAddress, WeightedAddressCount>,
/// The threshold that needs to be reached by the unlocked addresses in order to unlock the multi address.
threshold: u16,
Expand Down
6 changes: 5 additions & 1 deletion sdk/src/types/block/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -78,6 +78,8 @@ pub enum Error {
threshold: u16,
},
InvalidWeightedAddressCount(<WeightedAddressCount as TryFrom<usize>>::Error),
InvalidMultiUnlockCount(<UnlocksCount as TryFrom<usize>>::Error),
MultiUnlockRecursion,
WeightedAddressesNotUniqueSorted,
InvalidContextInputKind(u8),
InvalidContextInputCount(<ContextInputCount as TryFrom<usize>>::Error),
Expand Down Expand Up @@ -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")
}
Expand Down
43 changes: 43 additions & 0 deletions sdk/src/types/block/unlock/empty.rs
Original file line number Diff line number Diff line change
@@ -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<EmptyUnlockDto> for EmptyUnlock {
type Error = Error;

fn try_from(_: EmptyUnlockDto) -> Result<Self, Self::Error> {
Ok(Self)
}
}

crate::impl_serde_typed_dto!(EmptyUnlock, EmptyUnlockDto, "empty unlock");
}
95 changes: 65 additions & 30 deletions sdk/src/types/block/unlock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

mod account;
mod anchor;
mod empty;
mod multi;
mod nft;
mod reference;
mod signature;
Expand All @@ -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},
Expand Down Expand Up @@ -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<SignatureUnlock> for Unlock {
Expand All @@ -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),
}
}
}
Expand All @@ -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() }>;
Expand Down Expand Up @@ -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<const VERIFY: bool>(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)?,
}
}
}
Expand Down
79 changes: 79 additions & 0 deletions sdk/src/types/block/unlock/multi.rs
Original file line number Diff line number Diff line change
@@ -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<Unlock, UnlocksCount>);

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<Item = Unlock>) -> Result<Self, Error> {
let unlocks = unlocks.into_iter().collect::<Box<[_]>>();

verify_unlocks::<true>(&unlocks, &())?;

Ok(Self(
BoxedSlicePrefix::<Unlock, UnlocksCount>::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<const VERIFY: bool>(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<Unlock>,
}

impl From<&MultiUnlock> for MultiUnlockDto {
fn from(value: &MultiUnlock) -> Self {
Self {
kind: MultiUnlock::KIND,
unlocks: value.0.to_vec(),
}
}
}

impl TryFrom<MultiUnlockDto> for MultiUnlock {
type Error = Error;

fn try_from(value: MultiUnlockDto) -> Result<Self, Self::Error> {
Self::new(value.unlocks)
}
}

crate::impl_serde_typed_dto!(MultiUnlock, MultiUnlockDto, "multi unlock");
}

0 comments on commit b1762a3

Please sign in to comment.