Skip to content

fix(dpp): bind token config update action_id to payload value (v1)#3346

Merged
QuantumExplorer merged 13 commits into
v3.1-devfrom
fix/token-config-action-id-bind-payload
Mar 17, 2026
Merged

fix(dpp): bind token config update action_id to payload value (v1)#3346
QuantumExplorer merged 13 commits into
v3.1-devfrom
fix/token-config-action-id-bind-payload

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented Mar 16, 2026

Issue being fixed or feature implemented

Fix a vote-swap vulnerability in the token config update action_id calculation.

The v0 calculate_action_id for TokenConfigUpdateTransition hashes only the u8 enum discriminant (u8_item_index()), not the actual payload value. This means that, for example, MaxSupply(100) and MaxSupply(999999999999) produce identical action_id values. A malicious group member could propose one config value, collect votes from other members, then swap to a completely different value with the same discriminant during finalization.

What was done?

Introduced a versioned action_id calculation that binds the full config change item value into the hash:

Platform version layer (rs-platform-version):

  • Added token_config_update_action_id_version field to DPPTokenVersions
  • TOKEN_VERSIONS_V1: sets field to 0 (current production behavior, unchanged)
  • TOKEN_VERSIONS_V2: sets field to 1 (fixed behavior, to be activated in a future protocol version)

DPP layer (rs-dpp):

  • Added calculate_action_id_with_fields_v1 on TokenConfigUpdateTransition that serializes the full TokenConfigurationChangeItem via bincode and includes it in the double-hash
  • Extended AllowedAsMultiPartyAction trait with a calculate_action_id_versioned default method (delegates to the existing calculate_action_id by default, so all other transition types are unaffected)
  • Overrode calculate_action_id_versioned on TokenConfigUpdateTransitionV0 to dispatch between v0/v1 based on the platform version
  • Added calculate_action_id_versioned to TokenTransitionV0Methods and its implementation on TokenTransition
  • Updated validate_basic_structure to use the versioned method
  • Updated batch transition construction (v1_methods.rs) to use the versioned method

No existing behavior changes: All current platform versions (v1-v12) use TOKEN_VERSIONS_V1 with token_config_update_action_id_version: 0, so the fix is dormant until a future protocol version activates it.

How Has This Been Tested?

6 unit tests covering:

  • v0 vulnerability documentation: confirms two different MaxSupply values produce the same action_id under v0
  • v1 fix verification: confirms different values produce different action_ids under v1
  • v1 cross-type: confirms different config item types produce different action_ids
  • v0 vs v1 divergence: confirms v0 and v1 produce different hashes for the same input
  • Version dispatch: confirms the current platform version dispatches to v0
  • v1 determinism: confirms identical items produce the same action_id under v1
cargo test -p dpp --features "state-transitions" -- "token_config_update_transition"
# 6 tests passed

Full build verification:

cargo check -p drive-abci  # compiles cleanly

Breaking Changes

None. This is a backward-compatible change. v0 behavior is preserved for all existing platform versions. The fix activates only when a future platform version references TOKEN_VERSIONS_V2.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Introduced versioned token configuration update action ID calculation with v1 supporting payload-based generation
    • Enhanced platform version system to track token configuration action ID versions
  • Bug Fixes

    • Resolved security vulnerability in token configuration update action ID computation
  • Refactor

    • Updated token transition operations to support platform-version-aware action ID calculations
    • Enhanced error handling across token transition methods with Result-based returns

The v0 action_id calculation for token config updates used only the u8
enum discriminant (via u8_item_index()), not the actual payload value.
This allowed a group member to propose one config value, collect votes,
then swap to a different value with the same discriminant during
finalization (e.g. MaxSupply(100) vs MaxSupply(999999999999) both
hashed identically).

Introduce a versioned action_id calculation:
- v0 (unchanged): hashes only the u8 discriminant for backward compat
- v1 (new): serializes the full TokenConfigurationChangeItem via bincode
  and includes it in the double-hash, binding the voted-on value

The version is controlled by a new platform version field
(token_config_update_action_id_version) so v1 activates in a future
protocol version without breaking existing production data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added this to the v3.1.0 milestone Mar 16, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements version-aware token config update action ID calculation across multiple token transition types. The trait method calculate_action_id signature is updated to accept platform_version and return Result<Identifier, ProtocolError>. Platform version v2 is introduced with v1 token config update action ID hashing that includes payload serialization, while v0 remains for backward compatibility. New serialization utilities are added for token configuration items and authorized action takers.

Changes

