Skip to content

fix(drive)!: create distribution trees for tokens added by contract update - #4834

Merged
QuantumExplorer merged 5 commits into
v4.2-devfrom
claude/hungry-tu-ee9d28
Sep 19, 2026
Merged

QuantumExplorer merged 5 commits into
v4.2-devfrom
claude/hungry-tu-ee9d28

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Sep 19, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

A token added to a contract through a data contract update never gets its distribution storage, so its perpetual and pre-programmed distributions can not be claimed.

insert_contract v1 creates the per-token distribution storage at registration (add_perpetual_distribution, add_pre_programmed_distributions). update_contract v1 only calls create_token_trees_operations for each token, even though validate_update_tokens explicitly allows an update to add tokens, distributions included, and the update's state validation even checks the distribution recipients exist. For such a token none of this is written:

  • [Tokens, distributions, perpetual(64), token_id] with its info item and last-claim subtree
  • [Tokens, distributions, pre-programmed(192), token_id] with its last-claim subtree and per-release sum trees
  • the [Tokens, distributions, timed(128), ms(128), time] references each pre-programmed release is scheduled under

Confirmed with failing tests before the fix. A claim reads the missing last-claim path as None, computes a payout, and then fails while applying:

  • perpetual: invalid path: could not get key for parent of subtree for batch at path [0x10/0x20/@/<token_id>] for key 0xc0 (the last-claim insert)
  • pre-programmed: ... at path [0x10/0x20/0x80/0x80] for key 0x0000000000000064 (the delete of the scheduled reference, which fails before the last-claim insert is reached)

Both surface as InternalError: never paid for, stripped from every proposal, so the distribution is unclaimable for good.

What was done?

update_contract v2 (Drive), selected by protocol version 14 only. It calls update_contract_operations_v1 and then adds the perpetual, pre-programmed and once-per-identity distribution storage, with the same helpers the insert uses, for tokens that are absent from original_contract.tokens(). This is the way v1 already builds on update_contract_operations_v0. The helpers error when the token's tree already exists, and a token config update reaches the same method with its token present in the original contract, so the gate is absence from the original. v1's only change is the visibility of that one function.

Version tables. DRIVE_CONTRACT_METHOD_VERSIONS_V4, which protocol version 14 already introduced, is amended in place (update.update_contract: 2). No new table version.

No backfill for tokens added by update before the upgrade. v2 leaves tokens the contract already had alone, so a token added by update before protocol version 14 stays without storage. An earlier revision of this PR repaired those on the first block of version 14; it was removed because mainnet has nothing to repair, checked at block 436796 (platform-explorer.pshenmic.dev for the transition list, dpp for the decoding):

  • all 20 data contract updates ever broadcast (12 succeeded) decode to a contract without tokens, and an update that adds a token has to carry it
  • the 5 tokens on mainnet all belong to contracts still at version 1, so none was ever touched by a contract update or a token config update, and none has a perpetual or pre-programmed distribution

The walk could only ever be a no-op there, and it was the riskiest part of the change (state rewritten on the upgrade block, where an error halts the chain). transition_to_version_14 is back to what v4.2-dev has. Testnet and devnets were not checked: a token added by update there before the upgrade stays unclaimable. The window stays open until protocol version 14 activates, so this is worth one more look right before the mainnet upgrade.

Not in this PR (found on the way, flagged separately)

  • The base supply of a token added by update is never minted either (create_token_trees_operations initialises supply to 0; only the insert credits base_supply). Monetary, so left for a separate decision.
  • A token config update that introduces or changes a perpetual distribution goes through update_contract with its token present in the original contract, so it would create or refresh no storage. A follow-up investigation found this unreachable today (structure validation refuses that change item), so it is latent only.
  • Two tokens in one contract sharing a pre-programmed release time fail contract create with insertion order error (reproduced); v2 behaves identically when one update adds two such tokens.
  • A perpetual distribution of a token added by update pays from the contract's creation moment, not from the block the token was added. Unchanged here.

Interaction with #4827 (once-per-identity distribution)

