Skip to content

fix(drive-abci): free advanced structure validation for invalid batch transitions#3608

Closed
shumkov wants to merge 5 commits intov3.1-devfrom
fix/issue-2867-bump-nonce-on-failed-batch-document-transitions
Closed

fix(drive-abci): free advanced structure validation for invalid batch transitions#3608
shumkov wants to merge 5 commits intov3.1-devfrom
fix/issue-2867-bump-nonce-on-failed-batch-document-transitions

Conversation

@shumkov
Copy link
Copy Markdown
Collaborator

@shumkov shumkov commented May 7, 2026

Issue being fixed or feature implemented

Validators currently do advanced-structure validation work for free on invalid batch document transitions. When a per-transition check fails — find_replaced_document_v0, check_ownership_of_old_replaced_document_v0, check_revision_is_bumped_by_one_during_replace_v0, DocumentNotForSaleError, DocumentIncorrectPurchasePriceError — the failure path returns errors-only with no action data. The empty BatchTransitionAction flows up through flatten / merge_many and the transition is recorded as PaidConsensusError, but because there's no BumpIdentityDataContractNonce action, no UpdateIdentityContractNonce drive op is created. Two consequences:

  • The user is only charged the bare-bump fee (≤ 41880 credits), not for the actual validation work the validator performed (document fetch + ownership/revision check).
  • The contract nonce in state stays at the pre-tx value, so the same exact bytes can be re-broadcast — they pass CheckTx FirstTimeCheck again because the nonce slot is still free.