Cohort / File(s) Summary
Trait Definition
packages/rs-dpp/src/state_transition/.../multi_party_action.rs
Updated AllowedAsMultiPartyAction::calculate_action_id trait signature to accept platform_version: &PlatformVersion and return Result<Identifier, ProtocolError>.
Token Config Update Implementation
packages/rs-dpp/src/state_transition/.../token_config_update_transition/v0/v0_methods.rs, packages/rs-dpp/src/state_transition/.../token_config_update_transition/v0_methods.rs
Implemented version-aware action ID calculation with v0 (existing) and v1 (new with payload) hashing strategies, platform version dispatch, and error handling.
Token Transition Type Implementations
packages/rs-dpp/src/state_transition/.../token_burn_transition/.../v0_methods.rs, packages/rs-dpp/src/state_transition/.../token_mint_transition/.../v0_methods.rs, packages/rs-dpp/src/state_transition/.../token_freeze_transition/.../v0_methods.rs, packages/rs-dpp/src/state_transition/.../token_unfreeze_transition/.../v0_methods.rs, packages/rs-dpp/src/state_transition/.../token_destroy_frozen_funds_transition/.../v0_methods.rs, packages/rs-dpp/src/state_transition/.../token_emergency_action_transition/.../v0_methods.rs, packages/rs-dpp/src/state_transition/.../token_set_price_for_direct_purchase_transition/.../v0_methods.rs
Updated calculate_action_id implementations to accept platform_version and return Result, delegating to versioned v0 implementations with error propagation.
Token Transition Orchestration
packages/rs-dpp/src/state_transition/.../token_transition.rs, packages/rs-dpp/src/state_transition/.../batch_transition/v1/v1_methods.rs
Updated method signatures to thread platform_version through all token transition creators and dispatch paths, with result handling and error propagation.
Validation Layer
packages/rs-dpp/src/state_transition/.../validate_basic_structure/v0/mod.rs
Updated validation call to calculate_action_id to pass platform_version and handle Result type with transpose and error propagation.
Platform Version Configuration
packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs, packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs, packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs, packages/rs-platform-version/src/version/v12.rs
Added token_config_update_action_id_version field to DPPTokenVersions; created v2 token versions with v1 action ID hashing; updated platform v12 to use v2 token versions.
Serialization Utilities
packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs
Added payload_serialization() method to TokenConfigurationChangeItem for v1 hashing payload generation, supporting per-variant serialization strategies.
Authorization Serialization
packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs
Added to_bytes() and from_bytes() methods to AuthorizedActionTakers for round-trip binary serialization with tag-based encoding.
Test Updates
packages/rs-drive-abci/src/execution/validation/state_transition/.../token/config_update/mod.rs
Updated test call sites to pass TokenConfigurationChangeItem reference and platform_version to calculate_action_id_with_fields, replacing index-based parameter usage.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant TokenTransition
    participant TokenConfigUpdate
    participant PlatformVersion
    participant ActionIDCalculator
    
    Client->>TokenTransition: new_token_config_update_transition(platform_version)
    TokenTransition->>PlatformVersion: check token_config_update_action_id_version
    alt version = 1
        TokenTransition->>TokenConfigUpdate: calculate_action_id_with_fields_v1(payload)
        TokenConfigUpdate->>ActionIDCalculator: hash with payload
        ActionIDCalculator-->>TokenConfigUpdate: identifier_v1
    else version = 0
        TokenTransition->>TokenConfigUpdate: calculate_action_id_with_fields_v0()
        TokenConfigUpdate->>ActionIDCalculator: hash without payload
        ActionIDCalculator-->>TokenConfigUpdate: identifier_v0
    end
    TokenConfigUpdate-->>TokenTransition: Result<Identifier>
    TokenTransition-->>Client: transition with action_id or error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Possibly related PRs

Suggested reviewers

  • shumkov

Poem

🐰 A rabbit hops through versioned fields,
Where tokens dance and payloads yield,
From v0's hash to v1's fine art,
Platform versions play their part,
Result types catch what once was clear—
Safe calculations, crystal dear! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: fixing a token config update action_id vulnerability by binding it to the payload value (v1 implementation).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/token-config-action-id-bind-payload
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 16, 2026

Codecov Report

❌ Patch coverage is 75.42735% with 115 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.22%. Comparing base (040f1e2) to head (fb32011).
⚠️ Report is 12 commits behind head on v3.1-dev.

Files with missing lines Patch % Lines
...t/change_control_rules/authorized_action_takers.rs 26.66% 33 Missing ⚠️
...tract/associated_token/token_configuration_item.rs 42.85% 28 Missing ⚠️
...ition/token_config_update_transition/v0_methods.rs 92.99% 15 Missing ⚠️
...token_emergency_action_transition/v0/v0_methods.rs 0.00% 12 Missing ⚠️
...on/token_emergency_action_transition/v0_methods.rs 0.00% 7 Missing ⚠️
...nsition/token_unfreeze_transition/v0/v0_methods.rs 0.00% 7 Missing ⚠️
...transition/token_unfreeze_transition/v0_methods.rs 0.00% 6 Missing ⚠️
...sitions/document/batch_transition/v1/v1_methods.rs 83.33% 4 Missing ⚠️
..._transition/batched_transition/token_transition.rs 82.35% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           v3.1-dev    #3346      +/-   ##
============================================
+ Coverage     74.90%   75.22%   +0.32%     
============================================
  Files          3000     3052      +52     
  Lines        271665   283091   +11426     
============================================
+ Hits         203478   212952    +9474     
- Misses        68187    70139    +1952     
Components Coverage Δ
dpp 62.90% <75.42%> (+0.15%) ⬆️
drive 81.64% <ø> (+1.38%) ⬆️
drive-abci 85.99% <ø> (ø)
sdk 31.25% <ø> (ø)
dapi-client 79.06% <ø> (ø)
platform-version 24.44% <ø> (∅)
platform-value 58.46% <ø> (ø)
platform-wallet 60.40% <ø> (ø)
drive-proof-verifier 48.00% <ø> (∅)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs (1)

308-328: Reduce duplicate action-id routing to avoid branch drift.

Line 313–327 duplicates calculate_action_id logic almost entirely. Consider delegating non-ConfigUpdate cases to self.calculate_action_id(owner_id).

