Skip to content

Commit

Permalink
Add MultiAddress (#1573)
Browse files Browse the repository at this point in the history
* Add MultiAddress

* Fix common feature set CI

* Add verify_weight

* Add verify_address

* Docs

* Add verify_threshold

* Add impl Packable for MultiAddress

* Bring back Address Debug

* Nit

* Add verify_cumulative_weight

* Add getters

* Add verify_addresses

* Error nits

* Allow anchor

* Nits

* MultiAddress Display

* Fix common feature sets CI

* Update sdk/src/types/block/address/multi.rs

Co-authored-by: DaughterOfMars <[email protected]>

* Use chain

* Use impl IntoIterator<Item = WeightedAddress>

* Impl better display

* Remove print

* Fix common features set

* Update sdk/src/types/block/address/bech32.rs

Co-authored-by: /alex/ <[email protected]>

* document unwrap

---------

Co-authored-by: DaughterOfMars <[email protected]>
Co-authored-by: /alex/ <[email protected]>
  • Loading branch information
3 people authored Nov 7, 2023
1 parent da32fad commit d2e73c9
Show file tree
Hide file tree
Showing 7 changed files with 343 additions and 7 deletions.
1 change: 1 addition & 0 deletions sdk/src/client/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub fn bech32_to_hex(bech32: impl ConvertTo<Bech32Address>) -> Result<String> {
Address::Nft(nft) => nft.to_string(),
Address::Anchor(anchor) => anchor.to_string(),
Address::ImplicitAccountCreation(implicit) => implicit.to_string(),
Address::Multi(multi) => multi.to_string(),
Address::Restricted(restricted) => restricted.to_string(),
})
}
Expand Down
21 changes: 15 additions & 6 deletions sdk/src/types/block/address/bech32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use alloc::{
};
use core::str::FromStr;

use crypto::hashes::{blake2b::Blake2b256, Digest};
use derive_more::{AsRef, Deref, Display};
use packable::{
error::{UnpackError, UnpackErrorExt},
Expand All @@ -15,7 +16,10 @@ use packable::{
Packable, PackableExt,
};

use crate::types::block::{address::Address, ConvertTo, Error};
use crate::types::block::{
address::{Address, MultiAddress},
ConvertTo, Error,
};

#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deref, Display)]
#[repr(transparent)]
Expand Down Expand Up @@ -172,11 +176,16 @@ impl Bech32Address {

impl core::fmt::Display for Bech32Address {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}",
bech32::encode::<bech32::Bech32>(self.hrp.0, &self.inner.pack_to_vec(),).unwrap()
)
let bytes = if self.inner.is_multi() {
core::iter::once(MultiAddress::KIND)
.chain(Blake2b256::digest(self.inner.pack_to_vec()))
.collect()
} else {
self.inner.pack_to_vec()
};

// PANIC: unwrap is fine as the Bech32Address has been validated at construction.
write!(f, "{}", bech32::encode::<bech32::Bech32>(self.hrp.0, &bytes).unwrap())
}
}

Expand Down
10 changes: 9 additions & 1 deletion sdk/src/types/block/address/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod anchor;
mod bech32;
mod ed25519;
mod implicit_account_creation;
mod multi;
mod nft;
mod restricted;

Expand All @@ -14,12 +15,14 @@ use alloc::boxed::Box;
use derive_more::{Display, From};
use packable::Packable;

pub(crate) use self::multi::WeightedAddressCount;
pub use self::{
account::AccountAddress,
anchor::AnchorAddress,
bech32::{Bech32Address, Hrp},
ed25519::Ed25519Address,
implicit_account_creation::ImplicitAccountCreationAddress,
multi::MultiAddress,
nft::NftAddress,
restricted::{AddressCapabilities, AddressCapabilityFlag, RestrictedAddress},
};
Expand Down Expand Up @@ -52,6 +55,9 @@ pub enum Address {
/// An implicit account creation address.
#[packable(tag = ImplicitAccountCreationAddress::KIND)]
ImplicitAccountCreation(ImplicitAccountCreationAddress),
/// A multi address.
#[packable(tag = MultiAddress::KIND)]
Multi(MultiAddress),
/// An address with restricted capabilities.
#[packable(tag = RestrictedAddress::KIND)]
#[from(ignore)]
Expand All @@ -72,6 +78,7 @@ impl core::fmt::Debug for Address {
Self::Nft(address) => address.fmt(f),
Self::Anchor(address) => address.fmt(f),
Self::ImplicitAccountCreation(address) => address.fmt(f),
Self::Multi(address) => address.fmt(f),
Self::Restricted(address) => address.fmt(f),
}
}
Expand All @@ -86,11 +93,12 @@ impl Address {
Self::Nft(_) => NftAddress::KIND,
Self::Anchor(_) => AnchorAddress::KIND,
Self::ImplicitAccountCreation(_) => ImplicitAccountCreationAddress::KIND,
Self::Multi(_) => MultiAddress::KIND,
Self::Restricted(_) => RestrictedAddress::KIND,
}
}