The mainnet duplicate-transaction symptom (issue #286735C08D…313C reappearing in multiple blocks) is the most visible consequence. The architectural mainnet fix is in #3616 (return data: None from the aggregators so all-failed batches become UnpaidConsensusError and are removed from the block). This PR is the complementary layer: when a transition does land in a block but its per-transition check fails, the user pays for the work that ran.

What was done?

Patched all 13 per-transition failure paths in packages/rs-drive-abci/.../batch/transformer/v0/mod.rs::transform_document_transition_v0 to emit a BumpIdentityDataContractNonceAction:

Arm Failure paths bumped
Replace find_replaced, check_ownership, check_revision
Transfer find_replaced, check_ownership, check_revision
UpdatePrice find_replaced, check_ownership, check_revision
Purchase find_replaced, DocumentNotForSale, DocumentIncorrectPurchasePrice, check_revision

Cleaned up the now-dead let result = ConsensusValidationResult::new(); and if result.is_valid() { ... } else { Ok(result) } tails in those four arms.

How Has This Been Tested?

New regression test at batch/tests/document/replacement.rs::replayed_failed_replace_with_consumed_nonce_must_be_rejected_at_check_tx:

  1. Create at nonce 2 → succeeds, doc at revision 1.
  2. Replace at nonce 3 with revision 3 → check_revision_is_bumped_by_one fails.
  3. Assert the bump advanced the contract nonce in state.
  4. Re-submit the same exact bytes through state_transition_to_execution_event_for_check_tx with FirstTimeCheck.
  5. Assert it's rejected with InvalidIdentityNonceError.

Three sibling tests had hard-coded fee assertions matching the old (under-charged) numbers; updated:

  • test_document_replace_on_document_type_that_is_not_mutable: 41880445700
  • test_document_transfer_that_does_not_yet_exist: 36200517400
  • test_document_set_price_on_not_owned_document: 36200571240

The new fee covers the fetch + structural check that ran before the failure (same drive ops, just charged correctly).

  • cargo test -p drive-abci --lib batch::tests::document: 72 passed, 0 failed.

Breaking Changes

Consensus-affecting for PROTOCOL_VERSION_12 (current v3.1-dev, not yet active on mainnet — mainnet runs PROTOCOL_VERSION_11):

  1. State: identity_contract_nonce advances on every per-transition failure path now, not just on find_replaced_document_v0 for Replace.
  2. Fees: validators that fail the same transition pay a higher processing fee — the fee covers the document fetch + ownership/revision check that already ran. Those drive ops were happening before; they were just being charged as 0.

Activation should coincide with the v3.1 hard-fork.

Checklist

  • Self-reviewed
  • Comments on hard-to-understand areas
  • Added/updated tests
  • "!" in title — n/a (pre-release internal protocol version; consensus impact described above; no public API break)
  • Documentation changes — n/a

Summary by CodeRabbit

  • Bug Fixes

    • Improved cost handling for failed document operations; validation fees are now properly assessed when operations fail validation checks.
  • Tests

    • Enhanced test coverage for document operations with version-specific fee validations across protocol versions.

Closes #2867.

The Documents Batch transformer's per-transition handler was emitting
BumpIdentityDataContractNonceAction only on `find_replaced_document_v0`
failure for the Replace arm. Every other failure path (ownership-check,
revision-check, plus the analogous paths in Transfer / UpdatePrice /
Purchase, and Purchase's listed-price / price-mismatch checks) returned
ConsensusValidationResult errors-only with no action data. The empty
BatchTransitionAction that flowed up the chain still triggered fee
accounting (PaidConsensusError) but the bump action's
UpdateIdentityContractNonce drive op was never created — so
identity_contract_nonce in state stayed at the pre-tx value.

Consequence on mainnet: a user's SDK could re-broadcast the same exact
serialized bytes; CheckTx FirstTimeCheck would still pass (committed
state still says the nonce is unused), the proposer would include the
tx in a later block, deliver_tx would fail again with the same error,
and the cycle could repeat. Result: the same state-transition hash
appearing in multiple blocks — exactly what mainnet ST hash
35C08D574302D32D7160E603D8159042C8606BE12FD97D952CF5FD40DB57313C did
on 2026-05-04 (block 361774 idx 1 + a later block past the explorer
indexer's panic point).

Fix: copy the bump-emission pattern from the existing
find_replaced_document_v0 failure path (lines 672-686) to all other
failure sites in the Replace / Transfer / UpdatePrice / Purchase
handlers. Each now returns
`new_with_data_and_errors(BumpIdentityDataContractNonce(bump_action), errors)`
so the contract nonce advances regardless of where the per-transition
validation fails.

This is a consensus-affecting change on PROTOCOL_VERSION_12 (current
v3.1-dev). Validators that fail the same transition under the new
behavior pay a higher fee than under the old behavior because the
fee now covers the document fetch + ownership/revision check work
that ran before the failure (those drive ops were already happening,
just charged as 0). Three sibling tests had hard-coded fee assertions
matching the old (under-charged) numbers; they're updated with comments
explaining the change.

Test plan
- Added regression test
  `replayed_failed_replace_with_consumed_nonce_must_be_rejected_at_check_tx`
  in batch/tests/document/replacement.rs that reproduces the
  Techawanka.dash mainnet 35C0 shape (Create at nonce 2 → Replace at
  nonce 3 with mismatching revision → assert state nonce advanced AND
  CheckTx of identical bytes returns InvalidIdentityNonceError).
  RED on parent commit, GREEN with the fix.
- Added decoder pin
  `should_decode_mainnet_35c0_documents_batch_replace_replay`
  in rs-dpp serialization tests confirming the mainnet wire bytes
  deserialize cleanly to a Batch / Replace / nonce=3 — the explorer's
  reported "Cannot deserialize" is its own version-skew, not a
  chain-bytes issue.
- `cargo test -p drive-abci --lib`: 2424 passed, 0 failed (full suite).
- `cargo test -p drive-abci --lib batch::tests::`: 254 passed, 0 failed.

Out of scope
- platform-explorer's `UNIQUE(hash)` constraint on `state_transitions`
  is too strict regardless of this fix; Tenderdash never guaranteed
  cross-block tx-hash uniqueness (failed-but-paid txs are committed by
  protocol design, like Ethereum reverts). Recommend
  `UNIQUE(block_height, hash)` upstream in pshenmic/platform-explorer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shumkov shumkov requested a review from QuantumExplorer as a code owner May 7, 2026 06:16
@github-actions github-actions Bot added this to the v3.1.0 milestone May 7, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a new failed_per_transition_action feature version field to batch document transition validation configuration, enabling protocol version 12+ to emit BumpIdentityDataContractNonce batched actions when document transitions fail validation. The transformer is refactored to route Replace, Transfer, UpdatePrice, and Purchase failures through a helper that returns both bump actions and errors (or errors-only for older protocol versions). Test expectations are updated to reflect higher processing fees that include bump action costs, and a regression test validates that failed transitions consume identity contract nonce to prevent replay attacks.

Changes

Batch Document Transition Fee Bump Actions

Layer / File(s) Summary
Platform Version Configuration Schema
packages/rs-platform-version/src/version/.../mod.rs
Adds failed_per_transition_action: FeatureVersion field to DriveAbciDocumentsStateTransitionValidationVersions with documentation describing emitted action behavior across protocol versions.
Protocol Version Initialization
packages/rs-platform-version/src/version/.../v1.rs, v2.rs, v3.rs, v4.rs, v5.rs, v6.rs, v7.rs, v8.rs
Field initialized to 0 (disabled) in v1–v7, and set to 1 (enabled) in v8 for PROTOCOL_VERSION_12. v5 also enables masternode vote nonce validation.
Transformer Core Implementation
packages/rs-drive-abci/src/.../transformer/v0/mod.rs
Replace, Transfer, UpdatePrice, Purchase branches refactored to emit BumpIdentityDataContractNonce actions on validation failures (missing document, ownership/revision mismatch, price validation). New helper routes behavior by protocol version.
Test Updates – NFT, Transfer, Replace
packages/rs-drive-abci/src/.../tests/document/nft.rs, transfer.rs, replacement.rs
Tests refactored with protocol-version-aware helpers asserting version-specific expected fees (e.g., 571240 for PV12+, legacy values for PV11). Regression test validates nonce consumption prevents replay.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A hop and a bump in the ledger we go,
Protocol-version fees now steal the show!
When transitions stumble, the nonce takes a leap,
Preventing replays in the blockchain's keep. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and clearly summarizes the main change: ensuring identity contract nonce is bumped (free advanced structure validation) for invalid batch transitions, which is the core fix described in the PR objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-2867-bump-nonce-on-failed-batch-document-transitions

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 May 7, 2026

Codecov Report

❌ Patch coverage is 61.11111% with 42 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.29%. Comparing base (d048dc8) to head (24437ef).

Files with missing lines Patch % Lines
...tion/state_transitions/batch/transformer/v0/mod.rs 61.11% 42 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           v3.1-dev    #3608      +/-   ##
============================================
- Coverage     88.29%   88.29%   -0.01%     
============================================
  Files          2479     2479              
  Lines        301659   301710      +51     
============================================
+ Hits         266347   266381      +34     
- Misses        35312    35329      +17     
Components Coverage Δ
dpp 87.95% <ø> (ø)
drive 87.37% <ø> (ø)
drive-abci 90.23% <61.11%> (-0.02%) ⬇️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.17% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 55.66% <ø> (ø)
🚀 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.

@thepastaclaw
Copy link
Copy Markdown
Collaborator

thepastaclaw commented May 7, 2026

✅ Review complete (commit 24437ef)

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-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs (1)

667-758: ⚡ Quick win

Factor out the nonce-bump failure return and drop the inert result.

result is never mutated here, so if result.is_valid() is always true and the else branch is dead. The repeated BumpIdentityDataContractNonce construction across these arms is also the exact pattern that drifted and caused this bug class in the first place. A small helper that builds the bump+errors result would make the control flow clearer and reduce the chance of missing another failure path later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs`
around lines 667 - 758, The code creates identical BumpIdentityDataContractNonce
actions and returns ConsensusValidationResult with errors in several places
while keeping an inert local result
(ConsensusValidationResult::<BatchedTransitionAction> result) that is never
mutated; remove that dead variable and factor the repeated failure-return into a
small helper (e.g. build_bump_failure_result or bump_with_errors) that takes the
borrowed DocumentBaseTransition (document_replace_transition.base()), owner_id
and the validation errors and returns
ConsensusValidationResult::new_with_data_and_errors(BatchedTransitionAction::BumpIdentityDataContractNonce(<...>),
errors) using
BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition;
replace each repeated block that constructs the bump action and returns with
calls to this helper, and after creating execution_context.add_operation(...)
simply return the successful DocumentReplaceTransitionAction instead of checking
the unused result.
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs (1)

868-880: ⚡ Quick win

Assert the exact consumed nonce here.

assert_ne! proves “something changed,” but this regression is really about consuming exactly one contract nonce on the failed replace. If this ever starts double-bumping, this test would still pass while consensus behavior changed.

Suggested tightening
-        assert_ne!(
-            post_replace_nonce, post_create_nonce,
-            "BUG: failed Replace's bump action did not advance the contract \
-             nonce. Stored nonce is still {:`#x`} (= post-create value), so the \
-             same exact serialized bytes can be replayed forever. \
-             Root cause: batch/transformer/v0/mod.rs:712-715 (and 697-700) \
-             return Ok(result) with errors-only and no BumpIdentityDataContractNonce \
-             action when check_revision_is_bumped_by_one_during_replace_v0 (or \
-             check_ownership_of_old_replaced_document_v0) fails — unlike the \
-             find_replaced_document_v0 failure path on the same arm (lines \
-             672-686) which does emit the bump.",
-            post_create_nonce
-        );
+        assert_eq!(
+            post_replace_nonce,
+            post_create_nonce + 1,
+            "failed Replace should consume exactly one identity_contract_nonce"
+        );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs`
around lines 868 - 880, The assertion currently only checks that
post_replace_nonce differs from post_create_nonce (assert_ne!), but we must
assert it was advanced by exactly one; update the test to assert that
post_replace_nonce equals post_create_nonce + 1 (use the appropriate increment/
wrapping_add for the nonce type) so the test fails if nonce is not consumed
exactly once (refer to the variables post_replace_nonce and post_create_nonce in
the replacement test).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs`:
- Around line 868-880: The assertion currently only checks that
post_replace_nonce differs from post_create_nonce (assert_ne!), but we must
assert it was advanced by exactly one; update the test to assert that
post_replace_nonce equals post_create_nonce + 1 (use the appropriate increment/
wrapping_add for the nonce type) so the test fails if nonce is not consumed
exactly once (refer to the variables post_replace_nonce and post_create_nonce in
the replacement test).

In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs`:
- Around line 667-758: The code creates identical BumpIdentityDataContractNonce
actions and returns ConsensusValidationResult with errors in several places
while keeping an inert local result
(ConsensusValidationResult::<BatchedTransitionAction> result) that is never
mutated; remove that dead variable and factor the repeated failure-return into a
small helper (e.g. build_bump_failure_result or bump_with_errors) that takes the
borrowed DocumentBaseTransition (document_replace_transition.base()), owner_id
and the validation errors and returns
ConsensusValidationResult::new_with_data_and_errors(BatchedTransitionAction::BumpIdentityDataContractNonce(<...>),
errors) using
BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition;
replace each repeated block that constructs the bump action and returns with
calls to this helper, and after creating execution_context.add_operation(...)
simply return the successful DocumentReplaceTransitionAction instead of checking
the unused result.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 71594617-1229-483e-aab0-52132a18b224

📥 Commits

Reviewing files that changed from the base of the PR and between ec0a605 and 5cdc306.

📒 Files selected for processing (5)
  • packages/rs-dpp/src/state_transition/serialization.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs

Copy link
Copy Markdown
Member

@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.

We need to add a test that the state transition that causes a dedup continues to do the same (bad) thing in version 11 and below. Also the fix you did is not versioned which would lead to a chain stall.

BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition(
document_replace_transition.base(),
owner_id,
0,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be the user_fee_increase, not 0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nvm, it can be anything, but we should leave a comment saying that it's okay that it's 0 because it's in a batch_action, and the batch_action will override the user fee increase.

// Transfer find-replaced-document failure path now emits a bump action
// (previously dropped, leaking nonce-replay risk). Cost covers the
// fetch+validation work that was already happening.
assert_eq!(processing_result.aggregated_fees().processing_fee, 517400);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We need to add a test to verify that in version 11 the fees stay constant.

// PROTOCOL_VERSION_12. Issue #2867 fix: the failure path now emits the
// bump after running the document fetch + validation work, so the fee
// covers that work — same drive ops, just charged correctly.
assert_eq!(processing_result.aggregated_fees().processing_fee, 445700);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We need to add a test that in version 11 the fees stay the same.

// UpdatePrice ownership-mismatch failure path now emits a bump action
// (previously dropped, leaking nonce-replay risk). Cost covers the
// fetch+validation work that was already happening.
assert_eq!(processing_result.aggregated_fees().processing_fee, 571240);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We need to add a test that in version 11 the fees stay the same.

@shumkov shumkov marked this pull request as draft May 7, 2026 15:05
shumkov added a commit that referenced this pull request May 7, 2026
…atch transformer

Per review feedback on PR #3616: rather than versioning the ~1100-line
batch transformer to switch aggregator behavior at PROTOCOL_VERSION_12,
version `ConsensusValidationResult::flatten` / `merge_many` themselves.

The semantic change for issue #2867 is at the aggregator layer (return
`data: None` when no input contributed any data, instead of the legacy
`Some(empty_vec)`), so the version gate now lives where the actual
behavior change is — the transformer is single-version and just calls
the facade with `platform_version`.

- New `validation_result/{mod.rs,v0.rs,v1.rs}` — facade dispatches via
  `platform_version.dpp.validation.validation_result.{flatten, merge_many}`
- `DPP_VALIDATION_VERSIONS_V3` (PROTOCOL_VERSION_12) bumps both fields to 1
- `transformer/v1/mod.rs` deleted; `batch/mod.rs` dispatcher reverted
- Legacy `_or_empty_vec` aggregators removed (only the v0 internal
  module retains that behavior, gated by platform_version)
- Net: +414 / −743 vs the previous +1700 / −44 shape of this PR

Also adds a TODO at `system_limits/v1.rs::max_transitions_in_documents_batch`
(currently 1) noting the broader batch-pipeline issues that must be
fixed before lifting the cap: non-atomic application of partial-success
batches, and unclear nonce-bump semantics for mixed success/failure
batches (see issue #2867 and PR #3608).
@shumkov shumkov changed the title fix(drive-abci): bump nonce on failed batch document transitions fix(drive-abci): free advanced structure validation for invalid batch transitions May 7, 2026
…mer + tests

Reframe the change from a mainnet duplicate-tx narrative to its actual
purpose: the user pays for the per-transition validation work that ran
before a failure (document fetch + ownership / revision check), instead
of getting it for free because the failure path emitted no action.

- Drop the dead `let result = ConsensusValidationResult::new();` and
  `if result.is_valid() { ... } else { Ok(result) }` tails in Replace,
  Transfer, UpdatePrice and Purchase arms — `result` is never modified
  after the per-path failure handlers were rewritten to return early.
- One short doc on `transform_document_transition_v0` explaining why
  every per-transition failure emits a bump (charge for the work, advance
  the nonce); strip per-site narrative comments that pointed at issue
  #2867 / mainnet 35C0…313C.
- Slim the regression test doc: keep what it pins, drop the mainnet
  escalation timeline.
- Slim the fee-change comments in replacement / nft / transfer to a
  one-liner ("covers the fetch + check that ran before the failure").
- Drop the mainnet 35C0 decoder pin in
  `packages/rs-dpp/src/state_transition/serialization.rs` — it was a
  one-off debug artifact, not a behavior pin for this PR.

The architectural mainnet fix (return `data: None` from aggregators so
empty-action batches become UnpaidConsensusError → tx removed from
block) lives in PR #3616. This PR is the complementary "user pays for
advanced-structure validation work" layer for paths that DO produce a
real per-transition error inside an otherwise-valid batch.
@shumkov shumkov self-assigned this May 7, 2026
@shumkov shumkov marked this pull request as ready for review May 7, 2026 19:22
shumkov added 2 commits May 8, 2026 02:24
…aired tests

Address PR #3608 review comments from QuantumExplorer.

- The previous commit emitted bump actions on every per-transition failure
  unconditionally, which would diverge PROTOCOL_VERSION_11 chain replay
  (the user-paid fees and stored contract nonce both shifted). Gate the
  bump emission on a new `failed_per_transition_action: FeatureVersion`
  field on `DriveAbciDocumentsStateTransitionValidationVersions`:
    - v0 (PROTOCOL_VERSION_11 and below): errors-only, no action data —
      preserves chain history bit-for-bit.
    - v1 (PROTOCOL_VERSION_12+, set in DRIVE_ABCI_VALIDATION_VERSIONS_V8):
      emit `BumpIdentityDataContractNonce` action.
- Factor the 13 per-transition failure sites in `transform_document_transition`
  into a single `Self::failed_per_transition_action(...)` helper that
  dispatches on the new field.
- Add a function-level doc on `transform_document_transition_v0` noting that
  the `0` user_fee_increase passed into each
  `BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition`
  call is overridden by the outer Documents Batch's `user_fee_increase`
  when the per-transition action rolls up into the `BatchTransitionAction`,
  so any per-site value would be discarded.
- Refactor three sibling tests into paired-version tests so the v11
  baseline fees stay pinned alongside the v12 post-fix fees:
    - test_document_replace_on_document_type_that_is_not_mutable +
      _protocol_version_11 (445700 / 41880)
    - test_document_transfer_that_does_not_yet_exist +
      _protocol_version_11 (517400 / 36200)
    - test_document_set_price_on_not_owned_document +
      _protocol_version_11 (571240 / 36200)
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-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs (1)

2836-2840: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Pin this fee test to protocol 12 instead of latest().

Line 2838 currently binds an exact fee (571240) to PlatformVersion::latest(). That can become flaky when a newer protocol updates fee accounting. Pinning explicitly to protocol 12 keeps this regression stable and scoped to the intended activation boundary.

Suggested change
-    async fn test_document_set_price_on_not_owned_document() {
+    async fn test_document_set_price_on_not_owned_document_protocol_version_12() {
         run_document_set_price_on_not_owned_document_at_protocol_version(
-            PlatformVersion::latest().protocol_version,
+            12,
             571240,
         )
         .await;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs`
around lines 2836 - 2840, The test pins an exact fee (571240) to
PlatformVersion::latest(), which can break as newer protocols change fees;
update the test to explicitly target protocol 12 by replacing
PlatformVersion::latest() with the protocol-12 variant (e.g.,
PlatformVersion::V12 or the equivalent constructor that yields protocol 12)
where the fee 571240 is asserted so the fee expectation remains stable and
scoped to the intended activation boundary.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs`:
- Around line 873-879: The current assertion uses assert_ne!(post_replace_nonce,
post_create_nonce) which allows the nonce to decrease; change it to assert that
post_replace_nonce is strictly greater than post_create_nonce (e.g., use
assert!(post_replace_nonce > post_create_nonce, "...")) so the test enforces
monotonic nonce advancement after the Replace bump action; update the failure
message to reflect that we expect a larger (advanced) nonce and reference the
variables post_replace_nonce and post_create_nonce in the message.

---

Outside diff comments:
In
`@packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs`:
- Around line 2836-2840: The test pins an exact fee (571240) to
PlatformVersion::latest(), which can break as newer protocols change fees;
update the test to explicitly target protocol 12 by replacing
PlatformVersion::latest() with the protocol-12 variant (e.g.,
PlatformVersion::V12 or the equivalent constructor that yields protocol 12)
where the fee 571240 is asserted so the fee expectation remains stable and
scoped to the intended activation boundary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4fdabee4-8c7b-41c0-9bec-dcfede804d50

📥 Commits

Reviewing files that changed from the base of the PR and between 5cdc306 and be4a538.

📒 Files selected for processing (13)
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs
✅ Files skipped from review due to trivial changes (1)
  • packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs

Comment on lines +873 to +879
assert_ne!(
post_replace_nonce, post_create_nonce,
"failed Replace's bump action did not advance the contract \
nonce — stored nonce is still {:#x} (= post-create value), so \
the same serialized bytes can be replayed",
post_create_nonce
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Strengthen nonce assertion to enforce monotonic advancement.

assert_ne! can pass even if nonce changes in the wrong direction. This test should assert that post-failure nonce is strictly greater than post-create nonce.

Suggested assertion hardening
-        assert_ne!(
-            post_replace_nonce, post_create_nonce,
-            "failed Replace's bump action did not advance the contract \
-             nonce — stored nonce is still {:`#x`} (= post-create value), so \
-             the same serialized bytes can be replayed",
-            post_create_nonce
-        );
+        assert!(
+            post_replace_nonce > post_create_nonce,
+            "failed Replace must strictly advance contract nonce: post_create={:`#x`}, post_replace={:`#x`}",
+            post_create_nonce,
+            post_replace_nonce
+        );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert_ne!(
post_replace_nonce, post_create_nonce,
"failed Replace's bump action did not advance the contract \
nonce — stored nonce is still {:#x} (= post-create value), so \
the same serialized bytes can be replayed",
post_create_nonce
);
assert!(
post_replace_nonce > post_create_nonce,
"failed Replace must strictly advance contract nonce: post_create={:`#x`}, post_replace={:`#x`}",
post_create_nonce,
post_replace_nonce
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs`
around lines 873 - 879, The current assertion uses
assert_ne!(post_replace_nonce, post_create_nonce) which allows the nonce to
decrease; change it to assert that post_replace_nonce is strictly greater than
post_create_nonce (e.g., use assert!(post_replace_nonce > post_create_nonce,
"...")) so the test enforces monotonic nonce advancement after the Replace bump
action; update the failure message to reflect that we expect a larger (advanced)
nonce and reference the variables post_replace_nonce and post_create_nonce in
the message.

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

I verified the PR at be4a538372563e2ed75cefbe7f4129675b0b03ad and did not find a correctness or consensus-safety defect in the implementation. The only issue I could confirm is a regression-coverage gap: purchase failures now use the same consensus-visible version gate as other per-transition document failures, but the tests do not pin that behavior across protocol versions.

Reviewed commit: be4a538

🟡 1 suggestion(s)

1 additional finding

🟡 suggestion: Purchase failure paths are not pinned with the same version-specific assertions as the other new failed-per-transition cases

packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs (lines 1666-1861)

DocumentTransition::Purchase now routes both DocumentNotForSaleError and DocumentIncorrectPurchasePriceError through failed_per_transition_action, so from protocol 12 onward these failures must emit a BumpIdentityDataContractNonce action, charge the higher processing fee, and consume the contract nonce. The existing purchase regression in this file only checks that the wrong-price path becomes a PaidConsensusError with the expected message, and there is no paired protocol-11/protocol-12 assertion for either wrong-price or not-for-sale purchase failures. That leaves the purchase arm less protected than the replace, transfer, and update-price arms, whose tests now pin the version-specific fee behavior and, for replace, nonce consumption semantics. Because both fee accounting and nonce advancement are consensus-visible, this is a real regression-coverage gap.

🤖 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-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs`:
- [SUGGESTION] lines 1666-1861: Purchase failure paths are not pinned with the same version-specific assertions as the other new failed-per-transition cases
  `DocumentTransition::Purchase` now routes both `DocumentNotForSaleError` and `DocumentIncorrectPurchasePriceError` through `failed_per_transition_action`, so from protocol 12 onward these failures must emit a `BumpIdentityDataContractNonce` action, charge the higher processing fee, and consume the contract nonce. The existing purchase regression in this file only checks that the wrong-price path becomes a `PaidConsensusError` with the expected message, and there is no paired protocol-11/protocol-12 assertion for either wrong-price or not-for-sale purchase failures. That leaves the purchase arm less protected than the replace, transfer, and update-price arms, whose tests now pin the version-specific fee behavior and, for replace, nonce consumption semantics. Because both fee accounting and nonce advancement are consensus-visible, this is a real regression-coverage gap.

…mp-nonce-on-failed-batch-document-transitions
shumkov added a commit that referenced this pull request May 8, 2026
…failures (issue #2867)

Two pre-existing NFT tests asserted PaidConsensusError + invalid_paid_count=1
for single-transition batches whose lone Purchase fails on
DocumentNotForSaleError or DocumentIncorrectPurchasePriceError.

With the v1 `flatten` / `merge_many` aggregators introduced in this PR
(PROTOCOL_VERSION_12+), an all-failed single-transition batch now
returns `data: None` instead of the legacy `Some(empty_vec)`. The
result flows down the unpaid path: `UnpaidConsensusError` and the tx
is removed from the block by prepare_proposal. Update the assertions
to reflect that.

Note that PR #3608 (separate, pending) restores Paid behavior for
these specific paths by emitting a BumpIdentityDataContractNonce
action so the user pays for the validation work — at which point
these tests will need to flip back to PaidConsensusError. For now this
PR alone produces UnpaidConsensus for these cases, which matches
v3.1-dev's current state without #3608's bump emission.
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 implementation at 24437ef8b3279d856a39437ccf62f94748b4f7c9 is internally consistent: DocumentTransition::Purchase now goes through the same failed_per_transition_action version gate as the other per-transition document failures, and protocol 12 correctly enables nonce-bumping actions while protocol 11 preserves legacy behavior. The remaining issue is a real regression-coverage gap: the purchase-specific failure tests still do not pin the protocol-11 versus protocol-12 fee and nonce semantics that this PR made consensus-visible.

Reviewed commit: 24437ef

🟡 1 suggestion(s)

1 additional finding

🟡 suggestion: Purchase failure paths are still not pinned across the protocol 11/12 gate

packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs (lines 1666-2358)

DocumentTransition::Purchase now routes both wrong-price and not-for-sale failures through failed_per_transition_action, and that helper is explicitly versioned: protocol 11 returns errors only, while protocol 12 emits BumpIdentityDataContractNonce, charges for the fetch/validation work that already ran, and consumes the contract nonce. In nft.rs, test_document_set_price_and_try_purchase_at_different_amount and test_document_set_price_and_purchase_then_try_buy_back only run at PlatformVersion::latest() and only assert the PaidConsensusError message. Unlike the replace, transfer, and update-price failure tests added elsewhere in this PR, these purchase-specific branches do not assert the version-specific processing-fee baseline or that replay is prevented once the nonce is consumed. Because fee accounting and nonce advancement are consensus-visible, leaving these purchase paths unpinned makes it easy for a later refactor to silently regress protocol 12 back to the old protocol 11 semantics.

🤖 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-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs`:
- [SUGGESTION] lines 1666-2358: Purchase failure paths are still not pinned across the protocol 11/12 gate
  `DocumentTransition::Purchase` now routes both wrong-price and not-for-sale failures through `failed_per_transition_action`, and that helper is explicitly versioned: protocol 11 returns errors only, while protocol 12 emits `BumpIdentityDataContractNonce`, charges for the fetch/validation work that already ran, and consumes the contract nonce. In `nft.rs`, `test_document_set_price_and_try_purchase_at_different_amount` and `test_document_set_price_and_purchase_then_try_buy_back` only run at `PlatformVersion::latest()` and only assert the `PaidConsensusError` message. Unlike the replace, transfer, and update-price failure tests added elsewhere in this PR, these purchase-specific branches do not assert the version-specific processing-fee baseline or that replay is prevented once the nonce is consumed. Because fee accounting and nonce advancement are consensus-visible, leaving these purchase paths unpinned makes it easy for a later refactor to silently regress protocol 12 back to the old protocol 11 semantics.

@shumkov
Copy link
Copy Markdown
Collaborator Author

shumkov commented May 8, 2026

Consolidated into #3616. Branch merged on top; PR #3616 now carries both the aggregator versioning and the per-transition bump emission as a single fix for issue #2867.

@shumkov shumkov closed this May 8, 2026
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.

3 participants