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
4 changes: 2 additions & 2 deletions book/src/contributing/coding-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,8 @@ behaviour-preserving. Add the next `SYSTEM_LIMITS_V{n+1}` for the unreleased
protocol version with the new value. Keep a real method version only where the
logic differs: in `packages/rs-dpp/src/withdrawal/daily_withdrawal_limit/`,
`v0` computes a tiered percentage of total credits and `v2` reads
`daily_withdrawal_limit_percent` and `max_daily_withdrawal_amount` from
`SystemLimits`; that is a logic change and earns its own version. Raising the
`daily_withdrawal_limit_percent` from `SystemLimits`; that is a logic change
and earns its own version. Raising the
percentage later would be a table edit, not a `v3`.

Why: reviewers look for limits in the tables. A constant hidden in a method
Expand Down
11 changes: 7 additions & 4 deletions book/src/versioning/feature-versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,10 @@ pub struct SystemLimits {
pub max_withdrawal_amount: u64,
/// `None` for the protocol versions that predate the relative rule.
pub daily_withdrawal_limit_percent: Option<u8>,
pub max_daily_withdrawal_amount: Option<u64>,
pub core_credit_pool_unlock_limit_percent: Option<u8>,
pub core_credit_pool_unlock_limit_floor: Option<u64>,
pub core_credit_pool_window_blocks: Option<u32>,
pub regtest_core_credit_pool_window_blocks: Option<u32>,
pub min_withdrawal_amount: u64,
pub max_contract_group_size: u16,
pub max_token_redemption_cycles: u32,
Expand All @@ -355,6 +358,7 @@ pub struct DriveAbciWithdrawalConstants {
pub core_expiration_blocks: u32,
pub cleanup_expired_locks_of_withdrawal_amounts_limit: u16,
pub total_credits_history_prune_limit: u16,
pub core_blocks_scanned_per_block_limit: u16,
}

// drive_abci_versions/drive_abci_validation_versions/mod.rs
Expand Down Expand Up @@ -405,9 +409,8 @@ the situation the tables exist to prevent, and it leaves a dead module behind
every time the number moves. A new method version is warranted only when the
*logic* changes. `daily_withdrawal_limit` in `rs-dpp` is the reference case:
`v0` derives the limit from the current total credits, `v2` reads
`daily_withdrawal_limit_percent` and `max_daily_withdrawal_amount` from
`SystemLimits`. Raising the percentage later is a `SYSTEM_LIMITS_V5`, not a
`v3`.
`daily_withdrawal_limit_percent` from `SystemLimits`. Raising the percentage
later is a `SYSTEM_LIMITS_V5`, not a `v3`.

## How Subsystem Version Constants Compose

Expand Down
7 changes: 5 additions & 2 deletions book/src/versioning/versioned-dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -705,8 +705,11 @@ The Drive helpers that build the initial state structure follow the same rule
for the same reason: they run once, at chain creation, under the chain's
initial protocol version, and a chain that already exists gets the same trees
from its upgrade rung. `add_initial_withdrawal_state_structure_operations`
adds the withdrawal sum trees behind `>= 4` and the credit history trees behind
`>= 14`; replaying mainnet's genesis at protocol version 1 takes neither branch.
adds the withdrawal sum trees behind `>= 4`, which replaying mainnet's genesis at
protocol version 1 does not take. The withdrawal limit trees of protocol version
14 are not in that batch: genesis and `transition_to_version_14` both add them
one insert at a time through `Drive::insert_withdrawal_limit_trees`, so both
build the withdrawals Merk in the same shape.

## Rules

Expand Down
1 change: 1 addition & 0 deletions packages/dashmate/configs/defaults/getBaseConfigFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/
export default function getBaseConfigFactory() {
const prereleaseTag = semver.prerelease(version) === null ? '' : `-${semver.prerelease(version)[0]}`;
const dockerImageVersion = `${semver.major(version)}${prereleaseTag}`;

Check warning on line 19 in packages/dashmate/configs/defaults/getBaseConfigFactory.js

View workflow job for this annotation

GitHub Actions / JS packages (dashmate) / Linting

'dockerImageVersion' is assigned a value but never used

/**
* @typedef {function} getBaseConfig
Expand Down Expand Up @@ -94,6 +94,7 @@
'getbestchainlock', 'getblockchaininfo', 'getrawtransaction', 'submitchainlock',
'verifychainlock', 'protxlistdiff', 'quorumlistextended', 'quoruminfo',
'getassetunlockstatuses', 'sendrawtransaction', 'mnsyncstatus', 'getblockheader', 'getblockhash',
'getspecialtxes',
],
lowPriority: false,
},
Expand Down
14 changes: 14 additions & 0 deletions packages/dashmate/configs/getConfigFileMigrationsFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -1777,6 +1777,20 @@ export default function getConfigFileMigrationsFactory(homeDir, defaultConfigs)

return configFile;
},
'5.0.0-beta.2': (configFile) => {
Object.entries(configFile.configs)
.forEach(([, options]) => {
// Drive's withdrawal limit (protocol version 14) reads Core's credit pool
// balance from each block's coinbase (getspecialtxes). Core refuses it to the
// consensus user until its whitelist names it, and loads the whitelist only
// when Core itself restarts.
if (options.core?.rpc?.users?.drive_consensus) {
options.core.rpc.users.drive_consensus.whitelist = base.getStored('core.rpc.users.drive_consensus.whitelist');
}
});

return configFile;
},
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'fs';
import path from 'path';
import getBaseConfigFactory from '../../../../configs/defaults/getBaseConfigFactory.js';
import getConfigFileMigrationsFactory from '../../../../configs/getConfigFileMigrationsFactory.js';
import getConfigFormatVersion from '../../../../src/config/configFile/getConfigFormatVersion.js';
import migrateConfigFileFactory from '../../../../src/config/configFile/migrateConfigFileFactory.js';
import { PACKAGE_ROOT_DIR } from '../../../../src/constants.js';

Expand Down Expand Up @@ -38,7 +39,10 @@ describe('Tenderdash image migration', () => {
expect(migrated.configs.withoutDocker).to.deep.equal({
platform: { drive: { tenderdash: {} } },
});
expect(migrated.configFormatVersion).to.equal(version);
// Stamped with the format this build produces: the newest migration while it is ahead
// of the package version, the package version once a release has passed it.
expect(migrated.configFormatVersion)
.to.equal(getConfigFormatVersion(getMigrations(), version));
expect(migrateConfigFile(migrated, migrated.configFormatVersion, version)).to.equal(migrated);
});
}
Expand Down
119 changes: 119 additions & 0 deletions packages/rs-dpp/src/withdrawal/core_credit_pool_unlock_limit/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use crate::fee::Credits;
use crate::ProtocolError;
use dashcore::Network;
use platform_version::version::PlatformVersion;

mod v0;

/// Core's credit pool window on `network`: how many Core blocks before an asset unlock's block
/// lies the balance Core v24 measures the unlock limit from (`CreditPoolPeriodBlocks` in Dash
/// Core's chain parameters), as the protocol version's system limits pin it
/// (`core_credit_pool_window_blocks`, `regtest_core_credit_pool_window_blocks` on regtest).
///
/// # Errors
///
/// `ProtocolError::CorruptedCodeExecution` when the protocol version predates the
/// Core-anchored withdrawal limit and sets no window.
pub fn core_credit_pool_window_blocks(
network: Network,
platform_version: &PlatformVersion,
) -> Result<u32, ProtocolError> {
let system_limits = &platform_version.system_limits;
let window_blocks = match network {
Network::Mainnet | Network::Testnet | Network::Devnet => {
system_limits.core_credit_pool_window_blocks
}
Network::Regtest => system_limits.regtest_core_credit_pool_window_blocks,
};
window_blocks.ok_or_else(|| {
ProtocolError::CorruptedCodeExecution(
"the protocol version sets no Core credit pool window".to_string(),
)
})
}

/// Returns how much Core's credit pool may still give up to asset unlocks, given its balance
/// now and its balance at the start of the window the limit is measured over, both in credits.
///
/// This is the Core-anchored half of the withdrawal limit: a stricter copy of Core v24's own
/// asset unlock rule, so Platform never pools a withdrawal Core will refuse to mine. The pool
/// may end no lower than its window start balance minus an allowed drop
/// (`core_credit_pool_unlock_limit_percent` of that balance, at least
/// `core_credit_pool_unlock_limit_floor`); what it gained since the window start (asset locks,
/// the per-block Platform reward) is withdrawable on top, and the pool can never go negative.
///
/// # Parameters
///
/// * `balance`: Core's credit pool balance now, in credits.
/// * `window_start_balance`: Core's credit pool balance at the window start, in credits; `0`
/// when that block has no credit pool.
/// * `platform_version`: The platform version.
///
/// # Returns
///
/// * `Ok(Credits)`: The credits that may still be unlocked, between `0` and `balance`.
/// * `Err(ProtocolError)`: When the method version is unknown or not active, or the system
/// limits it reads are not configured.
pub fn core_credit_pool_unlock_limit(
balance: Credits,
window_start_balance: Credits,
platform_version: &PlatformVersion,
) -> Result<Credits, ProtocolError> {
match platform_version.dpp.methods.core_credit_pool_unlock_limit {
Some(0) => {
v0::core_credit_pool_unlock_limit_v0(balance, window_start_balance, platform_version)
}
Some(version) => Err(ProtocolError::UnknownVersionMismatch {
method: "core_credit_pool_unlock_limit".to_string(),
known_versions: vec![0],
received: version,
}),
None => Err(ProtocolError::UnknownVersionError(
"core_credit_pool_unlock_limit is not active in this protocol version".to_string(),
)),
}
}

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

#[test]
fn should_use_cores_window_of_each_network() {
let v14 = PlatformVersion::get(14).expect("expected protocol version 14");
for (network, window_blocks) in [
(Network::Mainnet, 576),
(Network::Testnet, 576),
(Network::Devnet, 576),
(Network::Regtest, 100),
] {
assert_eq!(
core_credit_pool_window_blocks(network, v14).expect("expected the window"),
window_blocks
);
}

let v13 = PlatformVersion::get(13).expect("expected protocol version 13");
assert!(core_credit_pool_window_blocks(Network::Mainnet, v13).is_err());
}

#[test]
fn should_only_exist_from_protocol_version_14() {
let v13 = PlatformVersion::get(13).expect("expected protocol version 13");
assert!(core_credit_pool_unlock_limit(
dash_to_credits!(10000),
dash_to_credits!(10000),
v13
)
.is_err());

let v14 = PlatformVersion::get(14).expect("expected protocol version 14");
// 15% of a 20,000 Dash pool that has not moved.
assert_eq!(
core_credit_pool_unlock_limit(dash_to_credits!(20000), dash_to_credits!(20000), v14)
.expect("expected the limit"),
dash_to_credits!(3000)
);
}
}
167 changes: 167 additions & 0 deletions packages/rs-dpp/src/withdrawal/core_credit_pool_unlock_limit/v0/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
use crate::fee::Credits;
use crate::ProtocolError;
use platform_version::version::PlatformVersion;

/// The pool may not end below `window_start_balance - allowed_drop`, where
/// `allowed_drop = max(window_start_balance * percent / 100, floor)`; everything above that
/// line is withdrawable, but never more than the pool holds:
///
/// `limit = min(max(0, allowed_drop - (window_start_balance - balance)), balance)`
///
/// Core v24 applies the same shape with 20% and a 2000 Dash floor over its window (576 blocks,
/// 100 on regtest); the system limits of protocol version 14 set a lower percent and floor, and
/// the caller picks the highest balance among the window starts Core may use, so the result
/// never exceeds what Core admits. Integer arithmetic in u128 throughout; truncation only ever makes
/// the limit stricter.
pub(super) fn core_credit_pool_unlock_limit_v0(
balance: Credits,
window_start_balance: Credits,
platform_version: &PlatformVersion,
) -> Result<Credits, ProtocolError> {
let percent = platform_version
.system_limits
.core_credit_pool_unlock_limit_percent
.ok_or_else(|| {
ProtocolError::CorruptedCodeExecution(
"core_credit_pool_unlock_limit v0 requires system_limits.core_credit_pool_unlock_limit_percent"
.to_string(),
)
})?;

let floor = platform_version
.system_limits
.core_credit_pool_unlock_limit_floor
.ok_or_else(|| {
ProtocolError::CorruptedCodeExecution(
"core_credit_pool_unlock_limit v0 requires system_limits.core_credit_pool_unlock_limit_floor"
.to_string(),
)
})?;

let allowed_drop =
((window_start_balance as u128) * (percent as u128) / 100).max(floor as u128);

// What may leave: the allowed drop plus whatever the pool gained since the window start,
// or minus whatever it already lost. u128 holds the sum of any two u64 values.
let withdrawable =
(allowed_drop + balance as u128).saturating_sub(window_start_balance as u128);

let limit = withdrawable.min(balance as u128);

Credits::try_from(limit).map_err(|_| ProtocolError::Overflow("core credit pool unlock limit"))
}

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

fn limit(balance: Credits, window_start_balance: Credits) -> Credits {
core_credit_pool_unlock_limit_v0(balance, window_start_balance, PlatformVersion::latest())
.expect("expected the limit")
}

#[test]
fn should_allow_the_percent_of_an_unchanged_pool() {
// 15% of 37,000 Dash, the mainnet pool #7712 measured.
assert_eq!(
limit(dash_to_credits!(37000), dash_to_credits!(37000)),
dash_to_credits!(5550)
);
}

#[test]
fn should_apply_the_floor_to_a_small_pool() {
// 15% of 5,000 Dash is 750 Dash, below the 1,500 Dash floor.
assert_eq!(
limit(dash_to_credits!(5000), dash_to_credits!(5000)),
dash_to_credits!(1500)
);
}

#[test]
fn should_add_what_the_pool_gained_inside_the_window() {
// A 4,000 Dash deposit inside the window is withdrawable on top of the allowed drop.
assert_eq!(
limit(dash_to_credits!(41000), dash_to_credits!(37000)),
dash_to_credits!(9550)
);
}

#[test]
fn should_subtract_what_the_pool_already_lost_inside_the_window() {
// 2,000 Dash already unlocked inside the window leaves 3,550 of the 5,550 allowed.
assert_eq!(
limit(dash_to_credits!(35000), dash_to_credits!(37000)),
dash_to_credits!(3550)
);
// Once the drop reaches the allowance nothing is left, and it never goes negative.
assert_eq!(limit(dash_to_credits!(31450), dash_to_credits!(37000)), 0);
assert_eq!(limit(dash_to_credits!(20000), dash_to_credits!(37000)), 0);
}

#[test]
fn should_never_exceed_the_pool() {
// A pool that grew from nothing inside the window: everything in it is withdrawable,
// but no more than it holds.
assert_eq!(limit(dash_to_credits!(1000), 0), dash_to_credits!(1000));
assert_eq!(limit(0, 0), 0);
}

#[test]
fn should_stay_below_cores_own_v24_limit() {
// Core v24: max(20% of the window start, 2000 Dash) - (window start - balance), at most
// the balance, over the same window.
fn core_v24(balance: Credits, window_start_balance: Credits) -> Credits {
let allowed_drop = (window_start_balance / 5).max(dash_to_credits!(2000));
(allowed_drop + balance)
.saturating_sub(window_start_balance)
.min(balance)
}

for (balance, window_start_balance) in [
(dash_to_credits!(37000), dash_to_credits!(37000)),
(dash_to_credits!(41000), dash_to_credits!(37000)),
(dash_to_credits!(35000), dash_to_credits!(37000)),
(dash_to_credits!(5000), dash_to_credits!(5000)),
(dash_to_credits!(12000), dash_to_credits!(11000)),
(dash_to_credits!(1000), 0),
] {
assert!(
limit(balance, window_start_balance) <= core_v24(balance, window_start_balance),
"balance {balance}, window start {window_start_balance}"
);
}
}

#[test]
fn should_not_overflow_at_the_largest_balances() {
assert_eq!(
limit(Credits::MAX, Credits::MAX),
((Credits::MAX as u128) * 15 / 100) as Credits
);
assert_eq!(limit(Credits::MAX, 0), Credits::MAX);
assert_eq!(limit(0, Credits::MAX), 0);
}

#[test]
fn should_fail_when_the_limits_are_not_configured() {
let mut platform_version = PlatformVersion::latest().clone();
platform_version
.system_limits
.core_credit_pool_unlock_limit_percent = None;
assert!(matches!(
core_credit_pool_unlock_limit_v0(1, 1, &platform_version),
Err(ProtocolError::CorruptedCodeExecution(_))
));

let mut platform_version = PlatformVersion::latest().clone();
platform_version
.system_limits
.core_credit_pool_unlock_limit_floor = None;
assert!(matches!(
core_credit_pool_unlock_limit_v0(1, 1, &platform_version),
Err(ProtocolError::CorruptedCodeExecution(_))
));
}
}
Loading
Loading