Skip to content
4 changes: 2 additions & 2 deletions book/src/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,12 @@ Several smaller crates provide cross-cutting infrastructure:

The versioning backbone. Defines `PlatformVersion`, `ProtocolVersion`, and the
version tables for every consensus-critical method across DPP, Drive, and
Drive-ABCI. Currently tracks 14 protocol versions (v1 through v14).
Drive-ABCI. Currently tracks 15 protocol versions (v1 through v15).

```rust
// From 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_15;
pub const INITIAL_PROTOCOL_VERSION: ProtocolVersion = 1;
```

Expand Down
29 changes: 15 additions & 14 deletions book/src/versioning/platform-version.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 platform has fifteen 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_15;
pub const INITIAL_PROTOCOL_VERSION: ProtocolVersion = 1;
pub const ALL_VERSIONS: RangeInclusive<ProtocolVersion> = 1..=LATEST_VERSION;
```

These fourteen snapshots are collected into a single static array in
These fifteen snapshots are collected into a single static array in
`protocol_version.rs`:

```rust
Expand All @@ -90,22 +90,23 @@ pub const PLATFORM_VERSIONS: &[PlatformVersion] = &[
PLATFORM_V12,
PLATFORM_V13,
PLATFORM_V14,
PLATFORM_V15,
];

pub const LATEST_PLATFORM_VERSION: &PlatformVersion = &PLATFORM_V14;
pub const LATEST_PLATFORM_VERSION: &PlatformVersion = &PLATFORM_V15;
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_V15` at index 14. This
simple layout is what makes the `get` function so fast.

One file, one protocol version. `v14.rs` was created when the first consensus
change after version 13 shipped needed somewhere to live, and every later
change destined for version 14 amends that same file. The day version 14 is
One file, one protocol version. `v15.rs` was created when the first consensus
change after version 14 shipped needed somewhere to live, and every later
change destined for version 15 amends that same file. The day version 15 is
released the file freezes: from then on it is part of the chain's historical
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
record, and the next consensus change creates `v16.rs`. There is never a
`v15.rs` that means one thing on a node built last month and another on a node
built today.

## What a Version Snapshot Looks Like
Expand Down Expand Up @@ -153,7 +154,7 @@ pub const PLATFORM_V1: PlatformVersion = PlatformVersion {
};
```

