Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use platform_value::Identifier;
use platform_version::version::PlatformVersion;
use crate::ProtocolError;

pub trait AllowedAsMultiPartyAction {
fn calculate_action_id(&self, owner_id: Identifier) -> Identifier;
fn calculate_action_id(&self, owner_id: Identifier, platform_version: &PlatformVersion) -> Result<Identifier, ProtocolError>;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use platform_value::Identifier;
use platform_version::version::PlatformVersion;
use crate::ProtocolError;
use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction;
use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors;
use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition;
Expand Down Expand Up @@ -60,16 +62,16 @@ impl TokenBurnTransitionV0Methods for TokenBurnTransitionV0 {
}

impl AllowedAsMultiPartyAction for TokenBurnTransitionV0 {
fn calculate_action_id(&self, owner_id: Identifier) -> Identifier {
fn calculate_action_id(&self, owner_id: Identifier, _platform_version: &PlatformVersion) -> Result<Identifier, ProtocolError> {
let TokenBurnTransitionV0 {
base, burn_amount, ..
} = self;

TokenBurnTransition::calculate_action_id_with_fields(
Ok(TokenBurnTransition::calculate_action_id_with_fields_v0(
base.token_id().as_bytes(),
owner_id.as_bytes(),
base.identity_contract_nonce(),
*burn_amount,
)
))
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use platform_value::Identifier;
use platform_version::version::PlatformVersion;
use crate::balances::credits::TokenAmount;
use crate::prelude::IdentityNonce;
use crate::ProtocolError;
use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction;
use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors;
use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition;
Expand Down Expand Up @@ -61,15 +63,15 @@ impl TokenBurnTransitionV0Methods for TokenBurnTransition {
}

impl AllowedAsMultiPartyAction for TokenBurnTransition {
fn calculate_action_id(&self, owner_id: Identifier) -> Identifier {
fn calculate_action_id(&self, owner_id: Identifier, platform_version: &PlatformVersion) -> Result<Identifier, ProtocolError> {
match self {
TokenBurnTransition::V0(v0) => v0.calculate_action_id(owner_id),
TokenBurnTransition::V0(v0) => v0.calculate_action_id(owner_id, platform_version),
}
}
}

impl TokenBurnTransition {
pub fn calculate_action_id_with_fields(
pub fn calculate_action_id_with_fields_v0(
token_id: &[u8; 32],
owner_id: &[u8; 32],
identity_contract_nonce: IdentityNonce,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use platform_value::Identifier;
use platform_version::version::PlatformVersion;
use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem;
use crate::ProtocolError;
use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction;
use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors;
use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition;
Expand Down Expand Up @@ -69,18 +71,36 @@ impl TokenConfigUpdateTransitionV0Methods for TokenConfigUpdateTransitionV0 {
}

impl AllowedAsMultiPartyAction for TokenConfigUpdateTransitionV0 {
fn calculate_action_id(&self, owner_id: Identifier) -> Identifier {
/// v0 action_id: uses only the u8 discriminant (kept for backward compat).
fn calculate_action_id(&self, owner_id: Identifier, platform_version: &PlatformVersion,) -> Result<Identifier, ProtocolError> {
let TokenConfigUpdateTransitionV0 {
base,
update_token_configuration_item,
..
} = self;

TokenConfigUpdateTransition::calculate_action_id_with_fields(
base.token_id().as_bytes(),
owner_id.as_bytes(),
base.identity_contract_nonce(),
update_token_configuration_item.u8_item_index(),
)
match platform_version
.dpp
.token_versions
.token_config_update_action_id_version
{
0 => Ok(TokenConfigUpdateTransition::calculate_action_id_with_fields_v0(
base.token_id().as_bytes(),
owner_id.as_bytes(),
base.identity_contract_nonce(),
update_token_configuration_item.u8_item_index(),
)),
1 => Ok(TokenConfigUpdateTransition::calculate_action_id_with_fields_v1(
base.token_id().as_bytes(),
owner_id.as_bytes(),
base.identity_contract_nonce(),
update_token_configuration_item,
)),
version => Err(ProtocolError::UnknownVersionMismatch {
method: "calculate_action_id".to_string(),
known_versions: vec![0, 1],
received: version,
}),
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use platform_value::Identifier;
use platform_version::version::PlatformVersion;
use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem;
use crate::prelude::IdentityNonce;
use crate::ProtocolError;
use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction;
use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors;
use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition;
Expand Down Expand Up @@ -66,15 +68,17 @@ impl TokenConfigUpdateTransitionV0Methods for TokenConfigUpdateTransition {
}

impl AllowedAsMultiPartyAction for TokenConfigUpdateTransition {
fn calculate_action_id(&self, owner_id: Identifier) -> Identifier {
fn calculate_action_id(&self, owner_id: Identifier, platform_version: &PlatformVersion) -> Result<Identifier, ProtocolError> {
match self {
TokenConfigUpdateTransition::V0(v0) => v0.calculate_action_id(owner_id),
TokenConfigUpdateTransition::V0(v0) => v0.calculate_action_id(owner_id, platform_version),
}
}
}

impl TokenConfigUpdateTransition {
pub fn calculate_action_id_with_fields(
/// v0: action_id uses only the u8 discriminant of the config change item.
/// This is kept for backward compatibility with existing production data.
pub fn calculate_action_id_with_fields_v0(
token_id: &[u8; 32],
owner_id: &[u8; 32],
identity_contract_nonce: IdentityNonce,
Expand All @@ -88,4 +92,212 @@ impl TokenConfigUpdateTransition {

hash_double(bytes).into()
}

/// v1: action_id includes the full serialized config change item, binding
/// the voted-on value into the hash and preventing vote-swap attacks.
pub fn calculate_action_id_with_fields_v1(
token_id: &[u8; 32],
owner_id: &[u8; 32],
identity_contract_nonce: IdentityNonce,
update_token_configuration_item: &TokenConfigurationChangeItem,
) -> Identifier {
let serialized_item =
bincode::encode_to_vec(update_token_configuration_item, bincode::config::standard())
.expect("expected to encode token configuration change item");

let mut bytes = b"action_token_config_update".to_vec();
Comment thread
QuantumExplorer marked this conversation as resolved.
Outdated
bytes.extend_from_slice(token_id);
bytes.extend_from_slice(owner_id);
bytes.extend_from_slice(&identity_contract_nonce.to_be_bytes());
bytes.extend_from_slice(&serialized_item);

hash_double(bytes).into()
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem;
use crate::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods;
use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0;
use crate::state_transition::batch_transition::token_config_update_transition::TokenConfigUpdateTransitionV0;

fn make_transition(item: TokenConfigurationChangeItem) -> TokenConfigUpdateTransition {
TokenConfigUpdateTransition::V0(TokenConfigUpdateTransitionV0 {
base: TokenBaseTransition::V0(TokenBaseTransitionV0 {
identity_contract_nonce: 1,
token_contract_position: 0,
data_contract_id: Identifier::new([1u8; 32]),
token_id: Identifier::new([2u8; 32]),
using_group_info: None,
}),
update_token_configuration_item: item,
public_note: None,
})
}

#[test]
fn v0_action_id_same_discriminant_different_values_produces_same_id_vulnerability() {
// This test documents the v0 vulnerability: two different MaxSupply
// values produce the same action_id because only the u8 discriminant
// is hashed, not the actual value.
let owner_id = Identifier::new([3u8; 32]);

let t_small = make_transition(TokenConfigurationChangeItem::MaxSupply(Some(100)));
let t_large = make_transition(TokenConfigurationChangeItem::MaxSupply(Some(
999_999_999_999,
)));

let platform_version = PlatformVersion::first();
let id_small = t_small.calculate_action_id(owner_id, platform_version).expect("expected action id");
let id_large = t_large.calculate_action_id(owner_id, platform_version).expect("expected action id");

// v0: these are EQUAL -- the vulnerability
assert_eq!(
id_small, id_large,
"v0 should produce the same action_id for different MaxSupply values (vulnerability)"
);
}

#[test]
fn v1_action_id_same_discriminant_different_values_produces_different_ids() {
// After the fix, the full serialized config item is included in the
// hash, so different values produce different action_ids.
let token_id = [2u8; 32];
let owner_id = [3u8; 32];
let nonce = 1u64;

let item_small = TokenConfigurationChangeItem::MaxSupply(Some(100));
let item_large = TokenConfigurationChangeItem::MaxSupply(Some(999_999_999_999));

let id_small = TokenConfigUpdateTransition::calculate_action_id_with_fields_v1(
&token_id,
&owner_id,
nonce,
&item_small,
);
let id_large = TokenConfigUpdateTransition::calculate_action_id_with_fields_v1(
&token_id,
&owner_id,
nonce,
&item_large,
);

// v1: these must be DIFFERENT -- the fix
assert_ne!(
id_small, id_large,
"v1 should produce different action_ids for different MaxSupply values"
);
}
Comment thread
QuantumExplorer marked this conversation as resolved.

#[test]
fn v1_action_id_different_item_types_produces_different_ids() {
let token_id = [2u8; 32];
let owner_id = [3u8; 32];
let nonce = 1u64;

let item_max_supply = TokenConfigurationChangeItem::MaxSupply(Some(100));
let item_allow_dest = TokenConfigurationChangeItem::MintingAllowChoosingDestination(true);

let id_max = TokenConfigUpdateTransition::calculate_action_id_with_fields_v1(
&token_id,
&owner_id,
nonce,
&item_max_supply,
);
let id_dest = TokenConfigUpdateTransition::calculate_action_id_with_fields_v1(
&token_id,
&owner_id,
nonce,
&item_allow_dest,
);

assert_ne!(
id_max, id_dest,
"v1 should produce different action_ids for different config item types"
);
}

#[test]
fn v0_and_v1_produce_different_ids_for_same_input() {
// Verify v0 and v1 are not accidentally identical (they hash
// different payloads).
let token_id = [2u8; 32];
let owner_id = [3u8; 32];
let nonce = 1u64;
let item = TokenConfigurationChangeItem::MaxSupply(Some(100));

let id_v0 = TokenConfigUpdateTransition::calculate_action_id_with_fields_v0(
&token_id,
&owner_id,
nonce,
item.u8_item_index(),
);
let id_v1 = TokenConfigUpdateTransition::calculate_action_id_with_fields_v1(
&token_id, &owner_id, nonce, &item,
);

assert_ne!(
id_v0, id_v1,
"v0 and v1 should produce different action_ids for the same config item"
);
}

#[test]
fn versioned_dispatch_uses_v1_on_current_platform_version() {
// On the current platform version (v12), token_config_update_action_id_version
// is 1, so the versioned method should produce the v1 result (which includes
// the full config item payload), NOT the v0 result (discriminant only).
let owner_id = Identifier::new([3u8; 32]);
let t = make_transition(TokenConfigurationChangeItem::MaxSupply(Some(100)));

let platform_version = PlatformVersion::latest();

let id_plain_v0 = t.calculate_action_id(owner_id, PlatformVersion::first()).expect("expected action id");
let id_versioned = t.calculate_action_id(owner_id, platform_version).expect("expected action id");
Comment thread
QuantumExplorer marked this conversation as resolved.
Outdated

// v1 produces a different id from v0 because it hashes the full payload
assert_ne!(
id_plain_v0, id_versioned,
"on current platform version (v1), versioned should differ from plain (v0)"
);

// Verify the versioned result matches v1 directly
let base = t.base();
let id_v1 = TokenConfigUpdateTransition::calculate_action_id_with_fields_v1(
base.token_id().as_bytes(),
owner_id.as_bytes(),
base.identity_contract_nonce(),
t.update_token_configuration_item(),
);
assert_eq!(
id_versioned, id_v1,
"versioned dispatch should use v1 on current platform version"
);
}

#[test]
fn v1_action_id_identical_items_produces_same_id() {
// Sanity check: identical config items should produce the same
// action_id under v1.
let token_id = [2u8; 32];
let owner_id = [3u8; 32];
let nonce = 1u64;

let item1 = TokenConfigurationChangeItem::MaxSupply(Some(42));
let item2 = TokenConfigurationChangeItem::MaxSupply(Some(42));

let id1 = TokenConfigUpdateTransition::calculate_action_id_with_fields_v1(
&token_id, &owner_id, nonce, &item1,
);
let id2 = TokenConfigUpdateTransition::calculate_action_id_with_fields_v1(
&token_id, &owner_id, nonce, &item2,
);

assert_eq!(
id1, id2,
"v1 should produce the same action_id for identical config items"
);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use platform_value::Identifier;
use platform_version::version::PlatformVersion;
use crate::ProtocolError;
use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction;
use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors;
use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition;
Expand Down Expand Up @@ -61,18 +63,18 @@ impl TokenDestroyFrozenFundsTransitionV0Methods for TokenDestroyFrozenFundsTrans
}

impl AllowedAsMultiPartyAction for TokenDestroyFrozenFundsTransitionV0 {
fn calculate_action_id(&self, owner_id: Identifier) -> Identifier {
fn calculate_action_id(&self, owner_id: Identifier, _platform_version: &PlatformVersion) -> Result<Identifier, ProtocolError> {
let TokenDestroyFrozenFundsTransitionV0 {
base,
frozen_identity_id,
..
} = self;

TokenDestroyFrozenFundsTransition::calculate_action_id_with_fields(
Ok(TokenDestroyFrozenFundsTransition::calculate_action_id_with_fields_v0(
base.token_id().as_bytes(),
owner_id.as_bytes(),
base.identity_contract_nonce(),
frozen_identity_id.as_bytes(),
)
))
}
}
Loading
Loading