♻️ Suggested simplification
 fn calculate_action_id_versioned(
     &self,
     owner_id: Identifier,
     platform_version: &PlatformVersion,
 ) -> Option<Identifier> {
-    match self {
-        TokenTransition::Burn(t) => Some(t.calculate_action_id(owner_id)),
-        TokenTransition::Mint(t) => Some(t.calculate_action_id(owner_id)),
-        TokenTransition::Freeze(t) => Some(t.calculate_action_id(owner_id)),
-        TokenTransition::Unfreeze(t) => Some(t.calculate_action_id(owner_id)),
-        TokenTransition::Transfer(_) => None,
-        TokenTransition::DestroyFrozenFunds(t) => Some(t.calculate_action_id(owner_id)),
-        TokenTransition::Claim(_) => None,
-        TokenTransition::EmergencyAction(t) => Some(t.calculate_action_id(owner_id)),
-        TokenTransition::ConfigUpdate(t) => {
-            Some(t.calculate_action_id_versioned(owner_id, platform_version))
-        }
-        TokenTransition::DirectPurchase(_) => None,
-        TokenTransition::SetPriceForDirectPurchase(t) => Some(t.calculate_action_id(owner_id)),
-    }
+    match self {
+        TokenTransition::ConfigUpdate(t) => {
+            Some(t.calculate_action_id_versioned(owner_id, platform_version))
+        }
+        _ => self.calculate_action_id(owner_id),
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs`
around lines 308 - 328, The match in calculate_action_id_versioned duplicates
the per-variant call to calculate_action_id for almost every TokenTransition;
refactor by delegating all non-ConfigUpdate variants to
self.calculate_action_id(owner_id) and only special-case
TokenTransition::ConfigUpdate to call t.calculate_action_id_versioned(owner_id,
platform_version); update calculate_action_id_versioned to return
self.calculate_action_id(owner_id) wrapped in Some/None as appropriate for the
default path and keep TokenTransition::ConfigUpdate using
calculate_action_id_versioned so you avoid branch drift and duplicated
per-variant logic.
packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs (1)

256-272: Test may need updating when v1 is activated.

This test uses PlatformVersion::latest() and asserts that versioned and plain methods produce the same ID. When a future platform version activates v1 (via token_config_update_action_id_version = 1), this test will fail. Consider adding a comment noting this expected behavior change, or using an explicit platform version constant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs`
around lines 256 - 272, The test
versioned_dispatch_uses_v0_on_current_platform_version assumes
PlatformVersion::latest() maps to v0; update the test to avoid future breakage
by either using an explicit platform version constant (e.g., construct a
PlatformVersion with token_config_update_action_id_version = 0) or add a
clarifying comment explaining this will fail once
token_config_update_action_id_version becomes 1; locate the test and change the
PlatformVersion usage around the calls to calculate_action_id and
calculate_action_id_versioned (or add the comment near PlatformVersion::latest()
and references to calculate_action_id/calculate_action_id_versioned) so intent
is clear and the test won’t unexpectedly break when v1 is activated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs`:
- Around line 256-272: The test
versioned_dispatch_uses_v0_on_current_platform_version assumes
PlatformVersion::latest() maps to v0; update the test to avoid future breakage
by either using an explicit platform version constant (e.g., construct a
PlatformVersion with token_config_update_action_id_version = 0) or add a
clarifying comment explaining this will fail once
token_config_update_action_id_version becomes 1; locate the test and change the
PlatformVersion usage around the calls to calculate_action_id and
calculate_action_id_versioned (or add the comment near PlatformVersion::latest()
and references to calculate_action_id/calculate_action_id_versioned) so intent
is clear and the test won’t unexpectedly break when v1 is activated.

In
`@packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs`:
- Around line 308-328: The match in calculate_action_id_versioned duplicates the
per-variant call to calculate_action_id for almost every TokenTransition;
refactor by delegating all non-ConfigUpdate variants to
self.calculate_action_id(owner_id) and only special-case
TokenTransition::ConfigUpdate to call t.calculate_action_id_versioned(owner_id,
platform_version); update calculate_action_id_versioned to return
self.calculate_action_id(owner_id) wrapped in Some/None as appropriate for the
default path and keep TokenTransition::ConfigUpdate using
calculate_action_id_versioned so you avoid branch drift and duplicated
per-variant logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 71d7c64c-ed81-4f4d-b1ff-a7f6d908e8ad

📥 Commits

Reviewing files that changed from the base of the PR and between 040f1e2 and 47acf03.

📒 Files selected for processing (9)
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/multi_party_action.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v1_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Well-structured security fix for a real vote-swap vulnerability in token config update action_id calculation. The versioning approach correctly follows the project's established pattern — staging the fix as dormant code to be activated in a future protocol version. The implementation is clean and the test suite thoroughly documents both the vulnerability and the fix. Two architectural observations worth discussing.

Reviewed commit: 47acf03

🟡 2 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0/v0_methods.rs`:
- [SUGGESTION] line 120: panic! on unknown version — consider returning an error
  The fallback arm `version => panic!("unsupported token_config_update_action_id_version {version}")` will crash the node on an unexpected version. The rest of the codebase uses `Err(Error::UnknownVersionMismatch { ... })` for this pattern. The root cause is that the `AllowedAsMultiPartyAction` trait returns `Identifier` rather than `Result<Identifier, ProtocolError>`, so proper error propagation would require a trait signature change. If changing the trait is out of scope for this fix, documenting this as a known limitation would help. In practice the risk is low since version values come from hardcoded `PlatformVersion` structs, but a panic in a consensus node is a harsher failure mode than a rejected transition.

In `packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs`:
- [SUGGESTION] lines 117-119: v1 hash uses derived bincode serialization — consider explicit encoding for consensus stability
  `calculate_action_id_with_fields_v1()` hashes `bincode::encode_to_vec(item, bincode::config::standard())` where `TokenConfigurationChangeItem` derives `Encode`. The serialized discriminant comes from enum declaration order, which is a weaker consensus contract than the explicitly pinned `u8_item_index()` mapping used in v0. If a future change inserts or reorders enum variants without bumping `token_config_update_action_id_version`, nodes on the old and new code would compute different action_ids for the same logical config update, causing a consensus split. A safer approach for the v1 hash would be: `u8_item_index()` (pinned discriminant) plus an explicit encoding of the variant's payload, keeping the stable tag while still binding the full value.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/rs-platform-version/src/version/v12.rs`:
- Line 13: PLATFORM_V12 is currently bound to TOKEN_VERSIONS_V2 which enables
new action_id behavior; revert this binding so v12 preserves prior behavior by
changing the binding of PLATFORM_V12 to the older token-versions constant
(replace TOKEN_VERSIONS_V2 with TOKEN_VERSIONS_V1) in v12.rs, ensuring
PLATFORM_V12 references the legacy token versions instead of the v2 token
versions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6d4ff501-8010-4c2b-beac-31ad1a92254a

📥 Commits

Reviewing files that changed from the base of the PR and between 47acf03 and 71964ad.

📒 Files selected for processing (1)
  • packages/rs-platform-version/src/version/v12.rs

Comment thread packages/rs-platform-version/src/version/v12.rs
The test assumed calculate_action_id_versioned should match
calculate_action_id (v0) on the current platform version. But v12
uses TOKEN_VERSIONS_V2 which sets token_config_update_action_id_version
to 1, so the versioned call correctly uses v1 (full payload hash)
while the plain call uses v0 (discriminant only).

Updated the test to assert they differ and verify the versioned result
matches calculate_action_id_with_fields_v1 directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Clean incremental push activating the action_id fix in v12 (TOKEN_VERSIONS_V1 → TOKEN_VERSIONS_V2). The test correctly verifies v12 now dispatches to v1, confirming the fix is no longer dormant. Prior suggestions (panic! fallback, bincode consensus stability) remain applicable.

Reviewed commit: ebcb500

QuantumExplorer and others added 4 commits March 16, 2026 18:14
…sult

Merge calculate_action_id and calculate_action_id_versioned into a
single calculate_action_id(owner_id, platform_version) -> Result method.
This removes the separate versioned dispatch and makes platform_version
a required parameter for all action_id calculations.

Changes:
- AllowedAsMultiPartyAction::calculate_action_id now takes platform_version
  and returns Result<Identifier, ProtocolError>
- Remove calculate_action_id_versioned (merged into calculate_action_id)
- Rename calculate_action_id_with_fields to calculate_action_id_with_fields_v0
- Replace panic! with ProtocolError::UnknownVersionMismatch
- Update all 7 token transition impls and all callers
- Update TokenTransitionV0Methods::calculate_action_id signature

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…at call site

Revert AllowedAsMultiPartyAction trait back to the simple signature:
  fn calculate_action_id(&self, owner_id: Identifier) -> Identifier

The v0/v1 version dispatch for TokenConfigUpdateTransition now happens
at the call site (v1_methods.rs) rather than inside the trait impl.

Key changes:
- Trait has no platform_version parameter, no Result return
- All 7 non-config-update transitions use simple calculate_action_id
- calculate_action_id_with_fields_v1 takes (u8, Option<&[u8]>) instead
  of &TokenConfigurationChangeItem — callers serialize and pass payload
- Added calculate_action_id_with_platform_version on TokenTransition
  for use in validation where version-aware calculation is needed
- Version dispatch in v1_methods.rs ConfigUpdate arm only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ispatch at call site"

This reverts commit fb77572. Restoring the unified calculate_action_id
with platform_version and Result return type as designed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… Option payload

Change v1 signature from &TokenConfigurationChangeItem to
(u8 discriminant, Option<&[u8]> payload). Serialization of the config
item now happens at the call site, keeping the hash function generic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs (1)

76-95: ⚠️ Potential issue | 🔴 Critical

Hash the full direct-purchase schedule.

calculate_action_id_with_fields_v0() only appends .1 from minimum_purchase_amount_and_price(), so proposals that share a price but differ in minimum purchase amount collapse to the same action_id. That reintroduces the same vote-swap class this PR is fixing for token config updates. Please add a versioned path that binds the full TokenPricingSchedule payload and route multi-party direct-purchase action ID calculation through it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs`
around lines 76 - 95, calculate_action_id_with_fields_v0 currently only appends
the price (.1 of minimum_purchase_amount_and_price()) which collapses distinct
schedules; modify calculate_action_id_with_fields_v0 to include the full
TokenPricingSchedule payload when present (not just the price) by serializing
the entire schedule (use the existing TokenPricingSchedule
representation/serialization used elsewhere) and appending those bytes into the
bytes buffer before hashing with hash_double; add a versioned path/marker string
(e.g., include a version suffix in the initial bytes like
"action_token_set_price_for_direct_purchase_v1") so future changes are
versioned, and ensure callers that compute multi-party direct-purchase action
IDs route through this updated function so the full schedule differentiates
action_id values.
🧹 Nitpick comments (3)
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mint/mod.rs (1)

1605-1610: Centralize mint action_id derivation instead of repeating raw _v0 calls.

These eight call sites duplicate the mint hash inputs and pin the suite to the v0 helper. Where the proposer transition already exists, prefer its calculate_action_id(..., platform_version); for the “other signer goes first” case, a small local helper would keep the raw field list in one place.

Also applies to: 1871-1876, 2194-2199, 2414-2419, 2633-2638, 2922-2927, 3338-3343, 3475-3480

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mint/mod.rs`
around lines 1605 - 1610, The tests duplicate mint action_id derivation by
calling TokenMintTransition::calculate_action_id_with_fields_v0 directly;
replace those raw _v0 calls with the transition's stable API and centralize the
field list: use TokenMintTransition::calculate_action_id(..., platform_version)
where the proposer transition is available, and for the "other signer goes
first" scenario extract a small local helper in the test (e.g., local fn
derive_mint_action_id(token_id, identity, quantity, nonce, platform_version))
that calls the single derive implementation so the four input fields are defined
in one place and the suite is no longer pinned to the v0 helper. Ensure all
eight sites mentioned (including the given line and the other ranges) are
updated to call the centralized helper or calculate_action_id with
platform_version.
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/freeze/mod.rs (1)

1434-1439: Prefer the transition’s calculate_action_id in these tests.

These cases already have the proposer transition in scope, but they still hard-code the _v0 helper while running against PlatformVersion::latest(). Calling the versioned API here would keep the tests aligned with runtime dispatch and remove the duplicated nonce/target literals.

Also applies to: 1778-1783, 2226-2231

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/freeze/mod.rs`
around lines 1434 - 1439, Tests are calling the hardcoded versioned helper
TokenFreezeTransition::calculate_action_id_with_fields_v0; replace those calls
with the transition's non-versioned calculate_action_id API (e.g., call the
proposer transition instance's calculate_action_id with the same
fields/nonce/target) so the tests follow runtime dispatch via
PlatformVersion::latest(); update all occurrences of
calculate_action_id_with_fields_v0 (including the other similar sites flagged)
to use the transition.calculate_action_id entrypoint instead.
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/config_update/mod.rs (1)

1081-1086: Avoid pinning latest-version tests to the v0 hash.

These cases use PlatformVersion::latest() but still derive the confirmation ID via calculate_action_id_with_fields_v0(...). When the latest platform version switches token-config action IDs to the payload-binding variant, these confirmations will reference the wrong action_id and the tests will start failing for the wrong reason. Please compute the ID through the versioned transition API, or pin these tests to an explicitly v0 platform version.

Also applies to: 1296-1304, 1464-1469, 1687-1695, 1912-1920, 2149-2157, 2436-2444, 2580-2588

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/config_update/mod.rs`
around lines 1081 - 1086, The tests call
TokenConfigUpdateTransition::calculate_action_id_with_fields_v0(...) while using
PlatformVersion::latest(), which will break when the canonical action-id format
changes; update each test (e.g. the call at calculate_action_id_with_fields_v0
and similar sites) to obtain the action id via the versioned transition API for
the current platform version (e.g. use
TokenConfigUpdateTransition::calculate_action_id_with_fields(platform_version,
...) or the equivalent version-dispatch helper) or else explicitly construct a
PlatformVersion pinned to v0 and pass it into the versioned API so the
confirmation ID matches v0; update all occurrences noted in the comment (lines
around 1081, 1296-1304, 1464-1469, 1687-1695, 1912-1920, 2149-2157, 2436-2444,
2580-2588) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs`:
- Around line 248-258: The test function
versioned_dispatch_uses_v1_on_current_platform_version currently calls
PlatformVersion::latest(), which will drift as new protocol versions are added;
replace that call with PlatformVersion::get(2).expect("expected version 2") so
the test is pinned to the protocol version where
token_config_update_action_id_version becomes 1—update the PlatformVersion value
used when calling t.calculate_action_id(owner_id, platform_version) and keep the
rest of the test (TokenConfigurationChangeItem, calculate_action_id) unchanged.

---

Outside diff comments:
In
`@packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs`:
- Around line 76-95: calculate_action_id_with_fields_v0 currently only appends
the price (.1 of minimum_purchase_amount_and_price()) which collapses distinct
schedules; modify calculate_action_id_with_fields_v0 to include the full
TokenPricingSchedule payload when present (not just the price) by serializing
the entire schedule (use the existing TokenPricingSchedule
representation/serialization used elsewhere) and appending those bytes into the
bytes buffer before hashing with hash_double; add a versioned path/marker string
(e.g., include a version suffix in the initial bytes like
"action_token_set_price_for_direct_purchase_v1") so future changes are
versioned, and ensure callers that compute multi-party direct-purchase action
IDs route through this updated function so the full schedule differentiates
action_id values.

---

Nitpick comments:
In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/config_update/mod.rs`:
- Around line 1081-1086: The tests call
TokenConfigUpdateTransition::calculate_action_id_with_fields_v0(...) while using
PlatformVersion::latest(), which will break when the canonical action-id format
changes; update each test (e.g. the call at calculate_action_id_with_fields_v0
and similar sites) to obtain the action id via the versioned transition API for
the current platform version (e.g. use
TokenConfigUpdateTransition::calculate_action_id_with_fields(platform_version,
...) or the equivalent version-dispatch helper) or else explicitly construct a
PlatformVersion pinned to v0 and pass it into the versioned API so the
confirmation ID matches v0; update all occurrences noted in the comment (lines
around 1081, 1296-1304, 1464-1469, 1687-1695, 1912-1920, 2149-2157, 2436-2444,
2580-2588) accordingly.

In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/freeze/mod.rs`:
- Around line 1434-1439: Tests are calling the hardcoded versioned helper
TokenFreezeTransition::calculate_action_id_with_fields_v0; replace those calls
with the transition's non-versioned calculate_action_id API (e.g., call the
proposer transition instance's calculate_action_id with the same
fields/nonce/target) so the tests follow runtime dispatch via
PlatformVersion::latest(); update all occurrences of
calculate_action_id_with_fields_v0 (including the other similar sites flagged)
to use the transition.calculate_action_id entrypoint instead.

In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mint/mod.rs`:
- Around line 1605-1610: The tests duplicate mint action_id derivation by
calling TokenMintTransition::calculate_action_id_with_fields_v0 directly;
replace those raw _v0 calls with the transition's stable API and centralize the
field list: use TokenMintTransition::calculate_action_id(..., platform_version)
where the proposer transition is available, and for the "other signer goes
first" scenario extract a small local helper in the test (e.g., local fn
derive_mint_action_id(token_id, identity, quantity, nonce, platform_version))
that calls the single derive implementation so the four input fields are defined
in one place and the suite is no longer pinned to the v0 helper. Ensure all
eight sites mentioned (including the given line and the other ranges) are
updated to call the centralized helper or calculate_action_id with
platform_version.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9523e435-d9f2-43c7-8af4-c32ff6cffc5e

