Skip to content
55 changes: 43 additions & 12 deletions packages/rs-dpp/src/withdrawal/daily_withdrawal_limit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,32 @@ use platform_version::version::PlatformVersion;

mod v0;
mod v1;
mod v2;

/// Returns the maximum amount of credits Platform may pool into asset unlock
/// transactions per 24 hours.
///
/// `reference_total_credits` is the base the limit is derived from: the current
/// total credits in Platform for version 0 (10% of it, bounded; required), ignored
/// by version 1 (a flat 2000 Dash), and the total credits Platform held a day ago
/// for version 2 (`daily_withdrawal_limit_percent` of it, never below one maximal
/// withdrawal nor above `max_daily_withdrawal_amount`; the flat limit of version 1
/// while that day-old total is not known yet).
pub fn daily_withdrawal_limit(
total_credits_in_platform: Credits,
reference_total_credits: Option<Credits>,
platform_version: &PlatformVersion,
) -> Result<Credits, ProtocolError> {
match platform_version.dpp.methods.daily_withdrawal_limit {
0 => Ok(daily_withdrawal_limit_v0(total_credits_in_platform)),
1 => Ok(v1::daily_withdrawal_limit_v1(platform_version)),
0 => reference_total_credits
.map(daily_withdrawal_limit_v0)
.ok_or_else(|| {
ProtocolError::CorruptedCodeExecution(
"daily_withdrawal_limit v0 requires the current total credits in Platform"
.to_string(),
)
}),
1 => Ok(v1::daily_withdrawal_limit_v1()),
2 => v2::daily_withdrawal_limit_v2(reference_total_credits, platform_version),
v => Err(ProtocolError::UnknownVersionError(format!(
"Unknown daily_withdrawal_limit version {v}"
))),
Expand All @@ -25,24 +43,37 @@ mod tests {
use crate::dash_to_credits;

#[test]
fn should_double_flat_daily_withdrawal_limit_at_protocol_version_14() {
fn should_switch_from_flat_to_relative_daily_withdrawal_limit_at_protocol_version_14() {
let v13 = PlatformVersion::get(13).expect("expected protocol version 13");
let v14 = PlatformVersion::get(14).expect("expected protocol version 14");

// Both flat limits are independent of the credits held in Platform.
for total_credits in [
dash_to_credits!(50),
dash_to_credits!(5000),
dash_to_credits!(1000000),
for (total_credits_a_day_ago, expected_v14) in [
// Below one maximal withdrawal (500 Dash) the limit is floored there.
(dash_to_credits!(50), dash_to_credits!(500)),
(dash_to_credits!(2000), dash_to_credits!(500)),
(dash_to_credits!(20000), dash_to_credits!(3000)),
// Above Core's unlock capacity per day (4000 Dash) the limit is capped there.
(dash_to_credits!(30000), dash_to_credits!(4000)),
(dash_to_credits!(1000000), dash_to_credits!(4000)),
] {
// v13 keeps the flat 2000 Dash whatever the total is.
assert_eq!(
daily_withdrawal_limit(total_credits, v13).expect("expected v13 limit"),
daily_withdrawal_limit(Some(total_credits_a_day_ago), v13)
.expect("expected v13 limit"),
dash_to_credits!(2000)
);
// v14 allows 15% of the total credits a day ago.
assert_eq!(
daily_withdrawal_limit(total_credits, v14).expect("expected v14 limit"),
dash_to_credits!(4000)
daily_withdrawal_limit(Some(total_credits_a_day_ago), v14)
.expect("expected v14 limit"),
expected_v14
);
}

// Until the total credits a day ago are known, v14 keeps v13's flat limit.
assert_eq!(
daily_withdrawal_limit(None, v14).expect("expected v14 bootstrap limit"),
dash_to_credits!(2000)
);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::fee::Credits;
use platform_version::version::PlatformVersion;

/// Flat daily withdrawal limit, read from the protocol version's system limits
/// (`SystemLimits::daily_withdrawal_limit`): 2000 Dash up to protocol version 13
/// (the limit in Core v22), 4000 Dash from protocol version 14 (Core v24).
pub fn daily_withdrawal_limit_v1(platform_version: &PlatformVersion) -> Credits {
platform_version.system_limits.daily_withdrawal_limit
/// Flat daily withdrawal limit of 2000 Dash, matching the limit in Core v22
/// (`LimitAmountV22`). In force from protocol version 8 to protocol version 13;
/// superseded by the relative limit of version 2.
pub const fn daily_withdrawal_limit_v1() -> Credits {
// 2000 Dash
200_000_000_000_000
}
194 changes: 194 additions & 0 deletions packages/rs-dpp/src/withdrawal/daily_withdrawal_limit/v2/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
use crate::fee::Credits;
use crate::withdrawal::daily_withdrawal_limit::v1::daily_withdrawal_limit_v1;
use crate::ProtocolError;
use platform_version::version::PlatformVersion;

/// Relative daily withdrawal limit: `daily_withdrawal_limit_percent` (from the
/// protocol version's system limits) of the total credits Platform held a day ago.
/// Using a day-old base means a sudden jump in the total credits does not raise
/// the limit for a day.
///
/// Three guards keep it usable:
/// * it is never below `max_withdrawal_amount`, so every withdrawal Platform
/// accepts eventually fits the daily maximum and cannot block the pooling
/// queue behind it;
/// * it is never above `max_daily_withdrawal_amount`, Core's credit-pool unlock
/// capacity per day: pooling more than Core will mine only cycles those
/// unlocks through expiry and re-signing;
/// * while the total credits a day ago are not known (`None`: the history is
/// younger than a day, i.e. right after this rule activates), the flat limit
/// of version 1 applies, so the lag cannot be skipped by inflating the total
/// before or at activation.
pub fn daily_withdrawal_limit_v2(
total_credits_in_platform_a_day_ago: Option<Credits>,
platform_version: &PlatformVersion,
) -> Result<Credits, ProtocolError> {
let Some(total_credits_a_day_ago) = total_credits_in_platform_a_day_ago else {
return Ok(daily_withdrawal_limit_v1());
};

let percent = platform_version
.system_limits
.daily_withdrawal_limit_percent
.ok_or_else(|| {
ProtocolError::CorruptedCodeExecution(
"daily_withdrawal_limit v2 requires system_limits.daily_withdrawal_limit_percent"
.to_string(),
)
})?;

let max_daily_withdrawal_amount = platform_version
.system_limits
.max_daily_withdrawal_amount
.ok_or_else(|| {
ProtocolError::CorruptedCodeExecution(
"daily_withdrawal_limit v2 requires system_limits.max_daily_withdrawal_amount"
.to_string(),
)
})?;

let max_withdrawal_amount = platform_version.system_limits.max_withdrawal_amount;
if max_daily_withdrawal_amount < max_withdrawal_amount {
// A cap below one maximal withdrawal would let an accepted withdrawal never fit the
// daily maximum; that is a contradictory configuration, not a limit to apply.
return Err(ProtocolError::CorruptedCodeExecution(format!(
"daily_withdrawal_limit v2 requires system_limits.max_daily_withdrawal_amount ({max_daily_withdrawal_amount}) to be at least max_withdrawal_amount ({max_withdrawal_amount})"
)));
}

// u128 keeps `total * percent` from overflowing for any u64 total.
let relative_limit = (total_credits_a_day_ago as u128) * (percent as u128) / 100;
let relative_limit = Credits::try_from(relative_limit)
.map_err(|_| ProtocolError::Overflow("daily withdrawal limit overflow"))?;

Ok(relative_limit
.max(max_withdrawal_amount)
.min(max_daily_withdrawal_amount))
Comment thread
QuantumExplorer marked this conversation as resolved.
}

#[cfg(test)]
mod tests {
use super::*;
use crate::dash_to_credits;

fn platform_version_with(percent: Option<u8>) -> PlatformVersion {
let mut platform_version = PlatformVersion::latest().clone();
platform_version
.system_limits
.daily_withdrawal_limit_percent = percent;
platform_version.system_limits.max_withdrawal_amount = dash_to_credits!(500);
platform_version.system_limits.max_daily_withdrawal_amount = Some(dash_to_credits!(4000));
platform_version
}

#[test]
fn should_return_the_configured_percent_of_the_lagged_total() {
let platform_version = platform_version_with(Some(15));

assert_eq!(
daily_withdrawal_limit_v2(Some(dash_to_credits!(20000)), &platform_version)
.expect("expected limit"),
dash_to_credits!(3000)
);
// Rounds down to whole credits: 15% of 4000 Dash + 7 credits is 600 Dash + 1.05 credits.
assert_eq!(
daily_withdrawal_limit_v2(Some(dash_to_credits!(4000) + 7), &platform_version)
.expect("expected limit"),
dash_to_credits!(600) + 1
);
}

#[test]
fn should_never_go_below_one_maximal_withdrawal() {
let platform_version = platform_version_with(Some(15));

// 15% of 2000 Dash is 300 Dash, below the 500 Dash a single withdrawal may carry.
assert_eq!(
daily_withdrawal_limit_v2(Some(dash_to_credits!(2000)), &platform_version)
.expect("expected limit"),
dash_to_credits!(500)
);
assert_eq!(
daily_withdrawal_limit_v2(Some(0), &platform_version).expect("expected limit"),
dash_to_credits!(500)
);
// Exactly at the boundary the percent takes over.
assert_eq!(
daily_withdrawal_limit_v2(Some(dash_to_credits!(4000)), &platform_version)
.expect("expected limit"),
dash_to_credits!(600)
);
}

#[test]
fn should_never_exceed_cores_unlock_capacity_per_day() {
let platform_version = platform_version_with(Some(15));

// 15% of 30000 Dash is 4500 Dash, above what Core mines per day.
assert_eq!(
daily_withdrawal_limit_v2(Some(dash_to_credits!(30000)), &platform_version)
.expect("expected limit"),
dash_to_credits!(4000)
);
assert_eq!(
daily_withdrawal_limit_v2(Some(Credits::MAX), &platform_version)
.expect("expected limit"),
dash_to_credits!(4000)
);
// Just under the boundary the percent still applies.
assert_eq!(
daily_withdrawal_limit_v2(Some(dash_to_credits!(26666)), &platform_version)
.expect("expected limit"),
dash_to_credits!(3999.9)
);
}

#[test]
fn should_keep_the_flat_limit_until_the_lagged_total_is_known() {
let platform_version = platform_version_with(Some(15));

assert_eq!(
daily_withdrawal_limit_v2(None, &platform_version).expect("expected limit"),
dash_to_credits!(2000)
);
}

#[test]
fn should_fail_when_the_percent_or_the_cap_is_not_configured() {
let platform_version = platform_version_with(None);
assert!(matches!(
daily_withdrawal_limit_v2(Some(dash_to_credits!(100)), &platform_version),
Err(ProtocolError::CorruptedCodeExecution(_))
));

let mut platform_version = platform_version_with(Some(15));
platform_version.system_limits.max_daily_withdrawal_amount = None;
assert!(matches!(
daily_withdrawal_limit_v2(Some(dash_to_credits!(100)), &platform_version),
Err(ProtocolError::CorruptedCodeExecution(_))
));
}

#[test]
fn should_fail_when_the_cap_is_below_one_maximal_withdrawal() {
let mut platform_version = platform_version_with(Some(15));
platform_version.system_limits.max_daily_withdrawal_amount =
Some(dash_to_credits!(500) - 1);

// Whatever the total, a cap below the floor is a contradictory configuration.
for total in [0, dash_to_credits!(100), dash_to_credits!(30000)] {
assert!(matches!(
daily_withdrawal_limit_v2(Some(total), &platform_version),
Err(ProtocolError::CorruptedCodeExecution(_))
));
}

// Exactly the floor is allowed and the limit is that floor.
platform_version.system_limits.max_daily_withdrawal_amount = Some(dash_to_credits!(500));
assert_eq!(
daily_withdrawal_limit_v2(Some(dash_to_credits!(30000)), &platform_version)
.expect("expected limit"),
dash_to_credits!(500)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,16 @@ where

tracing::debug!(block_fees = ?processed_block_fees, "block fees are processed");

// Record the total credits in Platform if this block changed it: the daily withdrawal
// limit is a share of the total credits Platform held a day ago, read from this history.
// This runs after fees and epoch rewards, the last things in a block that can move the
// total, and before the app hash so the entry is part of this block's state.
self.record_total_credits_history_for_withdrawals(
&block_info,
transaction,
platform_version,
)?;
Comment thread
QuantumExplorer marked this conversation as resolved.

let root_hash = self
.drive
.grove
Expand Down
Loading
Loading