diff --git a/sdk/src/types/block/error.rs b/sdk/src/types/block/error.rs index aa6bcba79d..751270f18a 100644 --- a/sdk/src/types/block/error.rs +++ b/sdk/src/types/block/error.rs @@ -21,7 +21,7 @@ use crate::types::block::{ output::{ feature::{BlockIssuerKeyCount, FeatureCount}, unlock_condition::UnlockConditionCount, - AccountId, AnchorId, ChainId, MetadataFeatureKeyLength, MetadataFeatureLength, MetadataFeatureValueLength, + AccountId, AnchorId, ChainId, MetadataFeatureEntryCount, MetadataFeatureKeyLength, MetadataFeatureValueLength, NativeTokenCount, NftId, OutputIndex, TagFeatureLength, }, payload::{ @@ -110,7 +110,7 @@ pub enum Error { InvalidBlockLength(usize), InvalidManaValue(u64), InvalidMetadataFeature(String), - InvalidMetadataFeatureLength(>::Error), + InvalidMetadataFeatureEntryCount(>::Error), InvalidMetadataFeatureKeyLength(>::Error), InvalidMetadataFeatureValueLength(>::Error), InvalidNativeTokenCount(>::Error), @@ -316,8 +316,8 @@ impl fmt::Display for Error { Self::InvalidMetadataFeature(e) => { write!(f, "invalid metadata feature: {e}") } - Self::InvalidMetadataFeatureLength(length) => { - write!(f, "invalid metadata feature length: {length}") + Self::InvalidMetadataFeatureEntryCount(count) => { + write!(f, "invalid metadata feature entry count: {count}") } Self::InvalidMetadataFeatureKeyLength(length) => { write!(f, "invalid metadata feature key length: {length}") diff --git a/sdk/src/types/block/output/feature/metadata.rs b/sdk/src/types/block/output/feature/metadata.rs index e1280dfe22..f58c56c012 100644 --- a/sdk/src/types/block/output/feature/metadata.rs +++ b/sdk/src/types/block/output/feature/metadata.rs @@ -11,39 +11,78 @@ use core::ops::{Deref, RangeInclusive}; use packable::{ bounded::{BoundedU16, BoundedU8}, + error::{UnpackError, UnpackErrorExt}, + packer::Packer, prefix::{BTreeMapPrefix, BoxedSlicePrefix}, - PackableExt, + unpacker::{CounterUnpacker, Unpacker}, + Packable, PackableExt, }; use crate::types::block::{output::StorageScore, protocol::WorkScore, Error}; -pub(crate) type MetadataFeatureLength = - BoundedU16<{ *MetadataFeature::LENGTH_RANGE.start() }, { *MetadataFeature::LENGTH_RANGE.end() }>; - +pub(crate) type MetadataFeatureEntryCount = BoundedU8<1, { u8::MAX }>; pub(crate) type MetadataFeatureKeyLength = BoundedU8<1, { u8::MAX }>; pub(crate) type MetadataFeatureValueLength = BoundedU16<0, { u16::MAX }>; type MetadataBTreeMapPrefix = BTreeMapPrefix< BoxedSlicePrefix, BoxedSlicePrefix, - MetadataFeatureLength, + MetadataFeatureEntryCount, >; type MetadataBTreeMap = BTreeMap, BoxedSlicePrefix>; /// Defines metadata, arbitrary binary data, that will be stored in the output. -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, packable::Packable)] -#[packable(unpack_error = Error, with = |err| Error::InvalidMetadataFeature(err.to_string()))] +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct MetadataFeature( // Binary data. - #[packable(verify_with = verify_packable)] pub(crate) MetadataBTreeMapPrefix, + pub(crate) MetadataBTreeMapPrefix, ); -fn verify_packable(map: &MetadataBTreeMapPrefix) -> Result<(), Error> { - verify_keys_packable::(map)?; - verify_length_packable::(map)?; - Ok(()) +impl Packable for MetadataFeature { + type UnpackError = Error; + type UnpackVisitor = (); + + fn pack(&self, packer: &mut P) -> Result<(), P::Error> { + self.0.pack(packer)?; + + Ok(()) + } + + fn unpack( + unpacker: &mut U, + visitor: &Self::UnpackVisitor, + ) -> Result> { + let mut unpacker = CounterUnpacker::new(unpacker); + let start_opt = unpacker.read_bytes(); + + let map = MetadataBTreeMapPrefix::unpack::<_, VERIFY>(&mut unpacker, visitor) + .map_packable_err(|e| Error::InvalidMetadataFeature(e.to_string()))?; + + verify_keys_packable::(&map).map_err(UnpackError::Packable)?; + + let packed_len = if let (Some(start), Some(end)) = (start_opt, unpacker.read_bytes()) { + end - start + } else { + map.packed_len() + }; + + let packed_len = u16::try_from(packed_len).map_err(|_| { + UnpackError::Packable(Error::InvalidMetadataFeature(format!( + "Out of bounds byte length: {}", + packed_len + 1 + ))) + })? + 1; // +1 for the type byte + + if !Self::LENGTH_RANGE.contains(&(packed_len)) { + return Err(UnpackError::Packable(Error::InvalidMetadataFeature(format!( + "Out of bounds byte length: {packed_len}" + )))); + } + + Ok(Self(map)) + } } fn verify_keys_packable(map: &MetadataBTreeMapPrefix) -> Result<(), Error> { @@ -140,7 +179,7 @@ fn metadata_feature_from_iter(data: impl IntoIterator, Vec)> } Ok(MetadataFeature( - res.try_into().map_err(Error::InvalidMetadataFeatureLength)?, + res.try_into().map_err(Error::InvalidMetadataFeatureEntryCount)?, )) } diff --git a/sdk/src/types/block/output/feature/mod.rs b/sdk/src/types/block/output/feature/mod.rs index d07c2f4618..19faa7af65 100644 --- a/sdk/src/types/block/output/feature/mod.rs +++ b/sdk/src/types/block/output/feature/mod.rs @@ -22,7 +22,7 @@ pub use self::metadata::irc_27::{Attribute, Irc27Metadata}; pub use self::metadata::irc_30::Irc30Metadata; pub(crate) use self::{ block_issuer::BlockIssuerKeyCount, - metadata::{MetadataFeatureKeyLength, MetadataFeatureLength, MetadataFeatureValueLength}, + metadata::{MetadataFeatureEntryCount, MetadataFeatureKeyLength, MetadataFeatureValueLength}, tag::TagFeatureLength, }; pub use self::{ diff --git a/sdk/src/types/block/output/mod.rs b/sdk/src/types/block/output/mod.rs index 9a5315d02f..42e7fb9276 100644 --- a/sdk/src/types/block/output/mod.rs +++ b/sdk/src/types/block/output/mod.rs @@ -47,7 +47,7 @@ pub use self::{ unlock_condition::{UnlockCondition, UnlockConditions}, }; pub(crate) use self::{ - feature::{MetadataFeatureKeyLength, MetadataFeatureLength, MetadataFeatureValueLength, TagFeatureLength}, + feature::{MetadataFeatureEntryCount, MetadataFeatureKeyLength, MetadataFeatureValueLength, TagFeatureLength}, native_token::NativeTokenCount, output_id::OutputIndex, unlock_condition::AddressUnlockCondition, diff --git a/sdk/src/types/block/rand/mana.rs b/sdk/src/types/block/rand/mana.rs index 2476a75cdf..5e2332a1b6 100644 --- a/sdk/src/types/block/rand/mana.rs +++ b/sdk/src/types/block/rand/mana.rs @@ -11,7 +11,7 @@ use crate::types::block::{ pub fn rand_mana_allotment(params: &ProtocolParameters) -> ManaAllotment { ManaAllotment::new( rand_account_id(), - rand_number_range(0..params.mana_parameters().max_mana()), + rand_number_range(1..params.mana_parameters().max_mana()), ) .unwrap() } diff --git a/sdk/src/types/block/rand/output/feature.rs b/sdk/src/types/block/rand/output/feature.rs index 54a3c46dfe..4bc4a836ea 100644 --- a/sdk/src/types/block/rand/output/feature.rs +++ b/sdk/src/types/block/rand/output/feature.rs @@ -36,32 +36,51 @@ pub fn rand_issuer_feature() -> IssuerFeature { /// Generates a random [`MetadataFeature`]. pub fn rand_metadata_feature() -> MetadataFeature { let mut map = BTreeMap::new(); - let mut total_size = 0; + // Starting at 2 for type + entries count bytes + let mut total_size = 2; + let max_size = *MetadataFeature::LENGTH_RANGE.end() as usize; + let key_prefix_length = 1; + let value_prefix_length = 2; for _ in 0..10 { - if total_size >= (*MetadataFeature::LENGTH_RANGE.end() - 1) as usize - u8::MAX as usize { + // +1 since min key size is 1 + if total_size > (max_size - (key_prefix_length + value_prefix_length + 1)) as usize { break; } + // Key length - total_size += 1; - let key = Alphanumeric.sample_string( - &mut rand::thread_rng(), - rand_number_range(Range { - start: 1, - end: u8::MAX.into(), - }), - ); + total_size += key_prefix_length; + let max_val = if max_size - total_size - value_prefix_length < u8::MAX as usize { + max_size - total_size - value_prefix_length + } else { + u8::MAX.into() + }; + + let key = if max_val == 1 { + "a".to_string() + } else { + Alphanumeric.sample_string( + &mut rand::thread_rng(), + rand_number_range(Range { start: 1, end: max_val }), + ) + }; total_size += key.as_bytes().len(); - if total_size >= *MetadataFeature::LENGTH_RANGE.end() as usize - 2 { + if total_size > max_size - value_prefix_length { + // println!("breaking before adding more"); break; } + // Value length - total_size += 2; - let bytes = rand_bytes(rand_number_range(Range { - start: 0, - end: *MetadataFeature::LENGTH_RANGE.end() as usize - total_size, - }) as usize); + total_size += value_prefix_length; + let bytes = if max_size - total_size == 0 { + vec![] + } else { + rand_bytes(rand_number_range(Range { + start: 0, + end: max_size - total_size, + })) + }; total_size += bytes.len(); map.insert(key.into(), bytes); diff --git a/sdk/tests/types/output/feature/metadata.rs b/sdk/tests/types/output/feature/metadata.rs index b554ca16ae..75fde01ce6 100644 --- a/sdk/tests/types/output/feature/metadata.rs +++ b/sdk/tests/types/output/feature/metadata.rs @@ -46,7 +46,7 @@ fn serde_roundtrip() { #[test] fn unpack_invalid_order() { assert!(matches!( - MetadataFeature::unpack_verified([3, 0, 1, 99, 0, 0, 1, 98, 0, 0, 1, 97, 0, 0], &()), + MetadataFeature::unpack_verified([3, 1, 99, 0, 0, 1, 98, 0, 0, 1, 97, 0, 0], &()), Err(UnpackError::Packable(Error::InvalidMetadataFeature(error_msg))) if &error_msg == "unordered map" )); } @@ -54,7 +54,7 @@ fn unpack_invalid_order() { #[test] fn unpack_invalid_length() { assert!(matches!( - MetadataFeature::unpack_verified([vec![1, 0, 1, 33, 0, 32], vec![0u8; 8192]].concat(), &()), - Err(UnpackError::Packable(Error::InvalidMetadataFeature(len))) if &len == "Out of bounds byte length: 8199" + MetadataFeature::unpack_verified([vec![1, 1, 33, 0, 32], vec![0u8; 8192]].concat(), &()), + Err(UnpackError::Packable(Error::InvalidMetadataFeature(len))) if &len == "Out of bounds byte length: 8198" )); }