From 9c4c59351eb1506d75f565790e8449488ebacd6f Mon Sep 17 00:00:00 2001 From: DaughterOfMars Date: Wed, 13 Mar 2024 05:51:21 -0400 Subject: [PATCH 01/10] Predict how many slots it will take to generate mana (#2159) * Add slots until generated fn * Add slots remaining to insufficient mana error * better name * Add some preliminary checks * move check * add current slot to calculation and more checks * fix tests * oops * fix error usage * tweaking * Update sdk/src/types/block/mana/parameters.rs Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> --------- Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> --- .../transaction_builder/error.rs | 2 + .../block_builder/transaction_builder/mod.rs | 12 ++ sdk/src/types/block/mana/error.rs | 4 + sdk/src/types/block/mana/parameters.rs | 171 ++++++++++++++++++ sdk/src/types/block/output/account.rs | 9 +- sdk/src/types/block/output/anchor.rs | 9 +- sdk/src/types/block/output/basic.rs | 9 +- sdk/src/types/block/output/delegation.rs | 9 +- sdk/src/types/block/output/foundry.rs | 9 +- sdk/src/types/block/output/mod.rs | 12 ++ sdk/src/types/block/output/nft.rs | 9 +- .../client/transaction_builder/outputs.rs | 94 ++++++++++ 12 files changed, 337 insertions(+), 12 deletions(-) diff --git a/sdk/src/client/api/block_builder/transaction_builder/error.rs b/sdk/src/client/api/block_builder/transaction_builder/error.rs index 06dfa4c538..f5706133f4 100644 --- a/sdk/src/client/api/block_builder/transaction_builder/error.rs +++ b/sdk/src/client/api/block_builder/transaction_builder/error.rs @@ -45,6 +45,8 @@ pub enum TransactionBuilderError { found: u64, /// The required amount. required: u64, + /// The number of slots remaining before this transaction will have generated enough mana. + slots_remaining: u32, }, /// Insufficient native token amount provided. #[error("insufficient native token amount: found {found}, required {required}")] diff --git a/sdk/src/client/api/block_builder/transaction_builder/mod.rs b/sdk/src/client/api/block_builder/transaction_builder/mod.rs index 0e951614e4..2f2556367a 100644 --- a/sdk/src/client/api/block_builder/transaction_builder/mod.rs +++ b/sdk/src/client/api/block_builder/transaction_builder/mod.rs @@ -353,9 +353,21 @@ impl TransactionBuilder { let (input_mana, output_mana) = self.mana_sums(false)?; if input_mana < output_mana { + let total_generation_amount = self + .selected_inputs + .iter() + .map(|o| o.output.mana_generation_amount(&self.protocol_parameters)) + .sum::(); + let slots_remaining = self.protocol_parameters.slots_until_generated( + self.creation_slot, + total_generation_amount, + self.total_selected_mana(false)?, + output_mana - input_mana, + )?; return Err(TransactionBuilderError::InsufficientMana { found: input_mana, required: output_mana, + slots_remaining, }); } diff --git a/sdk/src/types/block/mana/error.rs b/sdk/src/types/block/mana/error.rs index b4d511d24e..a4c921bed6 100644 --- a/sdk/src/types/block/mana/error.rs +++ b/sdk/src/types/block/mana/error.rs @@ -18,6 +18,10 @@ pub enum ManaError { AllotmentsNotUniqueSorted, #[display(fmt = "invalid epoch diff: created {created}, target {target}")] EpochDiff { created: EpochIndex, target: EpochIndex }, + #[display(fmt = "insufficient amount to generate positive mana")] + InsufficientGenerationAmount, + #[display(fmt = "mana value {value} above maximum {max}")] + AboveMax { value: u64, max: u64 }, } #[cfg(feature = "std")] diff --git a/sdk/src/types/block/mana/parameters.rs b/sdk/src/types/block/mana/parameters.rs index 10fcb8edb6..762c7b96eb 100644 --- a/sdk/src/types/block/mana/parameters.rs +++ b/sdk/src/types/block/mana/parameters.rs @@ -196,6 +196,59 @@ impl ProtocolParameters { - (c >> mana_parameters.decay_factors_exponent()) }) } + + pub fn slots_until_generated( + &self, + current_slot: impl Into, + generation_amount: u64, + stored_mana: u64, + required_mana: u64, + ) -> Result { + if required_mana == 0 { + return Ok(0); + } + if required_mana > self.mana_parameters().max_mana() { + return Err(ManaError::AboveMax { + value: required_mana, + max: self.mana_parameters().max_mana(), + }); + } + let current_slot = current_slot.into(); + let mut num_slots = 0; + let mana_generated_per_epoch = self + .mana_parameters() + .generate_mana(generation_amount, self.slots_per_epoch()); + let mut required_mana_remaining = required_mana; + loop { + // Get the minimum number of slots required to achieve the needed mana (i.e. not including decay) + let additional_slots = u32::try_from( + (required_mana_remaining as u128 * self.slots_per_epoch() as u128) + / mana_generated_per_epoch.max(1) as u128, + ) + .map_err(|_| ManaError::InsufficientGenerationAmount)?; + + num_slots += additional_slots.max(1); + // Get the actual values after that many slots + let decayed_mana = + stored_mana - self.mana_with_decay(stored_mana, current_slot, current_slot + num_slots)?; + let generated_mana = + self.generate_mana_with_decay(generation_amount, current_slot, current_slot + num_slots)?; + // If we generated less than how much we lost, this is not going to work out + if generated_mana <= decayed_mana { + return Err(ManaError::InsufficientGenerationAmount); + } + if generated_mana - decayed_mana >= required_mana { + return Ok(num_slots); + } + let new_required_mana = (required_mana + decayed_mana) + .checked_sub(generated_mana) + .ok_or(ManaError::InsufficientGenerationAmount)?; + if new_required_mana >= required_mana_remaining { + return Err(ManaError::InsufficientGenerationAmount); + } + required_mana_remaining = new_required_mana; + } + } } /// Perform a multiplication and shift. @@ -544,4 +597,122 @@ mod test { 4.0, ) } + + #[derive(Debug)] + struct ManaTest { + current_slot: u32, + generation_amount: u64, + stored_mana: u64, + required_mana: u64, + } + + impl ManaTest { + fn mana_after(&self, slots: u32) -> u64 { + params() + .generate_mana_with_decay(self.generation_amount, self.current_slot, self.current_slot + slots) + .unwrap() + + params() + .mana_with_decay(self.stored_mana, self.current_slot, self.current_slot + slots) + .unwrap() + } + + fn slots_until_generated(&self) -> Result { + params().slots_until_generated( + self.current_slot, + self.generation_amount, + self.stored_mana, + self.required_mana, + ) + } + } + + #[test] + fn slots_until_generated() { + for test in [ + ManaTest { + current_slot: 100, + generation_amount: 100000, + stored_mana: 1000000, + required_mana: 50000, + }, + ManaTest { + current_slot: 1000000, + generation_amount: 500000, + stored_mana: 12345, + required_mana: 999999, + }, + ManaTest { + current_slot: 1294732685, + generation_amount: 300000, + stored_mana: 50, + required_mana: 1, + }, + ManaTest { + current_slot: 1294732685, + generation_amount: 500000, + stored_mana: 0, + required_mana: 600, + }, + ] { + let slots_left = test.slots_until_generated().expect(&format!("{test:?}")); + let mana_after_n_minus_1 = test.mana_after(slots_left - 1); + let mana_after_n = test.mana_after(slots_left); + let expected_mana = test.stored_mana + test.required_mana; + assert!( + mana_after_n_minus_1 < expected_mana, + "{test:?}: mana after {} slots should be lower than {expected_mana}, but found {mana_after_n_minus_1}", + slots_left - 1, + ); + assert!( + mana_after_n >= expected_mana, + "{test:?}: mana after {slots_left} slots should be greater than or equal to {expected_mana}, but found {mana_after_n}", + ); + } + } + + #[test] + fn slots_until_generated_insufficient_amount() { + for test in [ + ManaTest { + current_slot: 10000, + generation_amount: 1000, + stored_mana: 1000000, + required_mana: 50000, + }, + ManaTest { + current_slot: 10000, + generation_amount: 2000000, + stored_mana: 0, + required_mana: 1000000000, + }, + ManaTest { + current_slot: 10000, + generation_amount: 100000, + stored_mana: 1000000, + required_mana: 500000000000, + }, + ] { + let slots_left = test.slots_until_generated().unwrap_err(); + assert_eq!(slots_left, ManaError::InsufficientGenerationAmount); + } + } + + #[test] + fn slots_until_generated_required_above_max() { + let test = ManaTest { + current_slot: 10000, + generation_amount: 100000, + stored_mana: 1000000, + required_mana: 9999999999999999999, + }; + + let slots_left = test.slots_until_generated().unwrap_err(); + assert_eq!( + slots_left, + ManaError::AboveMax { + value: test.required_mana, + max: params().mana_parameters().max_mana() + } + ); + } } diff --git a/sdk/src/types/block/output/account.rs b/sdk/src/types/block/output/account.rs index ca0ea23646..66c02a1348 100644 --- a/sdk/src/types/block/output/account.rs +++ b/sdk/src/types/block/output/account.rs @@ -429,8 +429,7 @@ impl AccountOutput { creation_index: SlotIndex, target_index: SlotIndex, ) -> Result { - let min_deposit = self.minimum_amount(protocol_parameters.storage_score_parameters()); - let generation_amount = self.amount().saturating_sub(min_deposit); + let generation_amount = self.mana_generation_amount(protocol_parameters); let stored_mana = protocol_parameters.mana_with_decay(self.mana(), creation_index, target_index)?; let potential_mana = protocol_parameters.generate_mana_with_decay(generation_amount, creation_index, target_index)?; @@ -440,6 +439,12 @@ impl AccountOutput { potential: potential_mana, }) } + + /// Returns the mana generation amount of the output. + pub fn mana_generation_amount(&self, protocol_parameters: &ProtocolParameters) -> u64 { + let min_deposit = self.minimum_amount(protocol_parameters.storage_score_parameters()); + self.amount().saturating_sub(min_deposit) + } } impl StorageScore for AccountOutput { diff --git a/sdk/src/types/block/output/anchor.rs b/sdk/src/types/block/output/anchor.rs index 9c59ef5b45..0b1fe7ec31 100644 --- a/sdk/src/types/block/output/anchor.rs +++ b/sdk/src/types/block/output/anchor.rs @@ -486,8 +486,7 @@ impl AnchorOutput { creation_index: SlotIndex, target_index: SlotIndex, ) -> Result { - let min_deposit = self.minimum_amount(protocol_parameters.storage_score_parameters()); - let generation_amount = self.amount().saturating_sub(min_deposit); + let generation_amount = self.mana_generation_amount(protocol_parameters); let stored_mana = protocol_parameters.mana_with_decay(self.mana(), creation_index, target_index)?; let potential_mana = protocol_parameters.generate_mana_with_decay(generation_amount, creation_index, target_index)?; @@ -497,6 +496,12 @@ impl AnchorOutput { potential: potential_mana, }) } + + /// Returns the mana generation amount of the output. + pub fn mana_generation_amount(&self, protocol_parameters: &ProtocolParameters) -> u64 { + let min_deposit = self.minimum_amount(protocol_parameters.storage_score_parameters()); + self.amount().saturating_sub(min_deposit) + } } impl StorageScore for AnchorOutput { diff --git a/sdk/src/types/block/output/basic.rs b/sdk/src/types/block/output/basic.rs index ab2f6b94e4..e6761ea619 100644 --- a/sdk/src/types/block/output/basic.rs +++ b/sdk/src/types/block/output/basic.rs @@ -386,8 +386,7 @@ impl BasicOutput { creation_index: SlotIndex, target_index: SlotIndex, ) -> Result { - let min_deposit = self.minimum_amount(protocol_parameters.storage_score_parameters()); - let generation_amount = self.amount().saturating_sub(min_deposit); + let generation_amount = self.mana_generation_amount(protocol_parameters); let stored_mana = protocol_parameters.mana_with_decay(self.mana(), creation_index, target_index)?; let potential_mana = protocol_parameters.generate_mana_with_decay(generation_amount, creation_index, target_index)?; @@ -397,6 +396,12 @@ impl BasicOutput { potential: potential_mana, }) } + + /// Returns the mana generation amount of the output. + pub fn mana_generation_amount(&self, protocol_parameters: &ProtocolParameters) -> u64 { + let min_deposit = self.minimum_amount(protocol_parameters.storage_score_parameters()); + self.amount().saturating_sub(min_deposit) + } } impl StorageScore for BasicOutput { diff --git a/sdk/src/types/block/output/delegation.rs b/sdk/src/types/block/output/delegation.rs index eadc8ccd39..872bf18080 100644 --- a/sdk/src/types/block/output/delegation.rs +++ b/sdk/src/types/block/output/delegation.rs @@ -372,8 +372,7 @@ impl DelegationOutput { creation_index: SlotIndex, target_index: SlotIndex, ) -> Result { - let min_deposit = self.minimum_amount(protocol_parameters.storage_score_parameters()); - let generation_amount = self.amount().saturating_sub(min_deposit); + let generation_amount = self.mana_generation_amount(protocol_parameters); let potential_mana = protocol_parameters.generate_mana_with_decay(generation_amount, creation_index, target_index)?; @@ -382,6 +381,12 @@ impl DelegationOutput { potential: potential_mana, }) } + + /// Returns the mana generation amount of the output. + pub fn mana_generation_amount(&self, protocol_parameters: &ProtocolParameters) -> u64 { + let min_deposit = self.minimum_amount(protocol_parameters.storage_score_parameters()); + self.amount().saturating_sub(min_deposit) + } } impl StorageScore for DelegationOutput { diff --git a/sdk/src/types/block/output/foundry.rs b/sdk/src/types/block/output/foundry.rs index 6ed1c2a764..655a18d28b 100644 --- a/sdk/src/types/block/output/foundry.rs +++ b/sdk/src/types/block/output/foundry.rs @@ -441,8 +441,7 @@ impl FoundryOutput { creation_index: SlotIndex, target_index: SlotIndex, ) -> Result { - let min_deposit = self.minimum_amount(protocol_parameters.storage_score_parameters()); - let generation_amount = self.amount().saturating_sub(min_deposit); + let generation_amount = self.mana_generation_amount(protocol_parameters); let potential_mana = protocol_parameters.generate_mana_with_decay(generation_amount, creation_index, target_index)?; @@ -451,6 +450,12 @@ impl FoundryOutput { potential: potential_mana, }) } + + /// Returns the mana generation amount of the output. + pub fn mana_generation_amount(&self, protocol_parameters: &ProtocolParameters) -> u64 { + let min_deposit = self.minimum_amount(protocol_parameters.storage_score_parameters()); + self.amount().saturating_sub(min_deposit) + } } impl StorageScore for FoundryOutput { diff --git a/sdk/src/types/block/output/mod.rs b/sdk/src/types/block/output/mod.rs index 09d8b86761..129ea04627 100644 --- a/sdk/src/types/block/output/mod.rs +++ b/sdk/src/types/block/output/mod.rs @@ -207,6 +207,18 @@ impl Output { } } + /// Returns the mana generation amount of the output. + pub fn mana_generation_amount(&self, protocol_parameters: &ProtocolParameters) -> u64 { + match self { + Self::Basic(output) => output.mana_generation_amount(protocol_parameters), + Self::Account(output) => output.mana_generation_amount(protocol_parameters), + Self::Anchor(output) => output.mana_generation_amount(protocol_parameters), + Self::Foundry(output) => output.mana_generation_amount(protocol_parameters), + Self::Nft(output) => output.mana_generation_amount(protocol_parameters), + Self::Delegation(output) => output.mana_generation_amount(protocol_parameters), + } + } + /// Returns the unlock conditions of an [`Output`], if any. pub fn unlock_conditions(&self) -> Option<&UnlockConditions> { match self { diff --git a/sdk/src/types/block/output/nft.rs b/sdk/src/types/block/output/nft.rs index da879a6166..04c0c763bc 100644 --- a/sdk/src/types/block/output/nft.rs +++ b/sdk/src/types/block/output/nft.rs @@ -441,8 +441,7 @@ impl NftOutput { creation_index: SlotIndex, target_index: SlotIndex, ) -> Result { - let min_deposit = self.minimum_amount(protocol_parameters.storage_score_parameters()); - let generation_amount = self.amount().saturating_sub(min_deposit); + let generation_amount = self.mana_generation_amount(protocol_parameters); let stored_mana = protocol_parameters.mana_with_decay(self.mana(), creation_index, target_index)?; let potential_mana = protocol_parameters.generate_mana_with_decay(generation_amount, creation_index, target_index)?; @@ -452,6 +451,12 @@ impl NftOutput { potential: potential_mana, }) } + + /// Returns the mana generation amount of the output. + pub fn mana_generation_amount(&self, protocol_parameters: &ProtocolParameters) -> u64 { + let min_deposit = self.minimum_amount(protocol_parameters.storage_score_parameters()); + self.amount().saturating_sub(min_deposit) + } } impl StorageScore for NftOutput { diff --git a/sdk/tests/client/transaction_builder/outputs.rs b/sdk/tests/client/transaction_builder/outputs.rs index 24d8f778f6..2306523045 100644 --- a/sdk/tests/client/transaction_builder/outputs.rs +++ b/sdk/tests/client/transaction_builder/outputs.rs @@ -591,3 +591,97 @@ fn transition_no_more_than_needed_for_nft_amount() { assert_eq!(selected.inputs_data.len(), 1); assert!(unsorted_eq(selected.transaction.outputs(), &outputs)); } + +#[test] +fn insufficient_mana() { + let protocol_parameters = iota_mainnet_protocol_parameters(); + + let inputs = build_inputs( + [ + ( + Basic { + amount: 1_000_000, + mana: 0, + address: Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + native_token: None, + sender: None, + sdruc: None, + timelock: None, + expiration: None, + }, + None, + ), + ( + Basic { + amount: 1_000_000, + mana: 0, + address: Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + native_token: None, + sender: None, + sdruc: None, + timelock: None, + expiration: None, + }, + None, + ), + ], + Some(SLOT_INDEX), + ); + let outputs = build_outputs([Basic { + amount: 2_000_000, + mana: 10000, + address: Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + native_token: None, + sender: None, + sdruc: None, + timelock: None, + expiration: None, + }]); + + let selected = TransactionBuilder::new( + inputs.clone(), + outputs.clone(), + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters.clone(), + ) + .finish() + .unwrap_err(); + + let TransactionBuilderError::InsufficientMana { + found, + required, + slots_remaining, + } = selected + else { + panic!("expected insufficient mana error, found {selected:?}") + }; + assert_eq!(found, 0); + assert_eq!(required, 10000); + + // Re-running with any slot index less than the original plus the slots remaining will still error + let err = TransactionBuilder::new( + inputs.clone(), + outputs.clone(), + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX + slots_remaining - 1, + SLOT_COMMITMENT_ID, + protocol_parameters.clone(), + ) + .finish() + .unwrap_err(); + + assert!(matches!(err, TransactionBuilderError::InsufficientMana { .. })); + + TransactionBuilder::new( + inputs.clone(), + outputs, + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX + slots_remaining, + SLOT_COMMITMENT_ID, + protocol_parameters.clone(), + ) + .finish() + .unwrap(); +} From 8a3ec9ef72921a1c47980f59763d4ea3736a4a61 Mon Sep 17 00:00:00 2001 From: DaughterOfMars Date: Wed, 13 Mar 2024 05:51:54 -0400 Subject: [PATCH 02/10] Fix some TODOs (#2174) * some TODOs * review * fix --- bindings/core/src/method/client.rs | 3 -- .../client/block/02_block_custom_parents.rs | 39 +++++++++++++++---- sdk/src/client/api/block_builder/mod.rs | 9 +---- .../client/api/block_builder/transaction.rs | 1 - .../transaction_builder/error.rs | 3 +- .../transaction_builder/requirement/sender.rs | 4 +- .../transaction_builder/foundry_outputs.rs | 15 +++---- sdk/tests/types/tagged_data_payload.rs | 28 ++++++++----- sdk/tests/wallet/balance.rs | 11 +++++- 9 files changed, 71 insertions(+), 42 deletions(-) diff --git a/bindings/core/src/method/client.rs b/bindings/core/src/method/client.rs index d10431bf32..12272531d9 100644 --- a/bindings/core/src/method/client.rs +++ b/bindings/core/src/method/client.rs @@ -41,7 +41,6 @@ pub enum ClientMethod { // If not provided, minimum amount will be used #[serde(default, with = "option_string")] amount: Option, - // TODO: Determine if `default` is wanted here #[serde(default, with = "string")] mana: u64, account_id: AccountId, @@ -58,7 +57,6 @@ pub enum ClientMethod { // If not provided, minimum amount will be used #[serde(default, with = "option_string")] amount: Option, - // TODO: Determine if `default` is wanted here #[serde(default, with = "string")] mana: u64, unlock_conditions: Vec, @@ -86,7 +84,6 @@ pub enum ClientMethod { // If not provided, minimum amount will be used #[serde(default, with = "option_string")] amount: Option, - // TODO: Determine if `default` is wanted here #[serde(default, with = "string")] mana: u64, nft_id: NftId, diff --git a/sdk/examples/client/block/02_block_custom_parents.rs b/sdk/examples/client/block/02_block_custom_parents.rs index b7ce9ab6c8..ab0f30c289 100644 --- a/sdk/examples/client/block/02_block_custom_parents.rs +++ b/sdk/examples/client/block/02_block_custom_parents.rs @@ -15,7 +15,11 @@ use iota_sdk::{ secret::{SecretManager, SignBlock}, Client, }, - types::block::output::AccountId, + types::block::{ + core::{basic::MaxBurnedManaAmount, BasicBlockBodyBuilder, BlockHeader}, + output::AccountId, + UnsignedBlock, + }, }; #[tokio::main] @@ -39,16 +43,37 @@ async fn main() -> Result<(), Box> { let issuance = client.get_issuance().await?; println!("Issuance:\n{issuance:#?}"); + let protocol_params = client.get_protocol_parameters().await?; + // Create and send the block with custom parents. - // TODO build block with custom parents, but without `build_basic_block()` - let block = client - .build_basic_block(issuer_id, None) - .await? - .sign_ed25519(&secret_manager, Bip44::new(IOTA_COIN_TYPE)) - .await?; + let block = UnsignedBlock::new( + BlockHeader::new( + protocol_params.version(), + protocol_params.network_id(), + time::OffsetDateTime::now_utc().unix_timestamp_nanos() as _, + issuance.latest_commitment.id(), + issuance.latest_finalized_slot, + issuer_id, + ), + BasicBlockBodyBuilder::new( + issuance.strong_parents()?, + MaxBurnedManaAmount::MinimumAmount { + params: protocol_params.work_score_parameters(), + reference_mana_cost: client + .get_account_congestion(&issuer_id, None) + .await? + .reference_mana_cost, + }, + ) + .finish_block_body()?, + ) + .sign_ed25519(&secret_manager, Bip44::new(IOTA_COIN_TYPE)) + .await?; println!("{block:#?}"); + client.post_block(&block).await?; + println!( "Block with custom parents sent: {}/block/{}", std::env::var("EXPLORER_URL").unwrap(), diff --git a/sdk/src/client/api/block_builder/mod.rs b/sdk/src/client/api/block_builder/mod.rs index c632dea9c1..7df509a723 100644 --- a/sdk/src/client/api/block_builder/mod.rs +++ b/sdk/src/client/api/block_builder/mod.rs @@ -24,15 +24,10 @@ impl Client { let issuance = self.get_issuance().await?; let issuing_time = { - #[cfg(feature = "std")] - let issuing_time = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) + let issuing_time = instant::SystemTime::now() + .duration_since(instant::SystemTime::UNIX_EPOCH) .expect("Time went backwards") .as_nanos() as u64; - // TODO no_std way to have a nanosecond timestamp - // https://github.com/iotaledger/iota-sdk/issues/647 - #[cfg(not(feature = "std"))] - let issuing_time = 0; // Check that the issuing_time is in the range of +-5 minutes of the node to prevent potential issues if !(issuance.latest_parent_block_issuing_time - FIVE_MINUTES_IN_NANOSECONDS diff --git a/sdk/src/client/api/block_builder/transaction.rs b/sdk/src/client/api/block_builder/transaction.rs index 6edf74ee4e..6673047cbe 100644 --- a/sdk/src/client/api/block_builder/transaction.rs +++ b/sdk/src/client/api/block_builder/transaction.rs @@ -20,7 +20,6 @@ use crate::{ }, }; -// TODO this is wrong because of https://github.com/iotaledger/iota-sdk/issues/1208 const MAX_TX_LENGTH_FOR_BLOCK_WITH_8_PARENTS: usize = Block::LENGTH_MAX - Block::LENGTH_MIN - (7 * BlockId::LENGTH); // Length for unlocks with a single signature unlock (unlocks length + unlock type + signature type + public key + // signature) diff --git a/sdk/src/client/api/block_builder/transaction_builder/error.rs b/sdk/src/client/api/block_builder/transaction_builder/error.rs index f5706133f4..8affd1f350 100644 --- a/sdk/src/client/api/block_builder/transaction_builder/error.rs +++ b/sdk/src/client/api/block_builder/transaction_builder/error.rs @@ -78,8 +78,7 @@ pub enum TransactionBuilderError { UnfulfillableRequirement(Requirement), /// Unsupported address type. #[error("unsupported address type {0}")] - // TODO replace with string when 2.0 has Address::kind_str - UnsupportedAddressType(u8), + UnsupportedAddressType(String), /// Block error. #[error("{0}")] Block(#[from] BlockError), diff --git a/sdk/src/client/api/block_builder/transaction_builder/requirement/sender.rs b/sdk/src/client/api/block_builder/transaction_builder/requirement/sender.rs index 11d951cdef..29326f706f 100644 --- a/sdk/src/client/api/block_builder/transaction_builder/requirement/sender.rs +++ b/sdk/src/client/api/block_builder/transaction_builder/requirement/sender.rs @@ -79,7 +79,9 @@ impl TransactionBuilder { self.fulfill_sender_requirement(restricted_address.address()) } - _ => Err(TransactionBuilderError::UnsupportedAddressType(address.kind())), + _ => Err(TransactionBuilderError::UnsupportedAddressType( + address.kind_str().to_owned(), + )), } } } diff --git a/sdk/tests/client/transaction_builder/foundry_outputs.rs b/sdk/tests/client/transaction_builder/foundry_outputs.rs index daf19c3dd7..a4a2dc32c8 100644 --- a/sdk/tests/client/transaction_builder/foundry_outputs.rs +++ b/sdk/tests/client/transaction_builder/foundry_outputs.rs @@ -168,10 +168,9 @@ fn minted_native_tokens_in_new_remainder() { // Account next state + foundry + basic output with native tokens assert_eq!(selected.transaction.outputs().len(), 3); selected.transaction.outputs().iter().for_each(|output| { - if let Output::Basic(_basic_output) = &output { + if let Output::Basic(basic_output) = &output { // Basic output remainder has the minted native tokens - // TODO reenable when ISA supports NTs again - // assert_eq!(basic_output.native_token().unwrap().amount().as_u32(), 10); + assert_eq!(basic_output.native_token().unwrap().amount().as_u32(), 10); } }); } @@ -321,10 +320,9 @@ fn melt_native_tokens() { // Account next state + foundry + basic output with native tokens assert_eq!(selected.transaction.outputs().len(), 3); selected.transaction.outputs().iter().for_each(|output| { - if let Output::Basic(_basic_output) = &output { + if let Output::Basic(basic_output) = &output { // Basic output remainder has the remaining native tokens - // TODO reenable when ISA supports NTs again - // assert_eq!(basic_output.native_token().unwrap().amount().as_u32(), 5); + assert_eq!(basic_output.native_token().unwrap().amount().as_u32(), 5); } }); } @@ -1264,10 +1262,9 @@ fn melt_and_burn_native_tokens() { assert_eq!(selected.transaction.outputs().len(), 3); // Account state index is increased selected.transaction.outputs().iter().for_each(|output| { - if let Output::Basic(_basic_output) = &output { + if let Output::Basic(basic_output) = &output { // Basic output remainder has the remaining native tokens - // TODO reenable when ISA supports NTs again - // assert_eq!(basic_output.native_token().unwrap().amount().as_u32(), 421); + assert_eq!(basic_output.native_token().unwrap().amount().as_u32(), 421); } }); } diff --git a/sdk/tests/types/tagged_data_payload.rs b/sdk/tests/types/tagged_data_payload.rs index ccaca36750..04e12ac077 100644 --- a/sdk/tests/types/tagged_data_payload.rs +++ b/sdk/tests/types/tagged_data_payload.rs @@ -4,7 +4,6 @@ use iota_sdk::types::block::{ payload::{tagged_data::TaggedDataPayload, PayloadError}, rand::bytes::{rand_bytes, rand_bytes_array}, - Block, }; use packable::{ bounded::{TryIntoBoundedU32Error, TryIntoBoundedU8Error}, @@ -57,19 +56,28 @@ fn new_valid_tag_length_min() { #[test] fn new_invalid_tag_length_more_than_max() { - assert!(matches!( - TaggedDataPayload::new(rand_bytes(65), [0x42, 0xff, 0x84, 0xa2, 0x42, 0xff, 0x84, 0xa2]), - Err(PayloadError::TagLength(TryIntoBoundedU8Error::Invalid(65))) - )); + assert_eq!( + TaggedDataPayload::new( + [0u8; *TaggedDataPayload::TAG_LENGTH_RANGE.end() as usize + 1], + [0x42, 0xff, 0x84, 0xa2, 0x42, 0xff, 0x84, 0xa2], + ), + Err(PayloadError::TagLength(TryIntoBoundedU8Error::Invalid( + TaggedDataPayload::TAG_LENGTH_RANGE.end() + 1 + ))) + ); } #[test] fn new_invalid_data_length_more_than_max() { - assert!(matches!( - // TODO https://github.com/iotaledger/iota-sdk/issues/1226 - TaggedDataPayload::new(rand_bytes(32), [0u8; Block::LENGTH_MAX + 42]), - Err(PayloadError::TaggedDataLength(TryIntoBoundedU32Error::Invalid(l))) if l == Block::LENGTH_MAX as u32 + 42 - )); + assert_eq!( + TaggedDataPayload::new( + rand_bytes(32), + [0u8; *TaggedDataPayload::DATA_LENGTH_RANGE.end() as usize + 1] + ), + Err(PayloadError::TaggedDataLength(TryIntoBoundedU32Error::Invalid( + TaggedDataPayload::DATA_LENGTH_RANGE.end() + 1 + ))) + ); } #[test] diff --git a/sdk/tests/wallet/balance.rs b/sdk/tests/wallet/balance.rs index 5a28905c90..0fd52dc90b 100644 --- a/sdk/tests/wallet/balance.rs +++ b/sdk/tests/wallet/balance.rs @@ -181,8 +181,15 @@ async fn balance_expiration() -> Result<(), Box> { assert_eq!(balance.base_coin().available(), 0); // Wait until expired - // TODO wait for slots, not seconds - tokio::time::sleep(std::time::Duration::from_secs(slots_until_expired as u64)).await; + let seconds_per_slot = wallet_0 + .client() + .get_protocol_parameters() + .await? + .slot_duration_in_seconds(); + tokio::time::sleep(std::time::Duration::from_secs( + seconds_per_slot as u64 * slots_until_expired as u64, + )) + .await; // Wallet 1 balance after expiration let balance = wallet_1.sync(None).await?; From df5753b809195b23867a2ec1f2efa430e07cd47a Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Wed, 13 Mar 2024 10:55:11 +0100 Subject: [PATCH 03/10] Python: add compute_delegation_id() (#2173) --- bindings/python/iota_sdk/utils.py | 12 ++++++++++-- bindings/python/tests/test_offline.py | 11 +++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/bindings/python/iota_sdk/utils.py b/bindings/python/iota_sdk/utils.py index 02f27c60fe..2ed5a9466e 100644 --- a/bindings/python/iota_sdk/utils.py +++ b/bindings/python/iota_sdk/utils.py @@ -85,7 +85,15 @@ def compute_account_id(output_id: OutputId) -> HexStr: """Compute the account id for the given account output id. """ return _call_method('blake2b256Hash', { - 'bytes': repr(output_id) + 'bytes': output_id + }) + + @staticmethod + def compute_delegation_id(output_id: OutputId) -> HexStr: + """Compute the delegation id for the given account output id. + """ + return _call_method('blake2b256Hash', { + 'bytes': output_id }) @staticmethod @@ -113,7 +121,7 @@ def compute_nft_id(output_id: OutputId) -> HexStr: """Compute the NFT id for the given NFT output id. """ return _call_method('blake2b256Hash', { - 'bytes': repr(output_id) + 'bytes': output_id }) @staticmethod diff --git a/bindings/python/tests/test_offline.py b/bindings/python/tests/test_offline.py index b1c393f08e..1b47a9e42c 100644 --- a/bindings/python/tests/test_offline.py +++ b/bindings/python/tests/test_offline.py @@ -150,3 +150,14 @@ def test_irc_30(): } metadata_deser = Irc30Metadata.from_dict(metadata_dict) assert metadata == metadata_deser + + +def test_output_id_hashing(): + output_id = OutputId( + '0x0000000000000000000000000000000000000000000000000000000000000000000000000000') + assert Utils.compute_account_id( + output_id) == '0x0ebc2867a240719a70faacdfc3840e857fa450b37d95297ac4f166c2f70c3345' + assert Utils.compute_delegation_id( + output_id) == '0x0ebc2867a240719a70faacdfc3840e857fa450b37d95297ac4f166c2f70c3345' + assert Utils.compute_nft_id( + output_id) == '0x0ebc2867a240719a70faacdfc3840e857fa450b37d95297ac4f166c2f70c3345' From 8414c5fd6e9e28acd2dbf3c8c6916d3cbc78ca48 Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Wed, 13 Mar 2024 11:43:11 +0100 Subject: [PATCH 04/10] Consider account/nft addresses in can_output_be_unlocked functions (#2084) * Adjust can_output_be_unlocked_now() and can_output_be_unlocked_forever_from_now_on() to consider account and nft addresses * Resolve merge conflict * Allow additional input selection so account and NFTs can be transitioned to unlock inputs * Simplify name --------- Co-authored-by: DaughterOfMars --- sdk/src/wallet/operations/balance.rs | 19 +++++++----- sdk/src/wallet/operations/helpers/time.rs | 17 ++++++---- sdk/src/wallet/operations/output_claiming.rs | 31 ++++++++++++++++--- .../wallet/operations/output_consolidation.rs | 8 +++-- .../transaction/build_transaction.rs | 19 ++++++------ 5 files changed, 63 insertions(+), 31 deletions(-) diff --git a/sdk/src/wallet/operations/balance.rs b/sdk/src/wallet/operations/balance.rs index e137da3c04..e7747f8380 100644 --- a/sdk/src/wallet/operations/balance.rs +++ b/sdk/src/wallet/operations/balance.rs @@ -11,7 +11,7 @@ use crate::{ unlock_condition::UnlockCondition, DecayedMana, FoundryId, MinimumOutputAmount, NativeTokensBuilder, Output, }, wallet::{ - operations::{helpers::time::can_output_be_unlocked_forever_from_now_on, output_claiming::OutputsToClaim}, + operations::{helpers::time::can_output_be_unlocked_from_now_on, output_claiming::OutputsToClaim}, types::{Balance, NativeTokensBalance}, Wallet, WalletError, }, @@ -37,8 +37,12 @@ impl Wallet { #[cfg(feature = "participation")] let voting_output = wallet_ledger.get_voting_output(); - let claimable_outputs = - wallet_ledger.claimable_outputs(&wallet_address, OutputsToClaim::All, slot_index, &protocol_parameters)?; + let claimable_outputs = wallet_ledger.claimable_outputs( + wallet_address.inner().clone(), + OutputsToClaim::All, + slot_index, + &protocol_parameters, + )?; #[cfg(feature = "participation")] { @@ -49,6 +53,8 @@ impl Wallet { } } + let controlled_addresses = wallet_ledger.controlled_addresses(wallet_address.inner().clone()); + let mut reward_outputs = HashSet::new(); for (output_id, output_data) in &wallet_ledger.unspent_outputs { @@ -168,11 +174,8 @@ impl Wallet { if is_claimable { // check if output can be unlocked always from now on, in that case it should be // added to the total amount - let output_can_be_unlocked_now_and_in_future = can_output_be_unlocked_forever_from_now_on( - // We use the addresses with unspent outputs, because other addresses of - // the account without unspent - // outputs can't be related to this output - wallet_address.inner(), + let output_can_be_unlocked_now_and_in_future = can_output_be_unlocked_from_now_on( + &controlled_addresses, output, slot_index, protocol_parameters.committable_age_range(), diff --git a/sdk/src/wallet/operations/helpers/time.rs b/sdk/src/wallet/operations/helpers/time.rs index d44b99cb46..094d356e66 100644 --- a/sdk/src/wallet/operations/helpers/time.rs +++ b/sdk/src/wallet/operations/helpers/time.rs @@ -1,14 +1,16 @@ // Copyright 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use std::collections::HashSet; + use crate::{ types::block::{address::Address, output::Output, protocol::CommittableAgeRange, slot::SlotIndex}, wallet::{types::OutputData, WalletError}, }; -// Check if an output can be unlocked by the wallet address at the current time +// Check if an output can be unlocked by one of the provided addresses at the current time pub(crate) fn can_output_be_unlocked_now( - wallet_address: &Address, + controlled_addresses: &HashSet
, output_data: &OutputData, commitment_slot_index: impl Into + Copy, committable_age_range: CommittableAgeRange, @@ -24,13 +26,16 @@ pub(crate) fn can_output_be_unlocked_now( .required_address(commitment_slot_index.into(), committable_age_range)?; // In case of `None` the output can currently not be unlocked because of expiration unlock condition - Ok(required_address.map_or_else(|| false, |required_address| wallet_address == &required_address)) + Ok(required_address.map_or_else( + || false, + |required_address| controlled_addresses.contains(&required_address), + )) } // Check if an output can be unlocked by the wallet address at the current time and at any // point in the future -pub(crate) fn can_output_be_unlocked_forever_from_now_on( - wallet_address: &Address, +pub(crate) fn can_output_be_unlocked_from_now_on( + controlled_addresses: &HashSet
, output: &Output, slot_index: impl Into + Copy, committable_age_range: CommittableAgeRange, @@ -49,7 +54,7 @@ pub(crate) fn can_output_be_unlocked_forever_from_now_on( slot_index, committable_age_range, ) { - if address != expiration.return_address() || address != wallet_address { + if address != expiration.return_address() || !controlled_addresses.contains(address) { return false; } } else { diff --git a/sdk/src/wallet/operations/output_claiming.rs b/sdk/src/wallet/operations/output_claiming.rs index 6b2dc20dab..621b69c1e0 100644 --- a/sdk/src/wallet/operations/output_claiming.rs +++ b/sdk/src/wallet/operations/output_claiming.rs @@ -12,7 +12,7 @@ use crate::{ ClientError, }, types::block::{ - address::{Address, Bech32Address, Ed25519Address}, + address::{Address, Ed25519Address}, output::{ unlock_condition::AddressUnlockCondition, BasicOutput, NftOutputBuilder, Output, OutputId, UnlockCondition, }, @@ -47,13 +47,15 @@ impl WalletLedger { /// additional inputs pub(crate) fn claimable_outputs( &self, - wallet_address: &Bech32Address, + wallet_address: Address, outputs_to_claim: OutputsToClaim, slot_index: SlotIndex, protocol_parameters: &ProtocolParameters, ) -> Result, WalletError> { log::debug!("[OUTPUT_CLAIMING] claimable_outputs"); + let controlled_addresses = self.controlled_addresses(wallet_address); + // Get outputs for the claim let mut output_ids_to_claim: HashSet = HashSet::new(); for (output_id, output_data) in self @@ -71,7 +73,7 @@ impl WalletLedger { && can_output_be_unlocked_now( // We use the addresses with unspent outputs, because other addresses of the // account without unspent outputs can't be related to this output - wallet_address.inner(), + &controlled_addresses, output_data, slot_index, protocol_parameters.committable_age_range(), @@ -133,6 +135,26 @@ impl WalletLedger { ); Ok(output_ids_to_claim.into_iter().collect()) } + + // Returns the wallet address together with account and nft addresses that only have the address unlock condition + pub(crate) fn controlled_addresses(&self, wallet_address: Address) -> HashSet
{ + let mut controlled_addresses = HashSet::from([wallet_address]); + for o in self.unspent_outputs().values() { + match &o.output { + Output::Account(account) => { + controlled_addresses.insert(Address::Account(account.account_address(&o.output_id))); + } + Output::Nft(nft) => { + // Only consider addresses of NFTs with a single (address) unlock condition + if nft.unlock_conditions().len() == 1 { + controlled_addresses.insert(Address::Nft(nft.nft_address(&o.output_id))); + } + } + _ => {} // not interested in other outputs here + } + } + controlled_addresses + } } impl Wallet @@ -153,7 +175,7 @@ where let protocol_parameters = self.client().get_protocol_parameters().await?; wallet_ledger.claimable_outputs( - &self.address().await, + self.address().await.into_inner(), outputs_to_claim, slot_index, &protocol_parameters, @@ -298,7 +320,6 @@ where // add additional inputs .chain(possible_additional_inputs.iter().map(|o| o.output_id)) .collect(), - allow_additional_input_selection: false, ..Default::default() }, ) diff --git a/sdk/src/wallet/operations/output_consolidation.rs b/sdk/src/wallet/operations/output_consolidation.rs index e4c9450b5a..16e8d2332f 100644 --- a/sdk/src/wallet/operations/output_consolidation.rs +++ b/sdk/src/wallet/operations/output_consolidation.rs @@ -128,7 +128,7 @@ where &self, output_data: &OutputData, slot_index: SlotIndex, - wallet_address: &Address, + controlled_addresses: &HashSet
, ) -> Result { Ok(if let Output::Basic(basic_output) = &output_data.output { let protocol_parameters = self.client().get_protocol_parameters().await?; @@ -157,7 +157,7 @@ where } can_output_be_unlocked_now( - wallet_address, + controlled_addresses, output_data, slot_index, protocol_parameters.committable_age_range(), @@ -179,6 +179,8 @@ where let mut outputs_to_consolidate = Vec::new(); let mut native_token_inputs = HashMap::new(); + let controlled_addresses = wallet_ledger.controlled_addresses(wallet_address.inner().clone()); + for (output_id, output_data) in &wallet_ledger.unspent_outputs { // #[cfg(feature = "participation")] // if let Some(ref voting_output) = voting_output { @@ -190,7 +192,7 @@ where let is_locked_output = wallet_ledger.locked_outputs.contains(output_id); let should_consolidate_output = self - .should_consolidate_output(output_data, slot_index, wallet_address.as_ref()) + .should_consolidate_output(output_data, slot_index, &controlled_addresses) .await?; if !is_locked_output && should_consolidate_output { outputs_to_consolidate.push(output_data.clone()); diff --git a/sdk/src/wallet/operations/transaction/build_transaction.rs b/sdk/src/wallet/operations/transaction/build_transaction.rs index 72df49b0e3..a25b740432 100644 --- a/sdk/src/wallet/operations/transaction/build_transaction.rs +++ b/sdk/src/wallet/operations/transaction/build_transaction.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use alloc::collections::BTreeSet; +use std::collections::HashSet; use crypto::keys::bip44::Bip44; @@ -13,14 +14,12 @@ use crate::{ secret::{types::InputSigningData, SecretManage}, }, types::block::{ - address::Bech32Address, + address::{Address, Bech32Address}, output::{Output, OutputId}, protocol::CommittableAgeRange, slot::SlotIndex, }, - wallet::{ - operations::helpers::time::can_output_be_unlocked_forever_from_now_on, types::OutputData, Wallet, WalletError, - }, + wallet::{operations::helpers::time::can_output_be_unlocked_from_now_on, types::OutputData, Wallet, WalletError}, }; impl Wallet { @@ -60,10 +59,13 @@ impl Wallet { } } + let wallet_address = self.address().await; + let controlled_addresses = wallet_ledger.controlled_addresses(wallet_address.inner().clone()); // Filter inputs to not include inputs that require additional outputs for storage deposit return or could be // still locked. let available_inputs = filter_inputs( - &self.address().await, + &wallet_address, + &controlled_addresses, self.bip_path().await, wallet_ledger .unspent_outputs @@ -103,6 +105,7 @@ impl Wallet { #[allow(clippy::too_many_arguments)] fn filter_inputs<'a>( wallet_address: &Bech32Address, + controlled_addresses: &HashSet
, wallet_bip_path: Option, available_outputs: impl IntoIterator, slot_index: impl Into + Copy, @@ -113,10 +116,8 @@ fn filter_inputs<'a>( for output_data in available_outputs { if !required_inputs.contains(&output_data.output_id) { - let output_can_be_unlocked_now_and_in_future = can_output_be_unlocked_forever_from_now_on( - // We use the addresses with unspent outputs, because other addresses of the - // account without unspent outputs can't be related to this output - wallet_address.inner(), + let output_can_be_unlocked_now_and_in_future = can_output_be_unlocked_from_now_on( + controlled_addresses, &output_data.output, slot_index, committable_age_range, From 0ac4ff46671966c83296e329b284f422d434af19 Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Wed, 13 Mar 2024 11:43:34 +0100 Subject: [PATCH 05/10] Add missing CommitmentContextInput for automatically transitioned accounts (#2139) * Add missing CommitmentContextInput for automatically transitioned accounts * Rework context inputs additions * Don't check for commitment_context_input if we have one already --------- Co-authored-by: Thibault Martinez Co-authored-by: DaughterOfMars --- .../transaction_builder/context_inputs.rs | 111 ++++++++++++++++++ .../block_builder/transaction_builder/mod.rs | 25 ++-- .../requirement/context_inputs.rs | 95 --------------- .../transaction_builder/requirement/mod.rs | 4 - .../transaction_builder/account_outputs.rs | 68 +++++++++++ 5 files changed, 190 insertions(+), 113 deletions(-) create mode 100644 sdk/src/client/api/block_builder/transaction_builder/context_inputs.rs delete mode 100644 sdk/src/client/api/block_builder/transaction_builder/requirement/context_inputs.rs diff --git a/sdk/src/client/api/block_builder/transaction_builder/context_inputs.rs b/sdk/src/client/api/block_builder/transaction_builder/context_inputs.rs new file mode 100644 index 0000000000..4f8b468f92 --- /dev/null +++ b/sdk/src/client/api/block_builder/transaction_builder/context_inputs.rs @@ -0,0 +1,111 @@ +// Copyright 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use super::{TransactionBuilder, TransactionBuilderError}; +use crate::{ + client::secret::types::InputSigningData, + types::block::{ + context_input::{BlockIssuanceCreditContextInput, CommitmentContextInput}, + output::{AccountId, DelegationOutputBuilder, Output}, + }, +}; + +impl TransactionBuilder { + pub(crate) fn fulfill_context_inputs_requirements(&mut self, input: &InputSigningData) { + match &input.output { + // Transitioning an issuer account requires a BlockIssuanceCreditContextInput. + Output::Account(account) => { + if account.features().block_issuer().is_some() { + log::debug!("Adding block issuance context input for transitioned account output"); + self.bic_context_inputs.insert(BlockIssuanceCreditContextInput::from( + account.account_id_non_null(input.output_id()), + )); + } + } + // Transitioning an implicit account requires a BlockIssuanceCreditContextInput. + Output::Basic(basic) => { + if basic.is_implicit_account() { + log::debug!("Adding block issuance context input for transitioned implicit account output"); + self.bic_context_inputs + .insert(BlockIssuanceCreditContextInput::from(AccountId::from( + input.output_id(), + ))); + } + } + _ => (), + } + + if self.commitment_context_input.is_some() { + return; + } + let mut needs_commitment_context = false; + + // Inputs with timelock or expiration unlock condition require a CommitmentContextInput + if input + .output + .unlock_conditions() + .map_or(false, |u| u.iter().any(|u| u.is_timelock() || u.is_expiration())) + { + log::debug!("Adding commitment context input for timelocked or expiring output"); + needs_commitment_context = true; + } + + if self.mana_rewards.get(input.output_id()).is_some() { + log::debug!("Adding reward and commitment context input for output claiming mana rewards"); + self.reward_context_inputs.insert(*input.output_id()); + needs_commitment_context = true; + } + + // BlockIssuanceCreditContextInput requires a CommitmentContextInput. + if !self.bic_context_inputs.is_empty() { + // TODO https://github.com/iotaledger/iota-sdk/issues/1740 + log::debug!("Adding commitment context input for output with block issuance credit context input"); + needs_commitment_context = true; + } + + if needs_commitment_context { + // TODO https://github.com/iotaledger/iota-sdk/issues/1740 + self.commitment_context_input + .replace(CommitmentContextInput::new(self.latest_slot_commitment_id)); + } + } + + pub(crate) fn fulfill_output_context_inputs_requirements(&mut self) -> Result<(), TransactionBuilderError> { + let mut needs_commitment_context = false; + + for output in self + .provided_outputs + .iter_mut() + .chain(&mut self.added_outputs) + .filter(|o| o.is_delegation()) + { + // Created delegations have their start epoch set, and delayed delegations have their end set + if output.as_delegation().delegation_id().is_null() { + let start_epoch = self + .protocol_parameters + .delegation_start_epoch(self.latest_slot_commitment_id); + log::debug!("Setting created delegation start epoch to {start_epoch}"); + *output = DelegationOutputBuilder::from(output.as_delegation()) + .with_start_epoch(start_epoch) + .finish_output()?; + } else { + let end_epoch = self + .protocol_parameters + .delegation_end_epoch(self.latest_slot_commitment_id); + log::debug!("Setting delayed delegation end epoch to {end_epoch}"); + *output = DelegationOutputBuilder::from(output.as_delegation()) + .with_end_epoch(end_epoch) + .finish_output()?; + } + log::debug!("Adding commitment context input for delegation output"); + needs_commitment_context = true; + } + + if needs_commitment_context && self.commitment_context_input.is_none() { + // TODO https://github.com/iotaledger/iota-sdk/issues/1740 + self.commitment_context_input + .replace(CommitmentContextInput::new(self.latest_slot_commitment_id)); + } + Ok(()) + } +} diff --git a/sdk/src/client/api/block_builder/transaction_builder/mod.rs b/sdk/src/client/api/block_builder/transaction_builder/mod.rs index 2f2556367a..eef72a9e5d 100644 --- a/sdk/src/client/api/block_builder/transaction_builder/mod.rs +++ b/sdk/src/client/api/block_builder/transaction_builder/mod.rs @@ -4,6 +4,7 @@ //! Builder for transactions pub(crate) mod burn; +pub(crate) mod context_inputs; pub(crate) mod error; pub(crate) mod remainder; pub(crate) mod requirement; @@ -287,12 +288,10 @@ impl TransactionBuilder { *allotment_debt = self.mana_allotments.get(issuer_id).copied().unwrap_or_default(); } // Add initial requirements - self.requirements.extend([ - Requirement::Mana, - Requirement::ContextInputs, - Requirement::Amount, - Requirement::NativeTokens, - ]); + self.requirements + .extend([Requirement::Mana, Requirement::Amount, Requirement::NativeTokens]); + + self.fulfill_output_context_inputs_requirements()?; for required_input in self.required_inputs.clone() { // Checks that required input is available. @@ -489,7 +488,7 @@ impl TransactionBuilder { fn select_input(&mut self, input: InputSigningData) -> Result, TransactionBuilderError> { log::debug!("Selecting input {:?}", input.output_id()); - let mut added_output = None; + let mut added_output = false; if let Some(output) = self.transition_input(&input)? { // No need to check for `outputs_requirements` because // - the sender feature doesn't need to be verified as it has been removed @@ -497,7 +496,7 @@ impl TransactionBuilder { // - input doesn't need to be checked for as we just transitioned it // - foundry account requirement should have been met already by a prior `required_account_nft_addresses` self.added_outputs.push(output); - added_output = self.added_outputs.last(); + added_output = true; } if let Some(requirement) = self.required_account_nft_addresses(&input)? { @@ -505,14 +504,12 @@ impl TransactionBuilder { self.requirements.push(requirement); } - self.selected_inputs.push(input); + // New input may need context inputs + self.fulfill_context_inputs_requirements(&input); - // New inputs/outputs may need context inputs - if !self.requirements.contains(&Requirement::ContextInputs) { - self.requirements.push(Requirement::ContextInputs); - } + self.selected_inputs.push(input); - Ok(added_output) + Ok(if added_output { self.added_outputs.last() } else { None }) } /// Sets the required inputs of an [`TransactionBuilder`]. diff --git a/sdk/src/client/api/block_builder/transaction_builder/requirement/context_inputs.rs b/sdk/src/client/api/block_builder/transaction_builder/requirement/context_inputs.rs deleted file mode 100644 index 963754114c..0000000000 --- a/sdk/src/client/api/block_builder/transaction_builder/requirement/context_inputs.rs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use super::{TransactionBuilder, TransactionBuilderError}; -use crate::types::block::{ - context_input::{BlockIssuanceCreditContextInput, CommitmentContextInput}, - output::{AccountId, DelegationOutputBuilder, Output}, -}; - -impl TransactionBuilder { - pub(crate) fn fulfill_context_inputs_requirement(&mut self) -> Result<(), TransactionBuilderError> { - let mut needs_commitment_context = false; - - for input in &self.selected_inputs { - match &input.output { - // Transitioning an issuer account requires a BlockIssuanceCreditContextInput. - Output::Account(account) => { - if account.features().block_issuer().is_some() { - log::debug!("Adding block issuance context input for transitioned account output"); - self.bic_context_inputs.insert(BlockIssuanceCreditContextInput::from( - account.account_id_non_null(input.output_id()), - )); - } - } - // Transitioning an implicit account requires a BlockIssuanceCreditContextInput. - Output::Basic(basic) => { - if basic.is_implicit_account() { - log::debug!("Adding block issuance context input for transitioned implicit account output"); - self.bic_context_inputs - .insert(BlockIssuanceCreditContextInput::from(AccountId::from( - input.output_id(), - ))); - } - } - _ => (), - } - - // Inputs with timelock or expiration unlock condition require a CommitmentContextInput - if input - .output - .unlock_conditions() - .map_or(false, |u| u.iter().any(|u| u.is_timelock() || u.is_expiration())) - { - log::debug!("Adding commitment context input for timelocked or expiring output"); - needs_commitment_context = true; - } - - if self.mana_rewards.get(input.output_id()).is_some() { - log::debug!("Adding reward and commitment context input for output claiming mana rewards"); - self.reward_context_inputs.insert(*input.output_id()); - needs_commitment_context = true; - } - } - for output in self - .provided_outputs - .iter_mut() - .chain(&mut self.added_outputs) - .filter(|o| o.is_delegation()) - { - // Created delegations have their start epoch set, and delayed delegations have their end set - if output.as_delegation().delegation_id().is_null() { - let start_epoch = self - .protocol_parameters - .delegation_start_epoch(self.latest_slot_commitment_id); - log::debug!("Setting created delegation start epoch to {start_epoch}"); - *output = DelegationOutputBuilder::from(output.as_delegation()) - .with_start_epoch(start_epoch) - .finish_output()?; - } else { - let end_epoch = self - .protocol_parameters - .delegation_end_epoch(self.latest_slot_commitment_id); - log::debug!("Setting delayed delegation end epoch to {end_epoch}"); - *output = DelegationOutputBuilder::from(output.as_delegation()) - .with_end_epoch(end_epoch) - .finish_output()?; - } - log::debug!("Adding commitment context input for delegation output"); - needs_commitment_context = true; - } - // BlockIssuanceCreditContextInput requires a CommitmentContextInput. - if !self.bic_context_inputs.is_empty() { - // TODO https://github.com/iotaledger/iota-sdk/issues/1740 - log::debug!("Adding commitment context input for output with block issuance credit context input"); - needs_commitment_context = true; - } - - if needs_commitment_context && self.commitment_context_input.is_none() { - // TODO https://github.com/iotaledger/iota-sdk/issues/1740 - self.commitment_context_input - .replace(CommitmentContextInput::new(self.latest_slot_commitment_id)); - } - Ok(()) - } -} diff --git a/sdk/src/client/api/block_builder/transaction_builder/requirement/mod.rs b/sdk/src/client/api/block_builder/transaction_builder/requirement/mod.rs index 1d257b9d4f..91dfdba87d 100644 --- a/sdk/src/client/api/block_builder/transaction_builder/requirement/mod.rs +++ b/sdk/src/client/api/block_builder/transaction_builder/requirement/mod.rs @@ -3,7 +3,6 @@ pub(crate) mod account; pub(crate) mod amount; -pub(crate) mod context_inputs; pub(crate) mod delegation; pub(crate) mod ed25519; pub(crate) mod foundry; @@ -52,8 +51,6 @@ pub enum Requirement { Amount, /// Mana requirement. Mana, - /// Context inputs requirement. - ContextInputs, } impl TransactionBuilder { @@ -73,7 +70,6 @@ impl TransactionBuilder { Requirement::NativeTokens => self.fulfill_native_tokens_requirement(), Requirement::Amount => self.fulfill_amount_requirement(), Requirement::Mana => self.fulfill_mana_requirement(), - Requirement::ContextInputs => self.fulfill_context_inputs_requirement(), } } diff --git a/sdk/tests/client/transaction_builder/account_outputs.rs b/sdk/tests/client/transaction_builder/account_outputs.rs index a04fe40d95..4783903cb9 100644 --- a/sdk/tests/client/transaction_builder/account_outputs.rs +++ b/sdk/tests/client/transaction_builder/account_outputs.rs @@ -2364,3 +2364,71 @@ fn auto_transition_account_less_than_min_additional() { .unwrap(); assert_eq!(account_output.amount(), min_amount); } + +#[test] +fn account_transition_with_required_context_inputs() { + let protocol_parameters = iota_mainnet_protocol_parameters().clone(); + let account_id_1 = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + let ed25519_address = Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(); + + let inputs = [ + BasicOutputBuilder::new_with_amount(1_000_000) + .add_unlock_condition(AddressUnlockCondition::new(ed25519_address.clone())) + .finish_output() + .unwrap(), + AccountOutputBuilder::new_with_amount(1_000_000, account_id_1) + .with_mana(12000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_features([BlockIssuerFeature::new( + u32::MAX, + BlockIssuerKeys::from_vec(vec![ + Ed25519PublicKeyHashBlockIssuerKey::new(**ed25519_address.as_ed25519()).into(), + ]) + .unwrap(), + ) + .unwrap()]) + .finish_output() + .unwrap(), + ]; + let inputs = inputs + .into_iter() + .map(|input| InputSigningData { + output: input, + output_metadata: rand_output_metadata_with_id(rand_output_id_with_slot_index(SLOT_INDEX)), + chain: None, + }) + .collect::>(); + + let outputs = vec![ + BasicOutputBuilder::new_with_amount(1_000_000) + .add_unlock_condition(AddressUnlockCondition::new(ed25519_address.clone())) + .finish_output() + .unwrap(), + ]; + + let selected = TransactionBuilder::new( + inputs.clone(), + outputs.clone(), + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters, + ) + .with_min_mana_allotment(account_id_1, 2) + .finish() + .unwrap(); + + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 2); + assert!(selected.transaction.outputs()[1].is_account()); + assert_eq!(selected.transaction.allotments().len(), 1); + // Required context inputs are added when the account is transitioned + assert_eq!(selected.transaction.context_inputs().len(), 2); + assert!(selected.transaction.context_inputs().commitment().is_some()); + assert_eq!( + selected.transaction.context_inputs().block_issuance_credits().count(), + 1 + ); +} From f723ea75eec12e1c028abf078726ec06e08aa1fb Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:13:04 +0100 Subject: [PATCH 06/10] Nodejs: fix NativeTokenFeature (#2182) --- bindings/nodejs/lib/types/block/output/feature.ts | 14 ++++++++------ bindings/nodejs/lib/types/block/output/output.ts | 1 + 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/bindings/nodejs/lib/types/block/output/feature.ts b/bindings/nodejs/lib/types/block/output/feature.ts index 1f1e1ade19..ee5e0465ed 100644 --- a/bindings/nodejs/lib/types/block/output/feature.ts +++ b/bindings/nodejs/lib/types/block/output/feature.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { SlotIndex } from '../slot'; -import { Type } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; import { Address, AddressDiscriminator } from '../address'; import { BlockIssuerKey, @@ -11,7 +11,7 @@ import { import { u256, u64 } from '../../utils/type-aliases'; import { EpochIndex } from '../../block/slot'; import { NativeToken } from '../../models/native-token'; -import { HexEncodedString } from '../../utils/hex-encoding'; +import { HexEncodedString, hexToBigInt } from '../../utils/hex-encoding'; /** * Printable ASCII characters. @@ -147,16 +147,18 @@ class NativeTokenFeature extends Feature { /** * Amount of native tokens of the given Token ID. */ + @Transform((value) => hexToBigInt(value.value)) readonly amount: u256; /** * Creates a new `NativeTokenFeature`. - * @param nativeToken The native token stored with the feature. + * @param id The identifier of the native token. + * @param amount The native token amount. */ - constructor(nativeToken: NativeToken) { + constructor(id: HexEncodedString, amount: u256) { super(FeatureType.NativeToken); - this.id = nativeToken.id; - this.amount = nativeToken.amount; + this.id = id; + this.amount = amount; } /** diff --git a/bindings/nodejs/lib/types/block/output/output.ts b/bindings/nodejs/lib/types/block/output/output.ts index 0637133718..55cea2b67d 100644 --- a/bindings/nodejs/lib/types/block/output/output.ts +++ b/bindings/nodejs/lib/types/block/output/output.ts @@ -153,6 +153,7 @@ class BasicOutput extends CommonOutput { /** * @param amount The amount of the output. + * @param mana The mana of the output. * @param unlockConditions The unlock conditions for the output. */ constructor(amount: u64, mana: u64, unlockConditions: UnlockCondition[]) { From 89d20d3775a66680449cb0a0b28bf8850a3ee616 Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:13:29 +0100 Subject: [PATCH 07/10] Add slots_remaining to InsufficientMana Display (#2178) --- sdk/src/client/api/block_builder/transaction_builder/error.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sdk/src/client/api/block_builder/transaction_builder/error.rs b/sdk/src/client/api/block_builder/transaction_builder/error.rs index 8affd1f350..910501d47b 100644 --- a/sdk/src/client/api/block_builder/transaction_builder/error.rs +++ b/sdk/src/client/api/block_builder/transaction_builder/error.rs @@ -39,7 +39,9 @@ pub enum TransactionBuilderError { required: u64, }, /// Insufficient mana provided. - #[error("insufficient mana: found {found}, required {required}")] + #[error( + "insufficient mana: found {found}, required {required}, slots remaining until enough mana {slots_remaining}" + )] InsufficientMana { /// The amount found. found: u64, From 9c36a7cd7328f2e29359cdad2f9f7b1609ea6232 Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:13:56 +0100 Subject: [PATCH 08/10] Update getCommittee epoch query parameter (#2179) * Update getCommittee epoch query parameter * Update client method handler --- bindings/core/src/method/client.rs | 3 +-- bindings/core/src/method_handler/client.rs | 2 +- bindings/nodejs/lib/client/client.ts | 6 +++--- bindings/nodejs/lib/types/client/bridge/client.ts | 2 +- bindings/python/iota_sdk/client/_node_core_api.py | 6 +++--- sdk/src/client/node_api/core/routes.rs | 6 +++--- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/bindings/core/src/method/client.rs b/bindings/core/src/method/client.rs index 12272531d9..dcc7054473 100644 --- a/bindings/core/src/method/client.rs +++ b/bindings/core/src/method/client.rs @@ -187,10 +187,9 @@ pub enum ClientMethod { }, /// Return the information of committee members at the given epoch index. If epoch index is not provided, the /// current committee members are returned. - #[serde(rename_all = "camelCase")] GetCommittee { /// The epoch index to query. - epoch_index: Option, + epoch: Option, }, /// Get issuance GetIssuance, diff --git a/bindings/core/src/method_handler/client.rs b/bindings/core/src/method_handler/client.rs index f3159e5d5d..feba7de3ed 100644 --- a/bindings/core/src/method_handler/client.rs +++ b/bindings/core/src/method_handler/client.rs @@ -196,7 +196,7 @@ pub(crate) async fn call_client_method_internal( Response::Validators(client.get_validators(page_size, cursor).await?) } ClientMethod::GetValidator { account_id } => Response::Validator(client.get_validator(&account_id).await?), - ClientMethod::GetCommittee { epoch_index } => Response::Committee(client.get_committee(epoch_index).await?), + ClientMethod::GetCommittee { epoch } => Response::Committee(client.get_committee(epoch).await?), ClientMethod::GetIssuance => Response::Issuance(client.get_issuance().await?), ClientMethod::PostBlockRaw { block_bytes } => Response::BlockId( client diff --git a/bindings/nodejs/lib/client/client.ts b/bindings/nodejs/lib/client/client.ts index f158874354..cd42a7f507 100644 --- a/bindings/nodejs/lib/client/client.ts +++ b/bindings/nodejs/lib/client/client.ts @@ -252,13 +252,13 @@ export class Client { /** * Returns the information of committee members at the given epoch index. If epoch index is not provided, the * current committee members are returned. - * GET /api/core/v3/committee/?epochIndex + * GET /api/core/v3/committee/?epoch */ - async getCommittee(epochIndex?: EpochIndex): Promise { + async getCommittee(epoch?: EpochIndex): Promise { const response = await this.methodHandler.callMethod({ name: 'getCommittee', data: { - epochIndex, + epoch, }, }); diff --git a/bindings/nodejs/lib/types/client/bridge/client.ts b/bindings/nodejs/lib/types/client/bridge/client.ts index 3805397cbe..8e59066702 100644 --- a/bindings/nodejs/lib/types/client/bridge/client.ts +++ b/bindings/nodejs/lib/types/client/bridge/client.ts @@ -104,7 +104,7 @@ export interface __GetValidatorMethod__ { export interface __GetCommitteeMethod__ { name: 'getCommittee'; data: { - epochIndex?: EpochIndex; + epoch?: EpochIndex; }; } diff --git a/bindings/python/iota_sdk/client/_node_core_api.py b/bindings/python/iota_sdk/client/_node_core_api.py index f62b32525d..a83d076717 100644 --- a/bindings/python/iota_sdk/client/_node_core_api.py +++ b/bindings/python/iota_sdk/client/_node_core_api.py @@ -144,13 +144,13 @@ def get_validator(self, account_id: HexStr) -> ValidatorResponse: # Committee routes. def get_committee( - self, epoch_index: Optional[EpochIndex] = None) -> CommitteeResponse: + self, epoch: Optional[EpochIndex] = None) -> CommitteeResponse: """Returns the information of committee members at the given epoch index. If epoch index is not provided, the current committee members are returned. - GET /api/core/v3/committee/?epochIndex + GET /api/core/v3/committee/?epoch """ return CommitteeResponse.from_dict(self._call_method('getCommittee', { - 'epochIndex': epoch_index + 'epoch': epoch })) # Block routes. diff --git a/sdk/src/client/node_api/core/routes.rs b/sdk/src/client/node_api/core/routes.rs index c7b09ce4da..cd7694841e 100644 --- a/sdk/src/client/node_api/core/routes.rs +++ b/sdk/src/client/node_api/core/routes.rs @@ -167,13 +167,13 @@ impl Client { /// Returns the information of committee members at the given epoch index. If epoch index is not provided, the /// current committee members are returned. - /// GET /api/core/v3/committee/?epochIndex + /// GET /api/core/v3/committee/?epoch pub async fn get_committee( &self, - epoch_index: impl Into> + Send, + epoch: impl Into> + Send, ) -> Result { const PATH: &str = "api/core/v3/committee"; - let query = query_tuples_to_query_string([epoch_index.into().map(|i| ("epochIndex", i.to_string()))]); + let query = query_tuples_to_query_string([epoch.into().map(|i| ("epoch", i.to_string()))]); self.get_request(PATH, query.as_deref(), false).await } From 05cf6819f12bb172f3beb4f2e406b75de14bfbcb Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:14:35 +0100 Subject: [PATCH 09/10] Improve wait_for_transaction_acceptance (#2177) * Don't error with NotFound for the first attempts in wait_for_transaction_acceptance as the node may not have fully processed the tx at this point * Don't error on NotFound as nodes may haven't processed/got the tx yet --- sdk/src/client/api/wait_for_tx_acceptance.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/sdk/src/client/api/wait_for_tx_acceptance.rs b/sdk/src/client/api/wait_for_tx_acceptance.rs index fa2e0d668a..778a6a2f38 100644 --- a/sdk/src/client/api/wait_for_tx_acceptance.rs +++ b/sdk/src/client/api/wait_for_tx_acceptance.rs @@ -26,14 +26,20 @@ impl Client { .unwrap_or(DEFAULT_WAIT_FOR_TX_ACCEPTANCE_INTERVAL); for _ in 0..max_attempts.unwrap_or(DEFAULT_WAIT_FOR_TX_ACCEPTANCE_MAX_ATTEMPTS) { - let transaction_metadata = self.get_transaction_metadata(transaction_id).await?; - - match transaction_metadata.transaction_state { - TransactionState::Accepted | TransactionState::Committed | TransactionState::Finalized => { - return Ok(()); + match self.get_transaction_metadata(transaction_id).await { + Ok(transaction_metadata) => { + match transaction_metadata.transaction_state { + TransactionState::Accepted | TransactionState::Committed | TransactionState::Finalized => { + return Ok(()); + } + TransactionState::Failed => { + return Err(ClientError::TransactionAcceptance(transaction_id.to_string())); + } + TransactionState::Pending => {} // Just need to wait longer + }; } - TransactionState::Failed => return Err(ClientError::TransactionAcceptance(transaction_id.to_string())), - TransactionState::Pending => {} // Just need to wait longer + Err(ClientError::Node(crate::client::node_api::error::Error::NotFound(_))) => {} + Err(e) => return Err(e), }; #[cfg(target_family = "wasm")] From d2aea56a0a7e562b09de8295782d9e72e8c98975 Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Thu, 14 Mar 2024 18:38:14 +0100 Subject: [PATCH 10/10] Move tx length validation in builder (#2171) * Move tx length validation in builder * Move signed tx length validation in builder * format * Move and update consts * Align, update comment --- bindings/nodejs/lib/types/error.ts | 2 - .../offline_signing/2_sign_transaction.rs | 2 - .../client/api/block_builder/transaction.rs | 58 +------------------ .../block_builder/transaction_builder/mod.rs | 6 +- sdk/src/client/error.rs | 16 ----- sdk/src/client/node_api/core/routes.rs | 2 +- sdk/src/client/secret/mod.rs | 2 - sdk/src/types/api/core.rs | 2 +- sdk/src/types/block/payload/error.rs | 4 ++ .../block/payload/signed_transaction/mod.rs | 18 ++++++ .../payload/signed_transaction/transaction.rs | 40 +++++++++++++ .../transaction/sign_transaction.rs | 2 - sdk/tests/client/signing/account.rs | 8 +-- sdk/tests/client/signing/basic.rs | 12 +--- sdk/tests/client/signing/delegation.rs | 48 ++++----------- sdk/tests/client/signing/mod.rs | 4 +- sdk/tests/client/signing/nft.rs | 4 +- 17 files changed, 85 insertions(+), 145 deletions(-) diff --git a/bindings/nodejs/lib/types/error.ts b/bindings/nodejs/lib/types/error.ts index 78309d574e..ba27e9dc5f 100644 --- a/bindings/nodejs/lib/types/error.ts +++ b/bindings/nodejs/lib/types/error.ts @@ -30,8 +30,6 @@ export type ClientErrorName = | 'inputAddressNotFound' | 'invalidAmount' | 'invalidMnemonic' - | 'invalidTransactionLength' - | 'invalidSignedTransactionPayloadLength' | 'json' | 'missingParameter' | 'node' diff --git a/sdk/examples/wallet/offline_signing/2_sign_transaction.rs b/sdk/examples/wallet/offline_signing/2_sign_transaction.rs index dabf816d6a..db05836e11 100644 --- a/sdk/examples/wallet/offline_signing/2_sign_transaction.rs +++ b/sdk/examples/wallet/offline_signing/2_sign_transaction.rs @@ -48,8 +48,6 @@ async fn main() -> Result<(), Box> { let signed_transaction = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - signed_transaction.validate_length()?; - let signed_transaction_data = SignedTransactionData { payload: signed_transaction, inputs_data: prepared_transaction_data.inputs_data, diff --git a/sdk/src/client/api/block_builder/transaction.rs b/sdk/src/client/api/block_builder/transaction.rs index 6673047cbe..b4d7c20b70 100644 --- a/sdk/src/client/api/block_builder/transaction.rs +++ b/sdk/src/client/api/block_builder/transaction.rs @@ -3,30 +3,15 @@ //! Transaction preparation and signing -use packable::PackableExt; - use crate::{ - client::{ - api::{PreparedTransactionData, SignedTransactionData}, - ClientError, - }, + client::api::{PreparedTransactionData, SignedTransactionData}, types::block::{ output::{Output, OutputId}, - payload::signed_transaction::{SignedTransactionPayload, Transaction}, protocol::ProtocolParameters, semantic::{SemanticValidationContext, TransactionFailureReason}, - signature::Ed25519Signature, - Block, BlockId, }, }; -const MAX_TX_LENGTH_FOR_BLOCK_WITH_8_PARENTS: usize = Block::LENGTH_MAX - Block::LENGTH_MIN - (7 * BlockId::LENGTH); -// Length for unlocks with a single signature unlock (unlocks length + unlock type + signature type + public key + -// signature) -const SINGLE_UNLOCK_LENGTH: usize = 1 + 1 + Ed25519Signature::PUBLIC_KEY_LENGTH + Ed25519Signature::SIGNATURE_LENGTH; -// Type + reference index -const REFERENCE_ACCOUNT_NFT_UNLOCK_LENGTH: usize = 1 + 2; - impl PreparedTransactionData { /// Verifies the semantic of a prepared transaction. pub fn verify_semantic(&self, protocol_parameters: &ProtocolParameters) -> Result<(), TransactionFailureReason> { @@ -68,44 +53,3 @@ impl SignedTransactionData { context.validate() } } - -impl SignedTransactionPayload { - /// Verifies that the signed transaction payload doesn't exceed the block size limit with 8 parents. - pub fn validate_length(&self) -> Result<(), ClientError> { - let signed_transaction_payload_bytes = self.pack_to_vec(); - if signed_transaction_payload_bytes.len() > MAX_TX_LENGTH_FOR_BLOCK_WITH_8_PARENTS { - return Err(ClientError::InvalidSignedTransactionPayloadLength { - length: signed_transaction_payload_bytes.len(), - max_length: MAX_TX_LENGTH_FOR_BLOCK_WITH_8_PARENTS, - }); - } - Ok(()) - } -} - -impl Transaction { - /// Verifies that the transaction doesn't exceed the block size limit with 8 parents. - /// Assuming one signature unlock and otherwise reference/account/nft unlocks. - /// `validate_transaction_payload_length()` should later be used to check the length again with the correct - /// unlocks. - pub fn validate_length(&self) -> Result<(), ClientError> { - let transaction_bytes = self.pack_to_vec(); - - // Assuming there is only 1 signature unlock and the rest is reference/account/nft unlocks - let reference_account_nft_unlocks_amount = self.inputs().len() - 1; - - // Max tx payload length - length for one signature unlock (there might be more unlocks, we check with them - // later again, when we built the transaction payload) - let max_length = MAX_TX_LENGTH_FOR_BLOCK_WITH_8_PARENTS - - SINGLE_UNLOCK_LENGTH - - (reference_account_nft_unlocks_amount * REFERENCE_ACCOUNT_NFT_UNLOCK_LENGTH); - - if transaction_bytes.len() > max_length { - return Err(ClientError::InvalidTransactionLength { - length: transaction_bytes.len(), - max_length, - }); - } - Ok(()) - } -} diff --git a/sdk/src/client/api/block_builder/transaction_builder/mod.rs b/sdk/src/client/api/block_builder/transaction_builder/mod.rs index eef72a9e5d..75bf51aba0 100644 --- a/sdk/src/client/api/block_builder/transaction_builder/mod.rs +++ b/sdk/src/client/api/block_builder/transaction_builder/mod.rs @@ -169,11 +169,7 @@ impl Client { transaction_builder = transaction_builder.disable_additional_input_selection(); } - let prepared_transaction_data = transaction_builder.finish()?; - - prepared_transaction_data.transaction.validate_length()?; - - Ok(prepared_transaction_data) + Ok(transaction_builder.finish()?) } } diff --git a/sdk/src/client/error.rs b/sdk/src/client/error.rs index 12b202d576..95185a7d17 100644 --- a/sdk/src/client/error.rs +++ b/sdk/src/client/error.rs @@ -67,22 +67,6 @@ pub enum ClientError { /// Invalid mnemonic error #[error("invalid mnemonic {0}")] InvalidMnemonic(String), - /// The transaction is too large - #[error("the transaction is too large. Its length is {length}, max length is {max_length}")] - InvalidTransactionLength { - /// The found length. - length: usize, - /// The max supported length. - max_length: usize, - }, - /// The signed transaction payload is too large - #[error("the signed transaction payload is too large. Its length is {length}, max length is {max_length}")] - InvalidSignedTransactionPayloadLength { - /// The found length. - length: usize, - /// The max length. - max_length: usize, - }, /// JSON error #[error("{0}")] Json(#[from] serde_json::Error), diff --git a/sdk/src/client/node_api/core/routes.rs b/sdk/src/client/node_api/core/routes.rs index cd7694841e..098d57ed5e 100644 --- a/sdk/src/client/node_api/core/routes.rs +++ b/sdk/src/client/node_api/core/routes.rs @@ -37,7 +37,7 @@ use crate::{ pub(crate) static INFO_PATH: &str = "api/core/v3/info"; /// Contains the info and the url from the node (useful when multiple nodes are used) -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NodeInfoResponse { /// The returned info diff --git a/sdk/src/client/secret/mod.rs b/sdk/src/client/secret/mod.rs index 1ff12b4b86..dccb4e6839 100644 --- a/sdk/src/client/secret/mod.rs +++ b/sdk/src/client/secret/mod.rs @@ -666,8 +666,6 @@ where } = prepared_transaction_data; let tx_payload = SignedTransactionPayload::new(transaction, unlocks)?; - tx_payload.validate_length()?; - let data = SignedTransactionData { payload: tx_payload, inputs_data, diff --git a/sdk/src/types/api/core.rs b/sdk/src/types/api/core.rs index a076a2b42f..04d24589c6 100644 --- a/sdk/src/types/api/core.rs +++ b/sdk/src/types/api/core.rs @@ -34,7 +34,7 @@ pub struct RoutesResponse { /// Response of GET /api/core/v3/info. /// General information about the node. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct InfoResponse { pub name: String, diff --git a/sdk/src/types/block/payload/error.rs b/sdk/src/types/block/payload/error.rs index 0ee5331cd8..98e86f9458 100644 --- a/sdk/src/types/block/payload/error.rs +++ b/sdk/src/types/block/payload/error.rs @@ -42,8 +42,12 @@ pub enum PayloadError { InputCount(>::Error), #[display(fmt = "invalid output count: {_0}")] OutputCount(>::Error), + #[display(fmt = "the signed transaction payload is too large. Its length is {length}, max length is {max_length}")] + SignedTransactionPayloadLength { length: usize, max_length: usize }, #[display(fmt = "invalid transaction amount sum: {_0}")] TransactionAmountSum(u128), + #[display(fmt = "the transaction is too large. Its length is {length}, max length is {max_length}")] + TransactionLength { length: usize, max_length: usize }, #[display(fmt = "duplicate output chain: {_0}")] DuplicateOutputChain(ChainId), #[display(fmt = "duplicate UTXO {_0} in inputs")] diff --git a/sdk/src/types/block/payload/signed_transaction/mod.rs b/sdk/src/types/block/payload/signed_transaction/mod.rs index 5f07908aa2..429f31f3d0 100644 --- a/sdk/src/types/block/payload/signed_transaction/mod.rs +++ b/sdk/src/types/block/payload/signed_transaction/mod.rs @@ -8,6 +8,7 @@ mod transaction_id; use packable::{Packable, PackableExt}; +use self::transaction::MAX_TX_LENGTH_FOR_BLOCK_WITH_SINGLE_PARENT; pub(crate) use self::transaction::{InputCount, OutputCount}; pub use self::{ transaction::{Transaction, TransactionBuilder, TransactionCapabilities, TransactionCapabilityFlag}, @@ -69,9 +70,26 @@ fn verify_signed_transaction_payload(payload: &SignedTransactionPayload) -> Resu }); } + payload.validate_length()?; + Ok(()) } +impl SignedTransactionPayload { + /// Verifies that the transaction doesn't exceed the block size limit with 1 parent. + fn validate_length(&self) -> Result<(), PayloadError> { + let signed_transaction_payload_bytes = self.pack_to_vec(); + if signed_transaction_payload_bytes.len() > MAX_TX_LENGTH_FOR_BLOCK_WITH_SINGLE_PARENT { + return Err(PayloadError::SignedTransactionPayloadLength { + length: signed_transaction_payload_bytes.len(), + max_length: MAX_TX_LENGTH_FOR_BLOCK_WITH_SINGLE_PARENT, + }); + } + + Ok(()) + } +} + #[cfg(feature = "serde")] pub mod dto { use alloc::vec::Vec; diff --git a/sdk/src/types/block/payload/signed_transaction/transaction.rs b/sdk/src/types/block/payload/signed_transaction/transaction.rs index 3f7b688708..e297e359df 100644 --- a/sdk/src/types/block/payload/signed_transaction/transaction.rs +++ b/sdk/src/types/block/payload/signed_transaction/transaction.rs @@ -23,11 +23,22 @@ use crate::{ OptionalPayload, Payload, PayloadError, }, protocol::{ProtocolParameters, WorkScore, WorkScoreParameters}, + signature::Ed25519Signature, slot::SlotIndex, + Block, }, utils::merkle_hasher, }; +pub(crate) const BASIC_BLOCK_LENGTH_MIN: usize = 238; +pub(crate) const MAX_TX_LENGTH_FOR_BLOCK_WITH_SINGLE_PARENT: usize = Block::LENGTH_MAX - BASIC_BLOCK_LENGTH_MIN; +// Length for unlocks with a single signature unlock (unlocks length + unlock type + signature type + public key + +// signature) +pub(crate) const SINGLE_UNLOCK_LENGTH: usize = + 1 + 1 + Ed25519Signature::PUBLIC_KEY_LENGTH + Ed25519Signature::SIGNATURE_LENGTH; +// Type + reference index +pub(crate) const REFERENCE_ACCOUNT_NFT_UNLOCK_LENGTH: usize = 1 + 2; + /// A builder to build a [`Transaction`]. #[derive(Debug, Clone)] #[must_use] @@ -448,7 +459,36 @@ fn verify_transaction_packable(transaction: &Transaction, _: &ProtocolParameters verify_transaction(transaction) } +impl Transaction { + /// Verifies that the transaction doesn't exceed the block size limit with 1 parent. + /// Assuming one signature unlock and otherwise reference/account/nft unlocks. + /// `validate_transaction_payload_length()` should later be used to check the length again with the correct + /// unlocks. + fn validate_length(&self) -> Result<(), PayloadError> { + let transaction_bytes = self.pack_to_vec(); + + // Assuming there is only 1 signature unlock and the rest is reference/account/nft unlocks + let reference_account_nft_unlocks_amount = self.inputs().len() - 1; + + // Max tx payload length - length for one signature unlock (there might be more unlocks, we check with them + // later again, when we built the transaction payload) + let max_length = MAX_TX_LENGTH_FOR_BLOCK_WITH_SINGLE_PARENT + - SINGLE_UNLOCK_LENGTH + - (reference_account_nft_unlocks_amount * REFERENCE_ACCOUNT_NFT_UNLOCK_LENGTH); + + if transaction_bytes.len() > max_length { + return Err(PayloadError::TransactionLength { + length: transaction_bytes.len(), + max_length, + }); + } + Ok(()) + } +} + fn verify_transaction(transaction: &Transaction) -> Result<(), PayloadError> { + transaction.validate_length()?; + if transaction.context_inputs().commitment().is_none() { for output in transaction.outputs.iter() { if output.features().is_some_and(|f| f.staking().is_some()) { diff --git a/sdk/src/wallet/operations/transaction/sign_transaction.rs b/sdk/src/wallet/operations/transaction/sign_transaction.rs index 86f9bddee1..a9c4cd6193 100644 --- a/sdk/src/wallet/operations/transaction/sign_transaction.rs +++ b/sdk/src/wallet/operations/transaction/sign_transaction.rs @@ -84,8 +84,6 @@ where log::debug!("[TRANSACTION] signed transaction: {:?}", payload); - payload.validate_length()?; - Ok(SignedTransactionData { payload, inputs_data: prepared_transaction_data.inputs_data.clone(), diff --git a/sdk/tests/client/signing/account.rs b/sdk/tests/client/signing/account.rs index f1eb6c5bec..22e789792c 100644 --- a/sdk/tests/client/signing/account.rs +++ b/sdk/tests/client/signing/account.rs @@ -100,9 +100,7 @@ async fn sign_account_state_transition() -> Result<(), Box Result<(), Box> { _ => panic!("Invalid unlock"), } - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; prepared_transaction_data.verify_semantic(protocol_parameters)?; diff --git a/sdk/tests/client/signing/basic.rs b/sdk/tests/client/signing/basic.rs index 9afce0e84e..5fb18bf3d8 100644 --- a/sdk/tests/client/signing/basic.rs +++ b/sdk/tests/client/signing/basic.rs @@ -95,9 +95,7 @@ async fn single_ed25519_unlock() -> Result<(), Box> { assert_eq!(unlocks.len(), 1); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; prepared_transaction_data.verify_semantic(protocol_parameters)?; @@ -215,9 +213,7 @@ async fn ed25519_reference_unlocks() -> Result<(), Box> { _ => panic!("Invalid unlock"), } - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; prepared_transaction_data.verify_semantic(protocol_parameters)?; @@ -320,9 +316,7 @@ async fn two_signature_unlocks() -> Result<(), Box> { assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); assert_eq!((*unlocks).get(1).unwrap().kind(), SignatureUnlock::KIND); - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; prepared_transaction_data.verify_semantic(protocol_parameters)?; diff --git a/sdk/tests/client/signing/delegation.rs b/sdk/tests/client/signing/delegation.rs index e1289923c5..c96bd98fb1 100644 --- a/sdk/tests/client/signing/delegation.rs +++ b/sdk/tests/client/signing/delegation.rs @@ -105,9 +105,7 @@ async fn valid_creation() -> Result<(), Box> { assert_eq!(unlocks.len(), 1); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; prepared_transaction_data.verify_semantic(protocol_parameters)?; @@ -251,9 +249,7 @@ async fn non_null_id_creation() -> Result<(), Box> { assert_eq!(unlocks.len(), 1); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; let conflict = prepared_transaction_data.verify_semantic(protocol_parameters); @@ -336,9 +332,7 @@ async fn mismatch_amount_creation() -> Result<(), Box> { assert_eq!(unlocks.len(), 1); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; let conflict = prepared_transaction_data.verify_semantic(protocol_parameters); @@ -421,9 +415,7 @@ async fn non_zero_end_epoch_creation() -> Result<(), Box> assert_eq!(unlocks.len(), 1); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; let conflict = prepared_transaction_data.verify_semantic(protocol_parameters); @@ -504,9 +496,7 @@ async fn invalid_start_epoch_creation() -> Result<(), Box assert_eq!(unlocks.len(), 1); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; let conflict = prepared_transaction_data.verify_semantic(protocol_parameters); @@ -597,9 +587,7 @@ async fn delay_not_null_id() -> Result<(), Box> { assert_eq!(unlocks.len(), 1); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; let conflict = prepared_transaction_data.verify_semantic(protocol_parameters); @@ -693,9 +681,7 @@ async fn delay_modified_amount() -> Result<(), Box> { assert_eq!(unlocks.len(), 1); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; let conflict = prepared_transaction_data.verify_semantic(protocol_parameters); @@ -786,9 +772,7 @@ async fn delay_modified_validator() -> Result<(), Box> { assert_eq!(unlocks.len(), 1); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; let conflict = prepared_transaction_data.verify_semantic(protocol_parameters); @@ -879,9 +863,7 @@ async fn delay_modified_start_epoch() -> Result<(), Box> assert_eq!(unlocks.len(), 1); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; let conflict = prepared_transaction_data.verify_semantic(protocol_parameters); @@ -972,9 +954,7 @@ async fn delay_pre_registration_slot_end_epoch() -> Result<(), Box Result<(), Box> { assert_eq!(unlocks.len(), 1); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; prepared_transaction_data.verify_semantic(protocol_parameters)?; @@ -1154,9 +1132,7 @@ async fn destroy_reward_missing() -> Result<(), Box> { assert_eq!(unlocks.len(), 1); assert_eq!((*unlocks).first().unwrap().kind(), SignatureUnlock::KIND); - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; let conflict = prepared_transaction_data.verify_semantic(protocol_parameters); diff --git a/sdk/tests/client/signing/mod.rs b/sdk/tests/client/signing/mod.rs index 668f1a20b3..d9a0fcfc2e 100644 --- a/sdk/tests/client/signing/mod.rs +++ b/sdk/tests/client/signing/mod.rs @@ -432,9 +432,7 @@ async fn all_combined() -> Result<(), Box> { assert_eq!(unlocks.iter().filter(|u| u.is_account()).count(), 3); assert_eq!(unlocks.iter().filter(|u| u.is_nft()).count(), 3); - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; prepared_transaction_data.verify_semantic(protocol_parameters)?; diff --git a/sdk/tests/client/signing/nft.rs b/sdk/tests/client/signing/nft.rs index 2c2fcc9d0c..0fd011497d 100644 --- a/sdk/tests/client/signing/nft.rs +++ b/sdk/tests/client/signing/nft.rs @@ -157,9 +157,7 @@ async fn nft_reference_unlocks() -> Result<(), Box> { _ => panic!("Invalid unlock"), } - let tx_payload = SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; - - tx_payload.validate_length()?; + SignedTransactionPayload::new(prepared_transaction_data.transaction.clone(), unlocks)?; prepared_transaction_data.verify_semantic(protocol_parameters)?;