crate::def_is_as_opt!(Address: Ed25519, Account, Nft, Anchor, ImplicitAccountCreation, Restricted);
crate::def_is_as_opt!(Address: Ed25519, Account, Nft, Anchor, ImplicitAccountCreation, Multi, Restricted);

/// Tries to create an [`Address`] from a bech32 encoded string.
pub fn try_from_bech32(address: impl AsRef<str>) -> Result<Self, Error> {
Expand Down
236 changes: 236 additions & 0 deletions sdk/src/types/block/address/multi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
// Copyright 2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use alloc::{boxed::Box, string::ToString, vec::Vec};
use core::{fmt, ops::RangeInclusive};

use derive_more::{AsRef, Display, From};
use iterator_sorted::is_unique_sorted;
use packable::{
bounded::BoundedU8,
error::{UnpackError, UnpackErrorExt},
packer::Packer,
prefix::BoxedSlicePrefix,
unpacker::Unpacker,
Packable,
};

use crate::types::block::{address::Address, Error};

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)]
#[display(fmt = "{address}")]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WeightedAddress {
/// The unlocked address.
#[packable(verify_with = verify_address)]
address: Address,
/// The weight of the unlocked address.
#[packable(verify_with = verify_weight)]
weight: u8,
}

impl WeightedAddress {
/// Creates a new [`WeightedAddress`].
pub fn new(address: Address, weight: u8) -> Result<WeightedAddress, Error> {
verify_address::<true>(&address, &())?;
verify_weight::<true>(&weight, &())?;

Ok(Self { address, weight })
}

/// Returns the address of the [`WeightedAddress`].
pub fn address(&self) -> &Address {
&self.address
}

/// Returns the weight of the [`WeightedAddress`].
pub fn weight(&self) -> u8 {
self.weight
}
}

fn verify_address<const VERIFY: bool>(address: &Address, _visitor: &()) -> Result<(), Error> {
if VERIFY {
if !matches!(
address,
Address::Ed25519(_) | Address::Account(_) | Address::Nft(_) | Address::Anchor(_)
) {
return Err(Error::InvalidAddressKind(address.kind()));
}
}
Ok(())
}

fn verify_weight<const VERIFY: bool>(weight: &u8, _visitor: &()) -> Result<(), Error> {
if VERIFY && *weight == 0 {
return Err(Error::InvalidAddressWeight(*weight));
} else {
Ok(())
}
}

/// 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)]
pub struct MultiAddress {
/// The weighted unlocked addresses.
addresses: BoxedSlicePrefix<WeightedAddress, WeightedAddressCount>,
/// The threshold that needs to be reached by the unlocked addresses in order to unlock the multi address.
threshold: u16,
}

impl MultiAddress {
/// The [`Address`](crate::types::block::address::Address) kind of a [`MultiAddress`].
pub const KIND: u8 = 40;
/// The allowed range of inner [`Address`]es.
pub const ADDRESSES_COUNT: RangeInclusive<u8> = 1..=10;

/// Creates a new [`MultiAddress`].
#[inline(always)]
pub fn new(addresses: impl IntoIterator<Item = WeightedAddress>, threshold: u16) -> Result<Self, Error> {
let addresses = addresses.into_iter().collect::<Box<[_]>>();

verify_addresses::<true>(&addresses, &())?;
verify_threshold::<true>(&threshold, &())?;

let addresses = BoxedSlicePrefix::<WeightedAddress, WeightedAddressCount>::try_from(addresses)
.map_err(Error::InvalidWeightedAddressCount)?;

verify_cumulative_weight::<true>(&addresses, &threshold, &())?;

Ok(Self { addresses, threshold })
}

/// Returns the addresses of a [`MultiAddress`].
#[inline(always)]
pub fn addresses(&self) -> &[WeightedAddress] {
&self.addresses
}

/// Returns the threshold of a [`MultiAddress`].
#[inline(always)]
pub fn threshold(&self) -> u16 {
self.threshold
}
}

impl Packable for MultiAddress {
type UnpackError = Error;
type UnpackVisitor = ();

#[inline]
fn pack<P: Packer>(&self, packer: &mut P) -> Result<(), P::Error> {
self.addresses.pack(packer)?;
self.threshold.pack(packer)?;

Ok(())
}

#[inline]
fn unpack<U: Unpacker, const VERIFY: bool>(
unpacker: &mut U,
visitor: &Self::UnpackVisitor,
) -> Result<Self, UnpackError<Self::UnpackError, U::Error>> {
let addresses =
BoxedSlicePrefix::<WeightedAddress, WeightedAddressCount>::unpack::<_, VERIFY>(unpacker, visitor)
.map_packable_err(|e| e.unwrap_item_err_or_else(|e| Error::InvalidWeightedAddressCount(e.into())))?;

verify_addresses::<VERIFY>(&addresses, &()).map_err(UnpackError::Packable)?;

let threshold = u16::unpack::<_, VERIFY>(unpacker, visitor).coerce()?;

verify_threshold::<VERIFY>(&threshold, &()).map_err(UnpackError::Packable)?;
verify_cumulative_weight::<VERIFY>(&addresses, &threshold, &()).map_err(UnpackError::Packable)?;

Ok(Self { addresses, threshold })
}
}

