From b3e60eaf124b16590b41ef3647821e7f762aecc2 Mon Sep 17 00:00:00 2001 From: DCG-Claude Date: Sat, 12 Sep 2026 14:21:11 -0500 Subject: [PATCH 01/11] feat(platform)!: add protocol versions 15 to 17 with a drive table for the contract credits root Protocol version 17 is the provisional 5.0 version; 15 and 16 are placeholders identical to 14 so the registry, which is indexed by number, can hold 17. The new drive table selects the genesis and credit conservation generations that create and read the contract credits root sum tree. Co-Authored-By: Claude Fable 5.1 --- .../src/version/drive_versions/mod.rs | 1 + .../src/version/drive_versions/v10.rs | 159 ++++++++++++++++++ .../rs-platform-version/src/version/mod.rs | 7 +- .../src/version/protocol_version.rs | 8 +- .../rs-platform-version/src/version/v15.rs | 21 +++ .../rs-platform-version/src/version/v16.rs | 16 ++ .../rs-platform-version/src/version/v17.rs | 27 +++ 7 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 packages/rs-platform-version/src/version/drive_versions/v10.rs create mode 100644 packages/rs-platform-version/src/version/v15.rs create mode 100644 packages/rs-platform-version/src/version/v16.rs create mode 100644 packages/rs-platform-version/src/version/v17.rs diff --git a/packages/rs-platform-version/src/version/drive_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/mod.rs index 1d7c32caed6..889baec9f92 100644 --- a/packages/rs-platform-version/src/version/drive_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/mod.rs @@ -30,6 +30,7 @@ pub mod drive_token_method_versions; pub mod drive_verify_method_versions; pub mod drive_vote_method_versions; pub mod v1; +pub mod v10; pub mod v2; pub mod v3; pub mod v4; diff --git a/packages/rs-platform-version/src/version/drive_versions/v10.rs b/packages/rs-platform-version/src/version/drive_versions/v10.rs new file mode 100644 index 00000000000..f62a9887504 --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_versions/v10.rs @@ -0,0 +1,159 @@ +use crate::version::drive_versions::drive_address_funds_method_versions::v2::DRIVE_ADDRESS_FUNDS_METHOD_VERSIONS_V2; +use crate::version::drive_versions::drive_contract_group_method_versions::v1::DRIVE_CONTRACT_GROUP_METHOD_VERSIONS_V1; +use crate::version::drive_versions::drive_contract_method_versions::v4::DRIVE_CONTRACT_METHOD_VERSIONS_V4; +use crate::version::drive_versions::drive_credit_pool_method_versions::v1::CREDIT_POOL_METHOD_VERSIONS_V1; +use crate::version::drive_versions::drive_document_method_versions::v4::DRIVE_DOCUMENT_METHOD_VERSIONS_V4; +use crate::version::drive_versions::drive_group_method_versions::v1::DRIVE_GROUP_METHOD_VERSIONS_V1; +use crate::version::drive_versions::drive_group_method_versions::DriveShieldedMethodVersions; +use crate::version::drive_versions::drive_grove_method_versions::v1::DRIVE_GROVE_METHOD_VERSIONS_V1; +use crate::version::drive_versions::drive_identity_method_versions::v2::DRIVE_IDENTITY_METHOD_VERSIONS_V2; +use crate::version::drive_versions::drive_state_transition_method_versions::v4::DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4; +use crate::version::drive_versions::drive_structure_version::v1::DRIVE_STRUCTURE_V1; +use crate::version::drive_versions::drive_token_method_versions::v2::DRIVE_TOKEN_METHOD_VERSIONS_V2; +use crate::version::drive_versions::drive_verify_method_versions::v3::DRIVE_VERIFY_METHOD_VERSIONS_V3; +use crate::version::drive_versions::drive_vote_method_versions::v3::DRIVE_VOTE_METHOD_VERSIONS_V3; +use crate::version::drive_versions::{ + DriveAssetLockMethodVersions, DriveBalancesMethodVersions, DriveBatchOperationsMethodVersion, + DriveEstimatedCostsMethodVersions, DriveFeesMethodVersions, DriveFetchMethodVersions, + DriveInitializationMethodVersions, DriveMethodVersions, DriveOperationsMethodVersion, + DrivePlatformStateMethodVersions, DrivePlatformSystemMethodVersions, + DrivePrefundedSpecializedMethodVersions, DriveProtocolUpgradeVersions, + DriveProveMethodVersions, DriveSavedBlockTransactionsMethodVersions, + DriveSystemEstimationCostsMethodVersions, DriveVersion, +}; +use grovedb_version::version::v4::GROVE_V4; + +/// Drive version 10. +/// Introduced in protocol v17, the 5.0 protocol version, for the contract +/// credits root tree: a sum tree at root key 100 that holds, from later +/// changes, one sum subtree per contract with one sum item per credit bucket. +/// +/// * **Genesis**: `initialization.create_initial_state_structure` 4 -> 5 +/// inserts the empty `ContractCredits` sum tree as a standalone root +/// insert right after `ShieldedBalances`; the upgrade path creates the same +/// element with an insert-if-not-exists on the first block at v17, so a +/// fresh genesis and an upgraded node hold a byte-identical root element. +/// * **Credit conservation**: `balances.calculate_total_credits_balance` +/// 2 -> 3 reads the tree's aggregate as the sixth term of the equation. +/// Contracts wiped later have their subtree wrapped in a not-summed +/// element, so the aggregate only ever covers live contract credits. +/// +/// Everything else matches `DRIVE_VERSION_V9`. +pub const DRIVE_VERSION_V10: DriveVersion = DriveVersion { + structure: DRIVE_STRUCTURE_V1, + methods: DriveMethodVersions { + initialization: DriveInitializationMethodVersions { + create_initial_state_structure: 5, // changed in v10: adds the contract credits root sum tree (v4 added the ContractGroups root tree) + }, + credit_pools: CREDIT_POOL_METHOD_VERSIONS_V1, + protocol_upgrade: DriveProtocolUpgradeVersions { + clear_version_information: 0, + fetch_versions_with_counter: 0, + fetch_proved_versions_with_counter: 0, + fetch_validator_version_votes: 0, + fetch_proved_validator_version_votes: 0, + remove_validators_proposed_app_versions: 0, + update_validator_proposed_app_version: 0, + }, + prove: DriveProveMethodVersions { + prove_elements: 0, + prove_multiple_state_transition_results: 0, + prove_state_transition: 1, // changed in v9: a document batch proof carries the owner's balance (verify v1) + }, + balances: DriveBalancesMethodVersions { + add_to_system_credits: 0, + add_to_system_credits_operations: 0, + remove_from_system_credits: 0, + remove_from_system_credits_operations: 0, + calculate_total_credits_balance: 3, // changed in v10: ContractCredits root tree adds a sixth term to the equation + }, + document: DRIVE_DOCUMENT_METHOD_VERSIONS_V4, // changed in v9: v2 index walkers + v1 update walker (shared-prefix aggregate indexes become insertable) and the detect_ranked_mode slot + vote: DRIVE_VOTE_METHOD_VERSIONS_V3, // changed in v9: the end-date cleanup of ended contested vote polls removes an end date only once none of its polls remain + contract: DRIVE_CONTRACT_METHOD_VERSIONS_V4, // changed in v9: add_contract_to_storage v1 writes the contract version item beside the contract; update_contract v2 creates the distribution storage and mints the base supply of tokens added by an update + fees: DriveFeesMethodVersions { calculate_fee: 0 }, + estimated_costs: DriveEstimatedCostsMethodVersions { + add_estimation_costs_for_levels_up_to_contract: 0, + add_estimation_costs_for_levels_up_to_contract_document_type_excluded: 0, + add_estimation_costs_for_contested_document_tree_levels_up_to_contract: 0, + add_estimation_costs_for_contested_document_tree_levels_up_to_contract_document_type_excluded: 0, + }, + asset_lock: DriveAssetLockMethodVersions { + add_asset_lock_outpoint: 0, + add_estimation_costs_for_adding_asset_lock: 0, + fetch_asset_lock_outpoint_info: 0, + }, + verify: DRIVE_VERIFY_METHOD_VERSIONS_V3, // changed in v9: a document batch proof carries the owner's balance (verify state transition v1) + identity: DRIVE_IDENTITY_METHOD_VERSIONS_V2, // changed in v9: v1 withdrawal-by-transaction-index query builder (structural, identical lowering) + token: DRIVE_TOKEN_METHOD_VERSIONS_V2, // changed in v9: add_pre_programmed_distributions v1 queues the release-time tree shared by a contract's tokens once; evonode_participation_rewards v1 pays an evonode's claim only through the epochs it read + platform_system: DrivePlatformSystemMethodVersions { + estimation_costs: DriveSystemEstimationCostsMethodVersions { + for_total_system_credits_update: 0, + }, + }, + operations: DriveOperationsMethodVersion { + rollback_transaction: 0, + drop_cache: 0, + commit_transaction: 0, + apply_partial_batch_low_level_drive_operations: 0, + apply_partial_batch_grovedb_operations: 0, + apply_batch_low_level_drive_operations: 1, // changed: coalesces bound current-key alias writes per batch + apply_batch_grovedb_operations: 0, + }, + state_transitions: DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4, // changed: document_from_action generation 1 stamps built documents with the contract version (create assigns, replace re-assigns; paired with document serialization format 3) + batch_operations: DriveBatchOperationsMethodVersion { + convert_drive_operations_to_grove_operations: 0, + apply_drive_operations: 1, // changed: a batch carrying a storage refund forfeiture (a moderator's document deletion) refunds nobody; every write of one identity balance, fee pot or prefunded specialized balance in a batch is merged into one, a batch writing one token balance or supply twice is refused, and repaid identity debt goes to the processing fee pool + }, + platform_state: DrivePlatformStateMethodVersions { + fetch_platform_state_bytes: 0, + store_platform_state_bytes: 0, + fetch_platform_state_recent_bytes: 0, + store_platform_state_recent_bytes: 0, + fetch_platform_state_entries_bytes: 0, + store_platform_state_entry_bytes: 0, + delete_platform_state_entry: 0, + }, + fetch: DriveFetchMethodVersions { fetch_elements: 0 }, + prefunded_specialized_balances: DrivePrefundedSpecializedMethodVersions { + fetch_single: 0, + prove_single: 0, + add_prefunded_specialized_balance: 0, + add_prefunded_specialized_balance_operations: 1, + deduct_from_prefunded_specialized_balance: 1, + deduct_from_prefunded_specialized_balance_operations: 0, + estimated_cost_for_prefunded_specialized_balance_update: 1, // changed: the prefunded balances layer holds three trees, the voting balances and the two contract fee pot trees + empty_prefunded_specialized_balance: 0, + }, + group: DRIVE_GROUP_METHOD_VERSIONS_V1, + contract_group: DRIVE_CONTRACT_GROUP_METHOD_VERSIONS_V1, + address_funds: DRIVE_ADDRESS_FUNDS_METHOD_VERSIONS_V2, + shielded: DriveShieldedMethodVersions { + insert_note: 0, + insert_nullifiers: 0, + update_total_balance: 0, + record_anchor_if_changed: 0, + prune_anchors: 0, + has_anchor: 0, + has_nullifier: 0, + read_total_balance: 0, + notes_count: 0, + }, + saved_block_transactions: DriveSavedBlockTransactionsMethodVersions { + store_address_balances: 0, + fetch_address_balances: 0, + prove_compacted_address_balance_changes: 1, + compact_address_balances: 0, + cleanup_expired_address_balances: 0, + max_blocks_before_compaction: 64, + max_addresses_before_compaction: 2048, + }, + }, + grove_methods: DRIVE_GROVE_METHOD_VERSIONS_V1, + // changed in v9: GROVE_V4 activates the indexed-tree batch cleanup + // gates (overwrite inspection + delete-tree actual-type cleanup). + // Indexed trees only exist from protocol v14, so activating the + // stricter cleanup with them costs older versions nothing; staying + // on V3 would let a batch overwrite of a ranked index orphan its + // per-axis secondary storage. + grove_version: GROVE_V4, +}; diff --git a/packages/rs-platform-version/src/version/mod.rs b/packages/rs-platform-version/src/version/mod.rs index 1b1635efb42..18768b37fbe 100644 --- a/packages/rs-platform-version/src/version/mod.rs +++ b/packages/rs-platform-version/src/version/mod.rs @@ -1,6 +1,6 @@ mod protocol_version; -use crate::version::v14::PROTOCOL_VERSION_14; +use crate::version::v17::PROTOCOL_VERSION_17; pub use protocol_version::*; use std::ops::RangeInclusive; @@ -20,6 +20,9 @@ pub mod v11; pub mod v12; pub mod v13; pub mod v14; +pub mod v15; +pub mod v16; +pub mod v17; pub mod v2; pub mod v3; pub mod v4; @@ -33,5 +36,5 @@ pub type ProtocolVersion = u32; pub const ALL_VERSIONS: RangeInclusive = 1..=LATEST_VERSION; -pub const LATEST_VERSION: ProtocolVersion = PROTOCOL_VERSION_14; +pub const LATEST_VERSION: ProtocolVersion = PROTOCOL_VERSION_17; pub const INITIAL_PROTOCOL_VERSION: ProtocolVersion = 1; diff --git a/packages/rs-platform-version/src/version/protocol_version.rs b/packages/rs-platform-version/src/version/protocol_version.rs index 00cc470bbc7..96236d1b0a5 100644 --- a/packages/rs-platform-version/src/version/protocol_version.rs +++ b/packages/rs-platform-version/src/version/protocol_version.rs @@ -22,6 +22,9 @@ use crate::version::v11::PLATFORM_V11; use crate::version::v12::PLATFORM_V12; use crate::version::v13::PLATFORM_V13; use crate::version::v14::PLATFORM_V14; +use crate::version::v15::PLATFORM_V15; +use crate::version::v16::PLATFORM_V16; +use crate::version::v17::PLATFORM_V17; use crate::version::v2::PLATFORM_V2; use crate::version::v3::PLATFORM_V3; use crate::version::v4::PLATFORM_V4; @@ -61,6 +64,9 @@ pub const PLATFORM_VERSIONS: &[PlatformVersion] = &[ PLATFORM_V12, PLATFORM_V13, PLATFORM_V14, + PLATFORM_V15, + PLATFORM_V16, + PLATFORM_V17, ]; #[cfg(feature = "mock-versions")] @@ -69,7 +75,7 @@ pub static PLATFORM_TEST_VERSIONS: OnceLock> = OnceLock::ne #[cfg(feature = "mock-versions")] const DEFAULT_PLATFORM_TEST_VERSIONS: &[PlatformVersion] = &[TEST_PLATFORM_V2, TEST_PLATFORM_V3]; -pub const LATEST_PLATFORM_VERSION: &PlatformVersion = &PLATFORM_V14; +pub const LATEST_PLATFORM_VERSION: &PlatformVersion = &PLATFORM_V17; pub const DESIRED_PLATFORM_VERSION: &PlatformVersion = LATEST_PLATFORM_VERSION; diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs new file mode 100644 index 00000000000..d091a5f1aee --- /dev/null +++ b/packages/rs-platform-version/src/version/v15.rs @@ -0,0 +1,21 @@ +use crate::version::protocol_version::PlatformVersion; +use crate::version::v14::PLATFORM_V14; +use crate::version::ProtocolVersion; + +pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; + +/// Placeholder for the 4.3 protocol version. +/// +/// The DashVM allocation register reserves protocol version 15 for the 4.3 release, 16 for 4.4 +/// and 17 for 5.0, and the registry is indexed by number, so the 5.0 version cannot exist on +/// this branch without 15 and 16. Until the 4.3 branch merges its real `v15.rs` forward this +/// version is identical to v14. +/// +/// It is written as a struct update rather than a copy of `v14.rs` on purpose: when the real +/// file arrives the add/add conflict is resolved by taking the incoming file, and because v16 +/// and v17 are struct updates over their predecessor every table the incoming version changes +/// flows into them without a second edit. +pub const PLATFORM_V15: PlatformVersion = PlatformVersion { + protocol_version: PROTOCOL_VERSION_15, + ..PLATFORM_V14 +}; diff --git a/packages/rs-platform-version/src/version/v16.rs b/packages/rs-platform-version/src/version/v16.rs new file mode 100644 index 00000000000..e3d80c4b16e --- /dev/null +++ b/packages/rs-platform-version/src/version/v16.rs @@ -0,0 +1,16 @@ +use crate::version::protocol_version::PlatformVersion; +use crate::version::v15::PLATFORM_V15; +use crate::version::ProtocolVersion; + +pub const PROTOCOL_VERSION_16: ProtocolVersion = 16; + +/// Placeholder for the 4.4 protocol version. +/// +/// Reserved by the DashVM allocation register (15 for 4.3, 16 for 4.4, 17 for 5.0). Identical to +/// v15 until the 4.4 branch merges its real `v16.rs` forward; see `v15.rs` for why it is a +/// struct update and how the forward merge is resolved. Should 4.4 ship no consensus change, the +/// register drops this activation and the 5.0 version becomes 16. +pub const PLATFORM_V16: PlatformVersion = PlatformVersion { + protocol_version: PROTOCOL_VERSION_16, + ..PLATFORM_V15 +}; diff --git a/packages/rs-platform-version/src/version/v17.rs b/packages/rs-platform-version/src/version/v17.rs new file mode 100644 index 00000000000..62c3ef088e8 --- /dev/null +++ b/packages/rs-platform-version/src/version/v17.rs @@ -0,0 +1,27 @@ +use crate::version::drive_versions::v10::DRIVE_VERSION_V10; +use crate::version::protocol_version::PlatformVersion; +use crate::version::v16::PLATFORM_V16; +use crate::version::ProtocolVersion; + +pub const PROTOCOL_VERSION_17: ProtocolVersion = 17; + +/// The 5.0 protocol version, the one that introduces smart contracts. Provisional number from +/// the DashVM allocation register (15 for 4.3, 16 for 4.4, 17 for 5.0). +/// +/// One change over v16 so far: +/// +/// * `DRIVE_VERSION_V10` adds the contract credits root sum tree (`RootTree::ContractCredits`, +/// key 100) to the state: `create_initial_state_structure` 4 -> 5 creates it at genesis, the +/// first block at this version creates it on upgraded nodes, and +/// `calculate_total_credits_balance` 2 -> 3 reads it as the sixth term of the credit +/// conservation equation. Contract credit buckets, their rules and their proofs arrive with +/// later changes; until then the tree stays empty and the term is zero. +/// +/// The root key value is provisional (the allocation register leaves new root values +/// unallocated) and is revised, if at all, before any network is asked to propose this +/// version. +pub const PLATFORM_V17: PlatformVersion = PlatformVersion { + protocol_version: PROTOCOL_VERSION_17, + drive: DRIVE_VERSION_V10, // changed: contract credits root sum tree at genesis, on upgrade and in credit conservation + ..PLATFORM_V16 +}; From 7b6c7c2347aaba50c3603f7f5e1b6194925c28ef Mon Sep 17 00:00:00 2001 From: DCG-Claude Date: Sat, 12 Sep 2026 14:43:29 -0500 Subject: [PATCH 02/11] feat(drive)!: add the contract credits root sum tree to genesis, upgrade and credit conservation RootTree::ContractCredits (key 100) is an ordinary sum tree created by the v5 initial state structure (v4 is the protocol 14 generation that adds the ContractGroups root tree) and, on upgraded nodes, by the first block at protocol version 17. The v3 credit conservation calculator reads its aggregate as the sixth term of the equation; the earlier generations backfill the new field with zero. The tree is described in the area's structure.rs with a fixture that builds a live and a wiped contract, and grovedb-structure.json is regenerated at protocol version 17. Co-Authored-By: Claude Fable 5.1 --- .../src/balances/total_credits_balance/mod.rs | 82 ++- .../v0/mod.rs | 491 ++++++++++++++++++ packages/rs-drive/grovedb-structure.json | 138 +++-- .../calculate_total_credits_balance/mod.rs | 8 +- .../calculate_total_credits_balance/v0/mod.rs | 4 + .../calculate_total_credits_balance/v1/mod.rs | 4 + .../calculate_total_credits_balance/v2/mod.rs | 5 + .../calculate_total_credits_balance/v3/mod.rs | 245 +++++++++ .../src/drive/contract/balances/mod.rs | 69 +++ .../src/drive/contract/balances/structure.rs | 45 ++ packages/rs-drive/src/drive/contract/mod.rs | 3 + .../rs-drive/src/drive/initialization/mod.rs | 4 +- .../src/drive/initialization/v5/mod.rs | 189 +++++++ packages/rs-drive/src/drive/mod.rs | 22 +- packages/rs-drive/src/drive/structure.rs | 2 + packages/rs-drive/src/structure/tests.rs | 61 ++- .../src/util/batch/grovedb_op_batch/mod.rs | 20 +- 17 files changed, 1341 insertions(+), 51 deletions(-) create mode 100644 packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v3/mod.rs create mode 100644 packages/rs-drive/src/drive/contract/balances/mod.rs create mode 100644 packages/rs-drive/src/drive/contract/balances/structure.rs create mode 100644 packages/rs-drive/src/drive/initialization/v5/mod.rs diff --git a/packages/rs-dpp/src/balances/total_credits_balance/mod.rs b/packages/rs-dpp/src/balances/total_credits_balance/mod.rs index 03318cc1d34..55101620028 100644 --- a/packages/rs-dpp/src/balances/total_credits_balance/mod.rs +++ b/packages/rs-dpp/src/balances/total_credits_balance/mod.rs @@ -18,6 +18,9 @@ pub struct TotalCreditsBalance { pub total_in_addresses: SignedCredits, /// all the credits inside shielded credit pools pub total_in_shielded_balances: SignedCredits, + /// all the live credits held by contracts in their credit buckets; a + /// wiped contract's retained credits are excluded by the tree layout + pub total_in_contract_credits: SignedCredits, } impl fmt::Display for TotalCreditsBalance { @@ -46,9 +49,14 @@ impl fmt::Display for TotalCreditsBalance { )?; writeln!( f, - " total_in_shielded_balances: {}", + " total_in_shielded_balances: {},", self.total_in_shielded_balances )?; + writeln!( + f, + " total_in_contract_credits: {}", + self.total_in_contract_credits + )?; write!(f, "}}") } } @@ -64,6 +72,7 @@ impl TotalCreditsBalance { total_specialized_balances, total_in_addresses, total_in_shielded_balances, + total_in_contract_credits, } = *self; if total_in_pools < 0 { @@ -96,6 +105,12 @@ impl TotalCreditsBalance { )); } + if total_in_contract_credits < 0 { + return Err(ProtocolError::CriticalCorruptedCreditsCodeExecution( + "Credits held by contracts are less than 0".to_string(), + )); + } + if total_credits_in_platform > MAX_CREDITS { return Err(ProtocolError::CriticalCorruptedCreditsCodeExecution( "Total credits in platform more than max credits size".to_string(), @@ -107,6 +122,7 @@ impl TotalCreditsBalance { .and_then(|partial_sum| partial_sum.checked_add(total_specialized_balances)) .and_then(|partial_sum| partial_sum.checked_add(total_in_addresses)) .and_then(|partial_sum| partial_sum.checked_add(total_in_shielded_balances)) + .and_then(|partial_sum| partial_sum.checked_add(total_in_contract_credits)) .ok_or(ProtocolError::CriticalCorruptedCreditsCodeExecution( "Overflow of total credits".to_string(), ))?; @@ -122,6 +138,7 @@ impl TotalCreditsBalance { total_specialized_balances, total_in_addresses, total_in_shielded_balances, + total_in_contract_credits, .. } = *self; @@ -130,6 +147,7 @@ impl TotalCreditsBalance { .and_then(|partial_sum| partial_sum.checked_add(total_specialized_balances)) .and_then(|partial_sum| partial_sum.checked_add(total_in_addresses)) .and_then(|partial_sum| partial_sum.checked_add(total_in_shielded_balances)) + .and_then(|partial_sum| partial_sum.checked_add(total_in_contract_credits)) .ok_or(ProtocolError::CriticalCorruptedCreditsCodeExecution( "Overflow of total credits".to_string(), ))?; @@ -137,3 +155,65 @@ impl TotalCreditsBalance { Ok(total_in_trees.to_unsigned()) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn balanced() -> TotalCreditsBalance { + TotalCreditsBalance { + total_credits_in_platform: 600, + total_in_pools: 100, + total_identity_balances: 100, + total_specialized_balances: 100, + total_in_addresses: 100, + total_in_shielded_balances: 100, + total_in_contract_credits: 100, + } + } + + #[test] + fn should_count_contract_credits_as_a_term_of_the_equation() { + let balance = balanced(); + assert!(balance.ok().expect("no overflow")); + assert_eq!(balance.total_in_trees().expect("no overflow"), 600); + + let short = TotalCreditsBalance { + total_in_contract_credits: 0, + ..balance + }; + assert!( + !short.ok().expect("no overflow"), + "dropping the contract credits term must unbalance the equation" + ); + } + + #[test] + fn should_reject_negative_contract_credits() { + let negative = TotalCreditsBalance { + total_in_contract_credits: -1, + total_credits_in_platform: 499, + ..balanced() + }; + assert!(matches!( + negative.ok(), + Err(ProtocolError::CriticalCorruptedCreditsCodeExecution(_)) + )); + } + + #[test] + fn should_report_overflow_when_the_contract_credits_term_overflows_the_sum() { + let overflowing = TotalCreditsBalance { + total_in_contract_credits: SignedCredits::MAX, + ..balanced() + }; + assert!(matches!( + overflowing.ok(), + Err(ProtocolError::CriticalCorruptedCreditsCodeExecution(_)) + )); + assert!(matches!( + overflowing.total_in_trees(), + Err(ProtocolError::CriticalCorruptedCreditsCodeExecution(_)) + )); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index 229787b8b78..c8fa7a44aef 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -119,6 +119,10 @@ impl Platform { self.transition_to_version_14(block_info, transaction, platform_version)?; } + if previous_protocol_version < 17 && platform_version.protocol_version >= 17 { + self.transition_to_version_17(transaction, platform_version)?; + } + Ok(()) } @@ -845,6 +849,34 @@ impl Platform { Ok(()) } + + /// When transitioning to version 17 we add the contract credits root sum + /// tree. Contract credit buckets live under it as + /// `contract_id (SumTree) / bucket_key (SumItem)`, and its aggregate is + /// the sixth term of the credit conservation equation from this version. + /// + /// CONSENSUS-CRITICAL: the genesis path + /// (`Drive::create_initial_state_structure_v5`) inserts the same empty sum + /// tree as a standalone root insert, so a fresh genesis-v17 node and an + /// in-place-upgraded v17 node hold a byte-identical `[ContractCredits]` + /// element. The insert is idempotent so a retried block after a rejected + /// proposal leaves the tree exactly as the first attempt would have. + fn transition_to_version_17( + &self, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + self.drive.grove_insert_if_not_exists( + SubtreePath::empty(), + &[RootTree::ContractCredits as u8], + Element::empty_sum_tree(), + Some(transaction), + None, + &platform_version.drive, + )?; + + Ok(()) + } } #[cfg(test)] @@ -3548,6 +3580,465 @@ mod tests { diffs.join("\n"), ); } + + /// CONSENSUS-CRITICAL equivalence guard for the v16 -> v17 boundary. + /// + /// The `[ContractCredits]` root element is built two ways that MUST be + /// byte-identical: + /// + /// * GENESIS path: a node that state-syncs a fresh v17 chain runs the real + /// `Drive::create_initial_state_structure_v5`, which inserts the empty + /// sum tree as a standalone root insert. + /// * UPGRADE path: a node already on v16 runs the real + /// `Platform::transition_to_version_17` at the activation block, which + /// inserts the same element with an insert-if-not-exists. + /// + /// Both the root element itself and the (empty) subtree under it are + /// compared, so a flag, a tree type or a stray child on either side fails + /// here. The named subtree is compared rather than the whole-DB root hash + /// for the reason given on `collect_subtree_diffs`. + #[test] + fn test_genesis_v17_and_upgrade_to_v17_build_identical_contract_credits_tree() { + let platform_version_17 = PlatformVersion::get(17).expect("expected v17"); + let grove_version = &platform_version_17.drive.grove_version; + + // ---- Platform A: REAL fresh genesis at protocol v17. ----------------- + let platform_a = TestPlatformBuilder::new() + .with_initial_protocol_version(17) + .build_with_mock_rpc() + .set_genesis_state(); + + // ---- Platform B: REAL v16 genesis, then REAL transition_to_version_17. + let platform_b = TestPlatformBuilder::new() + .with_initial_protocol_version(16) + .build_with_mock_rpc() + .set_genesis_state(); + + // Sanity: a genuine v16 genesis must NOT contain ContractCredits yet. + let contract_credits_root_pre = platform_b + .drive + .grove + .get( + SubtreePath::empty(), + &[RootTree::ContractCredits as u8], + None, + grove_version, + ) + .unwrap(); + assert!( + contract_credits_root_pre.is_err(), + "v16 genesis must not contain ContractCredits before the upgrade; got {:?}", + contract_credits_root_pre + ); + + let txn_b = platform_b.drive.grove.start_transaction(); + platform_b + .transition_to_version_17(&txn_b, platform_version_17) + .expect("upgrade: transition_to_version_17 should succeed"); + + let element_a = platform_a + .drive + .grove + .get( + SubtreePath::empty(), + &[RootTree::ContractCredits as u8], + None, + grove_version, + ) + .unwrap() + .expect("genesis: [ContractCredits] element"); + let element_b = platform_b + .drive + .grove + .get( + SubtreePath::empty(), + &[RootTree::ContractCredits as u8], + Some(&txn_b), + grove_version, + ) + .unwrap() + .expect("upgrade: [ContractCredits] element"); + assert_eq!( + element_a, + Element::empty_sum_tree(), + "genesis must create an empty sum tree without flags" + ); + assert_eq!( + element_a, element_b, + "CONSENSUS FORK: the [ContractCredits] root element differs between a fresh \ + genesis-v17 node and an in-place-upgraded v17 node" + ); + + let diffs = collect_subtree_diffs( + &platform_a, + &platform_b, + &txn_b, + vec![vec![RootTree::ContractCredits as u8]], + ); + assert!( + diffs.is_empty(), + "CONSENSUS FORK: the [ContractCredits] subtree differs between a fresh genesis-v17 \ + node and an in-place-upgraded v17 node.\n{}", + diffs.join("\n"), + ); + } + + /// The v17 calculator reads the new root tree as a sixth term, so the + /// equation must still hold on a chain that upgraded into v17 with an + /// empty tree, and the frozen v16 calculator must keep ignoring it. + #[test] + fn should_pass_credit_conservation_after_upgrade_to_v17() { + let platform_version_16 = PlatformVersion::get(16).expect("expected v16"); + let platform_version_17 = PlatformVersion::get(17).expect("expected v17"); + + let platform = TestPlatformBuilder::new() + .with_initial_protocol_version(16) + .build_with_mock_rpc() + .set_genesis_state(); + + let transaction = platform.drive.grove.start_transaction(); + platform + .transition_to_version_17(&transaction, platform_version_17) + .expect("expected the transition to succeed"); + + let total_at_17 = platform + .drive + .calculate_total_credits_balance(Some(&transaction), &platform_version_17.drive) + .expect("expected to calculate the total credits balance at v17"); + assert_eq!(total_at_17.total_in_contract_credits, 0); + assert!(total_at_17.ok().expect("no overflow")); + + let total_at_16 = platform + .drive + .calculate_total_credits_balance(Some(&transaction), &platform_version_16.drive) + .expect("expected to calculate the total credits balance at v16"); + assert_eq!(total_at_16.total_in_contract_credits, 0); + assert!(total_at_16.ok().expect("no overflow")); + } + + /// Drives the v16 -> v17 boundary through the public + /// `perform_events_on_first_block_of_protocol_change` dispatcher on a + /// populated state, the way `run_block_proposal` does, including the + /// rejected-proposal shape where the transaction that ran the hook is + /// dropped and a later round runs it again. + #[test] + fn should_activate_the_contract_credits_root_through_the_protocol_change_hook() { + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::data_contract::document_type::random_document::{ + CreateRandomDocument, DocumentFieldFillSize, DocumentFieldFillType, + }; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::v0::IdentityV0; + use dpp::identity::{Identity, IdentityPublicKey}; + use dpp::platform_value::Bytes32; + use drive::util::object_size_info::DocumentInfo::DocumentRefInfo; + use drive::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; + use rand::rngs::StdRng; + use rand::SeedableRng; + use std::collections::BTreeMap; + + let platform_version_16 = PlatformVersion::get(16).expect("expected v16"); + let platform_version_17 = PlatformVersion::get(17).expect("expected v17"); + let grove_version = &platform_version_17.drive.grove_version; + + let platform = TestPlatformBuilder::new() + .with_initial_protocol_version(16) + .build_with_mock_rpc() + .set_genesis_state(); + let platform_state = platform.state.load(); + + // Populate the committed state at v16: an identity with a balance + // backed by system credits, and a document, so the root Merk is not + // the bare genesis shape and conservation has non-zero terms. + let mut rng = StdRng::seed_from_u64(1704); + let balance: Credits = 1_000_000_000; + let (master_key, _) = IdentityPublicKey::random_ecdsa_master_authentication_key_with_rng( + 0, + &mut rng, + platform_version_16, + ) + .expect("expected a master key"); + let identity: Identity = IdentityV0 { + id: Identifier::random_with_rng(&mut rng), + public_keys: BTreeMap::from([(0, master_key)]), + balance, + revision: 0, + } + .into(); + platform + .drive + .add_to_system_credits(balance, None, platform_version_16) + .expect("expected to add to system credits"); + platform + .drive + .add_new_identity( + identity.clone(), + false, + &BlockInfo::default(), + true, + None, + platform_version_16, + ) + .expect("expected to add the identity"); + + let dashpay = platform + .drive + .cache + .system_data_contracts + .load_dashpay(platform_version_16) + .expect("expected the dashpay contract"); + let profile = dashpay + .document_type_for_name("profile") + .expect("expected the profile document type"); + let entropy = Bytes32::random_with_rng(&mut rng); + let document = profile + .random_document_with_identifier_and_entropy( + &mut rng, + identity.id(), + entropy, + DocumentFieldFillType::FillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + platform_version_16, + ) + .expect("expected a random profile document"); + platform + .drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&document, None)), + owner_id: None, + }, + contract: &dashpay, + document_type: profile, + }, + false, + BlockInfo::default(), + true, + None, + platform_version_16, + None, + ) + .expect("expected to insert the document"); + + let root_absent = |transaction: drive::grovedb::TransactionArg| { + platform + .drive + .grove + .get( + SubtreePath::empty(), + &[RootTree::ContractCredits as u8], + transaction, + grove_version, + ) + .unwrap() + .is_err() + }; + let committed_root_hash = || { + platform + .drive + .grove + .root_hash(None, grove_version) + .unwrap() + .expect("expected the committed root hash") + }; + + assert!(root_absent(None), "a v16 chain must not hold the root yet"); + let pre_upgrade_root_hash = committed_root_hash(); + + let block_info = BlockInfo { + time_ms: 2_000_000, + height: 200, + core_height: 200, + epoch: Epoch::new(1).expect("expected epoch"), + }; + + // Round 1: the hook runs inside a transaction that is then dropped, + // as happens when the proposal that carried the upgrade is rejected. + { + let transaction = platform.drive.grove.start_transaction(); + platform + .perform_events_on_first_block_of_protocol_change( + &platform_state, + &block_info, + &transaction, + 16, + platform_version_17, + ) + .expect("expected the protocol change events to succeed"); + + let element = platform + .drive + .grove + .get( + SubtreePath::empty(), + &[RootTree::ContractCredits as u8], + Some(&transaction), + grove_version, + ) + .unwrap() + .expect("the root must exist inside the upgrading transaction"); + assert_eq!(element, Element::empty_sum_tree()); + + let total = platform + .drive + .calculate_total_credits_balance(Some(&transaction), &platform_version_17.drive) + .expect("expected to calculate the total credits balance"); + assert_eq!(total.total_in_contract_credits, 0); + assert_eq!(total.total_identity_balances, balance as i64); + assert!(total.ok().expect("no overflow")); + assert_eq!( + platform + .drive + .fetch_identity_balance( + identity.id().to_buffer(), + Some(&transaction), + platform_version_17, + ) + .expect("expected to fetch the identity balance"), + Some(balance), + "the upgrade must not touch identity balances" + ); + + platform + .drive + .grove + .rollback_transaction(&transaction) + .expect("expected to roll back the rejected round"); + } + assert_eq!( + committed_root_hash(), + pre_upgrade_root_hash, + "a rejected round must leave the committed state untouched" + ); + assert!(root_absent(None)); + + // Round 2: the retry a validator performs after the rejected round. + let transaction = platform.drive.grove.start_transaction(); + platform + .perform_events_on_first_block_of_protocol_change( + &platform_state, + &block_info, + &transaction, + 16, + platform_version_17, + ) + .expect("expected the protocol change events to succeed on retry"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit the upgrade"); + + assert!(!root_absent(None), "the committed state must hold the root"); + let committed_upgraded_root_hash = committed_root_hash(); + assert_ne!(committed_upgraded_root_hash, pre_upgrade_root_hash); + + let genesis_17 = TestPlatformBuilder::new() + .with_initial_protocol_version(17) + .build_with_mock_rpc() + .set_genesis_state(); + let read_transaction = platform.drive.grove.start_transaction(); + assert_eq!( + platform + .drive + .grove + .get( + SubtreePath::empty(), + &[RootTree::ContractCredits as u8], + Some(&read_transaction), + grove_version, + ) + .unwrap() + .expect("the committed root element"), + genesis_17 + .drive + .grove + .get( + SubtreePath::empty(), + &[RootTree::ContractCredits as u8], + None, + grove_version, + ) + .unwrap() + .expect("the genesis root element"), + "the upgraded root element must match the genesis-v17 one" + ); + let diffs = collect_subtree_diffs( + &genesis_17, + &platform, + &read_transaction, + vec![vec![RootTree::ContractCredits as u8]], + ); + assert!( + diffs.is_empty(), + "the committed [ContractCredits] subtree must match a genesis-v17 one.\n{}", + diffs.join("\n"), + ); + drop(read_transaction); + + let total = platform + .drive + .calculate_total_credits_balance(None, &platform_version_17.drive) + .expect("expected to calculate the total credits balance"); + assert_eq!(total.total_in_contract_credits, 0); + assert!(total.ok().expect("no overflow")); + + // Round 3: running the hook once more must be a no-op. + let transaction = platform.drive.grove.start_transaction(); + platform + .perform_events_on_first_block_of_protocol_change( + &platform_state, + &block_info, + &transaction, + 16, + platform_version_17, + ) + .expect("expected the protocol change events to be idempotent"); + assert_eq!( + platform + .drive + .grove + .root_hash(Some(&transaction), grove_version) + .unwrap() + .expect("expected the root hash"), + committed_upgraded_root_hash, + "a third run must not change the state" + ); + drop(transaction); + + // Negative control: a block that stays at v17 must not create anything. + let steady = TestPlatformBuilder::new() + .with_initial_protocol_version(16) + .build_with_mock_rpc() + .set_genesis_state(); + let steady_state = steady.state.load(); + let transaction = steady.drive.grove.start_transaction(); + steady + .perform_events_on_first_block_of_protocol_change( + &steady_state, + &block_info, + &transaction, + 17, + platform_version_17, + ) + .expect("expected no events for a same-version block"); + assert!( + steady + .drive + .grove + .get( + SubtreePath::empty(), + &[RootTree::ContractCredits as u8], + Some(&transaction), + grove_version, + ) + .unwrap() + .is_err(), + "the guard must not fire when the previous version is already 17" + ); + } } #[cfg(test)] diff --git a/packages/rs-drive/grovedb-structure.json b/packages/rs-drive/grovedb-structure.json index 4e4c8f0a377..c07eb96fb7e 100644 --- a/packages/rs-drive/grovedb-structure.json +++ b/packages/rs-drive/grovedb-structure.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "latest_protocol_version": 14, + "latest_protocol_version": 17, "element_kinds": [ { "name": "Item", @@ -3924,6 +3924,69 @@ } ] }, + { + "id": "contract_credits", + "key": { + "type": "fixed", + "hex": "64", + "label": "ContractCredits", + "constant": "RootTree::ContractCredits" + }, + "kinds": [ + "SumTree" + ], + "since": 17, + "presence": "always", + "source": "packages/rs-drive/src/drive/mod.rs", + "book": "drive/contract-credit-buckets.md", + "description": "The credits every contract holds, in buckets. An ordinary sum tree, so its aggregate is the total of live contract credits and is a term of the credit conservation equation. Key 100 is provisional and hangs below Misc.", + "children": [ + { + "id": "contract_credits.contract", + "key": { + "type": "dynamic", + "name": "contract_id", + "matcher": { + "type": "len", + "len": 32 + }, + "encoding": "identifier32", + "description": "The data contract id" + }, + "kinds": [ + "SumTree" + ], + "since": 17, + "presence": "always", + "source": "packages/rs-drive/src/drive/contract/balances/mod.rs", + "description": "One contract's credit buckets. A wiped contract's tree is wrapped in a not-summed element, so its retained credits leave the root aggregate.", + "children": [ + { + "id": "contract_credits.contract.bucket", + "key": { + "type": "dynamic", + "name": "bucket_position", + "matcher": { + "type": "len", + "len": 2 + }, + "encoding": "u16_be", + "description": "The bucket position" + }, + "kinds": [ + "SumItem" + ], + "value": "credits", + "since": 17, + "presence": "always", + "source": "packages/rs-drive/src/drive/contract/balances/mod.rs", + "description": "The credits in one bucket.", + "children": [] + } + ] + } + ] + }, { "id": "misc", "key": { @@ -5206,7 +5269,7 @@ }, "layer_shapes": { "contract_groups": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "00", "right": { @@ -5215,7 +5278,7 @@ } }, "contract_groups.groups.group": { - "origin": "fixture contract_groups_and_bound_keys@14", + "origin": "fixture contract_groups_and_bound_keys@17", "tree": { "hex": "02", "left": { @@ -5230,7 +5293,7 @@ } }, "contract_groups.members.contract": { - "origin": "fixture contract_groups_and_bound_keys@14", + "origin": "fixture contract_groups_and_bound_keys@17", "tree": { "hex": "01", "left": { @@ -5242,7 +5305,7 @@ } }, "contracts.contract": { - "origin": "fixture contracts_with_documents@14", + "origin": "fixture contracts_with_documents@17", "tree": { "hex": "01", "left": { @@ -5254,7 +5317,7 @@ } }, "contracts.contract.other": { - "origin": "fixture moderated_contract@14", + "origin": "fixture moderated_contract@17", "tree": { "hex": "80", "left": { @@ -5272,7 +5335,7 @@ } }, "group_actions.contract.group": { - "origin": "fixture tokens_and_group_actions@14", + "origin": "fixture tokens_and_group_actions@17", "tree": { "hex": "4d", "left": { @@ -5284,7 +5347,7 @@ } }, "group_actions.contract.group.active.action": { - "origin": "fixture tokens_and_group_actions@14", + "origin": "fixture tokens_and_group_actions@17", "tree": { "hex": "53", "left": { @@ -5293,7 +5356,7 @@ } }, "group_actions.contract.group.closed.action": { - "origin": "fixture tokens_and_group_actions@14", + "origin": "fixture tokens_and_group_actions@17", "tree": { "hex": "53", "left": { @@ -5302,7 +5365,7 @@ } }, "identities.identity": { - "origin": "fixture contract_groups_and_bound_keys@14", + "origin": "fixture contract_groups_and_bound_keys@17", "tree": { "hex": "80", "left": { @@ -5327,7 +5390,7 @@ "states": [ { "state": "created", - "origin": "fixture identities@14", + "origin": "fixture identities@17", "tree": { "hex": "80", "left": { @@ -5346,7 +5409,7 @@ }, { "state": "used_with_a_contract", - "origin": "fixture identities@14", + "origin": "fixture identities@17", "tree": { "hex": "80", "left": { @@ -5368,7 +5431,7 @@ }, { "state": "budgeted_key_and_contract", - "origin": "fixture contract_groups_and_bound_keys@14", + "origin": "fixture contract_groups_and_bound_keys@17", "tree": { "hex": "80", "left": { @@ -5394,7 +5457,7 @@ ] }, "identities.identity.contract_info.bound": { - "origin": "fixture contract_groups_and_bound_keys@14", + "origin": "fixture contract_groups_and_bound_keys@17", "tree": { "hex": "01", "left": { @@ -5403,7 +5466,7 @@ } }, "identities.identity.key_references": { - "origin": "fixture identities@14", + "origin": "fixture identities@17", "tree": { "hex": "03", "left": { @@ -5415,7 +5478,7 @@ } }, "identities.identity.key_references.authentication": { - "origin": "fixture identities@14", + "origin": "fixture identities@17", "tree": { "hex": "02", "left": { @@ -5430,7 +5493,7 @@ } }, "misc": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "45", "left": { @@ -5442,7 +5505,7 @@ } }, "pools.epoch": { - "origin": "fixture current_epoch@14", + "origin": "fixture current_epoch@17", "tree": { "hex": "6d", "left": { @@ -5470,14 +5533,14 @@ "states": [ { "state": "future", - "origin": "fixture current_epoch@14", + "origin": "fixture current_epoch@17", "tree": { "hex": "73" } }, { "state": "running", - "origin": "fixture current_epoch@14", + "origin": "fixture current_epoch@17", "tree": { "hex": "6d", "left": { @@ -5505,7 +5568,7 @@ }, { "state": "paid", - "origin": "fixture paid_epoch@14", + "origin": "fixture paid_epoch@17", "tree": { "hex": "74", "left": { @@ -5528,7 +5591,7 @@ ] }, "prefunded_balances": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "80", "left": { @@ -5540,7 +5603,7 @@ } }, "root": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "40", "left": { @@ -5584,7 +5647,10 @@ "right": { "hex": "70", "left": { - "hex": "68" + "hex": "68", + "left": { + "hex": "64" + } }, "right": { "hex": "78", @@ -5597,7 +5663,7 @@ } }, "saved_block_transactions": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "65", "left": { @@ -5609,7 +5675,7 @@ } }, "shielded_balances.main_pool": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "80", "left": { @@ -5627,7 +5693,7 @@ } }, "tokens": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "80", "left": { @@ -5648,7 +5714,7 @@ } }, "tokens.distributions": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "80", "left": { @@ -5663,7 +5729,7 @@ } }, "tokens.distributions.perpetual.token": { - "origin": "fixture token_distributions_unclaimed@14", + "origin": "fixture token_distributions_unclaimed@17", "tree": { "hex": "c0", "left": { @@ -5672,7 +5738,7 @@ } }, "tokens.distributions.timed": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "80", "left": { @@ -5684,7 +5750,7 @@ } }, "versions": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "01", "left": { @@ -5693,7 +5759,7 @@ } }, "votes": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "64", "left": { @@ -5705,7 +5771,7 @@ } }, "votes.contested_resource": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "70", "left": { @@ -5714,7 +5780,7 @@ } }, "votes.contested_resource.active_polls.contract.document_type": { - "origin": "fixture contested_documents@14", + "origin": "fixture contested_documents@17", "tree": { "hex": "01", "left": { @@ -5723,7 +5789,7 @@ } }, "votes.contested_resource.active_polls.contract.document_type.indexes.value.contender": { - "origin": "fixture contested_documents@14", + "origin": "fixture contested_documents@17", "tree": { "hex": "01", "left": { @@ -5732,7 +5798,7 @@ } }, "withdrawals": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "03", "left": { diff --git a/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/mod.rs b/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/mod.rs index e88cd2d56ea..3bd55b442cf 100644 --- a/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/mod.rs +++ b/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/mod.rs @@ -1,6 +1,7 @@ mod v0; mod v1; mod v2; +mod v3; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -12,7 +13,9 @@ use grovedb::TransactionArg; impl Drive { /// Calculates the total credits balance. /// - /// This function verifies that the sum tree identity credits + pool credits + refunds are equal to the total credits in the system. + /// This function verifies that the sum tree identity credits + pool credits + refunds + + /// address credits + shielded credits + contract credits are equal to the total credits + /// in the system. /// /// # Arguments /// @@ -40,9 +43,10 @@ impl Drive { 0 => self.calculate_total_credits_balance_v0(transaction, drive_version), 1 => self.calculate_total_credits_balance_v1(transaction, drive_version), 2 => self.calculate_total_credits_balance_v2(transaction, drive_version), + 3 => self.calculate_total_credits_balance_v3(transaction, drive_version), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "calculate_total_credits_balance".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1, 2, 3], received: version, })), } diff --git a/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v0/mod.rs b/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v0/mod.rs index 428e964f639..af39b4e0e22 100644 --- a/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v0/mod.rs +++ b/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v0/mod.rs @@ -67,6 +67,10 @@ impl Drive { total_specialized_balances, total_in_addresses: 0, total_in_shielded_balances: 0, + // v0 predates the ContractCredits root tree (introduced at + // protocol v17 / drive v10 alongside the v3 calculator), so it + // leaves the field zeroed. + total_in_contract_credits: 0, }) } } diff --git a/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v1/mod.rs b/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v1/mod.rs index 13c00b3136c..2a91f768752 100644 --- a/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v1/mod.rs +++ b/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v1/mod.rs @@ -80,6 +80,10 @@ impl Drive { // pre-v12 chains the tree does not exist, so v1 does not read // it and leaves the field zeroed. total_in_shielded_balances: 0, + // v1 also predates the ContractCredits root tree (introduced at + // protocol v17 / drive v10 alongside the v3 calculator), so it + // leaves that field zeroed too. + total_in_contract_credits: 0, }) } } diff --git a/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v2/mod.rs b/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v2/mod.rs index a6e17adf685..b43f88558e6 100644 --- a/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v2/mod.rs +++ b/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v2/mod.rs @@ -89,6 +89,11 @@ impl Drive { total_specialized_balances, total_in_addresses, total_in_shielded_balances, + // v2 predates the ContractCredits root tree (introduced at + // protocol v17 / drive v10 alongside the v3 calculator). On + // pre-v17 chains the tree does not exist, so v2 does not read + // it and leaves the field zeroed. + total_in_contract_credits: 0, }) } } diff --git a/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v3/mod.rs b/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v3/mod.rs new file mode 100644 index 00000000000..24b18420148 --- /dev/null +++ b/packages/rs-drive/src/drive/balances/calculate_total_credits_balance/v3/mod.rs @@ -0,0 +1,245 @@ +use crate::drive::balances::TOTAL_SYSTEM_CREDITS_STORAGE_KEY; +use crate::drive::system::misc_path; +use crate::drive::{Drive, RootTree}; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::util::grove_operations::DirectQueryType; +use dpp::balances::total_credits_balance::TotalCreditsBalance; +use dpp::version::drive_versions::DriveVersion; +use grovedb::TransactionArg; +use grovedb_path::SubtreePath; + +impl Drive { + /// Verify that the sum tree identity credits + pool credits + refunds + address + /// credits + shielded credits + contract credits are equal to the Total credits + /// in the system. + /// + /// v3 adds the `ContractCredits` root sum tree (introduced at protocol v17 / + /// drive v10) as a sixth term in the equation. The tree's aggregate only + /// covers live contracts: a wiped contract's subtree is wrapped in a + /// not-summed element and contributes nothing. Earlier calculators do not + /// read it because the tree does not exist on pre-v17 chains. + #[inline(always)] + pub(super) fn calculate_total_credits_balance_v3( + &self, + transaction: TransactionArg, + drive_version: &DriveVersion, + ) -> Result { + let mut drive_operations = vec![]; + let path_holding_total_credits = misc_path(); + let total_credits_in_platform = self + .grove_get_raw_value_u64_from_encoded_var_vec( + (&path_holding_total_credits).into(), + TOTAL_SYSTEM_CREDITS_STORAGE_KEY, + DirectQueryType::StatefulDirectQuery, + transaction, + &mut drive_operations, + drive_version, + )? + .ok_or(Error::Drive(DriveError::CriticalCorruptedState( + "Credits not found in Platform", + )))?; + + let total_identity_balances = self.grove_get_sum_tree_total_value( + SubtreePath::empty(), + Into::<&[u8; 1]>::into(RootTree::Balances), + DirectQueryType::StatefulDirectQuery, + transaction, + &mut drive_operations, + drive_version, + )?; + + let total_specialized_balances = self.grove_get_sum_tree_total_value( + SubtreePath::empty(), + Into::<&[u8; 1]>::into(RootTree::PreFundedSpecializedBalances), + DirectQueryType::StatefulDirectQuery, + transaction, + &mut drive_operations, + drive_version, + )?; + + let total_in_pools = self.grove_get_sum_tree_total_value( + SubtreePath::empty(), + Into::<&[u8; 1]>::into(RootTree::Pools), + DirectQueryType::StatefulDirectQuery, + transaction, + &mut drive_operations, + drive_version, + )?; + + let total_in_addresses = self.grove_get_sum_tree_total_value( + SubtreePath::empty(), + Into::<&[u8; 1]>::into(RootTree::AddressBalances), + DirectQueryType::StatefulDirectQuery, + transaction, + &mut drive_operations, + drive_version, + )?; + + let total_in_shielded_balances = self.grove_get_sum_tree_total_value( + SubtreePath::empty(), + Into::<&[u8; 1]>::into(RootTree::ShieldedBalances), + DirectQueryType::StatefulDirectQuery, + transaction, + &mut drive_operations, + drive_version, + )?; + + let total_in_contract_credits = self.grove_get_sum_tree_total_value( + SubtreePath::empty(), + Into::<&[u8; 1]>::into(RootTree::ContractCredits), + DirectQueryType::StatefulDirectQuery, + transaction, + &mut drive_operations, + drive_version, + )?; + + Ok(TotalCreditsBalance { + total_credits_in_platform, + total_in_pools, + total_identity_balances, + total_specialized_balances, + total_in_addresses, + total_in_shielded_balances, + total_in_contract_credits, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::drive::contract::balances::{contract_credits_path, contract_credits_root_path}; + use crate::drive::Drive; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::version::PlatformVersion; + use grovedb::Element; + use grovedb_path::SubtreePath; + + /// Credits a contract directly in the contract credits tree, bypassing + /// the bucket operations that later changes add: a raw sum subtree for + /// the contract holding one sum item. Enough to move the root aggregate. + fn credit_contract_raw( + drive: &Drive, + contract_id: [u8; 32], + amount: i64, + platform_version: &PlatformVersion, + ) { + let grove_version = &platform_version.drive.grove_version; + drive + .grove + .insert( + SubtreePath::from(&contract_credits_root_path()), + &contract_id, + Element::empty_sum_tree(), + None, + None, + grove_version, + ) + .unwrap() + .expect("expected to insert the contract sum tree"); + drive + .grove + .insert( + SubtreePath::from(&contract_credits_path(&contract_id)), + &[0, 1], + Element::new_sum_item(amount), + None, + None, + grove_version, + ) + .unwrap() + .expect("expected to insert the bucket sum item"); + } + + #[test] + fn should_balance_credits_with_empty_contract_credits_root() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + + let total = drive + .calculate_total_credits_balance(None, &platform_version.drive) + .expect("expected to calculate the total credits balance"); + + assert_eq!(total.total_in_contract_credits, 0); + assert!(total.ok().expect("no overflow")); + } + + #[test] + fn should_read_contract_credits_as_a_term_of_the_equation() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + + credit_contract_raw(&drive, [1u8; 32], 700, platform_version); + credit_contract_raw(&drive, [2u8; 32], 300, platform_version); + + // Credits in the contract trees without the matching system credits + // are not balanced: the term is read, not assumed. + let unbalanced = drive + .calculate_total_credits_balance(None, &platform_version.drive) + .expect("expected to calculate the total credits balance"); + assert_eq!(unbalanced.total_in_contract_credits, 1000); + assert!(!unbalanced.ok().expect("no overflow")); + + drive + .add_to_system_credits(1000, None, platform_version) + .expect("expected to add to system credits"); + + let balanced = drive + .calculate_total_credits_balance(None, &platform_version.drive) + .expect("expected to calculate the total credits balance"); + assert_eq!(balanced.total_in_contract_credits, 1000); + assert!(balanced.ok().expect("no overflow")); + assert_eq!(balanced.total_in_trees().expect("no overflow"), 1000); + } + + #[test] + fn should_exclude_a_not_summed_contract_tree_from_the_term() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let grove_version = &platform_version.drive.grove_version; + + credit_contract_raw(&drive, [1u8; 32], 700, platform_version); + + // A wiped contract keeps its subtree, wrapped so the root aggregate + // ignores it. The retained amount is real storage but not live credit. + let wiped_id = [9u8; 32]; + drive + .grove + .insert( + SubtreePath::from(&contract_credits_root_path()), + &wiped_id, + Element::new_not_summed(Element::empty_sum_tree()) + .expect("a sum tree can be wrapped"), + None, + None, + grove_version, + ) + .unwrap() + .expect("expected to insert the wiped contract tree"); + drive + .grove + .insert( + SubtreePath::from(&contract_credits_path(&wiped_id)), + &[0, 1], + Element::new_sum_item(5000), + None, + None, + grove_version, + ) + .unwrap() + .expect("expected to insert the retained bucket"); + + drive + .add_to_system_credits(700, None, platform_version) + .expect("expected to add to system credits"); + + let total = drive + .calculate_total_credits_balance(None, &platform_version.drive) + .expect("expected to calculate the total credits balance"); + assert_eq!( + total.total_in_contract_credits, 700, + "the retained credits of the wiped contract must not be counted" + ); + assert!(total.ok().expect("no overflow")); + } +} diff --git a/packages/rs-drive/src/drive/contract/balances/mod.rs b/packages/rs-drive/src/drive/contract/balances/mod.rs new file mode 100644 index 00000000000..6a507cefc75 --- /dev/null +++ b/packages/rs-drive/src/drive/contract/balances/mod.rs @@ -0,0 +1,69 @@ +//! Contract balances. +//! +//! Contracts are not identities and hold no identity balance. Their credits +//! live in a dedicated root sum tree, `RootTree::ContractCredits`, laid out +//! as `[ContractCredits] / contract_id (SumTree) / bucket_key (SumItem)`. +//! The root aggregate is therefore the total of live contract credits and is +//! read by credit conservation as its own term. A contract whose credits +//! were wiped keeps its subtree wrapped in a not-summed element, so the root +//! aggregate never counts retained balances. +//! +//! This module holds the path helpers. The bucket operations, the wiped +//! lifecycle wrapper and the proofs arrive with later changes. + +use crate::drive::RootTree; + +/// The structure description of the contract credits tree +#[cfg(all(feature = "server", any(test, feature = "structure")))] +pub(crate) mod structure; + +/// The path to the contract credits root tree. +pub fn contract_credits_root_path() -> [&'static [u8]; 1] { + [Into::<&[u8; 1]>::into(RootTree::ContractCredits)] +} + +/// The path to the contract credits root tree as a vec. +pub fn contract_credits_root_path_vec() -> Vec> { + vec![vec![RootTree::ContractCredits as u8]] +} + +/// The path to the credit bucket sum tree of one contract. +pub fn contract_credits_path(contract_id: &[u8; 32]) -> [&[u8]; 2] { + [ + Into::<&[u8; 1]>::into(RootTree::ContractCredits), + contract_id, + ] +} + +/// The path to the credit bucket sum tree of one contract as a vec. +pub fn contract_credits_path_vec(contract_id: [u8; 32]) -> Vec> { + vec![vec![RootTree::ContractCredits as u8], contract_id.to_vec()] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_place_the_contract_credits_root_at_the_root_key() { + let root = contract_credits_root_path(); + assert_eq!(root, [&[RootTree::ContractCredits as u8][..]]); + assert_eq!( + contract_credits_root_path_vec(), + root.iter().map(|part| part.to_vec()).collect::>() + ); + } + + #[test] + fn should_place_a_contract_tree_directly_under_the_root() { + let contract_id = [7u8; 32]; + let path = contract_credits_path(&contract_id); + assert_eq!(path.len(), 2); + assert_eq!(path[0], &[RootTree::ContractCredits as u8][..]); + assert_eq!(path[1], &contract_id[..]); + assert_eq!( + contract_credits_path_vec(contract_id), + path.iter().map(|part| part.to_vec()).collect::>() + ); + } +} diff --git a/packages/rs-drive/src/drive/contract/balances/structure.rs b/packages/rs-drive/src/drive/contract/balances/structure.rs new file mode 100644 index 00000000000..e2294091852 --- /dev/null +++ b/packages/rs-drive/src/drive/contract/balances/structure.rs @@ -0,0 +1,45 @@ +use crate::drive::RootTree; +use crate::structure::{ElementKind, KeyEncoding, KeyMatcher, StructureNode}; + +/// Contract credits: one sum subtree per contract holding its credit buckets +pub(crate) fn structure() -> StructureNode { + StructureNode::fixed( + "contract_credits", + &[RootTree::ContractCredits as u8], + "ContractCredits", + "RootTree::ContractCredits", + ) + .kind(ElementKind::SumTree) + .since(17) + .source("packages/rs-drive/src/drive/mod.rs") + .book("drive/contract-credit-buckets.md") + .describe( + "The credits every contract holds, in buckets. An \ + ordinary sum tree, so its aggregate is the total \ + of live contract credits and is a term of the \ + credit conservation equation. Key 100 is \ + provisional and hangs below Misc.", + ) + .child( + StructureNode::identifier("contract", "contract_id", "The data contract id") + .kind(ElementKind::SumTree) + .source("packages/rs-drive/src/drive/contract/balances/mod.rs") + .describe( + "One contract's credit buckets. A wiped contract's \ + tree is wrapped in a not-summed element, so its \ + retained credits leave the root aggregate.", + ) + .child( + StructureNode::dynamic( + "bucket", + "bucket_position", + KeyMatcher::Len(2), + KeyEncoding::U16Be, + "The bucket position", + ) + .kind(ElementKind::SumItem) + .value("credits") + .describe("The credits in one bucket."), + ), + ) +} diff --git a/packages/rs-drive/src/drive/contract/mod.rs b/packages/rs-drive/src/drive/contract/mod.rs index 0fcf9567739..1d70ff68223 100644 --- a/packages/rs-drive/src/drive/contract/mod.rs +++ b/packages/rs-drive/src/drive/contract/mod.rs @@ -5,6 +5,9 @@ #[cfg(feature = "server")] mod apply; +/// Contract credit balances: the contract credits root tree and its paths +#[cfg(any(feature = "server", feature = "verify"))] +pub mod balances; #[cfg(feature = "server")] mod contract_fetch_info; #[cfg(feature = "server")] diff --git a/packages/rs-drive/src/drive/initialization/mod.rs b/packages/rs-drive/src/drive/initialization/mod.rs index ce157e315c0..5db1db9fdb7 100644 --- a/packages/rs-drive/src/drive/initialization/mod.rs +++ b/packages/rs-drive/src/drive/initialization/mod.rs @@ -6,6 +6,7 @@ mod v1; mod v2; mod v3; mod v4; +mod v5; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -32,9 +33,10 @@ impl Drive { 2 => self.create_initial_state_structure_v2(transaction, platform_version), 3 => self.create_initial_state_structure_v3(transaction, platform_version), 4 => self.create_initial_state_structure_v4(transaction, platform_version), + 5 => self.create_initial_state_structure_v5(transaction, platform_version), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "create_initial_state_structure".to_string(), - known_versions: vec![0, 1, 2, 3, 4], + known_versions: vec![0, 1, 2, 3, 4, 5], received: version, })), } diff --git a/packages/rs-drive/src/drive/initialization/v5/mod.rs b/packages/rs-drive/src/drive/initialization/v5/mod.rs new file mode 100644 index 00000000000..b20b03b8978 --- /dev/null +++ b/packages/rs-drive/src/drive/initialization/v5/mod.rs @@ -0,0 +1,189 @@ +//! Drive Initialization +//! +//! Version 5: version 4 plus the `ContractCredits` root sum tree (protocol version 17). + +use crate::drive::{Drive, RootTree}; +use crate::error::Error; +use dpp::version::PlatformVersion; +use grovedb::{TransactionArg, TreeType}; +use grovedb_path::SubtreePath; + +impl Drive { + /// Creates the initial state structure. + pub(super) fn create_initial_state_structure_v5( + &self, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let drive_version = &platform_version.drive; + self.create_initial_state_structure_top_level_0(transaction, platform_version)?; + + self.grove_insert_empty_tree( + SubtreePath::empty(), + &[RootTree::GroupActions as u8], + TreeType::NormalTree, + transaction, + None, + &mut vec![], + drive_version, + )?; + + // AddressBalances top-level sum tree (introduced in v2) + self.grove_insert_empty_tree( + SubtreePath::empty(), + &[RootTree::AddressBalances as u8], + TreeType::SumTree, + transaction, + None, + &mut vec![], + drive_version, + )?; + + // ShieldedBalances top-level sum tree — separate from AddressBalances so + // per-pool internal trees (notes, nullifiers, anchors, …) cannot + // contaminate the address-credit aggregate via sum propagation. + self.grove_insert_empty_tree( + SubtreePath::empty(), + &[RootTree::ShieldedBalances as u8], + TreeType::SumTree, + transaction, + None, + &mut vec![], + drive_version, + )?; + + // ContractCredits top-level sum tree (introduced in v5). Contract credit + // buckets live under it as `contract_id (SumTree) / bucket_key (SumItem)`, + // so its aggregate is the total of live contract credits and is read by + // credit conservation as its own term. CONSENSUS-CRITICAL: this is a + // standalone non-batch root insert placed right after ShieldedBalances, + // and the upgrade path (`Platform::transition_to_version_17`) creates + // the same empty sum tree with an insert-if-not-exists, so a fresh + // genesis-v17 node and an in-place-upgraded v17 node hold a + // byte-identical `[ContractCredits]` element. + self.grove_insert_empty_tree( + SubtreePath::empty(), + &[RootTree::ContractCredits as u8], + TreeType::SumTree, + transaction, + None, + &mut vec![], + drive_version, + )?; + + // SavedBlockTransactions for address-based transaction sync + self.grove_insert_empty_tree( + SubtreePath::empty(), + &[RootTree::SavedBlockTransactions as u8], + TreeType::NormalTree, + transaction, + None, + &mut vec![], + drive_version, + )?; + + // ContractGroups root tree (introduced in protocol version 14): identity-owned sets of + // contracts, contract document types and contract tokens, with a backwards index from + // each member contract. The upgrade path (`Platform::transition_to_version_14`) calls + // the same helper, so both node populations build a byte-identical subtree. + self.insert_contract_groups_structure(transaction, platform_version)?; + + // On lower layers we can use batching + + let mut batch = + self.create_initial_state_structure_lower_layers_operations_0(platform_version)?; + + self.initial_state_structure_lower_layers_add_operations_2(&mut batch, platform_version)?; + + self.grove_apply_batch(batch, false, transaction, drive_version)?; + + // Add the shielded pool structures AFTER the batch apply so the + // top-level `[ShieldedBalances]` SumTree (inserted above) already + // exists. CONSENSUS-CRITICAL: this MUST go through the shared + // `insert_shielded_pool_structure` helper — the same sequential builder + // the upgrade path (`Platform::transition_to_version_12`) uses — so a + // fresh-genesis-v12 node and an in-place-upgraded v12 node build a + // byte-identical `[ShieldedBalances]` subtree. Building the pool here in + // the sorted `GroveDbOpBatch` instead would root the parent Merk at the + // batch's median key (`[160]`) rather than the intended NOTES-at-root + // (`[128]`) layout, diverging from the upgrade path and forking the + // network at the v11→v12 boundary. + self.insert_shielded_pool_structure(transaction, platform_version)?; + + // Once-per-identity distributions root tree (protocol version 14): one claims subtree + // per token that lets every identity claim a fixed amount once. The upgrade path + // (`Platform::transition_to_version_14`) calls the same helper. + self.insert_once_per_identity_distributions_root_tree(transaction, platform_version)?; + + // Contract fee pot trees (protocol version 14): the two sum trees, beside the voting + // balances, that hold what every contract's document action fees have collected. They + // go in after the batch apply, which creates the voting balances tree, and one after + // the other, exactly as the upgrade path (`Platform::transition_to_version_14`) does + // through the same helper: the prefunded balances Merk is then built by the same + // sequence of inserts on both node populations. + self.insert_contract_fee_pot_trees(transaction, platform_version)?; + + // Document time to live trees (protocol version 14): the documents expirations tree + // under `Misc`, which indexes every document of a type declaring a `ttl` by the time + // it expires, and the lifetime storage fee pools sum tree under `Pools`, which holds + // their storage fees until an epoch change spreads them. After the batch apply, which + // creates `Misc` and the fee pools under `Pools`, and through the same helper as the + // upgrade path (`Platform::transition_to_version_14`), in the same position: last. + self.insert_document_ttl_trees(transaction, platform_version)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::drive::RootTree; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::version::PlatformVersion; + use grovedb::Element; + use grovedb_path::SubtreePath; + + #[test] + fn should_create_contract_credits_root_at_latest_genesis() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + + let element = drive + .grove + .get( + SubtreePath::empty(), + &[RootTree::ContractCredits as u8], + None, + &platform_version.drive.grove_version, + ) + .unwrap() + .expect("the contract credits root should exist at the latest genesis"); + + assert_eq!( + element, + Element::empty_sum_tree(), + "the contract credits root must be an empty sum tree with no flags" + ); + } + + #[test] + fn should_not_create_contract_credits_root_at_protocol_14_genesis() { + let platform_version = PlatformVersion::get(14).expect("protocol version 14 exists"); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + + let result = drive + .grove + .get( + SubtreePath::empty(), + &[RootTree::ContractCredits as u8], + None, + &platform_version.drive.grove_version, + ) + .unwrap(); + + assert!( + result.is_err(), + "a protocol 14 genesis must not contain the contract credits root, got {result:?}" + ); + } +} diff --git a/packages/rs-drive/src/drive/mod.rs b/packages/rs-drive/src/drive/mod.rs index 120bc8f1e45..587ea89d513 100644 --- a/packages/rs-drive/src/drive/mod.rs +++ b/packages/rs-drive/src/drive/mod.rs @@ -199,12 +199,15 @@ pub struct Drive { // Tokens 16 Pools 48 WithdrawalTransactions 80 Votes 112 // / \ / \ / \ / \ // NUPKH->I 8 UPKH->I 24 PreFundedSpecializedBalances 40 AddressBalances 56 SpentAssetLockTransactions 72 GroupActions 88 Misc 104 Versions 120 -// / / \ -// Saved Block Transactions 36 ShieldedBalances 52 ContractGroups 124 +// / / / \ +// Saved Block Transactions 36 ShieldedBalances 52 ContractCredits 100 ContractGroups 124 // // This is the shape of a fresh chain. `drive::structure` describes every level below the root as // code, and `packages/rs-drive/grovedb-structure.json` records this shape from a real GroveDB -// (`layer_shapes.root`), so a test fails when the two drift apart. +// (`layer_shapes.root`), so a test fails when the two drift apart. Keys added after genesis of +// an earlier protocol version (ShieldedBalances 52, ContractGroups 124, ContractCredits 100) +// are placed by AVL rebalancing at insertion time, so their exact depth depends on the +// insertion order that the initialization and upgrade paths share. /// Keys for the root tree. #[cfg(any(feature = "server", feature = "verify"))] @@ -254,6 +257,16 @@ pub enum RootTree { /// tokens, with a backwards index from each member contract to its groups (protocol /// version 14). ContractGroups = 124, + /// Contract credits: one sum subtree per contract holding its credit + /// buckets as sum items. An ordinary sum tree so the root aggregate is + /// the total of live contract credits; a wiped contract's subtree is + /// wrapped in a not-summed element and contributes nothing here + /// (protocol version 17). + /// + /// The key value is provisional: the allocation register leaves new + /// root keys unallocated, and 100 was chosen as a free value near the + /// other balance trees. + ContractCredits = 100, } #[cfg(any(feature = "server", feature = "verify"))] @@ -281,6 +294,7 @@ impl fmt::Display for RootTree { RootTree::Votes => "Votes", RootTree::GroupActions => "GroupActions", RootTree::ContractGroups => "ContractGroups", + RootTree::ContractCredits => "ContractCredits", }; write!(f, "{}", variant_name) } @@ -328,6 +342,7 @@ impl TryFrom for RootTree { 112 => Ok(RootTree::Votes), 88 => Ok(RootTree::GroupActions), 124 => Ok(RootTree::ContractGroups), + 100 => Ok(RootTree::ContractCredits), _ => Err(Error::Drive(DriveError::NotSupported( "unknown root tree item", ))), @@ -357,6 +372,7 @@ impl From for &'static [u8; 1] { RootTree::Votes => &[112], RootTree::GroupActions => &[88], RootTree::ContractGroups => &[124], + RootTree::ContractCredits => &[100], } } } diff --git a/packages/rs-drive/src/drive/structure.rs b/packages/rs-drive/src/drive/structure.rs index 2fab2a6ae84..bdb88772319 100644 --- a/packages/rs-drive/src/drive/structure.rs +++ b/packages/rs-drive/src/drive/structure.rs @@ -1,6 +1,7 @@ use crate::drive::address_funds::structure::structure as address_balances; use crate::drive::asset_lock::structure::structure as spent_asset_locks; use crate::drive::balances::structure::structure as balances; +use crate::drive::contract::balances::structure::structure as contract_credits; use crate::drive::contract::structure::structure as contracts_and_documents; use crate::drive::contract_groups::structure::structure as contract_groups; use crate::drive::credit_pools::structure::structure as pools; @@ -45,5 +46,6 @@ pub(crate) fn root_structure() -> StructureNode { votes(), versions(), contract_groups(), + contract_credits(), ]) } diff --git a/packages/rs-drive/src/structure/tests.rs b/packages/rs-drive/src/structure/tests.rs index 5102d8c3e41..8412c38a324 100644 --- a/packages/rs-drive/src/structure/tests.rs +++ b/packages/rs-drive/src/structure/tests.rs @@ -119,7 +119,13 @@ fn should_record_a_contract_layer_with_its_documents_on_top() { // fixture. Documents are read most and sit at the root of the layer; the // contract itself and everything else hang below. let contract = &json["layer_shapes"]["contracts.contract"]; - assert_eq!(contract["origin"], "fixture contracts_with_documents@14"); + assert_eq!( + contract["origin"], + format!( + "fixture contracts_with_documents@{}", + PlatformVersion::latest().protocol_version + ) + ); assert_eq!(contract["tree"]["hex"], "01"); assert_eq!(contract["tree"]["left"]["hex"], "00"); assert_eq!(contract["tree"]["right"]["hex"], "02"); @@ -1649,6 +1655,58 @@ mod fixtures { conformance_of(&drive, "expiring_documents", run); } + /// A live contract's credit bucket beside a wiped contract's retained + /// bucket, written as raw elements: the bucket operations that create + /// them arrive with a later change, and the layout is what they must + /// produce. + fn contract_credits(run: &mut FixtureRun) { + use crate::drive::contract::balances::{contract_credits_path, contract_credits_root_path}; + use grovedb::Element; + use grovedb_path::SubtreePath; + + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let grove_version = &platform_version.drive.grove_version; + + for (contract_id, element, credits) in [ + ([1u8; 32], Element::empty_sum_tree(), 700), + ( + [2u8; 32], + Element::new_not_summed(Element::empty_sum_tree()) + .expect("a sum tree can be wrapped"), + 5_000, + ), + ] { + drive + .grove + .insert( + SubtreePath::from(&contract_credits_root_path()), + &contract_id, + element, + None, + None, + grove_version, + ) + .unwrap() + .expect("expected to insert the contract sum tree"); + for position in [0u16, 1] { + drive + .grove + .insert( + SubtreePath::from(&contract_credits_path(&contract_id)), + &position.to_be_bytes(), + Element::new_sum_item(credits), + None, + None, + grove_version, + ) + .unwrap() + .expect("expected to insert the bucket sum item"); + } + } + conformance_of(&drive, "contract_credits", run); + } + /// Runs every fixture pub(super) fn run_all() -> FixtureRun { let mut run = FixtureRun::default(); @@ -1666,6 +1724,7 @@ mod fixtures { token_distributions(&mut run); contract_groups_and_bound_keys(&mut run); spent_nullifiers(&mut run); + contract_credits(&mut run); run } diff --git a/packages/rs-drive/src/util/batch/grovedb_op_batch/mod.rs b/packages/rs-drive/src/util/batch/grovedb_op_batch/mod.rs index 06b93cc50bc..bd563322c90 100644 --- a/packages/rs-drive/src/util/batch/grovedb_op_batch/mod.rs +++ b/packages/rs-drive/src/util/batch/grovedb_op_batch/mod.rs @@ -76,6 +76,7 @@ enum KnownPath { SingleUseKeyBalancesRoot, //Level 1 ShieldedBalancesRoot, //Level 1 ContractGroupsRoot, //Level 1 + ContractCreditsRoot, //Level 1 } impl From for KnownPath { @@ -103,6 +104,7 @@ impl From for KnownPath { RootTree::AddressBalances => KnownPath::SingleUseKeyBalancesRoot, RootTree::ShieldedBalances => KnownPath::ShieldedBalancesRoot, RootTree::ContractGroups => KnownPath::ContractGroupsRoot, + RootTree::ContractCredits => KnownPath::ContractCreditsRoot, } } } @@ -146,13 +148,17 @@ fn readable_key_info(known_path: KnownPath, key_info: &KeyInfo) -> (String, Opti ), None, ), - KnownPath::DataContractAndDocumentsRoot if key.len() == 32 => ( - format!( - "ContractId(bs58::{})", - Identifier::from_vec(key.clone()).unwrap() - ), - None, - ), + KnownPath::DataContractAndDocumentsRoot | KnownPath::ContractCreditsRoot + if key.len() == 32 => + { + ( + format!( + "ContractId(bs58::{})", + Identifier::from_vec(key.clone()).unwrap() + ), + None, + ) + } KnownPath::DataContractAndDocumentsRoot if key.len() == 1 => match key[0] { 0 => ( "DataContractStorage(0)".to_string(), From f4694d64a0902e35e5e3f77395443412438f9296 Mon Sep 17 00:00:00 2001 From: DCG-Claude Date: Sat, 12 Sep 2026 14:43:29 -0500 Subject: [PATCH 03/11] test(drive): re-pin the latest genesis shape for the contract credits root The root Merk now holds 19 keys. ContractCredits (100) lands as the left child of Misc (104), so the Misc proof carries one more child hash and the new key proves at the same size as its level 4 siblings. The protocol 13 heights test now builds a protocol 13 genesis, as its name says, so its Versions pin is the 285 bytes of a node without the ContractGroups child. Co-Authored-By: Claude Fable 5.1 --- .../src/drive/initialization/v0/mod.rs | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/rs-drive/src/drive/initialization/v0/mod.rs b/packages/rs-drive/src/drive/initialization/v0/mod.rs index be563ef7953..ff89e7d578e 100644 --- a/packages/rs-drive/src/drive/initialization/v0/mod.rs +++ b/packages/rs-drive/src/drive/initialization/v0/mod.rs @@ -316,7 +316,7 @@ mod tests { &platform_version.drive, ) .expect("expected to get root elements"); - assert_eq!(elements.len(), 18); + assert_eq!(elements.len(), 19); // 18 before protocol version 17 added ContractCredits } // PROTOCOL_VERSION_13: the released root layout has 17 trees; the ContractGroups tree @@ -1301,6 +1301,11 @@ mod tests { #[test] /// Proof sizes differ from v11 by +33/+35 bytes on nodes touching the /// shielded pool subtree, which uses `KVValueHashFeatureTypeWithChildHash`. + /// + /// From protocol v17 the `ContractCredits` root sum tree (key 100) sits + /// as the left child of `Misc` (104) at Merk level 4, so the `Misc` proof + /// grows by the child hash it now carries and the new key proves at the + /// same size as the other level 4 keys under a normal tree parent. fn test_initial_state_structure_proper_heights_in_latest_protocol_version() { let drive = setup_drive_with_initial_state_structure(None); @@ -1627,7 +1632,7 @@ mod tests { drive_version, ) .expect("expected to get root elements"); - assert_eq!(proof.len(), 285); + assert_eq!(proof.len(), 319); // 285 before v17: Misc now carries the ContractCredits child hash let mut query = Query::new(); query.insert_key(vec![RootTree::Versions as u8]); @@ -1672,15 +1677,36 @@ mod tests { ) .expect("expected to get root elements"); assert_eq!(proof.len(), 319); + + let mut query = Query::new(); + query.insert_key(vec![RootTree::ContractCredits as u8]); + let root_path_query = PathQuery::new( + vec![], + SizedQuery { + query, + limit: None, + offset: None, + }, + ); + let mut drive_operations = vec![]; + let proof = drive + .grove_get_proved_path_query( + &root_path_query, + None, + &mut drive_operations, + drive_version, + ) + .expect("expected to get root elements"); + assert_eq!(proof.len(), 285); // ContractCredits root sum tree, added in protocol version 17 } #[test] /// PROTOCOL_VERSION_13: the root Merk shape before the ContractGroups tree (structure v3), /// pinned so v13 proof sizes stay reproducible. fn test_initial_state_structure_proper_heights_in_protocol_version_13() { - let drive = setup_drive_with_initial_state_structure(None); - let platform_version = PlatformVersion::get(13).expect("expected platform version 13"); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let drive_version = &platform_version.drive; // Merk Level 0 @@ -2003,7 +2029,7 @@ mod tests { drive_version, ) .expect("expected to get root elements"); - assert_eq!(proof.len(), 319); + assert_eq!(proof.len(), 285); // 319 at the latest version, where ContractGroups (124) hangs below Versions (120) // Merk Level 4 From 52c55c68e9044c375a7fe461013a6889785ac3a0 Mon Sep 17 00:00:00 2001 From: DCG-Claude Date: Sat, 12 Sep 2026 14:43:29 -0500 Subject: [PATCH 04/11] docs: describe the contract credits root tree and its conservation term Co-Authored-By: Claude Fable 5.1 --- book/src/SUMMARY.md | 1 + book/src/drive/contract-credit-buckets.md | 70 +++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 book/src/drive/contract-credit-buckets.md diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 8e83c3ffb7f..109d824b2bc 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -115,6 +115,7 @@ - [Ranked Index Examples](drive/ranked-index-examples.md) - [Time-Range Index TTL](drive/time-range-ttl.md) - [Index-Only Document Types](drive/index-only-document-types.md) +- [Contract Credit Buckets](drive/contract-credit-buckets.md) # Testing diff --git a/book/src/drive/contract-credit-buckets.md b/book/src/drive/contract-credit-buckets.md new file mode 100644 index 00000000000..0d8de55d266 --- /dev/null +++ b/book/src/drive/contract-credit-buckets.md @@ -0,0 +1,70 @@ +# Contract Credit Buckets + +Ordinary data contracts are not identities. They have no keys, no nonce and no identity balance, and the smart contract work keeps it that way. When a contract needs to hold credits (to pay for scheduled work, to escrow a purchase, to fund its own storage) those credits live in a dedicated structure that is separate from every identity tree: the contract credits root tree. This chapter describes that tree, the lifecycle wrapper that marks a wiped contract, and how the tree takes part in credit conservation. Later chapters add the bucket identifiers and rules, the storage operations, the proofs and the matching token holdings as those pieces land. + +## Why a separate tree + +Identity balances sit in the `Balances` root sum tree keyed by identity id. Reusing that tree for contracts would give a contract an identity-shaped balance element, and every query, proof and validation rule that reads `Balances` would have to learn that some keys are not identities. A separate root tree avoids all of that: nothing under `Identities` or `Balances` changes, identity balance proofs stay exactly as they were, and a contract's credits are found by walking a path that names the contract, not an identity. + +A sum tree was chosen over the specialized provable-sum collections because the two questions that matter for contract credits are answered by a single authenticated element: + +- How much does one bucket hold? That is a `SumItem` under the contract's subtree, proven like any other item. +- How much does the whole contract hold? That is the stored sum of the contract's `SumTree` element, which the parent Merk commits to and a proof of the parent path authenticates. + +Range sums across buckets are not a query anyone makes, so the cost of a provable sum tree buys nothing here. Range-sum proof capabilities stay with the specialized collections used by document sum indexes. + +## Layout + +The root tree is `RootTree::ContractCredits`, key `100`, an ordinary `SumTree`. The key value is provisional: the allocation register leaves new root keys unallocated and `100` was chosen as a free value next to the other balance trees. + +```text +Root Merk + 100 ContractCredits SumTree sum = total of live contract credits + contract_id (32 bytes) SumTree a live contract; sum = its total + contract_id (32 bytes) NotSummed(SumTree) a wiped contract; inner sum retained, contributes 0 + bucket_key (2 bytes, big-endian u16) SumItem one bucket, 0 <= value <= MAX_CREDITS +``` + +The path helpers live in `packages/rs-drive/src/drive/contract/balances/mod.rs`: `contract_credits_root_path()` for `[100]` and `contract_credits_path(contract_id)` for `[100, contract_id]`, each with a `_vec` form. + +## Genesis and upgrade + +The tree exists from the 5.0 protocol version (provisionally 17) on both kinds of node: + +- A fresh chain creates it in `Drive::create_initial_state_structure_v5`, as a standalone root insert placed right after `ShieldedBalances`, before the lower-layer batch. +- A node that upgrades in place creates it in `Platform::transition_to_version_17`, which runs from `perform_events_on_first_block_of_protocol_change` on the first block at the new version, with an insert-if-not-exists. + +Both paths insert the same `Element::empty_sum_tree()` with no flags, and a test in the protocol change hook builds one platform each way and compares the subtree byte for byte. The insert-if-not-exists also makes the upgrade idempotent: a validator that ran the hook inside a rejected proposal and runs it again in the next round produces the same state as one that ran it once. + +## Lifecycle: live and wiped + +A contract's subtree is one of two things: + +- **Live**: an `Element::SumTree` whose stored sum is the contract's spendable total. Deposits and spends are allowed. +- **Wiped**: the same subtree wrapped in `Element::NotSummed`. GroveDB reports a not-summed element's contribution to its parent as zero, so the root aggregate no longer counts it, while the buckets underneath keep their values for cleanup. Nothing may deposit into or spend from a wiped contract, and no reader may report a retained bucket as available credit. + +The wrapper is a property of the parent element, so a proof of the contract's path authenticates the lifecycle along with the total. The operation that wraps a populated live tree is not part of this change; the layout above is what it must produce. + +## Credit conservation + +Since protocol version 12 the end-of-block check compares the total credits in Platform with the sum of five trees. From the 5.0 protocol version the equation has a sixth term, read by `calculate_total_credits_balance` v3 from the root aggregate of the contract credits tree: + +```text +total_credits_in_platform == total_in_pools + + total_identity_balances + + total_specialized_balances + + total_in_addresses + + total_in_shielded_balances + + total_in_contract_credits +``` + +Because the root aggregate is a `SumTree` sum and wiped subtrees are `NotSummed`, retained credits of wiped contracts are excluded by construction. A wipe that moves a contract's live total somewhere else (the processing pool of the current epoch, by the owner-confirmed policy) must therefore move it exactly once, or the equation fails at the end of that block. + +The `TotalCreditsBalance` type in `rs-dpp` carries the new field `total_in_contract_credits`; the earlier calculator generations set it to zero because the tree does not exist on the chains they run against. + +## What is not here yet + +- Bucket identifiers (a two-byte position per contract), bucket spending and deposit rules, and the Drive operations that create a contract's subtree and move credits in and out of a bucket. +- Proofs of a single bucket and of a contract's total, including the live or wiped state. +- Contract token holdings, which follow the same shape under the per-token ledger. +- The wipe itself, the transfer state transitions that call the bucket operations, and the query surface. From f940345fb86bc8cc76b1657c85c7cf4f8740018d Mon Sep 17 00:00:00 2001 From: DCG-Claude Date: Sat, 12 Sep 2026 14:53:55 -0500 Subject: [PATCH 05/11] test(drive-abci): re-pin latest-version fees for the contract credits root key Every write under Misc hashes one more child from protocol version 17, which moves the processing fee of the identity create, top up, token burn confirmer and direct purchase pins and the solitude strategy balance by 1_480 credits at latest. The protocol 14 values are kept as frozen pins next to the moved ones. Co-Authored-By: Claude Fable 5.1 --- .../batch/tests/token/burn/mod.rs | 18 ++++++- .../batch/tests/token/direct_selling/mod.rs | 14 +++++- .../state_transitions/identity_create/mod.rs | 50 +++++++++++++++++-- .../state_transitions/identity_top_up/mod.rs | 16 ++++-- .../test_cases/identity_and_document_tests.rs | 11 +++- 5 files changed, 98 insertions(+), 11 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs index 24f4a46a3ab..b4654cd159e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs @@ -3963,7 +3963,23 @@ mod token_burn_tests { // +740 per document write from protocol version 14: the contract's version item is // one more node to rehash; -12_820: the documents expirations tree joins `Misc` // beside the token supplies tree the burn rewrites, reshaping the `Misc` Merk - 4_356_200, + // + // PROTOCOL_VERSION_17: +1_480, ContractCredits (100) became the + // left child of Misc (104), so the total supply write under Misc + // hashes one more child + 4_357_680, + ) + .await; + } + + /// PROTOCOL_VERSION_14: the root Merk has no contract credits key yet, so + /// the fee must be exactly what it was before that root tree was added. + /// Pinned so v14 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_token_burn_group_action_confirmer_fee_includes_transformer_reads_protocol_version_14( + ) { + run_token_burn_group_action_confirmer_fee_includes_transformer_reads_at_protocol_version( + 14, 4_356_200, ) .await; } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs index 816a5a5f6d4..50d5874038b 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs @@ -32,11 +32,23 @@ mod token_selling_tests { // reads the total supply even though the token sets no max supply. 12_820 credits // less in fees: the documents expirations tree joins `Misc` beside the token // supplies tree the purchase rewrites, reshaping the `Misc` Merk - 699_868_049_840, + // + // PROTOCOL_VERSION_17: 1_480 credits more in fees, ContractCredits + // (100) became the left child of Misc (104), so the total supply + // write under Misc hashes one more child + 699_868_048_360, ) .await; } + /// PROTOCOL_VERSION_14: the root Merk has no contract credits key yet, so + /// the buyer balance must be exactly what it was before that root tree + /// was added. Pinned so v14 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_successful_direct_purchase_single_price_protocol_version_14() { + run_successful_direct_purchase_single_price_at_protocol_version(14, 699_868_049_840).await; + } + /// PROTOCOL_VERSION_13: pre-stamp buyer balance — genesis system documents /// are not stamped before document serialization format 3 (v14), so v13 /// costs must be exactly what they were before the `requiredSince` diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_create/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_create/mod.rs index 2332d7d399d..5ff9db713c0 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_create/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_create/mod.rs @@ -348,13 +348,23 @@ mod tests { async fn test_identity_create_validation_latest_protocol_version() { run_test_identity_create_validation_at_protocol_version( PlatformVersion::latest().protocol_version, - // PROTOCOL_VERSION_14: 4,960 credits less, see the protocol version 13 twin - 1914580, - 99913872420, + // PROTOCOL_VERSION_14: 4,960 credits less, see the protocol version 13 twin. + // PROTOCOL_VERSION_17: 1,480 credits more, see the protocol version 14 twin + 1916060, + 99913870940, ) .await; } + /// PROTOCOL_VERSION_14: 1,480 credits less processing than at the latest version. v17 + /// adds the `ContractCredits` root tree (key 100) as the left child of `Misc` (104), so the + /// total system credits write this transition makes under `Misc` hashes one more child. + /// Pinned so v14 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_identity_create_validation_protocol_version_14() { + run_test_identity_create_validation_at_protocol_version(14, 1914580, 99913872420).await; + } + /// PROTOCOL_VERSION_13: 4,960 credits more processing than at the latest version. v14 /// adds the documents expirations tree under `Misc` (key `E`), beside the total system /// credits item this transition rewrites, and the extra key reshapes the `Misc` Merk @@ -1092,7 +1102,22 @@ mod tests { async fn test_identity_create_asset_lock_reuse_after_issue_latest_protocol_version() { run_test_identity_create_asset_lock_reuse_after_issue_at_protocol_version( PlatformVersion::latest().protocol_version, - // PROTOCOL_VERSION_14: 4,960 credits less, see the protocol version 13 twin + // PROTOCOL_VERSION_14: 4,960 credits less, see the protocol version 13 twin. + // PROTOCOL_VERSION_17: 1,480 credits more, see the protocol version 14 twin + 2191720, + 99909265580, + ) + .await; + } + + /// PROTOCOL_VERSION_14: 1,480 credits less processing than at the latest version. v17 + /// adds the `ContractCredits` root tree (key 100) as the left child of `Misc` (104), so the + /// total system credits write this transition makes under `Misc` hashes one more child. + /// Pinned so v14 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_identity_create_asset_lock_reuse_after_issue_protocol_version_14() { + run_test_identity_create_asset_lock_reuse_after_issue_at_protocol_version( + 14, 2190240, 99909267060, ) @@ -2069,7 +2094,22 @@ mod tests { async fn test_identity_create_asset_lock_replay_attack_latest_protocol_version() { run_test_identity_create_asset_lock_replay_attack_at_protocol_version( PlatformVersion::latest().protocol_version, - // PROTOCOL_VERSION_14: 4,960 credits less, see the protocol version 13 twin + // PROTOCOL_VERSION_14: 4,960 credits less, see the protocol version 13 twin. + // PROTOCOL_VERSION_17: 1,480 credits more, see the protocol version 14 twin + 2191720, + 99909265580, + ) + .await; + } + + /// PROTOCOL_VERSION_14: 1,480 credits less processing than at the latest version. v17 + /// adds the `ContractCredits` root tree (key 100) as the left child of `Misc` (104), so the + /// total system credits write this transition makes under `Misc` hashes one more child. + /// Pinned so v14 chain history stays bit-for-bit reproducible. + #[tokio::test] + async fn test_identity_create_asset_lock_replay_attack_protocol_version_14() { + run_test_identity_create_asset_lock_replay_attack_at_protocol_version( + 14, 2190240, 99909267060, ) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_top_up/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_top_up/mod.rs index d6a54ae5397..540219a7aba 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_top_up/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_top_up/mod.rs @@ -259,12 +259,22 @@ mod tests { fn test_identity_top_up_validation_latest_version() { run_test_identity_top_up_validation_at_protocol_version( PlatformVersion::latest().protocol_version, - // PROTOCOL_VERSION_14: 4,960 credits less, see the protocol version 13 twin - 583880, - 149993611120, + // PROTOCOL_VERSION_14: 4,960 credits less, see the protocol version 13 twin. + // PROTOCOL_VERSION_17: 1,480 credits more, see the protocol version 14 twin + 585360, + 149993609640, ); } + /// PROTOCOL_VERSION_14: 1,480 credits less processing than at the latest version. v17 + /// adds the `ContractCredits` root tree (key 100) as the left child of `Misc` (104), so the + /// total system credits write this transition makes under `Misc` hashes one more child. + /// Pinned so v14 chain history stays bit-for-bit reproducible. + #[test] + fn test_identity_top_up_validation_protocol_version_14() { + run_test_identity_top_up_validation_at_protocol_version(14, 583880, 149993611120); + } + /// PROTOCOL_VERSION_13: 4,960 credits more processing than at the latest version. v14 /// adds the documents expirations tree under `Misc` (key `E`), beside the total system /// credits item this transition rewrites, and the extra key reshapes the `Misc` Merk diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/identity_and_document_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/identity_and_document_tests.rs index 9a8fb2d8fd4..cfcac0f06d5 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/test_cases/identity_and_document_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/identity_and_document_tests.rs @@ -122,6 +122,11 @@ mod tests { // This will cause the costs of insertion of a spent asset lock transition, since group actions now exist we will see a slight difference in processing costs // This is because WithdrawalTransactions will have a right element in the tree. + // + // From protocol version 17 the ContractCredits root tree (100) is the left child of + // Misc (104), so the system credits write under Misc hashes one more child and the + // identity keeps 1_480 credits less than at protocol version 14 (see the assertion + // below; the version 13 test pins the fee before the documents expirations tree). let platform_version = PlatformVersion::latest(); let strategy = NetworkStrategy { @@ -191,7 +196,11 @@ mod tests { // version 13. The documents expirations tree joins `Misc` (key `E`) beside the total // system credits item an identity created from an asset lock rewrites, and the extra // key reshapes the `Misc` Merk that write rehashes. - assert_eq!(balance, 99863968860) + // + // PROTOCOL_VERSION_17: the ContractCredits root tree (100) is the left child of + // Misc (104), so the system credits write under Misc hashes one more child and the + // identity keeps 1_480 credits less than at protocol version 14 (99863968860). + assert_eq!(balance, 99863967380) } #[tokio::test] From 9e0238e75dc1adda0014cf77987e12c18d02f6be Mon Sep 17 00:00:00 2001 From: DCG-Claude Date: Sat, 12 Sep 2026 14:53:55 -0500 Subject: [PATCH 06/11] docs: explain the placeholder protocol versions on the 5.0 branch Co-Authored-By: Claude Fable 5.1 --- book/src/versioning/platform-version.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/book/src/versioning/platform-version.md b/book/src/versioning/platform-version.md index 32e7db207f5..6364c4b61e8 100644 --- a/book/src/versioning/platform-version.md +++ b/book/src/versioning/platform-version.md @@ -108,6 +108,17 @@ record, and the next consensus change creates `v15.rs`. There is never a `v14.rs` that means one thing on a node built last month and another on a node built today. +Because the array is indexed by number, a version cannot be registered without +every number below it. The 5.0 development branch therefore carries protocol +version 17 (its own) together with 15 and 16, which the allocation register +reserves for the 4.3 and 4.4 releases. Until those branches merge their real +`v15.rs` and `v16.rs` forward, the two files are placeholders written as +struct updates over their predecessor +(`PlatformVersion { protocol_version: PROTOCOL_VERSION_15, ..PLATFORM_V14 }`). +A forward merge that brings the real file is resolved by taking the incoming +file; because 16 and 17 are struct updates too, every table the incoming +version changes flows into them without a second edit. + ## What a Version Snapshot Looks Like Here is the very first version, `PLATFORM_V1`, slightly abbreviated: From d5ea5ce6cc88ef394b789ca6395b3aca0893cafa Mon Sep 17 00:00:00 2001 From: DCG-Claude Date: Sat, 12 Sep 2026 15:29:46 -0500 Subject: [PATCH 07/11] chore: address review notes on the contract credits root part Move the activation test's imports to the test module, import TransactionArg there, and state in the versioning chapter that a field a later version overrides explicitly (protocol 17 names its own drive table) must be reconciled by hand on a forward merge. Co-Authored-By: Claude Fable 5.1 --- book/src/versioning/platform-version.md | 7 ++-- .../v0/mod.rs | 32 +++++++++---------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/book/src/versioning/platform-version.md b/book/src/versioning/platform-version.md index 6364c4b61e8..c0f913f4dd2 100644 --- a/book/src/versioning/platform-version.md +++ b/book/src/versioning/platform-version.md @@ -116,8 +116,11 @@ reserves for the 4.3 and 4.4 releases. Until those branches merge their real struct updates over their predecessor (`PlatformVersion { protocol_version: PROTOCOL_VERSION_15, ..PLATFORM_V14 }`). A forward merge that brings the real file is resolved by taking the incoming -file; because 16 and 17 are struct updates too, every table the incoming -version changes flows into them without a second edit. +file. Because 16 and 17 are struct updates too, every field the incoming +version changes flows into them automatically, except the fields a later +version overrides explicitly: `PLATFORM_V17` names its own `drive` table, so a +Drive change arriving with the real 15 or 16 must be reconciled into that table +by hand in the same merge. ## What a Version Snapshot Looks Like diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index c8fa7a44aef..cd08fa0cf77 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -886,6 +886,14 @@ mod tests { use crate::test::helpers::setup::TestPlatformBuilder; use dpp::block::block_info::BlockInfo; use dpp::block::epoch::Epoch; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::data_contract::document_type::random_document::{ + CreateRandomDocument, DocumentFieldFillSize, DocumentFieldFillType, + }; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::v0::IdentityV0; + use dpp::identity::{Identity, IdentityPublicKey}; + use dpp::platform_value::Bytes32; use dpp::version::PlatformVersion; use drive::drive::credit_pools::epochs::epochs_root_tree_key_constants::KEY_LIFETIME_STORAGE_FEE_POOLS; use drive::drive::credit_pools::pools_path; @@ -894,7 +902,13 @@ mod tests { shielded_credit_pool_path, MAIN_SHIELDED_CREDIT_POOL_KEY_U8, SHIELDED_ANCHORS_IN_POOL_KEY, SHIELDED_NOTES_KEY, SHIELDED_NULLIFIERS_KEY, }; + use drive::grovedb::TransactionArg; use drive::util::grove_operations::DirectQueryType; + use drive::util::object_size_info::DocumentInfo::DocumentRefInfo; + use drive::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; + use rand::rngs::StdRng; + use rand::SeedableRng; + use std::collections::BTreeMap; /// Recursively compares the GroveDB subtree rooted at `root_path` between /// two platforms and returns a list of human-readable differences (empty ⇒ @@ -924,7 +938,7 @@ mod tests { fn read_level( platform: &crate::platform_types::platform::Platform, - txn: drive::grovedb::TransactionArg, + txn: TransactionArg, path: &[Vec], ) -> std::collections::BTreeMap, Element> { let mut q = Query::new(); @@ -3723,20 +3737,6 @@ mod tests { /// dropped and a later round runs it again. #[test] fn should_activate_the_contract_credits_root_through_the_protocol_change_hook() { - use dpp::data_contract::accessors::v0::DataContractV0Getters; - use dpp::data_contract::document_type::random_document::{ - CreateRandomDocument, DocumentFieldFillSize, DocumentFieldFillType, - }; - use dpp::identity::accessors::IdentityGettersV0; - use dpp::identity::v0::IdentityV0; - use dpp::identity::{Identity, IdentityPublicKey}; - use dpp::platform_value::Bytes32; - use drive::util::object_size_info::DocumentInfo::DocumentRefInfo; - use drive::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; - use rand::rngs::StdRng; - use rand::SeedableRng; - use std::collections::BTreeMap; - let platform_version_16 = PlatformVersion::get(16).expect("expected v16"); let platform_version_17 = PlatformVersion::get(17).expect("expected v17"); let grove_version = &platform_version_17.drive.grove_version; @@ -3821,7 +3821,7 @@ mod tests { ) .expect("expected to insert the document"); - let root_absent = |transaction: drive::grovedb::TransactionArg| { + let root_absent = |transaction: TransactionArg| { platform .drive .grove From a08302269fe48bda49abbe41c7cd03a6729684dc Mon Sep 17 00:00:00 2001 From: DCG-Claude Date: Tue, 29 Sep 2026 20:09:29 -0500 Subject: [PATCH 08/11] refactor(drive): borrow the contract id in contract_credits_path_vec Match the sibling contract_credits_path and the other path helpers, which all borrow the id. Co-Authored-By: Claude Opus 5.5 --- packages/rs-drive/src/drive/contract/balances/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rs-drive/src/drive/contract/balances/mod.rs b/packages/rs-drive/src/drive/contract/balances/mod.rs index 6a507cefc75..7f8c84e913e 100644 --- a/packages/rs-drive/src/drive/contract/balances/mod.rs +++ b/packages/rs-drive/src/drive/contract/balances/mod.rs @@ -36,7 +36,7 @@ pub fn contract_credits_path(contract_id: &[u8; 32]) -> [&[u8]; 2] { } /// The path to the credit bucket sum tree of one contract as a vec. -pub fn contract_credits_path_vec(contract_id: [u8; 32]) -> Vec> { +pub fn contract_credits_path_vec(contract_id: &[u8; 32]) -> Vec> { vec![vec![RootTree::ContractCredits as u8], contract_id.to_vec()] } @@ -62,7 +62,7 @@ mod tests { assert_eq!(path[0], &[RootTree::ContractCredits as u8][..]); assert_eq!(path[1], &contract_id[..]); assert_eq!( - contract_credits_path_vec(contract_id), + contract_credits_path_vec(&contract_id), path.iter().map(|part| part.to_vec()).collect::>() ); } From 84060f446f9c15091e7d2dcf35edec16db5e4c3a Mon Sep 17 00:00:00 2001 From: DCG-Claude Date: Tue, 29 Sep 2026 20:09:29 -0500 Subject: [PATCH 09/11] test(drive-abci): compare root hashes of the genesis and upgrade paths at v17 Genesis inserts the contract credits root key before SavedBlockTransactions and ContractGroups while the upgrade inserts it last, and the root Merk shape depends on insertion order. After aligning the genesis epoch's recorded protocol version, the whole-database root hashes of both paths must match. Correct the root tree comment that claimed both paths share one insertion order. Co-Authored-By: Claude Opus 5.5 --- .../v0/mod.rs | 57 ++++++++++++++++++- packages/rs-drive/src/drive/mod.rs | 6 +- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index cd08fa0cf77..d4a0d14768d 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -895,7 +895,9 @@ mod tests { use dpp::identity::{Identity, IdentityPublicKey}; use dpp::platform_value::Bytes32; use dpp::version::PlatformVersion; + use drive::drive::credit_pools::epochs::epoch_key_constants::KEY_PROTOCOL_VERSION; use drive::drive::credit_pools::epochs::epochs_root_tree_key_constants::KEY_LIFETIME_STORAGE_FEE_POOLS; + use drive::drive::credit_pools::epochs::paths::EpochProposers; use drive::drive::credit_pools::pools_path; use drive::drive::document::expiration::paths::DOCUMENTS_EXPIRATIONS_KEY; use drive::drive::shielded::paths::{ @@ -3609,8 +3611,9 @@ mod tests { /// /// Both the root element itself and the (empty) subtree under it are /// compared, so a flag, a tree type or a stray child on either side fails - /// here. The named subtree is compared rather than the whole-DB root hash - /// for the reason given on `collect_subtree_diffs`. + /// here. The whole-DB root hashes are then compared too, after aligning the + /// one field that legitimately differs (see `collect_subtree_diffs`), so a + /// root layer that settles into a different shape fails as well. #[test] fn test_genesis_v17_and_upgrade_to_v17_build_identical_contract_credits_tree() { let platform_version_17 = PlatformVersion::get(17).expect("expected v17"); @@ -3695,6 +3698,56 @@ mod tests { node and an in-place-upgraded v17 node.\n{}", diffs.join("\n"), ); + + // The root Merk's shape depends on insertion order, and the two paths + // insert key 100 at different points: genesis before + // SavedBlockTransactions and ContractGroups, the upgrade after every + // other root key. Once the genesis epoch's recorded protocol version, + // the one field that legitimately differs (see `collect_subtree_diffs`), + // is aligned, the whole-database root hashes must match, which pins the + // root layer's shape as well as every subtree. + let genesis_epoch = Epoch::new(0).expect("expected epoch 0"); + let epoch_protocol_version_a = platform_a + .drive + .grove + .get( + &genesis_epoch.get_path(), + KEY_PROTOCOL_VERSION, + None, + grove_version, + ) + .unwrap() + .expect("genesis: the genesis epoch records its protocol version"); + platform_b + .drive + .grove + .insert( + &genesis_epoch.get_path(), + KEY_PROTOCOL_VERSION, + epoch_protocol_version_a, + None, + Some(&txn_b), + grove_version, + ) + .unwrap() + .expect("upgrade: align the genesis epoch's recorded protocol version"); + let root_hash_a = platform_a + .drive + .grove + .root_hash(None, grove_version) + .unwrap() + .expect("genesis: root hash"); + let root_hash_b = platform_b + .drive + .grove + .root_hash(Some(&txn_b), grove_version) + .unwrap() + .expect("upgrade: root hash"); + assert_eq!( + root_hash_a, root_hash_b, + "the root hash of a fresh genesis-v17 node differs from an in-place-upgraded v17 \ + node once the genesis epoch's recorded protocol version is aligned" + ); } /// The v17 calculator reads the new root tree as a sixth term, so the diff --git a/packages/rs-drive/src/drive/mod.rs b/packages/rs-drive/src/drive/mod.rs index 587ea89d513..cd1d8e10dd5 100644 --- a/packages/rs-drive/src/drive/mod.rs +++ b/packages/rs-drive/src/drive/mod.rs @@ -206,8 +206,10 @@ pub struct Drive { // code, and `packages/rs-drive/grovedb-structure.json` records this shape from a real GroveDB // (`layer_shapes.root`), so a test fails when the two drift apart. Keys added after genesis of // an earlier protocol version (ShieldedBalances 52, ContractGroups 124, ContractCredits 100) -// are placed by AVL rebalancing at insertion time, so their exact depth depends on the -// insertion order that the initialization and upgrade paths share. +// are placed by AVL rebalancing at insertion time, so their exact depth depends on insertion +// order, and genesis and the protocol upgrade insert them at different points. For +// ContractCredits the v17 upgrade test compares the root hash of both paths, so a root shape +// that differs between them fails there. /// Keys for the root tree. #[cfg(any(feature = "server", feature = "verify"))] From 62618c7e7c45db0554893af56f0780fcb2f5f204 Mon Sep 17 00:00:00 2001 From: DCG-Claude Date: Tue, 29 Sep 2026 20:09:29 -0500 Subject: [PATCH 10/11] docs: bring the version array example up to protocol version 17 Co-Authored-By: Claude Opus 5.5 --- book/src/versioning/platform-version.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/book/src/versioning/platform-version.md b/book/src/versioning/platform-version.md index c0f913f4dd2..00be3de42a6 100644 --- a/book/src/versioning/platform-version.md +++ b/book/src/versioning/platform-version.md @@ -59,19 +59,19 @@ function version so that execution is deterministic. ## The Version Array Each protocol version gets its own constant, defined in a separate file. At -the time of writing, the platform has fourteen versions: +the time of writing, the 5.0 development branch has seventeen versions: ```rust // packages/rs-platform-version/src/version/mod.rs pub type ProtocolVersion = u32; -pub const LATEST_VERSION: ProtocolVersion = PROTOCOL_VERSION_14; +pub const LATEST_VERSION: ProtocolVersion = PROTOCOL_VERSION_17; pub const INITIAL_PROTOCOL_VERSION: ProtocolVersion = 1; pub const ALL_VERSIONS: RangeInclusive = 1..=LATEST_VERSION; ``` -These fourteen snapshots are collected into a single static array in +These seventeen snapshots are collected into a single static array in `protocol_version.rs`: ```rust @@ -90,14 +90,17 @@ pub const PLATFORM_VERSIONS: &[PlatformVersion] = &[ PLATFORM_V12, PLATFORM_V13, PLATFORM_V14, + PLATFORM_V15, + PLATFORM_V16, + PLATFORM_V17, ]; -pub const LATEST_PLATFORM_VERSION: &PlatformVersion = &PLATFORM_V14; +pub const LATEST_PLATFORM_VERSION: &PlatformVersion = &PLATFORM_V17; pub const DESIRED_PLATFORM_VERSION: &PlatformVersion = LATEST_PLATFORM_VERSION; ``` The array is indexed by protocol version number minus one (since versions are -1-indexed). `PLATFORM_V1` sits at index 0, `PLATFORM_V14` at index 13. This +1-indexed). `PLATFORM_V1` sits at index 0, `PLATFORM_V17` at index 16. This simple layout is what makes the `get` function so fast. One file, one protocol version. `v14.rs` was created when the first consensus @@ -167,7 +170,8 @@ pub const PLATFORM_V1: PlatformVersion = PlatformVersion { }; ``` -Now compare with `PLATFORM_V14`, the latest at the time of writing. By +Now compare with `PLATFORM_V14`, the latest released version at the time of +writing. By convention, each sub-constant slot that was bumped carries a trailing `// changed:` comment saying what changed. The `protocol_version` field is the snapshot's identity and is never annotated. One bumped slot in this snapshot, From baaef2e16fa61cd3734c0ea7ec09c9f11c284b7b Mon Sep 17 00:00:00 2001 From: DCG-Claude Date: Tue, 29 Sep 2026 21:44:38 -0500 Subject: [PATCH 11/11] refactor(platform): write drive version 10 as a struct update over version 9 Only the genesis structure and the total credits calculator differ from DRIVE_VERSION_V9, so name those two slots and inherit the rest, as the feature versions chapter asks for new table constants. Note on PLATFORM_V17 that its explicit drive table does not follow a real v15 or v16 on a forward merge. Co-Authored-By: Claude Opus 5.5 --- .../src/version/drive_versions/v10.rs | 136 +----------------- .../rs-platform-version/src/version/v17.rs | 4 + 2 files changed, 10 insertions(+), 130 deletions(-) diff --git a/packages/rs-platform-version/src/version/drive_versions/v10.rs b/packages/rs-platform-version/src/version/drive_versions/v10.rs index f62a9887504..3fdbd8ac06d 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v10.rs @@ -1,27 +1,8 @@ -use crate::version::drive_versions::drive_address_funds_method_versions::v2::DRIVE_ADDRESS_FUNDS_METHOD_VERSIONS_V2; -use crate::version::drive_versions::drive_contract_group_method_versions::v1::DRIVE_CONTRACT_GROUP_METHOD_VERSIONS_V1; -use crate::version::drive_versions::drive_contract_method_versions::v4::DRIVE_CONTRACT_METHOD_VERSIONS_V4; -use crate::version::drive_versions::drive_credit_pool_method_versions::v1::CREDIT_POOL_METHOD_VERSIONS_V1; -use crate::version::drive_versions::drive_document_method_versions::v4::DRIVE_DOCUMENT_METHOD_VERSIONS_V4; -use crate::version::drive_versions::drive_group_method_versions::v1::DRIVE_GROUP_METHOD_VERSIONS_V1; -use crate::version::drive_versions::drive_group_method_versions::DriveShieldedMethodVersions; -use crate::version::drive_versions::drive_grove_method_versions::v1::DRIVE_GROVE_METHOD_VERSIONS_V1; -use crate::version::drive_versions::drive_identity_method_versions::v2::DRIVE_IDENTITY_METHOD_VERSIONS_V2; -use crate::version::drive_versions::drive_state_transition_method_versions::v4::DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4; -use crate::version::drive_versions::drive_structure_version::v1::DRIVE_STRUCTURE_V1; -use crate::version::drive_versions::drive_token_method_versions::v2::DRIVE_TOKEN_METHOD_VERSIONS_V2; -use crate::version::drive_versions::drive_verify_method_versions::v3::DRIVE_VERIFY_METHOD_VERSIONS_V3; -use crate::version::drive_versions::drive_vote_method_versions::v3::DRIVE_VOTE_METHOD_VERSIONS_V3; +use crate::version::drive_versions::v9::DRIVE_VERSION_V9; use crate::version::drive_versions::{ - DriveAssetLockMethodVersions, DriveBalancesMethodVersions, DriveBatchOperationsMethodVersion, - DriveEstimatedCostsMethodVersions, DriveFeesMethodVersions, DriveFetchMethodVersions, - DriveInitializationMethodVersions, DriveMethodVersions, DriveOperationsMethodVersion, - DrivePlatformStateMethodVersions, DrivePlatformSystemMethodVersions, - DrivePrefundedSpecializedMethodVersions, DriveProtocolUpgradeVersions, - DriveProveMethodVersions, DriveSavedBlockTransactionsMethodVersions, - DriveSystemEstimationCostsMethodVersions, DriveVersion, + DriveBalancesMethodVersions, DriveInitializationMethodVersions, DriveMethodVersions, + DriveVersion, }; -use grovedb_version::version::v4::GROVE_V4; /// Drive version 10. /// Introduced in protocol v17, the 5.0 protocol version, for the contract @@ -40,120 +21,15 @@ use grovedb_version::version::v4::GROVE_V4; /// /// Everything else matches `DRIVE_VERSION_V9`. pub const DRIVE_VERSION_V10: DriveVersion = DriveVersion { - structure: DRIVE_STRUCTURE_V1, methods: DriveMethodVersions { initialization: DriveInitializationMethodVersions { create_initial_state_structure: 5, // changed in v10: adds the contract credits root sum tree (v4 added the ContractGroups root tree) }, - credit_pools: CREDIT_POOL_METHOD_VERSIONS_V1, - protocol_upgrade: DriveProtocolUpgradeVersions { - clear_version_information: 0, - fetch_versions_with_counter: 0, - fetch_proved_versions_with_counter: 0, - fetch_validator_version_votes: 0, - fetch_proved_validator_version_votes: 0, - remove_validators_proposed_app_versions: 0, - update_validator_proposed_app_version: 0, - }, - prove: DriveProveMethodVersions { - prove_elements: 0, - prove_multiple_state_transition_results: 0, - prove_state_transition: 1, // changed in v9: a document batch proof carries the owner's balance (verify v1) - }, balances: DriveBalancesMethodVersions { - add_to_system_credits: 0, - add_to_system_credits_operations: 0, - remove_from_system_credits: 0, - remove_from_system_credits_operations: 0, calculate_total_credits_balance: 3, // changed in v10: ContractCredits root tree adds a sixth term to the equation + ..DRIVE_VERSION_V9.methods.balances }, - document: DRIVE_DOCUMENT_METHOD_VERSIONS_V4, // changed in v9: v2 index walkers + v1 update walker (shared-prefix aggregate indexes become insertable) and the detect_ranked_mode slot - vote: DRIVE_VOTE_METHOD_VERSIONS_V3, // changed in v9: the end-date cleanup of ended contested vote polls removes an end date only once none of its polls remain - contract: DRIVE_CONTRACT_METHOD_VERSIONS_V4, // changed in v9: add_contract_to_storage v1 writes the contract version item beside the contract; update_contract v2 creates the distribution storage and mints the base supply of tokens added by an update - fees: DriveFeesMethodVersions { calculate_fee: 0 }, - estimated_costs: DriveEstimatedCostsMethodVersions { - add_estimation_costs_for_levels_up_to_contract: 0, - add_estimation_costs_for_levels_up_to_contract_document_type_excluded: 0, - add_estimation_costs_for_contested_document_tree_levels_up_to_contract: 0, - add_estimation_costs_for_contested_document_tree_levels_up_to_contract_document_type_excluded: 0, - }, - asset_lock: DriveAssetLockMethodVersions { - add_asset_lock_outpoint: 0, - add_estimation_costs_for_adding_asset_lock: 0, - fetch_asset_lock_outpoint_info: 0, - }, - verify: DRIVE_VERIFY_METHOD_VERSIONS_V3, // changed in v9: a document batch proof carries the owner's balance (verify state transition v1) - identity: DRIVE_IDENTITY_METHOD_VERSIONS_V2, // changed in v9: v1 withdrawal-by-transaction-index query builder (structural, identical lowering) - token: DRIVE_TOKEN_METHOD_VERSIONS_V2, // changed in v9: add_pre_programmed_distributions v1 queues the release-time tree shared by a contract's tokens once; evonode_participation_rewards v1 pays an evonode's claim only through the epochs it read - platform_system: DrivePlatformSystemMethodVersions { - estimation_costs: DriveSystemEstimationCostsMethodVersions { - for_total_system_credits_update: 0, - }, - }, - operations: DriveOperationsMethodVersion { - rollback_transaction: 0, - drop_cache: 0, - commit_transaction: 0, - apply_partial_batch_low_level_drive_operations: 0, - apply_partial_batch_grovedb_operations: 0, - apply_batch_low_level_drive_operations: 1, // changed: coalesces bound current-key alias writes per batch - apply_batch_grovedb_operations: 0, - }, - state_transitions: DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4, // changed: document_from_action generation 1 stamps built documents with the contract version (create assigns, replace re-assigns; paired with document serialization format 3) - batch_operations: DriveBatchOperationsMethodVersion { - convert_drive_operations_to_grove_operations: 0, - apply_drive_operations: 1, // changed: a batch carrying a storage refund forfeiture (a moderator's document deletion) refunds nobody; every write of one identity balance, fee pot or prefunded specialized balance in a batch is merged into one, a batch writing one token balance or supply twice is refused, and repaid identity debt goes to the processing fee pool - }, - platform_state: DrivePlatformStateMethodVersions { - fetch_platform_state_bytes: 0, - store_platform_state_bytes: 0, - fetch_platform_state_recent_bytes: 0, - store_platform_state_recent_bytes: 0, - fetch_platform_state_entries_bytes: 0, - store_platform_state_entry_bytes: 0, - delete_platform_state_entry: 0, - }, - fetch: DriveFetchMethodVersions { fetch_elements: 0 }, - prefunded_specialized_balances: DrivePrefundedSpecializedMethodVersions { - fetch_single: 0, - prove_single: 0, - add_prefunded_specialized_balance: 0, - add_prefunded_specialized_balance_operations: 1, - deduct_from_prefunded_specialized_balance: 1, - deduct_from_prefunded_specialized_balance_operations: 0, - estimated_cost_for_prefunded_specialized_balance_update: 1, // changed: the prefunded balances layer holds three trees, the voting balances and the two contract fee pot trees - empty_prefunded_specialized_balance: 0, - }, - group: DRIVE_GROUP_METHOD_VERSIONS_V1, - contract_group: DRIVE_CONTRACT_GROUP_METHOD_VERSIONS_V1, - address_funds: DRIVE_ADDRESS_FUNDS_METHOD_VERSIONS_V2, - shielded: DriveShieldedMethodVersions { - insert_note: 0, - insert_nullifiers: 0, - update_total_balance: 0, - record_anchor_if_changed: 0, - prune_anchors: 0, - has_anchor: 0, - has_nullifier: 0, - read_total_balance: 0, - notes_count: 0, - }, - saved_block_transactions: DriveSavedBlockTransactionsMethodVersions { - store_address_balances: 0, - fetch_address_balances: 0, - prove_compacted_address_balance_changes: 1, - compact_address_balances: 0, - cleanup_expired_address_balances: 0, - max_blocks_before_compaction: 64, - max_addresses_before_compaction: 2048, - }, + ..DRIVE_VERSION_V9.methods }, - grove_methods: DRIVE_GROVE_METHOD_VERSIONS_V1, - // changed in v9: GROVE_V4 activates the indexed-tree batch cleanup - // gates (overwrite inspection + delete-tree actual-type cleanup). - // Indexed trees only exist from protocol v14, so activating the - // stricter cleanup with them costs older versions nothing; staying - // on V3 would let a batch overwrite of a ranked index orphan its - // per-axis secondary storage. - grove_version: GROVE_V4, + ..DRIVE_VERSION_V9 }; diff --git a/packages/rs-platform-version/src/version/v17.rs b/packages/rs-platform-version/src/version/v17.rs index 62c3ef088e8..adc50b8e511 100644 --- a/packages/rs-platform-version/src/version/v17.rs +++ b/packages/rs-platform-version/src/version/v17.rs @@ -20,6 +20,10 @@ pub const PROTOCOL_VERSION_17: ProtocolVersion = 17; /// The root key value is provisional (the allocation register leaves new root values /// unallocated) and is revised, if at all, before any network is asked to propose this /// version. +/// +/// Unlike the fields it inherits, `drive` does not follow v16: when the real v15 or v16 arrives +/// with a Drive change in a forward merge, that change must be reconciled into +/// `DRIVE_VERSION_V10` by hand in the same merge. pub const PLATFORM_V17: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_17, drive: DRIVE_VERSION_V10, // changed: contract credits root sum tree at genesis, on upgrade and in credit conservation