Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
f0699bc
feat(platform)!: random token transfer and mint amounts
QuantumExplorer Nov 3, 2026
f6d04b4
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 3, 2026
446c965
fix(platform): keep random token amounts inactive until settled, prep…
QuantumExplorer Nov 3, 2026
7474f61
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 3, 2026
e72c47a
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 3, 2026
722cc7f
fix(platform): bind random token history documents to their range
QuantumExplorer Nov 3, 2026
00704c7
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 3, 2026
f8fcc8f
fix(drive): refuse random token expectations the platform never executes
QuantumExplorer Nov 4, 2026
a2e7b38
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 4, 2026
6542187
test(platform): drive random token tests through the converters and t…
QuantumExplorer Nov 4, 2026
402dc1c
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 4, 2026
b798551
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 4, 2026
e6c85f2
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 4, 2026
1ad0459
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 4, 2026
29e05a8
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 4, 2026
274fef1
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 4, 2026
5e92ed9
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 4, 2026
7b501ae
fix(drive): prepay a random token amount's settlement with its histor…
QuantumExplorer Nov 4, 2026
bb8a5c2
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 4, 2026
33fdf44
fix(drive): refuse a batch reserving two random amounts of one token
QuantumExplorer Nov 4, 2026
a294183
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 4, 2026
159cb19
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 4, 2026
d5298ff
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 5, 2026
80a5bb7
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 6, 2026
df10cbe
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 6, 2026
9cd4cc2
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 7, 2026
f1a4f21
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 9, 2026
9db7ed4
test(drive): reserve and settle random token amounts inside one uncom…
QuantumExplorer Nov 9, 2026
84c6e8b
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 9, 2026
dd23171
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 10, 2026
27c035f
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 10, 2026
b21b3b0
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 10, 2026
e5e61e9
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 10, 2026
f9004da
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 10, 2026
8e6cb3d
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 11, 2026
ec923ac
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 11, 2026
87836c3
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 11, 2026
1a0e3ce
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 12, 2026
e415683
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 12, 2026
daa0330
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 12, 2026
6c25af0
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 12, 2026
80f9de4
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 12, 2026
01de6a6
Merge branch 'claude/platform-tenderdash-randomness-149e4e' into clau…
QuantumExplorer Nov 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@
version 15). This is a source change for Rust code whichever protocol version it runs: a
`GeneratedFrom` literal sets `range: None` for every function but a random one, and an
exhaustive match on `SystemFunction` needs a `Random` arm.
- **dpp, drive, rs-sdk (Rust API):** random token transfer and mint amounts (protocol
version 15) change these items for Rust code whichever protocol version it runs:
- `TokenEvent::build_historical_document_owned` and
`Drive::add_token_transaction_history_operations` take a `random_range` argument before
`block_info`. Pass `None` for the history of a fixed amount, which keeps its document id.
- `TokenTransferTransition` and `TokenMintTransition` gain a `V1` variant
(`TokenTransferTransitionV1`, `TokenMintTransitionV1`); `TokenOperationType` gains
`TokenReserveRandomTransfer`, `TokenReserveRandomMint` and `TokenRandomAmountHistory`; and
`BasicError` gains `RandomTokenMintNotAllowedError`. An exhaustive match on any of them
needs the new arms.
- `StateTransitionCreationOptions`, the drive actions `TokenTransferTransitionActionV0` and
`TokenMintTransitionActionV0`, and the rs-sdk `TokenTransferTransitionBuilder` and
`TokenMintTransitionBuilder` gain a public `random_minimum_amount` field. A struct literal
sets it to `None` for a fixed amount, or ends with `..Default::default()` where the type
has a default.
- The traits `TokenTransferTransitionV0Methods`, `TokenMintTransitionV0Methods`,
`TokenTransferTransitionActionAccessorsV0` and `TokenMintTransitionActionAccessorsV0` gain
a required `random_minimum_amount` method, and `TokenTransitionV0Methods` a required
`random_amount_range` method. An implementation outside these crates returns `None` for a
fixed amount.

- **drive, dpp (Rust API):** `Drive::calculate_fee` is now a method and no longer takes
`epochs_per_era` or `previous_fee_versions`:
Expand Down
23 changes: 18 additions & 5 deletions packages/rs-dpp/src/errors/consensus/basic/basic_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ use crate::consensus::basic::token::{
InvalidTokenDistributionTimeIntervalNotMinuteAlignedError,
InvalidTokenDistributionTimeIntervalTooShortError, InvalidTokenIdError,
InvalidTokenNoteTooBigError, InvalidTokenOncePerIdentityDistributionAmountError,
InvalidTokenPositionError, MissingDefaultLocalizationError,
InvalidTokenPositionError, MissingDefaultLocalizationError, RandomTokenMintNotAllowedError,
TokenNoteOnlyAllowedWhenProposerError, TokenPricingScheduleEmptyError,
TokenTransferToOurselfError,
};
Expand Down Expand Up @@ -903,6 +903,11 @@ pub enum BasicError {

#[error(transparent)]
DuplicatePrivateDocumentEntryError(DuplicatePrivateDocumentEntryError),

// A token mint asking for a random amount it cannot take: a group action, or one leaving
// its recipient to the token's configuration (protocol version 15).
#[error(transparent)]
RandomTokenMintNotAllowedError(RandomTokenMintNotAllowedError),
}