#4827 created the once-per-identity claims subtree of a token added by update inside update_contract v1, the generation protocol version 14 selected at the time. That kind is new in protocol version 14, which selects v2 after this PR, so the block and its test move into v2, next to the perpetual and pre-programmed storage and inside the same absent-from-the-original-contract gate. v1 is back to what it was before #4827, apart from the visibility of update_contract_operations_v1.

How Has This Been Tested?

Red first: the two drive-abci claim tests below failed with the errors quoted above before the fix, and pass after it.

  • cargo test -p drive --lib -- drive::contract::update::update_contract (27 passed), which includes:
    • update_contract::v2::tests: perpetual and pre-programmed storage exists for a token added by update and a claim can be recorded; tokens the contract already had (from registration or an earlier update) are not recreated; protocol version 13 through the same dispatcher still leaves the token without storage
    • feat(platform)!: once-per-identity token distribution #4827's once-per-identity update test, moved into update_contract::v2::tests
    • update_contract::v1::tests otherwise unchanged, which at the latest protocol version now run through v2
  • cargo test -p drive-abci --lib -- data_contract_update token::distribution once_per_identity (136 passed), which includes, through process_raw_state_transitions:
    • should_claim_perpetual_distribution_of_token_added_by_update, should_claim_pre_programmed_distribution_of_token_added_by_update
    • should_fail_to_claim_distributions_of_token_added_by_update_on_protocol_version_13 (still InternalError)
  • cargo clippy -p platform-version -p drive -p drive-abci --all-features --all-targets -- -D warnings
  • cargo fmt --all

Not run: the full drive and drive-abci suites and the strategy tests (left to CI).

Breaking Changes

Consensus change, gated to protocol version 14:

  • a data contract update that adds a token with a perpetual or pre-programmed distribution now writes that token's distribution storage (and is charged for it)
  • an update adding a token whose pre-programmed amounts do not fit a sum tree now fails as an internal error, exactly as the same token always has on contract create; before, the update was accepted and the distribution was silently unclaimable

Protocol versions up to 13 are unchanged.

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

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

…pdate

A token added through a data contract update never got its perpetual or
pre-programmed distribution storage: update_contract v1 only creates the
token's balance, identity info, status, contract info and supply entries,
while insert_contract v1 also calls add_perpetual_distribution and
add_pre_programmed_distributions. A claim on such a token read the missing
last-claim path as None and then failed while applying (the last-claim
insert for perpetual, the scheduled-reference delete for pre-programmed),
so it became an InternalError, was stripped from every proposal, and the
distribution was unclaimable.

- update_contract v2, selected by protocol version 14 only
  (DRIVE_CONTRACT_METHOD_VERSIONS_V4 amended in place), creates the
  storage for tokens absent from the original contract. v1 is unchanged
  outside its tests, which are now pinned to protocol version 13.
- transition_to_version_14 backfills the storage of tokens added by
  update before the upgrade
  (Drive::add_missing_token_distribution_storage_to_all_contracts). It is
  idempotent, runs inside the block transaction, and skips a
  pre-programmed distribution whose amounts no sum tree can hold instead
  of failing the upgrade block.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 26 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: dashpay/platform/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: cfe365f3-dc67-4243-88f3-2dbf9bbad5b0

📥 Commits

Reviewing files that changed from the base of the PR and between cf86db8 and dc3c53d.

📒 Files selected for processing (6)
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs
  • packages/rs-drive/src/drive/contract/update/update_contract/mod.rs
  • packages/rs-drive/src/drive/contract/update/update_contract/v1/mod.rs
  • packages/rs-drive/src/drive/contract/update/update_contract/v2/mod.rs
  • packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v4.rs
  • packages/rs-platform-version/src/version/drive_versions/v9.rs

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.

@github-actions github-actions Bot added this to the v4.2.0 milestone Sep 19, 2026
@thepastaclaw

thepastaclaw commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

🕓 Queued for automated review — 8th in line, estimated start in ~40 min (commit dc3c53d)
Estimated review time once started: ~15 min (two-phase automated review; median of recent runs).

  • Request priority review — click to move this review to the front of the queue.

@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

PR Hygiene

