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)?;