impl From<BasicError> for ConsensusError {
Expand Down Expand Up @@ -1141,16 +1146,14 @@ mod tests {
)),
209
);
// A Batch document transition on a private document type (protocol version 15): the
// tail of the enum.
// A Batch document transition on a private document type (protocol version 15).
assert_eq!(
discriminant_of(BasicError::DocumentTypePrivateError(
DocumentTypePrivateError::new(Identifier::from([1; 32]), "entry".to_string())
)),
210
);
// A `PrivateDocumentCreateFromShieldedPool`'s entries (protocol version 15): the tail
// of the enum.
// A `PrivateDocumentCreateFromShieldedPool`'s entries (protocol version 15).
assert_eq!(
discriminant_of(BasicError::InvalidPrivateDocumentEntryCountError(
InvalidPrivateDocumentEntryCountError::new(17, 16)
Expand All @@ -1169,6 +1172,16 @@ mod tests {
)),
213
);
// A token mint refusing a random amount (protocol version 15): the tail of the enum.
assert_eq!(
discriminant_of(BasicError::RandomTokenMintNotAllowedError(
RandomTokenMintNotAllowedError::new(
Identifier::from([1; 32]),
"reason".to_string()
)
)),
214
);
}

/// The variants that shipped in 4.1 keep the discriminants they were released with, so an
Expand Down
2 changes: 2 additions & 0 deletions packages/rs-dpp/src/errors/consensus/basic/token/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod invalid_token_note_too_big_error;
mod invalid_token_once_per_identity_distribution_amount_error;
mod invalid_token_position_error;
mod missing_default_localization;
mod random_token_mint_not_allowed_error;
mod token_note_only_allowed_on_proposer_error;
mod token_pricing_schedule_empty_error;
mod token_transfer_to_ourselves_error;
Expand All @@ -32,6 +33,7 @@ pub use invalid_token_note_too_big_error::*;
pub use invalid_token_once_per_identity_distribution_amount_error::*;
pub use invalid_token_position_error::*;
pub use missing_default_localization::*;
pub use random_token_mint_not_allowed_error::*;
pub use token_note_only_allowed_on_proposer_error::*;
pub use token_pricing_schedule_empty_error::*;
pub use token_transfer_to_ourselves_error::*;
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use crate::consensus::basic::BasicError;
use crate::consensus::ConsensusError;
use crate::ProtocolError;
use bincode::{Decode, DecodeUntrusted, Encode};
use platform_serialization_derive::{
PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize,
};
use platform_value::Identifier;
use thiserror::Error;

/// A token mint asking for a random amount (protocol version 15) that cannot take one: it is a
/// group action, whose members would weigh an amount nobody knows, or it leaves its recipient
/// to the token's configuration, which the payout must not read after the block executes the
/// mint. `reason` says which.
#[derive(
Error,
Debug,
Clone,
PartialEq,
Eq,
Encode,
Decode,
PlatformSerialize,
PlatformDeserializeTrusted,
PlatformDeserializeUntrusted,
DecodeUntrusted,
)]
#[error("A mint of token {token_id} cannot issue a random amount: {reason}")]
#[platform_serialize(unversioned)]
pub struct RandomTokenMintNotAllowedError {
/*

DO NOT CHANGE ORDER OF FIELDS WITHOUT INTRODUCING OF NEW VERSION

*/
token_id: Identifier,
reason: String,
}

impl RandomTokenMintNotAllowedError {
pub fn new(token_id: Identifier, reason: String) -> Self {
Self { token_id, reason }
}

pub fn token_id(&self) -> Identifier {
self.token_id
}

pub fn reason(&self) -> &str {
&self.reason
}
}

