Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MultiUnlock #1590

Merged
merged 27 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e1f123b
Add MultiUnlock
thibault-martinez Nov 7, 2023
eb2036c
Add EmptyUnlock
thibault-martinez Nov 7, 2023
146dc22
Merge branch '2.0' into multi-unlock
thibault-martinez Nov 7, 2023
0fcefff
Make WeightedAddress public
thibault-martinez Nov 7, 2023
5d58545
Merge branch '2.0' into multi-unlock
thibault-martinez Nov 9, 2023
b615263
Fix packable compilation issue
thibault-martinez Nov 9, 2023
c5c4cf5
Nits
thibault-martinez Nov 9, 2023
17df149
Nit
thibault-martinez Nov 9, 2023
f048f69
return errors
thibault-martinez Nov 9, 2023
7a3b28c
Remove variable
thibault-martinez Nov 9, 2023
f1f01ef
Derive deref
thibault-martinez Nov 9, 2023
6519e53
Typo
thibault-martinez Nov 9, 2023
8f9d038
More deref
thibault-martinez Nov 9, 2023
0c7a13a
UnlocksCount = WeightedAddressCount
thibault-martinez Nov 9, 2023
1523fa9
nit
thibault-martinez Nov 9, 2023
86e03fc
Fix verify_unlocks
thibault-martinez Nov 9, 2023
9fb200e
no_std
thibault-martinez Nov 9, 2023
2a839bb
Update sdk/src/types/block/unlock/multi.rs
thibault-martinez Nov 9, 2023
871d30e
Update sdk/src/types/block/unlock/empty.rs
thibault-martinez Nov 9, 2023
de0eaef
Merge branch '2.0' into multi-unlock
thibault-martinez Nov 9, 2023
34673f7
Fmt
thibault-martinez Nov 9, 2023
7bf436c
Comment and rename
thibault-martinez Nov 9, 2023
55f1172
Move import to dto
thibault-martinez Nov 9, 2023
470f87a
Merge branch '2.0' into multi-unlock
thibault-martinez Nov 9, 2023
5502da1
Order
thibault-martinez Nov 9, 2023
a029910
review
thibault-martinez Nov 9, 2023
4a23ae7
Merge branch '2.0' into multi-unlock
Nov 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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),
Thoralf-M marked this conversation as resolved.
Show resolved Hide resolved
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");
}
Loading