📥 Commits

Reviewing files that changed from the base of the PR and between 71964ad and ba261c2.

📒 Files selected for processing (26)
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/multi_party_action.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v1_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs
  • packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/config_update/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/freeze/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mint/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/transfer/mod.rs

Copy link
Copy Markdown
Member Author

@QuantumExplorer QuantumExplorer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs (1)

306-320: ⚠️ Potential issue | 🟡 Minor

Pin this test to the activation protocol version instead of PlatformVersion::latest().

Line 313 currently uses latest(), which can change over time and make this test nondeterministic as new protocol versions are introduced.

Proposed patch
-        let platform_version = PlatformVersion::latest();
+        let platform_version = PlatformVersion::get(2).expect("expected version 2");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs`
around lines 306 - 320, The test
versioned_dispatch_uses_v1_on_current_platform_version is using
PlatformVersion::latest(), making it non-deterministic; replace that call with
the specific activation protocol PlatformVersion instance where
token_config_update_action_id_version is 1 (i.e., pin to the protocol version
that introduced v1 behavior) so calculate_action_id(owner_id, platform_version)
uses a stable, known PlatformVersion rather than PlatformVersion::latest();
update the test to obtain that concrete PlatformVersion instead of calling
latest().
🧹 Nitpick comments (5)
packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs (1)

61-75: Consider adding strict length validation for single-byte variants.

The Identity and Group variants enforce exact byte lengths (33 and 3 respectively), but NoOne, ContractOwner, and MainGroup accept any input with a matching tag byte, ignoring trailing bytes. For example, from_bytes(&[0, 0xFF, 0xFF]) returns Ok(NoOne).

For consistency and to avoid potential ambiguity in security-sensitive contexts (action_id calculation), consider validating exact lengths for all variants.

♻️ Proposed fix for consistent length validation
         match tag {
-            0 => Ok(AuthorizedActionTakers::NoOne),
-            1 => Ok(AuthorizedActionTakers::ContractOwner),
+            0 => {
+                if bytes.len() != 1 {
+                    return Err(ProtocolError::DecodingError(format!(
+                        "expected 1 byte for AuthorizedActionTakers::NoOne, got {}",
+                        bytes.len()
+                    )));
+                }
+                Ok(AuthorizedActionTakers::NoOne)
+            }
+            1 => {
+                if bytes.len() != 1 {
+                    return Err(ProtocolError::DecodingError(format!(
+                        "expected 1 byte for AuthorizedActionTakers::ContractOwner, got {}",
+                        bytes.len()
+                    )));
+                }
+                Ok(AuthorizedActionTakers::ContractOwner)
+            }
             2 => {
                 if bytes.len() != 33 {
                     return Err(ProtocolError::DecodingError(format!(
                         "expected 33 bytes for AuthorizedActionTakers::Identity, got {}",
                         bytes.len()
                     )));
                 }
                 let identifier = Identifier::from_bytes(&bytes[1..])
                     .map_err(|e| ProtocolError::DecodingError(e.to_string()))?;
                 Ok(AuthorizedActionTakers::Identity(identifier))
             }
-            3 => Ok(AuthorizedActionTakers::MainGroup),
+            3 => {
+                if bytes.len() != 1 {
+                    return Err(ProtocolError::DecodingError(format!(
+                        "expected 1 byte for AuthorizedActionTakers::MainGroup, got {}",
+                        bytes.len()
+                    )));
+                }
+                Ok(AuthorizedActionTakers::MainGroup)
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs`
around lines 61 - 75, The deserializer currently accepts extra trailing bytes
for single-byte variants (tags 0,1,3); update the from_bytes implementation (the
match on tag that constructs AuthorizedActionTakers::NoOne, ::ContractOwner,
::MainGroup and the existing Identity branch) to enforce exact lengths: require
bytes.len() == 1 for NoOne, ContractOwner and MainGroup (keep bytes.len()==33
for Identity and bytes.len()==3 for Group where applicable), and return a
ProtocolError::DecodingError with a clear message when lengths mismatch so all
variants are validated consistently against trailing data.
packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0/v0_methods.rs (1)