fn verify_addresses<const VERIFY: bool>(addresses: &[WeightedAddress], _visitor: &()) -> Result<(), Error> {
if VERIFY && !is_unique_sorted(addresses.iter().map(WeightedAddress::address)) {
return Err(Error::WeightedAddressesNotUniqueSorted);
} else {
Ok(())
}
}

fn verify_threshold<const VERIFY: bool>(threshold: &u16, _visitor: &()) -> Result<(), Error> {
if VERIFY && *threshold == 0 {
return Err(Error::InvalidMultiAddressThreshold(*threshold));
} else {
Ok(())
}
}

fn verify_cumulative_weight<const VERIFY: bool>(
addresses: &[WeightedAddress],
threshold: &u16,
_visitor: &(),
) -> Result<(), Error> {
if VERIFY {
let cumulative_weight = addresses.iter().map(|address| address.weight as u16).sum::<u16>();

if cumulative_weight < *threshold {
return Err(Error::InvalidMultiAddressCumulativeWeight {
cumulative_weight,
threshold: *threshold,
});
}
}
Ok(())
}

impl fmt::Display for MultiAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}]",
self.addresses()
.iter()
.map(|address| address.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
}

#[cfg(feature = "serde")]
mod dto {
use serde::{Deserialize, Serialize};

use super::*;

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MultiAddressDto {
#[serde(rename = "type")]
kind: u8,
addresses: Vec<WeightedAddress>,
threshold: u16,
}

impl From<&MultiAddress> for MultiAddressDto {
fn from(value: &MultiAddress) -> Self {
Self {
kind: MultiAddress::KIND,
addresses: value.addresses.to_vec(),
threshold: value.threshold,
}
}
}

impl TryFrom<MultiAddressDto> for MultiAddress {
type Error = Error;

fn try_from(value: MultiAddressDto) -> Result<Self, Self::Error> {
Self::new(value.addresses, value.threshold)
}
}

crate::impl_serde_typed_dto!(MultiAddress, MultiAddressDto, "multi address");
}
24 changes: 24 additions & 0 deletions sdk/src/types/block/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use primitive_types::U256;

use super::slot::EpochIndex;
use crate::types::block::{
address::WeightedAddressCount,
context_input::RewardContextInputIndex,
input::UtxoInput,
mana::ManaAllotmentCount,
Expand Down Expand Up @@ -70,6 +71,14 @@ pub enum Error {
deposit: u64,
required: u64,
},
InvalidAddressWeight(u8),
InvalidMultiAddressThreshold(u16),
InvalidMultiAddressCumulativeWeight {
cumulative_weight: u16,
threshold: u16,
},
InvalidWeightedAddressCount(<WeightedAddressCount as TryFrom<usize>>::Error),
WeightedAddressesNotUniqueSorted,
InvalidContextInputKind(u8),
InvalidContextInputCount(<ContextInputCount as TryFrom<usize>>::Error),
InvalidFeatureCount(<FeatureCount as TryFrom<usize>>::Error),
Expand Down Expand Up @@ -261,6 +270,21 @@ impl fmt::Display for Error {
"storage deposit return of {deposit} exceeds the original output amount of {amount}"
),
Self::InvalidContextInputCount(count) => write!(f, "invalid context input count: {count}"),
Self::InvalidAddressWeight(w) => write!(f, "invalid address weight: {w}"),
Self::InvalidMultiAddressThreshold(t) => write!(f, "invalid multi address threshold: {t}"),
Self::InvalidMultiAddressCumulativeWeight {
cumulative_weight,
threshold,
} => {
write!(
f,
"invalid multi address cumulative weight {cumulative_weight} < threshold {threshold}"
)
}
Self::InvalidWeightedAddressCount(count) => write!(f, "invalid weighted address count: {count}"),
Self::WeightedAddressesNotUniqueSorted => {
write!(f, "weighted addresses are not unique and/or sorted")
}
Self::InvalidContextInputKind(k) => write!(f, "invalid context input kind: {k}"),
Self::InvalidFeatureCount(count) => write!(f, "invalid feature count: {count}"),
Self::InvalidFeatureKind(k) => write!(f, "invalid feature kind: {k}"),
Expand Down
1 change: 1 addition & 0 deletions sdk/tests/types/address/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
mod account;
mod bech32;
mod ed25519;
mod multi;
mod nft;
mod restricted;

Expand Down
Loading

0 comments on commit d2e73c9

Please sign in to comment.