impl From<RandomTokenMintNotAllowedError> for ConsensusError {
fn from(err: RandomTokenMintNotAllowedError) -> Self {
Self::BasicError(BasicError::RandomTokenMintNotAllowedError(err))
}
}
1 change: 1 addition & 0 deletions packages/rs-dpp/src/errors/consensus/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ impl ErrorWithCode for BasicError {
Self::InvalidTokenNoteTooBigError(_) => 10459,
Self::TokenNoteOnlyAllowedWhenProposerError(_) => 10460,
Self::TokenPricingScheduleEmptyError(_) => 10461,
Self::RandomTokenMintNotAllowedError(_) => 10462,

// Identity Errors: 10500-10599
Self::DuplicatedIdentityPublicKeyBasicError(_) => 10500,
Expand Down
188 changes: 186 additions & 2 deletions packages/rs-dpp/src/state_transition/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ use platform_version::version::feature_initial_protocol_versions::{
PRIVATE_DOCUMENT_CREATE_FROM_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION,
YES_NO_VOTE_POLL_INITIAL_PROTOCOL_VERSION,
};
use platform_version::version::{PlatformVersion, ProtocolVersion, ALL_VERSIONS, LATEST_VERSION};
use platform_version::version::{
PlatformVersion, ProtocolVersion, ALL_VERSIONS, LATEST_VERSION, PLATFORM_VERSIONS,
};

#[cfg(any(
feature = "state-transition-signing",
Expand Down Expand Up @@ -102,7 +104,9 @@ use crate::state_transition::batch_transition::batched_transition::document_tran
use crate::state_transition::batch_transition::batched_transition::BatchedTransitionRef;
#[cfg(feature = "state-transition-signing")]
use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0;
use crate::state_transition::batch_transition::{BatchTransition, BatchTransitionSignable};
use crate::state_transition::batch_transition::{
BatchTransition, BatchTransitionSignable, TokenMintTransition, TokenTransferTransition,
};
use crate::state_transition::contract_fee_claim_transition::{
ContractFeeClaimTransition, ContractFeeClaimTransitionSignable,
};
Expand Down Expand Up @@ -1159,6 +1163,45 @@ fn batch_carries_a_version_2_document_base(batch_transition: &BatchTransition) -
})
}

/// Whether `batch_transition` carries a token transfer or a token mint of format version 1, the
/// format that can move a random amount.
fn batch_carries_a_version_1_token_transfer_or_mint(batch_transition: &BatchTransition) -> bool {
batch_transition
.transitions_iter()
.any(|transition| match transition {
BatchedTransitionRef::Token(TokenTransition::Transfer(transfer)) => {
matches!(transfer, TokenTransferTransition::V1(_))
}
BatchedTransitionRef::Token(TokenTransition::Mint(mint)) => {
matches!(mint, TokenMintTransition::V1(_))
}
BatchedTransitionRef::Token(_) | BatchedTransitionRef::Document(_) => false,
})
}

/// The active range of a batch carrying a token transfer or a token mint of format version 1
/// (a random amount), among `versions`, the protocol versions in order: from the first whose
/// table admits a random token amount (`random_token_amount` is `Some`), and `NO_VERSION` while
/// none does. The tables decide it, so the release that settles the amounts activates them by
/// setting the slot in its table alone. Every table of protocol version 14 and earlier keeps it
/// `None` (no binary of theirs decodes the format), so the start is never below 15.
fn random_token_amount_active_version_range(
versions: &[PlatformVersion],
) -> RangeInclusive<ProtocolVersion> {
versions
.iter()
.find(|platform_version| {
platform_version
.dpp
.state_transition_serialization_versions
.random_token_amount
.is_some()
})
.map_or(NO_VERSION, |platform_version| {
platform_version.protocol_version..=LATEST_VERSION
})
}