74-75: Clarify the doc comment to reflect version-dispatched behavior.

The current wording reads like unconditional v0 hashing, but this method accepts platform_version and routes through version-aware calculation. A short note listing both v0/v1 behaviors would prevent confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0/v0_methods.rs`
around lines 74 - 75, Update the doc comment for the function
calculate_action_id to state that it is version-dispatched via the
platform_version parameter and describe both behaviors: for v0 it uses only the
u8 discriminant (backward-compatible hashing), while for v1 (and later) it
performs the newer, full action-id hashing; mention the platform_version
argument so readers know the method routes to the appropriate versioned
implementation rather than always using v0 logic.
packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs (1)

168-170: Use the production payload serializer in tests to avoid encoding drift.

The test helper re-encodes items directly with bincode. Reusing payload_serialization() in test vectors would keep tests aligned with the production hashing path if serialization details evolve.

Also applies to: 224-237, 255-268, 291-297, 331-337, 355-368

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs`
around lines 168 - 170, The test helper function serialize_item currently
encodes TokenConfigurationChangeItem directly with bincode; replace that direct
bincode call with the production serializer payload_serialization() so test
vectors use the same serialization path as production (i.e., call
payload_serialization().serialize(item) or the equivalent API used elsewhere),
and make the same replacement for the other test helper re-encodings in this
file (the additional serialize_* helpers around the other item arrays) so all
test encodings mirror the production hashing/serialization implementation.
packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs (2)