Now compare with `PLATFORM_V14`, the latest at the time of writing. By
Now compare with `PLATFORM_V14`, a snapshot that bumped many slots at once. 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,
Expand Down Expand Up @@ -278,8 +279,8 @@ impl PlatformVersion {
}
```

This is a simple array lookup. Protocol version 1 maps to index 0, version 14
to index 13. If the version number is out of range, you get a clear error. No
This is a simple array lookup. Protocol version 1 maps to index 0, version 15
to index 14. If the version number is out of range, you get a clear error. No
hash maps, no runtime registration, no dynamic dispatch -- just a static array
of compile-time constants.

Expand Down Expand Up @@ -425,7 +426,7 @@ version 14 means.
every node that was there at the time.
- Never add a new field to `PlatformVersion` without also updating every
`PLATFORM_V*` constant. The compiler will enforce this, but be aware that
the fix is updating fourteen files, not one.
the fix is updating fifteen files, not one.
- Never use `PlatformVersion::latest()` in consensus-critical code paths.
Always use the version from the current platform state, obtained via
`platform_state.current_platform_version()`. The "latest" version is what
Expand Down
2 changes: 1 addition & 1 deletion book/src/versioning/versioned-dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ meaningfully.

This is a lot of steps, but each one is mechanical and the compiler guides you
through most of it. If you add a field to a version struct and forget to set it
in one of the fourteen platform version constants, the build fails.
in one of the fifteen platform version constants, the build fails.

## Passing Version References

Expand Down
108 changes: 108 additions & 0 deletions packages/rs-dpp/src/data_contract/methods/apply_update/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
mod v0;

use crate::block::block_info::BlockInfo;
use crate::data_contract::update_values::DataContractUpdateValues;
use crate::data_contract::DataContract;
use crate::validation::operations::ProtocolValidationOperation;
use crate::validation::ConsensusValidationResult;
use crate::ProtocolError;
use platform_version::version::feature_initial_protocol_versions::DATA_CONTRACT_UPDATE_V1_INITIAL_PROTOCOL_VERSION;
use platform_version::version::PlatformVersion;

impl DataContract {
/// Merges a delta-based contract update onto this contract and returns
/// the updated contract.
///
/// This is the materialization step of a V1 data contract update
/// transition: the caller fetched this contract from state, and the
/// result is the contract the update would store. The result still has
/// to pass [`validate_update`](crate::data_contract::validate_update::DataContractUpdateValidationMethodsV0::validate_update)
/// against this contract, exactly like a full contract sent by a V0
/// transition; this method only checks what the delta shape itself
/// makes checkable (the submitter owns the contract, updated entries
/// exist, new entries do not) and rebuilds the contract.
///
/// # Arguments
/// - `update_values`: the delta to apply.
/// - `block_info`: the block the update executes in; it stamps the
/// `updatedAt` fields.
/// - `full_validation`: whether to fully validate the rebuilt contract.
/// - `validation_operations`: collects the validation work done for fees.
/// - `platform_version`: the current platform version.
///
/// # Returns
/// - `Ok(ConsensusValidationResult<DataContract>)`: the updated contract,
/// or the consensus errors that reject the delta.
/// - `Err(ProtocolError)`: the platform version predates delta-based
/// updates (the slot is `None`), is unknown, or rebuilding the contract
/// failed.
pub fn apply_update(
&self,
update_values: DataContractUpdateValues<'_>,
block_info: &BlockInfo,
full_validation: bool,
validation_operations: &mut Vec<ProtocolValidationOperation>,
platform_version: &PlatformVersion,
) -> Result<ConsensusValidationResult<DataContract>, ProtocolError> {
match platform_version.dpp.contract_versions.methods.apply_update {
None => Err(ProtocolError::UnknownVersionError(format!(
"DataContract::apply_update is not active at protocol version {}, delta-based contract updates arrive with protocol version {}",
platform_version.protocol_version, DATA_CONTRACT_UPDATE_V1_INITIAL_PROTOCOL_VERSION
))),
Some(0) => self.apply_update_v0(
update_values,
block_info,
full_validation,
validation_operations,
platform_version,
),
Some(version) => Err(ProtocolError::UnknownVersionMismatch {
method: "DataContract::apply_update".to_string(),
known_versions: vec![0],
received: version,
}),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::data_contract::accessors::v0::DataContractV0Getters;
use crate::data_contract::update_values::{DataContractUpdateValues, DescriptionUpdate};
use crate::tests::fixtures::get_data_contract_fixture;
use assert_matches::assert_matches;
use std::collections::BTreeMap;

#[test]
fn apply_update_is_not_active_before_protocol_version_15() {
let platform_version_14 = PlatformVersion::get(14).expect("protocol version 14");
let contract = get_data_contract_fixture(None, 0, platform_version_14.protocol_version)
.data_contract_owned();
let empty = BTreeMap::new();
let update_values = DataContractUpdateValues {
owner_id: contract.owner_id(),
version: contract.version() + 1,
config: None,
updated_schema_defs: &empty,
new_schema_defs: &empty,
updated_document_schemas: &empty,
new_document_schemas: &empty,
new_groups: &BTreeMap::new(),
new_tokens: &BTreeMap::new(),
add_keywords: &[],
remove_keywords: &[],
description: &DescriptionUpdate::Keep,
};

let result = contract.apply_update(
update_values,
&BlockInfo::default(),
false,
&mut vec![],
platform_version_14,
);

assert_matches!(result, Err(ProtocolError::UnknownVersionError(message)) if message.contains("not active"));
}
}
70 changes: 70 additions & 0 deletions packages/rs-dpp/src/data_contract/methods/apply_update/v0/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use crate::block::block_info::BlockInfo;
use crate::consensus::basic::data_contract::NonContiguousContractTokenPositionsError;
use crate::data_contract::serialized_version::DataContractInSerializationFormat;
use crate::data_contract::update_values::DataContractUpdateValues;
use crate::data_contract::{DataContract, TokenContractPosition};
use crate::validation::operations::ProtocolValidationOperation;
use crate::validation::ConsensusValidationResult;
use crate::ProtocolError;
use platform_version::version::PlatformVersion;

impl DataContract {
#[inline(always)]
pub(super) fn apply_update_v0(
&self,
update_values: DataContractUpdateValues<'_>,
block_info: &BlockInfo,
full_validation: bool,
validation_operations: &mut Vec<ProtocolValidationOperation>,
platform_version: &PlatformVersion,
) -> Result<ConsensusValidationResult<DataContract>, ProtocolError> {
let merged = match update_values.merge_onto_v0(self, block_info) {
Ok(merged) => merged,
Err(consensus_error) => {
return Ok(ConsensusValidationResult::new_with_error(consensus_error))
}
};

if !update_values.new_groups.is_empty() {
// Positions must stay contiguous across the stored and the new
// groups, which only the merged map can show.
let validation_result =
DataContract::validate_groups(&merged.groups, platform_version)?;
if !validation_result.is_valid() {
return Ok(ConsensusValidationResult::new_with_errors(
validation_result.errors,
));
}
}

for (expected_position, position) in merged.tokens.keys().enumerate() {
let expected_position = expected_position as TokenContractPosition;
if *position != expected_position {
return Ok(ConsensusValidationResult::new_with_error(
NonContiguousContractTokenPositionsError::new(expected_position, *position)
.into(),
));
}
}
for token in update_values.new_tokens.values() {
// A new token may reference a group that arrives in the same
// update, so the check runs against the merged groups.
let validation_result =
token.validate_token_config_groups_exist(&merged.groups, platform_version)?;
if !validation_result.is_valid() {
return Ok(ConsensusValidationResult::new_with_errors(
validation_result.errors,
));
}
}

let updated_contract = DataContract::try_from_platform_versioned(
DataContractInSerializationFormat::V1(merged),
full_validation,
validation_operations,
platform_version,
)?;

Ok(ConsensusValidationResult::new_with_data(updated_contract))
}
}
2 changes: 2 additions & 0 deletions packages/rs-dpp/src/data_contract/methods/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#[cfg(feature = "validation")]
mod apply_update;
mod equal_ignoring_time_based_fields;
mod registration_cost;
pub mod schema;
Expand Down
3 changes: 2 additions & 1 deletion packages/rs-dpp/src/data_contract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub mod change_control_rules;
pub mod config;
pub mod group;
pub mod storage_requirements;
pub mod update_values;

use crate::data_contract::serialized_version::{
DataContractInSerializationFormat, CONTRACT_DESERIALIZATION_LIMIT,
Expand All @@ -69,7 +70,7 @@ use platform_versioning::PlatformVersioned;
pub use serde_json::Value as JsonValue;

type JsonSchema = JsonValue;
type DefinitionName = String;
pub type DefinitionName = String;
pub type DocumentName = String;
pub type TokenName = String;
pub type GroupContractPosition = u16;
Expand Down
3 changes: 3 additions & 0 deletions packages/rs-dpp/src/data_contract/serialized_version/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ use std::fmt;
pub(in crate::data_contract) mod v0;
pub(in crate::data_contract) mod v1;

#[cfg(feature = "serde-conversion")]
pub(crate) use v1::{deserialize_u16_group_map, deserialize_u16_token_configuration_map};

pub mod property_names {
pub const ID: &str = "id";
pub const OWNER_ID: &str = "ownerId";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ pub struct DataContractInSerializationFormatV1 {
pub description: Option<String>,
}

fn deserialize_u16_group_map<'de, D>(
pub(crate) fn deserialize_u16_group_map<'de, D>(
deserializer: D,
) -> Result<BTreeMap<GroupContractPosition, Group>, D::Error>
where
Expand All @@ -89,7 +89,7 @@ where
})
.collect()
}
fn deserialize_u16_token_configuration_map<'de, D>(
pub(crate) fn deserialize_u16_token_configuration_map<'de, D>(
deserializer: D,
) -> Result<BTreeMap<TokenContractPosition, TokenConfiguration>, D::Error>
where
Expand Down
Loading
Loading