State: waiting-bots · commit dc3c53de8b12aadff3a77f3303037003b9d930e2

  • coderabbitai has not reported for the current head
  • thepastaclaw has not reported for the current head

Self-review is an author attestation that you have read the diff:
/self-reviewed — covers everything pushed so far; post it again after a new push.

This report does not bypass CI or repository protection rules.

@codecov

codecov Bot commented Sep 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.10564% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.19%. Comparing base (cf86db8) to head (dc3c53d).
⚠️ Report is 5 commits behind head on v4.2-dev.

Files with missing lines Patch % Lines
...e/src/drive/contract/update/update_contract/mod.rs 59.25% 11 Missing ⚠️
...rc/drive/contract/update/update_contract/v2/mod.rs 98.00% 9 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           v4.2-dev    #4834      +/-   ##
============================================
+ Coverage     77.49%   79.19%   +1.70%     
============================================
  Files          2976     2978       +2     
  Lines        428621   432179    +3558     
============================================
+ Hits         332167   342273   +10106     
+ Misses        96454    89906    -6548     
Components Coverage Δ
dpp 76.49% <ø> (+1.95%) ⬆️
drive 79.95% <95.83%> (+0.79%) ⬆️
drive-abci 81.81% <100.00%> (+3.11%) ⬆️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 86.65% <ø> (+0.55%) ⬆️
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 29.26% <ø> (+0.60%) ⬆️
🚀 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.

QuantumExplorer and others added 4 commits September 19, 2026 11:40
Brings in #4827 (once-per-identity token distribution).

Textual conflict in transition_to_version_14: both sides appended a step.
Kept both, the token history contract v2 and the once-per-identity root
tree first, then the distribution storage backfill.

Semantic conflict git merged silently: #4827 created the once-per-identity
claims subtree of a token added by update inside update_contract v1, but
protocol version 14 selects v2 since this branch, so on the merged tree
such a token got no subtree and
test_update_contract_v1_adds_token_with_once_per_identity_distribution
failed. The block and its test move into v2, inside the same
absent-from-the-original-contract gate as the perpetual and pre-programmed
storage, and v1 returns to its shipped body with its tests pinned to
protocol version 13.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ng to repair

The first block of protocol version 14 walked every contract to create the
distribution storage of tokens that an update had added before the
upgrade. Mainnet has no such token, checked at block 436796:

- all 20 data contract updates ever broadcast (12 succeeded) decode to a
  contract without tokens, and an update that adds a token has to carry it
- the 5 tokens on mainnet all belong to contracts still at version 1, so
  none was ever touched by a contract update or a token config update, and
  none has a perpetual or pre-programmed distribution

So the walk could only ever be a no-op there, and it was the riskiest part
of the change: state rewritten on the upgrade block, where an error halts
the chain. transition_to_version_14 and the migration module are back to
what v4.2-dev has. update_contract v2 is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
v2 was a full copy of v1 with the distribution storage added. It now calls
update_contract_operations_v1 and appends the perpetual and pre-programmed
storage of the tokens the update adds, the way v1 builds on
update_contract_operations_v0.

v1 is back to what v4.2-dev has, apart from the visibility of that one
function. That includes the once-per-identity claims subtree #4827 put
there, which v2 now gets by delegation instead of carrying its own copy,
and v1's tests, which run through v2 at the latest protocol version and so
need neither pinning nor duplicating.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e_contract v2

#4827 created the claims subtree of a token added by update inside
update_contract v1, the generation protocol version 14 selected at the
time. The once-per-identity kind is new in protocol version 14 and that
version selects v2 now, so the block and its test move there, next to the
perpetual and pre-programmed storage and inside the same
absent-from-the-original-contract gate.

v1 is back to what it was before #4827, apart from the visibility of
update_contract_operations_v1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@QuantumExplorer QuantumExplorer left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reviewed

@QuantumExplorer
QuantumExplorer merged commit 3b06b15 into v4.2-dev Sep 19, 2026
18 checks passed
@QuantumExplorer
QuantumExplorer deleted the claude/hungry-tu-ee9d28 branch September 19, 2026 12:49
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