diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/mod.rs index 436d71adce8..ef78bbeea44 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/mod.rs @@ -40,6 +40,14 @@ pub const MIN_CORE_FEE_PER_BYTE: u32 = 1; pub const MIN_WITHDRAWAL_AMOUNT: u64 = (ASSET_UNLOCK_TX_SIZE as u64) * (MIN_CORE_FEE_PER_BYTE as u64) * CREDITS_PER_DUFF; +// Compile-time lock: if a dashcore `ASSET_UNLOCK_TX_SIZE` (or fee-rate) change moves this +// value, the build breaks here — a prompt to re-sync `SYSTEM_LIMITS_V1.min_withdrawal_amount` +// (the consensus source of truth) with the new figure. +const _: () = assert!( + MIN_WITHDRAWAL_AMOUNT == 190_000, + "MIN_WITHDRAWAL_AMOUNT changed; re-sync SYSTEM_LIMITS_V1.min_withdrawal_amount" +); + #[derive( Debug, Clone, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs index 5eba3e5f998..a49d1a1ec9e 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs @@ -256,8 +256,9 @@ mod tests { /// Uses an input large enough to cover the minimum withdrawal amount. fn valid_withdrawal_transition() -> AddressCreditWithdrawalTransitionV0 { let mut inputs = BTreeMap::new(); - // Need at least MIN_WITHDRAWAL_AMOUNT (190_000) for a valid withdrawal - inputs.insert(PlatformAddress::P2pkh([1u8; 20]), (0, 1_000_000)); + // Need at least the v12 min_withdrawal_amount (1_000_000) for a valid withdrawal; + // use 2_000_000 to sit comfortably above the floor rather than on its boundary. + inputs.insert(PlatformAddress::P2pkh([1u8; 20]), (0, 2_000_000)); AddressCreditWithdrawalTransitionV0 { inputs, @@ -608,16 +609,15 @@ mod tests { fn should_return_invalid_if_withdrawal_amount_below_minimum() { let platform_version = PlatformVersion::latest(); let mut transition = valid_withdrawal_transition(); - // Set input just barely above 0 withdrawal but below MIN_WITHDRAWAL_AMOUNT - // MIN_WITHDRAWAL_AMOUNT = 190_000, so if input=190_000, withdrawal would be too small - // to allow a meaningful withdrawal, we need input close to min_input_amount + // Set input below the v12 min_withdrawal_amount (1_000_000) so the resulting + // withdrawal is rejected as too small. transition.inputs.clear(); transition .inputs .insert(PlatformAddress::P2pkh([1u8; 20]), (0, 100_000)); let result = transition.validate_structure(platform_version); - // 100_000 withdrawal < MIN_WITHDRAWAL_AMOUNT (190_000), should fail + // 100_000 withdrawal < min_withdrawal_amount (1_000_000), should fail assert_matches!( result.errors.as_slice(), [ConsensusError::BasicError( diff --git a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/mod.rs index 46581cf532d..48f9e1915b1 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_withdrawal_transition/mod.rs @@ -48,6 +48,14 @@ pub const MIN_CORE_FEE_PER_BYTE: u32 = 1; pub const MIN_WITHDRAWAL_AMOUNT: u64 = (ASSET_UNLOCK_TX_SIZE as u64) * (MIN_CORE_FEE_PER_BYTE as u64) * CREDITS_PER_DUFF; +// Compile-time lock: if a dashcore `ASSET_UNLOCK_TX_SIZE` (or fee-rate) change moves this +// value, the build breaks here — a prompt to re-sync `SYSTEM_LIMITS_V1.min_withdrawal_amount` +// (the consensus source of truth) with the new figure. +const _: () = assert!( + MIN_WITHDRAWAL_AMOUNT == 190_000, + "MIN_WITHDRAWAL_AMOUNT changed; re-sync SYSTEM_LIMITS_V1.min_withdrawal_amount" +); + pub type IdentityCreditWithdrawalTransitionLatest = IdentityCreditWithdrawalTransitionV1; #[cfg_attr( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_withdrawal/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_withdrawal/tests.rs index 1a4c345eff7..24662b55a6b 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_withdrawal/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_withdrawal/tests.rs @@ -61,7 +61,7 @@ mod tests { fn create_default_shielded_withdrawal_transition() -> StateTransition { create_shielded_withdrawal_transition( vec![create_dummy_serialized_action()], - 131_548_800, // unshielding_amount: recipient (1_000_000, above MIN_WITHDRAWAL_AMOUNT) + min fee for 1 action + 131_548_800, // unshielding_amount: recipient (1_000_000, at the v12 min_withdrawal_amount floor) + min fee for 1 action [42u8; 32], // non-zero anchor vec![0u8; 100], // dummy proof bytes [0u8; 64], // dummy binding signature @@ -857,7 +857,7 @@ mod tests { let transition = create_shielded_withdrawal_transition( vec![action1, action2], // Both have nullifier [1u8; 32] - 162_097_600, // unshielding_amount: recipient (1_000_000, above MIN_WITHDRAWAL_AMOUNT) + min fee for 2 actions + 162_097_600, // unshielding_amount: recipient (1_000_000, at the v12 min_withdrawal_amount floor) + min fee for 2 actions anchor, vec![0u8; 100], [0u8; 64], diff --git a/packages/rs-platform-wallet/src/wallet/shielded/operations.rs b/packages/rs-platform-wallet/src/wallet/shielded/operations.rs index f5bc297f7ce..0cfe730a983 100644 --- a/packages/rs-platform-wallet/src/wallet/shielded/operations.rs +++ b/packages/rs-platform-wallet/src/wallet/shielded/operations.rs @@ -305,7 +305,7 @@ pub async fn unshield( // The builder computes and returns the fee authoritatively; `exact_fee` (== the // minimum) was already used above for note reservation. - let (state_transition, _fee_used) = build_unshield_transition( + let (state_transition, fee_used) = build_unshield_transition( spends, *to_address, amount, @@ -318,6 +318,12 @@ pub async fn unshield( sdk.version(), ) .map_err(|e| PlatformWalletError::ShieldedBuildError(e.to_string()))?; + // The builder's fee and the wallet's reserved `exact_fee` both come from + // compute_minimum_shielded_fee with the same action count; lock that they agree. + debug_assert_eq!( + fee_used, exact_fee, + "builder fee must match the reserved minimum fee" + ); trace!("Unshield: state transition built, broadcasting..."); state_transition @@ -402,7 +408,7 @@ pub async fn transfer( // The builder computes and returns the fee authoritatively; `exact_fee` (== the // minimum) was already used above for note reservation. - let (state_transition, _fee_used) = build_shielded_transfer_transition( + let (state_transition, fee_used) = build_shielded_transfer_transition( spends, &recipient_addr, amount, @@ -415,6 +421,12 @@ pub async fn transfer( sdk.version(), ) .map_err(|e| PlatformWalletError::ShieldedBuildError(e.to_string()))?; + // The builder's fee and the wallet's reserved `exact_fee` both come from + // compute_minimum_shielded_fee with the same action count; lock that they agree. + debug_assert_eq!( + fee_used, exact_fee, + "builder fee must match the reserved minimum fee" + ); trace!("Shielded transfer: state transition built, broadcasting..."); state_transition @@ -493,7 +505,7 @@ pub async fn withdraw( // The builder computes and returns the fee authoritatively; `exact_fee` (== the // minimum) was already used above for note reservation. - let (state_transition, _fee_used) = build_shielded_withdrawal_transition( + let (state_transition, fee_used) = build_shielded_withdrawal_transition( spends, amount, output_script, @@ -509,6 +521,12 @@ pub async fn withdraw( sdk.version(), ) .map_err(|e| PlatformWalletError::ShieldedBuildError(e.to_string()))?; + // The builder's fee and the wallet's reserved `exact_fee` both come from + // compute_minimum_shielded_fee with the same action count; lock that they agree. + debug_assert_eq!( + fee_used, exact_fee, + "builder fee must match the reserved minimum fee" + ); trace!("Shielded withdrawal: state transition built, broadcasting..."); state_transition