100-123: Add explicit boundary tests for the option-backed variants.

MaxSupply, PerpetualDistribution, NewTokensDestinationIdentity, and MainControlGroup now define consensus bytes here with different None conventions. A small table-driven regression test around None vs Some(...) for these four branches would directly protect the collision boundary this PR is closing before version 1 is enabled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs`
around lines 100 - 123, Add table-driven unit tests for the
TokenConfigurationChangeItem serialization branches to assert explicit boundary
behavior for option-backed variants: MaxSupply, PerpetualDistribution,
NewTokensDestinationIdentity, and MainControlGroup. For each variant (use
TokenConfigurationChangeItem::MaxSupply, ::PerpetualDistribution,
::NewTokensDestinationIdentity, ::MainControlGroup) create test cases covering
None and at least one Some(...) value, encode to consensus bytes using the same
path exercised in the file (the code that maps to vec bytes / bincode encoding),
and assert that None and Some(...) produce distinct, expected byte sequences and
do not collide with each other or with empty vec conventions used in
MainControlGroup. Place tests near existing token_configuration_item tests to
protect the boundary behavior before v1.

69-70: Avoid modeling an empty hash payload as Some(vec![]).

The caller in packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs on Lines 152-153 only appends bytes when payload is Some, so None and Some(empty) already collapse to the same action_id input. Returning Some(vec![]) for MainControlGroup(None) makes that equivalence easy to miss in a consensus path. Either normalize empty cases to None or make this API return raw bytes so the contract is explicit.

Also applies to: 120-123

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs`
around lines 69 - 70, The payload_serialization method currently returns
Some(vec![]) for empty hash payloads (e.g., when matching
MainControlGroup(None)), which conflates None and Some(empty) for callers like
state_transitions::...::v0_methods.rs that only check for Some; update
payload_serialization (and the similar match at the other site around lines
120-123) to normalize empty payloads to None instead of Some(empty) — i.e., when
the computed Vec<u8> is empty return Ok(None), otherwise return Ok(Some(bytes));
reference the payload_serialization function and the MainControlGroup match arm
to locate where to implement this normalization.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs`:
- Around line 306-320: The test
versioned_dispatch_uses_v1_on_current_platform_version is using
PlatformVersion::latest(), making it non-deterministic; replace that call with
the specific activation protocol PlatformVersion instance where
token_config_update_action_id_version is 1 (i.e., pin to the protocol version
that introduced v1 behavior) so calculate_action_id(owner_id, platform_version)
uses a stable, known PlatformVersion rather than PlatformVersion::latest();
update the test to obtain that concrete PlatformVersion instead of calling
latest().

---

Nitpick comments:
In
`@packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs`:
- Around line 100-123: Add table-driven unit tests for the
TokenConfigurationChangeItem serialization branches to assert explicit boundary
behavior for option-backed variants: MaxSupply, PerpetualDistribution,
NewTokensDestinationIdentity, and MainControlGroup. For each variant (use
TokenConfigurationChangeItem::MaxSupply, ::PerpetualDistribution,
::NewTokensDestinationIdentity, ::MainControlGroup) create test cases covering
None and at least one Some(...) value, encode to consensus bytes using the same
path exercised in the file (the code that maps to vec bytes / bincode encoding),
and assert that None and Some(...) produce distinct, expected byte sequences and
do not collide with each other or with empty vec conventions used in
MainControlGroup. Place tests near existing token_configuration_item tests to
protect the boundary behavior before v1.
- Around line 69-70: The payload_serialization method currently returns
Some(vec![]) for empty hash payloads (e.g., when matching
MainControlGroup(None)), which conflates None and Some(empty) for callers like
state_transitions::...::v0_methods.rs that only check for Some; update
payload_serialization (and the similar match at the other site around lines
120-123) to normalize empty payloads to None instead of Some(empty) — i.e., when
the computed Vec<u8> is empty return Ok(None), otherwise return Ok(Some(bytes));
reference the payload_serialization function and the MainControlGroup match arm
to locate where to implement this normalization.

In
`@packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs`:
- Around line 61-75: The deserializer currently accepts extra trailing bytes for
single-byte variants (tags 0,1,3); update the from_bytes implementation (the
match on tag that constructs AuthorizedActionTakers::NoOne, ::ContractOwner,
::MainGroup and the existing Identity branch) to enforce exact lengths: require
bytes.len() == 1 for NoOne, ContractOwner and MainGroup (keep bytes.len()==33
for Identity and bytes.len()==3 for Group where applicable), and return a
ProtocolError::DecodingError with a clear message when lengths mismatch so all
variants are validated consistently against trailing data.

In
`@packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs`:
- Around line 168-170: The test helper function serialize_item currently encodes
TokenConfigurationChangeItem directly with bincode; replace that direct bincode
call with the production serializer payload_serialization() so test vectors use
the same serialization path as production (i.e., call
payload_serialization().serialize(item) or the equivalent API used elsewhere),
and make the same replacement for the other test helper re-encodings in this
file (the additional serialize_* helpers around the other item arrays) so all
test encodings mirror the production hashing/serialization implementation.

In
`@packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0/v0_methods.rs`:
- Around line 74-75: Update the doc comment for the function calculate_action_id
to state that it is version-dispatched via the platform_version parameter and
describe both behaviors: for v0 it uses only the u8 discriminant
(backward-compatible hashing), while for v1 (and later) it performs the newer,
full action-id hashing; mention the platform_version argument so readers know
the method routes to the appropriate versioned implementation rather than always
using v0 logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 03b70135-1b65-4ca4-b760-306f4c3e855a

📥 Commits

Reviewing files that changed from the base of the PR and between 2184a81 and cdec469.

📒 Files selected for processing (23)
  • packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs
  • packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/multi_party_action.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_freeze_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v1_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/config_update/mod.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unfreeze_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/multi_party_action.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_set_price_for_direct_purchase_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_destroy_frozen_funds_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_emergency_action_transition/v0/v0_methods.rs

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The vulnerability fix is correct and the versioning wiring is sound. TOKEN_VERSIONS_V1 uses action_id v0 (existing behavior), TOKEN_VERSIONS_V2 uses action_id v1 (payload-bound). The core fix works. However, 4 of 6 unit tests construct expected v1 hashes using bincode serialization of the whole enum rather than the production payload_serialization() method, which means regressions in that method would go undetected for most variants. One integration-style test (versioned_dispatch) does exercise the production path, partially mitigating this.

Reviewed commit: 6d290b1

🟡 2 suggestion(s) | 💬 3 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/v0_methods.rs`:
- [SUGGESTION] lines 168-170: 4 of 6 v1 unit tests use different serialization than production code
  `serialize_item()` calls `bincode::encode_to_vec(item, bincode::config::standard())` which serializes the entire `TokenConfigurationChangeItem` enum (including bincode's variant encoding). Production uses `payload_serialization()` which returns custom per-variant bytes — e.g. `MaxSupply(Some(100))` returns `100u64.to_be_bytes()` (8 bytes), not bincode encoding of the full enum.

The `versioned_dispatch_uses_v1_on_current_platform_version` test (line 306) does exercise the production path via `item.payload_serialization()` at line 336, so the dispatch integration is tested. But the 4 direct v1 tests (`v1_action_id_same_discriminant_different_values_produces_different_ids`, `v1_action_id_different_item_types_produces_different_ids`, `v0_and_v1_produce_different_ids_for_same_input`, `v1_action_id_identical_items_produces_same_id`) all use `serialize_item()` — they prove the v1 mechanism works but with non-production bytes.
- [SUGGESTION] lines 186-244: No test exercises payload_serialization() for AuthorizedActionTakers variants
  20+ variants carry `AuthorizedActionTakers` payloads serialized via `to_bytes()` in production, but every v1 test only uses `MaxSupply` and `MintingAllowChoosingDestination`. A regression in `AuthorizedActionTakers::to_bytes()` or in how `payload_serialization()` handles those variants would not be caught. Consider adding at least one test with an `AuthorizedActionTakers`-based variant (e.g. `ManualMinting(AuthorizedActionTakers::Group(5))`) to verify `to_bytes()` is correctly integrated.

Comment on lines +120 to +123
TokenConfigurationChangeItem::MainControlGroup(position) => match position {
Some(pos) => Some(pos.to_be_bytes().to_vec()),
None => Some(vec![]),
},
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: MainControlGroup(None) returns Some(vec![]) while other Option::None variants return None

MaxSupply(None), NewTokensDestinationIdentity(None), and PerpetualDistribution(None) all return None from payload_serialization(), but MainControlGroup(None) returns Some(vec![]). Functionally this is harmless — Some(&[]) causes extend_from_slice(&[]) which is a no-op, producing the same hash as the None path. But the inconsistency may confuse future maintainers. Consider returning None for consistency, or adding a comment explaining the intentional difference.

source: ['claude-rust-quality']

The 3 v1 tests were using a bincode serialize_item() helper that
serialized the entire enum (including bincode's variant discriminant),
but production uses payload_serialization() which serializes only the
inner value. This meant the tests proved hash diffusion (trivially true)
rather than that the production serialization differentiates values.

All 3 tests now go through the full production calculate_action_id()
path with PlatformVersion::latest(), directly proving the vote-swap
fix works end-to-end. Removed the serialize_item() helper entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
QuantumExplorer added a commit that referenced this pull request Mar 17, 2026
…hedule

The action_id for TokenSetPriceForDirectPurchaseTransition only hashed
minimum_purchase_amount_and_price().1 (the credit price of the lowest
tier). This meant different pricing schedules with the same minimum-tier
price produced identical action_ids, enabling the same vote-swap attack
as the token config update vulnerability fixed in PR #3346.

Examples of collisions in the old code:
- SetPrices({1: 100, 10: 800}) vs SetPrices({1: 100, 10: 9999})
- SinglePrice(100) vs SetPrices({1: 100})
- SetPrices({5: 100, ...}) vs SetPrices({10: 100, ...})

Fix: serialize the full TokenPricingSchedule with bincode and include
it in the hash. This binds the voted-on pricing schedule into the
action_id, preventing vote-swap attacks.

Note: Unlike the config update fix, this is NOT gated behind a platform
version because SetPriceForDirectPurchase is a new feature (v3.1) that
has not been used in production yet. There is no backward compatibility
concern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…onsistency

- Fix MainControlGroup(None) to return None from payload_serialization()
  instead of Some(vec![]), matching MaxSupply(None) and other None variants
- Add test for AuthorizedActionTakers-based variants (ManualMinting with
  Group(5) vs Group(9) vs NoOne) through the production path
- Add test proving None-payload variants (MaxSupply(None),
  NewTokensDestinationIdentity(None), PerpetualDistribution(None),
  MainControlGroup(None)) produce different action_ids via discriminant

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
QuantumExplorer added a commit that referenced this pull request Mar 17, 2026
…hedule

The action_id for TokenSetPriceForDirectPurchaseTransition only hashed
minimum_purchase_amount_and_price().1 (the credit price of the lowest
tier). This meant different pricing schedules with the same minimum-tier
price produced identical action_ids, enabling the same vote-swap attack
as the token config update vulnerability fixed in PR #3346.

Examples of collisions in the old code:
- SetPrices({1: 100, 10: 800}) vs SetPrices({1: 100, 10: 9999})
- SinglePrice(100) vs SetPrices({1: 100})
- SetPrices({5: 100, ...}) vs SetPrices({10: 100, ...})

Fix: serialize the full TokenPricingSchedule with bincode and include
it in the hash. This binds the voted-on pricing schedule into the
action_id, preventing vote-swap attacks.

Note: Unlike the config update fix, this is NOT gated behind a platform
version because SetPriceForDirectPurchase is a new feature (v3.1) that
has not been used in production yet. There is no backward compatibility
concern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
QuantumExplorer added a commit that referenced this pull request Mar 17, 2026
- calculate_action_id_with_fields returns Result with map_err instead
  of using expect for bincode encoding
- Update wrapper and v0 impls to match the AllowedAsMultiPartyAction
  trait signature from #3346 (platform_version + Result)
- Fix all tests to pass platform_version and unwrap Results
- Rebase on #3346 branch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@QuantumExplorer QuantumExplorer merged commit 5e7d433 into v3.1-dev Mar 17, 2026
38 checks passed
@QuantumExplorer QuantumExplorer deleted the fix/token-config-action-id-bind-payload branch March 17, 2026 05:19
QuantumExplorer added a commit that referenced this pull request Mar 17, 2026
…hedule

The action_id for TokenSetPriceForDirectPurchaseTransition only hashed
minimum_purchase_amount_and_price().1 (the credit price of the lowest
tier). This meant different pricing schedules with the same minimum-tier
price produced identical action_ids, enabling the same vote-swap attack
as the token config update vulnerability fixed in PR #3346.

Examples of collisions in the old code:
- SetPrices({1: 100, 10: 800}) vs SetPrices({1: 100, 10: 9999})
- SinglePrice(100) vs SetPrices({1: 100})
- SetPrices({5: 100, ...}) vs SetPrices({10: 100, ...})

Fix: serialize the full TokenPricingSchedule with bincode and include
it in the hash. This binds the voted-on pricing schedule into the
action_id, preventing vote-swap attacks.

Note: Unlike the config update fix, this is NOT gated behind a platform
version because SetPriceForDirectPurchase is a new feature (v3.1) that
has not been used in production yet. There is no backward compatibility
concern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
QuantumExplorer added a commit that referenced this pull request Mar 17, 2026
- calculate_action_id_with_fields returns Result with map_err instead
  of using expect for bincode encoding
- Update wrapper and v0 impls to match the AllowedAsMultiPartyAction
  trait signature from #3346 (platform_version + Result)
- Fix all tests to pass platform_version and unwrap Results
- Rebase on #3346 branch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

All four prior findings are genuinely resolved. The incremental changes are correct: MainControlGroup(None) refactor is hash-equivalent, tests now use the production path, and new tests fill prior coverage gaps. The two new agent findings (PlatformVersion::latest() brittleness, v0-vs-v1 test naming) are technically valid but represent standard idiomatic patterns in this codebase and do not warrant action.

Reviewed commit: fb32011

QuantumExplorer added a commit that referenced this pull request Mar 17, 2026
Gate the action_id fix behind platform version, matching the pattern
from #3346 (token config update):

- Add token_set_price_action_id_version field to DPPTokenVersions
- v0: hashes only minimum_purchase_amount_and_price().1 (existing)
- v1: hashes the full serialized TokenPricingSchedule (fix)
- Create TOKEN_VERSIONS_V3 with both fixes enabled
- Update v12 to use TOKEN_VERSIONS_V3
- Add v0 vulnerability regression test and v0-vs-v1 differentiation test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
QuantumExplorer added a commit that referenced this pull request Mar 17, 2026
After #3346 merged, calculate_action_id takes platform_version as a
second argument. Updated 5 test calls to pass PlatformVersion::latest().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants