Skip to main content

platform_version/version/
protocol_version.rs

1use crate::error::PlatformVersionError;
2use crate::version::dpp_versions::DPPVersion;
3use crate::version::drive_abci_versions::DriveAbciVersion;
4use crate::version::drive_versions::DriveVersion;
5use crate::version::fee::FeeVersion;
6#[cfg(feature = "mock-versions")]
7use crate::version::mocks::v2_test::TEST_PLATFORM_V2;
8#[cfg(feature = "mock-versions")]
9use crate::version::mocks::v3_test::TEST_PLATFORM_V3;
10#[cfg(feature = "mock-versions")]
11use crate::version::mocks::TEST_PROTOCOL_VERSION_SHIFT_BYTES;
12use crate::version::system_data_contract_versions::SystemDataContractVersions;
13#[cfg(feature = "mock-versions")]
14use std::sync::OnceLock;
15
16use crate::version::consensus_versions::ConsensusVersions;
17use crate::version::system_limits::SystemLimits;
18
19use crate::version::v1::PLATFORM_V1;
20use crate::version::v10::PLATFORM_V10;
21use crate::version::v11::PLATFORM_V11;
22use crate::version::v12::PLATFORM_V12;
23use crate::version::v13::PLATFORM_V13;
24use crate::version::v14::PLATFORM_V14;
25use crate::version::v2::PLATFORM_V2;
26use crate::version::v3::PLATFORM_V3;
27use crate::version::v4::PLATFORM_V4;
28use crate::version::v5::PLATFORM_V5;
29use crate::version::v6::PLATFORM_V6;
30use crate::version::v7::PLATFORM_V7;
31use crate::version::v8::PLATFORM_V8;
32use crate::version::v9::PLATFORM_V9;
33
34use crate::version::ProtocolVersion;
35pub use versioned_feature_core::*;
36
37#[derive(Clone, Debug)]
38pub struct PlatformVersion {
39    pub protocol_version: ProtocolVersion,
40    pub dpp: DPPVersion,
41    pub drive: DriveVersion,
42    pub drive_abci: DriveAbciVersion,
43    pub consensus: ConsensusVersions,
44    pub fee_version: FeeVersion,
45    pub system_data_contracts: SystemDataContractVersions,
46    pub system_limits: SystemLimits,
47}
48
49pub const PLATFORM_VERSIONS: &[PlatformVersion] = &[
50    PLATFORM_V1,
51    PLATFORM_V2,
52    PLATFORM_V3,
53    PLATFORM_V4,
54    PLATFORM_V5,
55    PLATFORM_V6,
56    PLATFORM_V7,
57    PLATFORM_V8,
58    PLATFORM_V9,
59    PLATFORM_V10,
60    PLATFORM_V11,
61    PLATFORM_V12,
62    PLATFORM_V13,
63    PLATFORM_V14,
64];
65
66#[cfg(feature = "mock-versions")]
67// We use OnceLock to be able to modify the version mocks
68pub static PLATFORM_TEST_VERSIONS: OnceLock<Vec<PlatformVersion>> = OnceLock::new();
69#[cfg(feature = "mock-versions")]
70const DEFAULT_PLATFORM_TEST_VERSIONS: &[PlatformVersion] = &[TEST_PLATFORM_V2, TEST_PLATFORM_V3];
71
72pub const LATEST_PLATFORM_VERSION: &PlatformVersion = &PLATFORM_V14;
73
74pub const DESIRED_PLATFORM_VERSION: &PlatformVersion = LATEST_PLATFORM_VERSION;
75
76impl PlatformVersion {
77    pub fn get<'a>(version: ProtocolVersion) -> Result<&'a Self, PlatformVersionError> {
78        if version > 0 {
79            #[cfg(feature = "mock-versions")]
80            {
81                if version >> TEST_PROTOCOL_VERSION_SHIFT_BYTES > 0 {
82                    let test_version = version - (1 << TEST_PROTOCOL_VERSION_SHIFT_BYTES);
83
84                    // Init default set of test versions
85                    let versions = PLATFORM_TEST_VERSIONS
86                        .get_or_init(|| vec![TEST_PLATFORM_V2, TEST_PLATFORM_V3]);
87
88                    return versions.get(test_version as usize - 2).ok_or(
89                        PlatformVersionError::UnknownVersionError(format!(
90                            "no test platform version {test_version}"
91                        )),
92                    );
93                }
94            }
95            PLATFORM_VERSIONS.get(version as usize - 1).ok_or_else(|| {
96                PlatformVersionError::UnknownVersionError(format!("no platform version {version}"))
97            })
98        } else {
99            Err(PlatformVersionError::UnknownVersionError(format!(
100                "no platform version {version}"
101            )))
102        }
103    }
104
105    pub fn get_optional<'a>(version: ProtocolVersion) -> Option<&'a Self> {
106        if version > 0 {
107            #[cfg(feature = "mock-versions")]
108            {
109                if version >> TEST_PROTOCOL_VERSION_SHIFT_BYTES > 0 {
110                    let test_version = version - (1 << TEST_PROTOCOL_VERSION_SHIFT_BYTES);
111
112                    // Init default set of test versions
113                    let versions = PLATFORM_TEST_VERSIONS
114                        .get_or_init(|| vec![TEST_PLATFORM_V2, TEST_PLATFORM_V3]);
115
116                    return versions.get(test_version as usize - 2);
117                }
118            }
119            PLATFORM_VERSIONS.get(version as usize - 1)
120        } else {
121            None
122        }
123    }
124
125    pub fn get_version_or_latest<'a>(
126        version: Option<ProtocolVersion>,
127    ) -> Result<&'a Self, PlatformVersionError> {
128        if let Some(version) = version {
129            if version > 0 {
130                #[cfg(feature = "mock-versions")]
131                {
132                    if version >> TEST_PROTOCOL_VERSION_SHIFT_BYTES > 0 {
133                        let test_version = version - (1 << TEST_PROTOCOL_VERSION_SHIFT_BYTES);
134
135                        // Init default set of test versions
136                        let versions = PLATFORM_TEST_VERSIONS
137                            .get_or_init(|| Vec::from(DEFAULT_PLATFORM_TEST_VERSIONS));
138
139                        return versions.get(test_version as usize - 2).ok_or(
140                            PlatformVersionError::UnknownVersionError(format!(
141                                "no test platform version {test_version}"
142                            )),
143                        );
144                    }
145                }
146                PLATFORM_VERSIONS.get(version as usize - 1).ok_or(
147                    PlatformVersionError::UnknownVersionError(format!(
148                        "no platform version {version}"
149                    )),
150                )
151            } else {
152                Err(PlatformVersionError::UnknownVersionError(format!(
153                    "no platform version {version}"
154                )))
155            }
156        } else {
157            Ok(Self::latest())
158        }
159    }
160
161    pub fn first<'a>() -> &'a Self {
162        PLATFORM_VERSIONS
163            .first()
164            .expect("expected to have a platform version")
165    }
166
167    pub fn latest<'a>() -> &'a Self {
168        PLATFORM_VERSIONS
169            .last()
170            .expect("expected to have a platform version")
171    }
172
173    pub fn desired<'a>() -> &'a Self {
174        DESIRED_PLATFORM_VERSION
175    }
176
177    #[cfg(feature = "mock-versions")]
178    /// Set mock versions for testing
179    pub fn replace_test_versions(versions: Vec<PlatformVersion>) {
180        PLATFORM_TEST_VERSIONS
181            .set(versions)
182            .expect("failed to set test versions")
183    }
184}
185
186#[cfg(test)]
187mod shielded_pool_gating_tests {
188    use super::PlatformVersion;
189
190    // The shielded credit pool lives in GroveDB at
191    // `[RootTree::ShieldedBalances (52), MAIN_SHIELDED_CREDIT_POOL_KEY ("M" = 0x4d)]`.
192    // That subtree is created ONLY at the protocol v12 upgrade migration
193    // (`transition_to_version_12`) or on a v12 genesis (`create_initial_state_structure_v3`).
194    // It does not exist on a protocol-v11 state (v11 uses init structure v2).
195    //
196    // The block-end / recent-block-storage shielded methods below read that
197    // subtree unconditionally every block. If they are active before v12 they
198    // open `[52, "M"]` on a state where it was never created, producing the
199    // consensus-breaking GroveDB error
200    //   "path parent layer not found: could not get key 4d for parent [52]".
201    //
202    // These methods were inactive (`None`) through protocol v11 in the released
203    // 3.0.1 line, then accidentally turned on for v11 when the four fields were
204    // added as `Some(0)` to the SHARED `DRIVE_ABCI_METHOD_VERSIONS_V7` struct in
205    // the Medusa shielded-pool PR (#3177) — V7 is referenced by both v11.rs and
206    // v12.rs. These tests pin the invariant: shielded reads are gated to v12+.
207
208    #[test]
209    fn shielded_block_processing_methods_inactive_before_v12() {
210        let v11 = PlatformVersion::get(11).expect("protocol version 11 must exist");
211        let block_end = &v11.drive_abci.methods.block_end;
212
213        assert_eq!(
214            block_end.record_shielded_pool_anchor, None,
215            "v11 must NOT record shielded pool anchors: the [52, \"M\"] subtree does not exist before v12"
216        );
217        assert_eq!(
218            block_end.prune_shielded_pool_anchors, None,
219            "v11 must NOT prune shielded pool anchors: the [52, \"M\"] subtree does not exist before v12"
220        );
221    }
222
223    #[test]
224    fn shielded_block_processing_methods_active_at_v12() {
225        let v12 = PlatformVersion::get(12).expect("protocol version 12 must exist");
226        let block_end = &v12.drive_abci.methods.block_end;
227
228        // At v12 the shielded pool subtree is created by the upgrade migration,
229        // so the same methods must be active.
230        assert_eq!(block_end.record_shielded_pool_anchor, Some(0));
231        assert_eq!(block_end.prune_shielded_pool_anchors, Some(0));
232    }
233
234    // The v13 recorded-set expansion of the recent per-block address-balance set changes the
235    // committed state root, so it is gated to v13 via TWO method versions that both stay 0 before
236    // v13 and become 1 at v13:
237    //   - `record_added_balance_outputs`: v1 folds shielded-spend transparent credits (Unshield net
238    //     output, ShieldFromAssetLock surplus, identity-create fallback);
239    //   - `process_validation_result`: v1 records paid-invalid / unsuccessful-paid balance effects
240    //     that _v0 drops.
241    // Crucially the OUTER loop (`process_raw_state_transitions`) and the storage method
242    // (`store_address_balances_to_recent_block_storage`) are NOT bumped — their _v0 implementations
243    // did not change — so they stay 0 / Some(0) at BOTH versions.
244    #[test]
245    fn shielded_transparent_credit_recording_gated_to_v13() {
246        let v12 = PlatformVersion::get(12).expect("protocol version 12 must exist");
247        let v13 = PlatformVersion::get(13).expect("protocol version 13 must exist");
248        let stp12 = &v12.drive_abci.methods.state_transition_processing;
249        let stp13 = &v13.drive_abci.methods.state_transition_processing;
250
251        // The recording method version: v0 before v13 (drops shielded-spend), v1 from v13 (records).
252        assert_eq!(
253            stp12.record_added_balance_outputs, 0,
254            "v12 must use record_added_balance_outputs v0 (drops shielded-spend credits)"
255        );
256        assert_eq!(
257            stp13.record_added_balance_outputs, 1,
258            "v13 must use record_added_balance_outputs v1 (records shielded-spend credits)"
259        );
260
261        // The paid-invalid / unsuccessful-paid tracking version: v0 before v13 (drops those balance
262        // effects), v1 from v13 (records them).
263        assert_eq!(
264            stp12.process_validation_result, 0,
265            "v12 must use process_validation_result v0 (drops paid-invalid balance effects)"
266        );
267        assert_eq!(
268            stp13.process_validation_result, 1,
269            "v13 must use process_validation_result v1 (records paid-invalid balance effects)"
270        );
271
272        // The outer loop is UNCHANGED across the gate — its _v0 did not change, so it stays 0.
273        assert_eq!(
274            stp12.process_raw_state_transitions, 0,
275            "v12 outer loop stays process_raw_state_transitions v0"
276        );
277        assert_eq!(
278            stp13.process_raw_state_transitions, 0,
279            "v13 must NOT bump the unchanged outer loop"
280        );
281
282        // The storage method is UNCHANGED across the gate — we must not bump a method whose _v0
283        // implementation did not change. It stays Some(0) at both v12 and v13.
284        assert_eq!(
285            stp12.store_address_balances_to_recent_block_storage,
286            Some(0),
287            "v12 store method is Some(0)"
288        );
289        assert_eq!(
290            stp13.store_address_balances_to_recent_block_storage,
291            Some(0),
292            "v13 must NOT bump the unchanged store method"
293        );
294        // Cleanup mechanics likewise unchanged.
295        assert_eq!(stp13.cleanup_recent_block_storage_address_balances, Some(0));
296    }
297}