impl StateTransition {
/// Decodes a transition from bytes that arrived from outside this node and refuses one
/// whose version is not active at `platform_version`. Bytes left over after the transition
Expand Down Expand Up @@ -1248,6 +1291,16 @@ impl StateTransition {
active_version_range_for_contract(data_contract_update_transition.data_contract())
}
StateTransition::Batch(batch_transition) => {
// Version 1 of the token transfer and of the token mint (a random amount) is
// active from the first protocol version whose table admits a random token
// amount, and nowhere while none does: block execution does not settle the
// amounts yet, so a reservation would never be paid out. Binaries from before 15
// cannot decode one, so an earlier version rejects the batch without charging,
// exactly as they do. Checked first: the range starts at 15 at the earliest, the
// later start, so it holds whatever else the batch carries.
if batch_carries_a_version_1_token_transfer_or_mint(batch_transition) {
return random_token_amount_active_version_range(PLATFORM_VERSIONS);
}
// Version 2 of the document base (the action fee agreement) exists from
// protocol version 14. Binaries from before it cannot decode one, so an
// earlier version rejects the batch without charging, exactly as they do.
Expand Down Expand Up @@ -3646,6 +3699,137 @@ mod tests {
assert_eq!(recovered, version_2);
}

// Version 1 of the token transfer and of the token mint carries a random amount, which no
// table admits yet: block execution does not settle the amounts, so a reservation would
// never be paid out. New software must refuse the batch at every protocol version, 15
// included, though the format decodes; version 0 stays active everywhere its batch is.
#[test]
fn should_not_activate_a_version_1_token_transfer_or_mint_while_no_table_admits_it() {
use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable};
use crate::state_transition::batch_transition::batched_transition::BatchedTransition;
use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0;
use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition;
use crate::state_transition::batch_transition::token_mint_transition::{
TokenMintTransitionV0, TokenMintTransitionV1,
};
use crate::state_transition::batch_transition::token_transfer_transition::{
TokenTransferTransitionV0, TokenTransferTransitionV1,
};
use crate::state_transition::batch_transition::BatchTransitionV1;

let base = TokenBaseTransition::V0(TokenBaseTransitionV0 {
identity_contract_nonce: 1,
token_contract_position: 0,
data_contract_id: Identifier::from([1u8; 32]),
token_id: Identifier::from([2u8; 32]),
using_group_info: None,
});
let batch = |token_transition: TokenTransition| {
StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 {
owner_id: Identifier::from([9u8; 32]),
transitions: vec![BatchedTransition::Token(token_transition)],
user_fee_increase: 0,
signature_public_key_id: 0,
signature: BinaryData::new(vec![0u8; 65]),
}))
};
let transfer_v0 = TokenTransferTransitionV0 {
base: base.clone(),
amount: 100,
recipient_id: Identifier::from([3u8; 32]),
public_note: None,
shared_encrypted_note: None,
private_encrypted_note: None,
};
let mint_v0 = TokenMintTransitionV0 {
base,
issued_to_identity_id: Some(Identifier::from([3u8; 32])),
amount: 100,
public_note: None,
};

assert_eq!(
batch(TokenTransition::Transfer(TokenTransferTransition::V0(
transfer_v0.clone()
)))
.active_version_range(),
9..=LATEST_VERSION
);
assert_eq!(
batch(TokenTransition::Mint(TokenMintTransition::V0(
mint_v0.clone()
)))
.active_version_range(),
9..=LATEST_VERSION
);

let random_transfer = batch(TokenTransition::Transfer(TokenTransferTransition::V1(
TokenTransferTransitionV1::from_v0(transfer_v0, Some(10)),
)));
let random_mint = batch(TokenTransition::Mint(TokenMintTransition::V1(
TokenMintTransitionV1::from_v0(mint_v0, Some(10)),
)));
for random in [random_transfer, random_mint] {
assert_eq!(random.active_version_range(), NO_VERSION);
let bytes =
PlatformSerializable::serialize_to_bytes(&random).expect("serialize succeeds");
for platform_version in PLATFORM_VERSIONS {
assert!(
matches!(
StateTransition::deserialize_from_bytes_untrusted_in_version(
&bytes,
platform_version
),
Err(ProtocolError::StateTransitionError(
StateTransitionIsNotActiveError { .. }
))
),
"expected protocol version {} to refuse a random token amount",
platform_version.protocol_version
);
}
// The format itself decodes: only the gate refuses it
let recovered = StateTransition::deserialize_from_bytes_untrusted(&bytes)
.expect("expected a version 1 token transition to decode");
assert_eq!(recovered, random);
}
}

// The release that settles random token amounts activates them by setting
// `random_token_amount` in its table: the active range starts at the first protocol version
// whose table has it, and is empty while none has it.
#[test]
fn should_activate_random_token_amounts_from_the_first_table_admitting_them() {
// No table admits it yet
assert_eq!(
random_token_amount_active_version_range(PLATFORM_VERSIONS),
NO_VERSION
);
// No binary of protocol version 14 or earlier decodes the format, so none of their
// tables may ever admit it (14 shares its table with 15 for now)
assert!(PLATFORM_VERSIONS
.iter()
.filter(|platform_version| platform_version.protocol_version < 15)
.all(|platform_version| platform_version
.dpp
.state_transition_serialization_versions
.random_token_amount
.is_none()));

let mut versions = PLATFORM_VERSIONS.to_vec();
let version_15 = versions
.iter_mut()
.find(|platform_version| platform_version.protocol_version == 15)
.expect("platform version 15 exists");
version_15
.dpp
.state_transition_serialization_versions
.random_token_amount = Some(0);
let active = random_token_amount_active_version_range(&versions);
assert_eq!(active, 15..=LATEST_VERSION);
assert!(!active.contains(&14));
}

fn sample_batch_st_empty() -> StateTransition {
StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 {
owner_id: Identifier::from([1u8; 32]),
Expand Down
Loading
Loading