diff --git a/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs b/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs index d22a61f4a01..f969346ebf3 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs @@ -55,7 +55,8 @@ use crate::consensus::basic::identity::{ IdentityAssetLockStateTransitionReplayError, IdentityAssetLockTransactionIsNotFoundError, IdentityAssetLockTransactionOutPointAlreadyConsumedError, IdentityAssetLockTransactionOutPointNotEnoughBalanceError, - IdentityAssetLockTransactionOutputNotFoundError, IdentityCreditTransferToSelfError, + IdentityAssetLockTransactionOutputNotFoundError, + IdentityAssetLockTransactionTooManyInputsError, IdentityCreditTransferToSelfError, InvalidAssetLockProofCoreChainHeightError, InvalidAssetLockProofTransactionHeightError, InvalidAssetLockTransactionOutputReturnSizeError, InvalidCreditWithdrawalTransitionCoreFeeError, @@ -677,6 +678,9 @@ pub enum BasicError { #[error(transparent)] ShieldedEncryptedNoteSizeMismatchError(ShieldedEncryptedNoteSizeMismatchError), + + #[error(transparent)] + IdentityAssetLockTransactionTooManyInputsError(IdentityAssetLockTransactionTooManyInputsError), } impl From for ConsensusError { diff --git a/packages/rs-dpp/src/errors/consensus/basic/identity/identity_asset_lock_transaction_too_many_inputs_error.rs b/packages/rs-dpp/src/errors/consensus/basic/identity/identity_asset_lock_transaction_too_many_inputs_error.rs new file mode 100644 index 00000000000..1047aeea1b3 --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/basic/identity/identity_asset_lock_transaction_too_many_inputs_error.rs @@ -0,0 +1,47 @@ +use crate::consensus::basic::BasicError; +use crate::consensus::ConsensusError; +use crate::errors::ProtocolError; +use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize}; +use thiserror::Error; + +use bincode::{Decode, Encode}; + +#[derive( + Error, Debug, Clone, Encode, Decode, PlatformSerialize, PlatformDeserialize, PartialEq, +)] +#[error("Asset lock transaction has too many inputs: {actual_inputs} (max {max_inputs}). Consolidate UTXOs before creating the asset lock.")] +#[platform_serialize(unversioned)] +pub struct IdentityAssetLockTransactionTooManyInputsError { + /* + + DO NOT CHANGE ORDER OF FIELDS WITHOUT INTRODUCING OF NEW VERSION + + */ + max_inputs: u16, + actual_inputs: u16, +} + +impl IdentityAssetLockTransactionTooManyInputsError { + pub fn new(actual_inputs: u16, max_inputs: u16) -> Self { + Self { + max_inputs, + actual_inputs, + } + } + + pub fn max_inputs(&self) -> u16 { + self.max_inputs + } + + pub fn actual_inputs(&self) -> u16 { + self.actual_inputs + } +} + +impl From for ConsensusError { + fn from(err: IdentityAssetLockTransactionTooManyInputsError) -> Self { + Self::BasicError(BasicError::IdentityAssetLockTransactionTooManyInputsError( + err, + )) + } +} diff --git a/packages/rs-dpp/src/errors/consensus/basic/identity/mod.rs b/packages/rs-dpp/src/errors/consensus/basic/identity/mod.rs index 5ebabe61e01..9ab0839536d 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/identity/mod.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/identity/mod.rs @@ -8,6 +8,7 @@ pub use identity_asset_lock_transaction_is_not_found_error::*; pub use identity_asset_lock_transaction_out_point_already_consumed_error::*; pub use identity_asset_lock_transaction_out_point_not_enough_balance_error::*; pub use identity_asset_lock_transaction_output_not_found_error::*; +pub use identity_asset_lock_transaction_too_many_inputs_error::*; pub use identity_credit_transfer_to_self_error::*; pub use invalid_asset_lock_proof_core_chain_height_error::*; pub use invalid_asset_lock_proof_transaction_height_error::*; @@ -39,6 +40,7 @@ mod duplicated_identity_public_key_id_basic_error; mod identity_asset_lock_proof_locked_transaction_mismatch_error; mod identity_asset_lock_transaction_is_not_found_error; mod identity_asset_lock_transaction_out_point_already_consumed_error; +mod identity_asset_lock_transaction_too_many_inputs_error; mod identity_asset_lock_state_transition_replay_error; mod identity_asset_lock_transaction_out_point_not_enough_balance_error; diff --git a/packages/rs-dpp/src/errors/consensus/codes.rs b/packages/rs-dpp/src/errors/consensus/codes.rs index d850c4e046a..cece174c4ae 100644 --- a/packages/rs-dpp/src/errors/consensus/codes.rs +++ b/packages/rs-dpp/src/errors/consensus/codes.rs @@ -202,6 +202,7 @@ impl ErrorWithCode for BasicError { Self::IdentityAssetLockStateTransitionReplayError(_) => 10531, Self::WithdrawalOutputScriptNotAllowedWhenSigningWithOwnerKeyError(_) => 10532, Self::InvalidKeyPurposeForContractBoundsError(_) => 10533, + Self::IdentityAssetLockTransactionTooManyInputsError(_) => 10534, // State Transition Errors: 10600-10699 Self::InvalidStateTransitionTypeError { .. } => 10600, diff --git a/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/validate_asset_lock_transaction_structure/mod.rs b/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/validate_asset_lock_transaction_structure/mod.rs index 42255735b95..d818fa55c3f 100644 --- a/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/validate_asset_lock_transaction_structure/mod.rs +++ b/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/validate_asset_lock_transaction_structure/mod.rs @@ -21,6 +21,12 @@ pub fn validate_asset_lock_transaction_structure( 0 => Ok(v0::validate_asset_lock_transaction_structure_v0( transaction, output_index, + platform_version + .dpp + .state_transitions + .identities + .asset_locks + .max_asset_lock_transaction_inputs, )), version => Err(ProtocolError::UnknownVersionMismatch { method: "validate_asset_lock_transaction_structure".to_string(), diff --git a/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/validate_asset_lock_transaction_structure/v0/mod.rs b/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/validate_asset_lock_transaction_structure/v0/mod.rs index e55d94cc4b4..b84f8070242 100644 --- a/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/validate_asset_lock_transaction_structure/v0/mod.rs +++ b/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/validate_asset_lock_transaction_structure/v0/mod.rs @@ -1,5 +1,6 @@ use crate::consensus::basic::identity::{ - IdentityAssetLockTransactionOutputNotFoundError, InvalidIdentityAssetLockTransactionError, + IdentityAssetLockTransactionOutputNotFoundError, + IdentityAssetLockTransactionTooManyInputsError, InvalidIdentityAssetLockTransactionError, InvalidIdentityAssetLockTransactionOutputError, }; use crate::validation::ConsensusValidationResult; @@ -11,7 +12,19 @@ use dashcore::{Transaction, TxOut}; pub(super) fn validate_asset_lock_transaction_structure_v0( transaction: &Transaction, output_index: u32, + max_inputs: u16, ) -> ConsensusValidationResult { + let input_count = transaction.input.len(); + if input_count > max_inputs as usize { + return ConsensusValidationResult::new_with_error( + IdentityAssetLockTransactionTooManyInputsError::new( + u16::try_from(input_count).unwrap_or(u16::MAX), + max_inputs, + ) + .into(), + ); + } + // It must be an Asset Lock Special Transaction let Some(TransactionPayload::AssetLockPayloadType(ref payload)) = transaction.special_transaction_payload @@ -41,3 +54,129 @@ pub(super) fn validate_asset_lock_transaction_structure_v0( ConsensusValidationResult::new_with_data(output.clone()) } } + +#[cfg(test)] +mod tests { + use super::*; + use dashcore::secp256k1::rand::thread_rng; + use dashcore::secp256k1::Secp256k1; + use dashcore::transaction::special_transaction::asset_lock::AssetLockPayload; + use dashcore::{Network, OutPoint, PrivateKey, ScriptBuf, TxIn, Txid}; + use std::str::FromStr; + + fn make_asset_lock_transaction(num_inputs: usize) -> Transaction { + let secp = Secp256k1::new(); + let mut rng = thread_rng(); + + let input_secret_key = dashcore::secp256k1::SecretKey::new(&mut rng); + let private_key = PrivateKey::new(input_secret_key, Network::Testnet); + let public_key = private_key.public_key(&secp); + let public_key_hash = public_key.pubkey_hash(); + + let secret_key = dashcore::secp256k1::SecretKey::new(&mut rng); + let one_time_private_key = PrivateKey::new(secret_key, Network::Testnet); + let one_time_public_key = one_time_private_key.public_key(&secp); + let one_time_key_hash = one_time_public_key.pubkey_hash(); + + let base_txid = + Txid::from_str("a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458") + .unwrap(); + + let inputs: Vec = (0..num_inputs) + .map(|i| TxIn { + previous_output: OutPoint::new(base_txid, i as u32), + script_sig: ScriptBuf::new_p2pkh(&public_key_hash), + sequence: 0, + witness: Default::default(), + }) + .collect(); + + let funding_output = TxOut { + value: 100_000_000, + script_pubkey: ScriptBuf::new_p2pkh(&one_time_key_hash), + }; + + let burn_output = TxOut { + value: 100_000_000, + script_pubkey: ScriptBuf::new_op_return(&[]), + }; + + let payload = TransactionPayload::AssetLockPayloadType(AssetLockPayload { + version: 0, + credit_outputs: vec![funding_output], + }); + + Transaction { + version: 0, + lock_time: 0, + input: inputs, + output: vec![burn_output], + special_transaction_payload: Some(payload), + } + } + + #[test] + fn should_reject_transaction_with_too_many_inputs() { + let tx = make_asset_lock_transaction(101); + let result = validate_asset_lock_transaction_structure_v0(&tx, 0, 100); + + assert!(result.errors.len() == 1); + let error = &result.errors[0]; + match error { + crate::consensus::ConsensusError::BasicError( + crate::consensus::basic::BasicError::IdentityAssetLockTransactionTooManyInputsError( + e, + ), + ) => { + assert_eq!(e.actual_inputs(), 101); + assert_eq!(e.max_inputs(), 100); + } + other => { + panic!("Expected IdentityAssetLockTransactionTooManyInputsError, got: {other:?}") + } + } + } + + #[test] + fn should_accept_transaction_at_max_inputs() { + let tx = make_asset_lock_transaction(100); + let result = validate_asset_lock_transaction_structure_v0(&tx, 0, 100); + + let has_too_many_inputs_error = result.errors.iter().any(|e| { + matches!( + e, + crate::consensus::ConsensusError::BasicError( + crate::consensus::basic::BasicError::IdentityAssetLockTransactionTooManyInputsError(_) + ) + ) + }); + assert!( + !has_too_many_inputs_error, + "Should not reject exactly 100 inputs" + ); + assert!( + result.is_valid(), + "Transaction at max inputs should be valid" + ); + } + + #[test] + fn should_accept_transaction_with_one_input() { + let tx = make_asset_lock_transaction(1); + let result = validate_asset_lock_transaction_structure_v0(&tx, 0, 100); + + let has_too_many_inputs_error = result.errors.iter().any(|e| { + matches!( + e, + crate::consensus::ConsensusError::BasicError( + crate::consensus::basic::BasicError::IdentityAssetLockTransactionTooManyInputsError(_) + ) + ) + }); + assert!(!has_too_many_inputs_error, "Should not reject single input"); + assert!( + result.is_valid(), + "Single input transaction should be valid" + ); + } +} diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/mod.rs index 2ad3a68a067..1bde330b4db 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/mod.rs @@ -54,6 +54,7 @@ pub struct IdentityTransitionAssetLockVersions { pub required_asset_lock_duff_balance_for_processing_start_for_address_funding: u64, pub validate_asset_lock_transaction_structure: FeatureVersion, pub validate_instant_asset_lock_proof_structure: FeatureVersion, + pub max_asset_lock_transaction_inputs: u16, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v1.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v1.rs index 307a552ab2f..07637973453 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v1.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v1.rs @@ -22,6 +22,7 @@ pub const STATE_TRANSITION_VERSIONS_V1: DPPStateTransitionVersions = DPPStateTra required_asset_lock_duff_balance_for_processing_start_for_address_funding: 50000, validate_asset_lock_transaction_structure: 0, validate_instant_asset_lock_proof_structure: 0, + max_asset_lock_transaction_inputs: u16::MAX, }, credit_withdrawal: IdentityCreditWithdrawalTransitionVersions { default_constructor: 0, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v2.rs index 009f63be950..8f63e97b8c6 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v2.rs @@ -22,6 +22,7 @@ pub const STATE_TRANSITION_VERSIONS_V2: DPPStateTransitionVersions = DPPStateTra required_asset_lock_duff_balance_for_processing_start_for_address_funding: 50000, validate_asset_lock_transaction_structure: 0, validate_instant_asset_lock_proof_structure: 0, + max_asset_lock_transaction_inputs: u16::MAX, }, credit_withdrawal: IdentityCreditWithdrawalTransitionVersions { default_constructor: 1, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v3.rs index e6df43a1e46..cc43aa2771d 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v3.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_versions/v3.rs @@ -22,6 +22,7 @@ pub const STATE_TRANSITION_VERSIONS_V3: DPPStateTransitionVersions = DPPStateTra required_asset_lock_duff_balance_for_processing_start_for_address_funding: 50000, validate_asset_lock_transaction_structure: 0, validate_instant_asset_lock_proof_structure: 0, + max_asset_lock_transaction_inputs: 100, }, credit_withdrawal: IdentityCreditWithdrawalTransitionVersions { default_constructor: 1, diff --git a/packages/wasm-dpp/src/errors/consensus/basic/identity/identity_asset_lock_transaction_too_many_inputs_error.rs b/packages/wasm-dpp/src/errors/consensus/basic/identity/identity_asset_lock_transaction_too_many_inputs_error.rs new file mode 100644 index 00000000000..9d40eac4226 --- /dev/null +++ b/packages/wasm-dpp/src/errors/consensus/basic/identity/identity_asset_lock_transaction_too_many_inputs_error.rs @@ -0,0 +1,41 @@ +use dpp::consensus::basic::identity::IdentityAssetLockTransactionTooManyInputsError; +use dpp::consensus::codes::ErrorWithCode; +use dpp::consensus::ConsensusError; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name=IdentityAssetLockTransactionTooManyInputsError)] +pub struct IdentityAssetLockTransactionTooManyInputsErrorWasm { + inner: IdentityAssetLockTransactionTooManyInputsError, +} + +impl From<&IdentityAssetLockTransactionTooManyInputsError> + for IdentityAssetLockTransactionTooManyInputsErrorWasm +{ + fn from(e: &IdentityAssetLockTransactionTooManyInputsError) -> Self { + Self { inner: e.clone() } + } +} + +#[wasm_bindgen(js_class=IdentityAssetLockTransactionTooManyInputsError)] +impl IdentityAssetLockTransactionTooManyInputsErrorWasm { + #[wasm_bindgen(js_name=getMaxInputs)] + pub fn get_max_inputs(&self) -> u16 { + self.inner.max_inputs() + } + + #[wasm_bindgen(js_name=getActualInputs)] + pub fn get_actual_inputs(&self) -> u16 { + self.inner.actual_inputs() + } + + #[wasm_bindgen(js_name=getCode)] + pub fn get_code(&self) -> u32 { + ConsensusError::from(self.inner.clone()).code() + } + + #[wasm_bindgen(getter)] + pub fn message(&self) -> String { + self.inner.to_string() + } +} diff --git a/packages/wasm-dpp/src/errors/consensus/basic/identity/mod.rs b/packages/wasm-dpp/src/errors/consensus/basic/identity/mod.rs index 39b9fc46939..95db5d69870 100644 --- a/packages/wasm-dpp/src/errors/consensus/basic/identity/mod.rs +++ b/packages/wasm-dpp/src/errors/consensus/basic/identity/mod.rs @@ -5,6 +5,7 @@ mod identity_asset_lock_state_transition_replay_error; mod identity_asset_lock_transaction_is_not_found_error; mod identity_asset_lock_transaction_out_point_already_exists_error; mod identity_asset_lock_transaction_output_not_found_error; +mod identity_asset_lock_transaction_too_many_inputs_error; mod identity_credit_transfer_to_self_error; mod identity_insufficient_balance_error; mod invalid_asset_lock_proof_core_chain_height_error; @@ -34,6 +35,7 @@ pub use identity_asset_lock_state_transition_replay_error::*; pub use identity_asset_lock_transaction_is_not_found_error::*; pub use identity_asset_lock_transaction_out_point_already_exists_error::*; pub use identity_asset_lock_transaction_output_not_found_error::*; +pub use identity_asset_lock_transaction_too_many_inputs_error::*; pub use identity_credit_transfer_to_self_error::*; pub use identity_insufficient_balance_error::*; pub use invalid_asset_lock_proof_core_chain_height_error::*; diff --git a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs index 8d63701b0b5..d1966361478 100644 --- a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs +++ b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs @@ -12,7 +12,8 @@ use crate::errors::consensus::basic::identity::{ IdentityAssetLockTransactionIsNotFoundErrorWasm, IdentityAssetLockTransactionOutPointAlreadyExistsErrorWasm, IdentityAssetLockTransactionOutPointNotEnoughBalanceErrorWasm, - IdentityAssetLockTransactionOutputNotFoundErrorWasm, IdentityCreditTransferToSelfErrorWasm, + IdentityAssetLockTransactionOutputNotFoundErrorWasm, + IdentityAssetLockTransactionTooManyInputsErrorWasm, IdentityCreditTransferToSelfErrorWasm, IdentityInsufficientBalanceErrorWasm, InvalidAssetLockProofCoreChainHeightErrorWasm, InvalidAssetLockProofTransactionHeightErrorWasm, InvalidAssetLockTransactionOutputReturnSizeErrorWasm, @@ -38,7 +39,8 @@ use dpp::consensus::basic::BasicError::{ IdentityAssetLockStateTransitionReplayError, IdentityAssetLockTransactionIsNotFoundError, IdentityAssetLockTransactionOutPointAlreadyConsumedError, IdentityAssetLockTransactionOutPointNotEnoughBalanceError, - IdentityAssetLockTransactionOutputNotFoundError, IncompatibleProtocolVersionError, + IdentityAssetLockTransactionOutputNotFoundError, + IdentityAssetLockTransactionTooManyInputsError, IncompatibleProtocolVersionError, IncompatibleRe2PatternError, InvalidAssetLockProofCoreChainHeightError, InvalidAssetLockProofTransactionHeightError, InvalidAssetLockTransactionOutputReturnSizeError, InvalidCreditWithdrawalTransitionCoreFeeError, @@ -614,6 +616,9 @@ fn from_basic_error(basic_error: &BasicError) -> JsValue { InvalidIdentityAssetLockTransactionError(e) => { InvalidIdentityAssetLockTransactionErrorWasm::from(e).into() } + IdentityAssetLockTransactionTooManyInputsError(e) => { + IdentityAssetLockTransactionTooManyInputsErrorWasm::from(e).into() + } InvalidInstantAssetLockProofError(e) => { InvalidInstantAssetLockProofErrorWasm::from(e).into() }