Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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],
Expand Down
24 changes: 21 additions & 3 deletions packages/rs-platform-wallet/src/wallet/shielded/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ pub async fn unshield<S: ShieldedStore, P: OrchardProver>(

// 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,
Expand All @@ -318,6 +318,12 @@ pub async fn unshield<S: ShieldedStore, P: OrchardProver>(
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
Expand Down Expand Up @@ -402,7 +408,7 @@ pub async fn transfer<S: ShieldedStore, P: OrchardProver>(

// 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,
Expand All @@ -415,6 +421,12 @@ pub async fn transfer<S: ShieldedStore, P: OrchardProver>(
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
Expand Down Expand Up @@ -493,7 +505,7 @@ pub async fn withdraw<S: ShieldedStore, P: OrchardProver>(

// 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,
Expand All @@ -509,6 +521,12 @@ pub async fn withdraw<S: ShieldedStore, P: OrchardProver>(
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
Expand Down
Loading