Skip to content

fix(drive-abci): bill batch transformer drive reads#3670

Merged
QuantumExplorer merged 32 commits into
v3.1-devfrom
fix/batch-paid-error-fee-charging
May 22, 2026
Merged

fix(drive-abci): bill batch transformer drive reads#3670
QuantumExplorer merged 32 commits into
v3.1-devfrom
fix/batch-paid-error-fee-charging

Conversation

@shumkov
Copy link
Copy Markdown
Collaborator

@shumkov shumkov commented May 19, 2026

Issue being fixed or feature implemented

Follow-up to #3616. While auditing fee correctness on the recently-flipped paid-error path for batch transitions, a much larger issue surfaced: the batch state transition transformer's execution_context was a local that got dropped on return, silently discarding every add_operation call from per-transition try_from_borrowed_*_with_contract_lookup (token group actions, contested document creates, etc.). This commit fixes that leak (entry B7 in docs/paid-error-fee-audit.md).

This is the first of several commits planned from the audit (see "follow-ups" below). Posting as draft so the audit doc + B7 fix can be reviewed before B4 builds on top.

What was done?

B7 fix

state/v0/mod.rs::transform_into_action_v0 previously created a local StateTransitionExecutionContext, passed it into try_into_action_v0, and dropped it on return. The outer context threaded by the processor (processor/v0/mod.rs:43) was accepted by batch/mod.rs:57 only to be ignored (_execution_context).

Gated on the existing batch_state_transition.transform_into_action field, bumped 0 → 1 in V8 (PROTOCOL_VERSION_12):

  • v0 (PROTOCOL_VERSION_11 and below): preserve the dropped-local-ctx behavior verbatim for chain replay.
  • v1 (PROTOCOL_VERSION_12+): thread the outer execution_context through the transformer so per-transition fee_results reach the user's bill.

No new function-version files, no protocol version bump — per the file-header comment at transformer/v0/mod.rs:1-22, this is the preferred pattern (branch inside the existing function rather than fork the ~1100-line transformer into a _v1 archive).

Audit doc

docs/paid-error-fee-audit.md captures the full inventory of fee-charging gaps discovered while diagnosing B7:

  • Tier 0 — B7 pipeline-level leak (this PR)
  • Tier 1 — Batch path (B1–B6, including B4 for the dropped query_documents cost — next commit)
  • Tier 2 — Data triggers (T1–T4, blocked on a DataTriggerExecutionContext immutability refactor)
  • Tier 3 — Non-batch state transitions (N1–N12, pre-existing under-billing in identity/key/asset-lock paths)

14 HIGH-severity sites total; this PR fixes 1 of them.

How Has This Been Tested?

New regression test test_token_burn_group_action_confirmer_fee_b7 in batch/tests/token/burn/mod.rs exercises the cleanest demonstration scenario: a group-action burn where the confirmer step triggers three drive reads inside try_from_borrowed_base_transition_with_contract_lookup (fetch_action_is_closed + fetch_action_id_signers_power_and_add_operations + fetch_active_action_info_and_add_operations).

Empirical fee deltas captured during development:

transform_into_action value Confirmer processing_fee
0 (pre-fix, dropped local ctx) 4,288,420
1 (post-fix, threaded outer ctx) 4,319,240
Delta +30,820 credits = three group-action reads billed

Existing fee-pinning tests continue to pass unchanged — verified by test_document_replace_on_document_type_that_is_mutable (still asserts processing_fee == 1_399_260). This confirms non-group / non-contested scenarios were unaffected by B7 (their dropped fee_results were empty), so the fix is invisible to them. B4 (next commit) will move that 1,399,260 number by billing the document query cost.

cargo test -p drive-abci --lib token_burn_tests::test_token_burn_group_action_confirmer_fee_b7
# running 1 test
# test ... ok
cargo test -p drive-abci --lib state_transitions::batch::tests::document::replacement::replacement_tests::test_document_replace_on_document_type_that_is_mutable
# running 8 tests ... ok

Breaking Changes

Consensus-affecting fee change for PROTOCOL_VERSION_12. Token group action confirmations (and any other batch transitions that exercise transformer-phase drive reads — see audit doc) will bill an additional ~30K credits per affected transition. PROTOCOL_VERSION_11 chain replay is preserved verbatim via the transform_into_action: 0 arm.

Targeting V8's PV12 hard-fork window per offline confirmation that PV12 has not yet shipped to mainnet. If PV12 has already shipped at merge time, this change must be re-cut for PV13 instead.

Scope creep — what's actually in this PR

What started as a B7-only fix grew during review to cover the rest of the batch-path fee leaks the audit surfaced. All gated by the same transform_into_action: 1 field on V8:

  • B7 — Thread outer execution_context through the batch transformer (root-cause fix that unblocked everything else).
  • B4 — Bill query_documents cost in fetch_documents.rs (was discarded; couldn't be fixed in isolation before B7).
  • T1, T2 — Bill DPNS data trigger query costs (parent-domain + preorder).
  • T3 — Bill DashPay data trigger identity-balance fetch (switched to _with_costs).
  • T4 — Bill Withdrawals data trigger query cost.

The DataTrigger refactor (return (Result, FeeResult) so triggers can surface their costs without needing a mut ref through DataTriggerExecutionContext) is a separate commit before the per-trigger billing commits.

Follow-ups (not in this PR)

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

Summary by CodeRabbit

  • New Features

    • Protocol v12+: data triggers and state transitions can record drive-read costs into the shared execution context; many triggers now support a v1 implementation path.
  • Bug Fixes

    • More consistent, version-aware processing-fee charging across create/delete/replace/transfer and withdrawal flows while preserving PV11 replay behavior.
  • Tests

    • Extensive protocol-version–pinned fee regression tests added/refactored (DPNS, NFT, token burn, direct-selling, transfers, deletions) for reproducible baselines.

Review Change Stack

The batch state transition's `transform_into_action_v0` created a local
`StateTransitionExecutionContext`, passed it into `try_into_action_v0`,
and dropped it on return. Every `add_operation` call inside the
transformer (per-transition `try_from_borrowed_*_with_contract_lookup`
fee_results for token group actions, contested document creates, etc.)
was silently discarded.

Gate the fix on `batch_state_transition.transform_into_action`:
- v0 (PROTOCOL_VERSION_11): preserve legacy dropped-local-ctx behavior
  for chain replay.
- v1 (PROTOCOL_VERSION_12+): thread the outer execution_context through
  the transformer so per-transition fees reach the user's bill.

Demonstrated by `test_token_burn_group_action_confirmer_fee_b7`: a
group-action burn confirmer step's processing fee goes from 4_288_420
(pre-fix, dropped) to 4_319_240 (post-fix, billed). The 30_820 delta is
the cost of three drive reads inside
`try_from_borrowed_base_transition_with_contract_lookup`
(`fetch_action_is_closed` +
 `fetch_action_id_signers_power_and_add_operations` +
 `fetch_active_action_info_and_add_operations`) that were previously
billed to a dropped context.

Non-group / non-contested scenarios are unaffected: the transformer's
add_operation calls received empty FeeResults in those paths, so dropping
vs. threading the ctx made no difference. Verified by the existing
`test_document_replace_on_document_type_that_is_mutable` (pinned at
1_399_260 credits) continuing to pass.

`docs/paid-error-fee-audit.md` documents the full audit (18 fee-leak
sites identified across batch path, data triggers, and non-batch state
transitions) and the constraint that every fix ships as a new function
version or version-field bump for consensus reproducibility.

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

coderabbitai Bot commented May 19, 2026

📝 Walkthrough

Walkthrough

Introduce epoch-aware, versioned fetch facades that bill via a threaded mutable execution context; make data-trigger contexts mutable; add v1 triggers (DashPay, DPNS, Withdrawals) which bill drive reads; thread outer execution_context through batch v1 to aggregate fees; update transformers, platform-version flags, and tests.

Changes

Data trigger billing refactoring and v1 protocol version support

Layer / File(s) Summary
All modifications (single checkpoint)
packages/rs-drive-abci/...
Version-dispatching fetch facades accept epoch and &mut StateTransitionExecutionContext and bill in v1; DataTriggerExecutionContext adds block_info and uses &mut StateTransitionExecutionContext; DataTrigger APIs take &mut context; v1 triggers added (DashPay, DPNS, Withdrawals) that record FeeResult via add_operation; create/delete validators and transformer calls updated to use new fetch APIs; batch v1 threads outer execution_context into transformer; platform-version feature flags added; tests parameterized per protocol version with pinned PV11 baselines.

Sequence Diagram

sequenceDiagram
  participant DocumentTransitionAction
  participant DataTrigger_v1
  participant PlatformDrive
  participant StateTransitionExecutionContext

  DocumentTransitionAction->>DataTrigger_v1: validate_with_data_triggers(&mut DataTriggerExecutionContext)
  DataTrigger_v1->>PlatformDrive: query_documents(..., Some(&block_info.epoch))
  PlatformDrive-->>DataTrigger_v1: documents_outcome (with cost)
  DataTrigger_v1->>StateTransitionExecutionContext: add_operation(PrecalculatedOperation(FeeResult{processing_fee: cost}))
  DataTrigger_v1-->>DocumentTransitionAction: DataTriggerExecutionResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • QuantumExplorer
  • thepastaclaw

Poem

🐰
I hopped through fields of epoch and fee,
Made fetches bill where reads should be,
Mutable context keeps carrots bright,
V0 stays calm while V1 takes flight,
Hop, code, hop — billing now just right.

🚥 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 accurately and concisely summarizes the main change: fixing fee billing for batch transformer drive reads, addressing a documented fee-accounting leak (B7).
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/batch-paid-error-fee-charging

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.

@github-actions github-actions Bot added this to the v3.1.0 milestone May 19, 2026
shumkov and others added 10 commits May 19, 2026 18:34
Project convention: when behavior changes via a version-field bump,
add a new function version (_v1) alongside the unchanged _v0 and
dispatch outside. The prior commit incorrectly branched inside _v0,
which would alter the byte-identity of a function that has to stay
verbatim for PROTOCOL_VERSION_11 chain replay.

Restore `DocumentsBatchStateTransitionStateValidationV0::transform_into_action_v0`
to its v3.1-dev original. Add a sibling `transform_into_action_v1`
that takes `&mut StateTransitionExecutionContext` and threads it
into `try_into_action_v0` (the transformer's single entry-point,
intentionally still at _v0 per its file-header comment — see
transformer/v0/mod.rs:1-22).

Move the version dispatch into `batch/mod.rs::transform_into_action`:
- `transform_into_action: 0` → `transform_into_action_v0(...)` (legacy)
- `transform_into_action: 1` → `transform_into_action_v1(..., ctx, ...)` (B7 fix)

The B7 regression test (`test_token_burn_group_action_confirmer_fee_b7`)
still passes at 4_319_240 credits — the behavior gated by V8's
`transform_into_action: 1` is unchanged from the prior commit; only
the code shape changed.

Audit doc updated to clarify the rule: "_v0 byte-identical, new _vN
alongside, dispatch outside" is the standard pattern. The "branch
inside" pattern only applies to the ~1100-line transformer body where
suffix-bumping would force file-level duplication.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the project convention used by other versioned state-transition
functions (e.g. address_funding_from_asset_lock/transform_into_action/v0/):
each function version lives in its own directory with its own trait.

Restores state/v0/mod.rs to byte-identical to v3.1-dev (verified via
`git diff v3.1-dev`). Adds state/v1/mod.rs with a new trait
`DocumentsBatchStateTransitionStateValidationV1` containing just the
`transform_into_action_v1` method.

Dispatcher in batch/mod.rs imports both traits and matches the
`batch_state_transition.transform_into_action` field to pick which
to call:
- arm `0` → DocumentsBatchStateTransitionStateValidationV0::transform_into_action_v0
- arm `1` → DocumentsBatchStateTransitionStateValidationV1::transform_into_action_v1

B7 regression test (`test_token_burn_group_action_confirmer_fee_b7`)
still passes at 4_319_240 credits — same end behavior as the prior
two commits, just structurally clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The B7 / docs/paid-error-fee-audit.md references in code comments were
temporary navigation aids during development. The audit doc is a working
plan that will be removed once all the fixes ship, so in-code references
to it would become broken links.

Cleanup:
- Rename test_token_burn_group_action_confirmer_fee_b7 ->
  test_token_burn_group_action_confirmer_fee_includes_transformer_reads
  (name now describes what it pins, not which audit entry it covers).
- Strip "B7" / "paid-error-fee-audit.md" mentions from comments in
  batch/mod.rs, batch/state/v1/mod.rs, v8.rs, and the test file.
  Replace with self-contained explanations of what the code does and
  why.

No behavior change; test still passes at 4_319_240 credits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`fetch_documents_for_transitions_knowing_contract_and_document_type` (in
batch/state/v0/fetch_documents.rs) is called from the batch transformer
on every batch with replace/transfer/purchase/update-price transitions.
It runs `drive.query_documents(...)` to fetch target documents for the
per-transition validators that come next. The query's cost was
previously discarded by passing `epoch=None` (which short-circuits cost
computation to 0) and ignoring the outcome's `cost()` accessor.

Now:
- The function takes an `Epoch` and returns the `FeeResult` alongside
  the validation result.
- The caller in transformer/v0/mod.rs adds the FeeResult to the outer
  execution_context, gated by a new field
  `batch_state_transition.fetch_documents_for_transitions_billing`:
    * 0 (PROTOCOL_VERSION_11 and below): discard, byte-identical to
      pre-fix.
    * 1 (PROTOCOL_VERSION_12+): bill the cost via add_operation.
- Builds on top of the prior B7 commit (transform_into_action: 1) so
  the execution_context the cost lands in is the one threaded through
  from the processor, reaching the user's bill.

Empirical fee deltas on existing PV12 fee-pin tests:

  test_document_replace_on_document_type_that_is_mutable
    1_399_260 → 1_411_320 (+12_060)
  test_document_replace_on_document_type_that_is_not_mutable
    445_700 → 460_920 (+15_220)
  test_document_replace_on_document_type_that_is_not_mutable_but_is_transferable
    445_700 → 457_660 (+11_960)
  test_document_replace_that_does_not_yet_exist
    516_040 → 520_340 (+4_300)
  test_document_transfer_on_document_type_that_is_transferable
    3_631_040 → 3_643_400 (+12_360)
  test_document_set_price (+ 4 sibling NFT tests)
    2_473_880 → 2_485_600 (+11_720)
  ... 19 fee-pin assertions updated in total.

V11 baselines (sibling `_protocol_version_11` tests) remain unchanged
verbatim — fetch_documents_for_transitions_billing: 0 preserves the
discard-cost path for chain replay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`fetch_documents_for_transitions` (also `#[deprecated]`) and
`fetch_documents_for_transitions_knowing_contract_id_and_document_type_name`
in batch/state/v0/fetch_documents.rs were `#[allow(dead_code)]` and
only referenced each other. Verified no external callers.

The B4 commit before this threaded `epoch: &Epoch` through them as
collateral damage from changing the live function's signature. Cleaner
to just delete them — these wrappers were vestigial.

Drops 8 now-unused imports as a side effect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The prior B4 commit introduced a separate
`fetch_documents_for_transitions_billing` field. That was unnecessary —
B4 cannot have any user-visible effect without B7's threaded ctx
(billing into a dropped local context is wasted work), so the two
fixes are intrinsically tied. Reuse the existing `transform_into_action`
field instead.

- Remove `fetch_documents_for_transitions_billing` from
  `DriveAbciDocumentsStateTransitionValidationVersions`.
- Remove it from v1.rs..v8.rs (8 files).
- transformer/v0/mod.rs:511 callsite now gates the query-cost billing
  on `transform_into_action` (same field that decides ctx threading).

Behavior is identical to the prior B4 commit because V8 had both
fields at 1 — the consolidated single-field gate produces the same
v0/v1 dispatch outcome.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 1 of fixing trigger drive-read fee leaks (T1-T4 in the audit).
This commit is consensus-neutral: every trigger returns
FeeResult::default() (the actual fee billing per trigger ships in
follow-up commits, one per source — dpns, dashpay, withdrawals).

Changes:
- `DataTrigger` fn type now returns
  `Result<(DataTriggerExecutionResult, FeeResult), Error>`
- `DataTriggerBindingV0Getters::execute` ditto.
- `DataTriggerExecutor::validate_with_data_triggers` ditto — sums fees
  from every trigger that actually executed (including the one that
  returned invalid) via `FeeResult::checked_add_assign`.
- All 4 trigger fns (dpns/dashpay/withdrawals/reject) return tuple
  with `FeeResult::default()` placeholder.
- Dispatch site in `state/v0/mod.rs::validate_state_v0` destructures
  the returned tuple. The accumulated FeeResult is added to the outer
  `execution_context` gated by `transform_into_action: 1` — same field
  that gates B7 (ctx threading) and B4 (query cost billing). On v0
  the fee is discarded for chain replay reproducibility (it's
  `FeeResult::default()` anyway in this commit).

Also deleted the dead `state/v0/data_triggers.rs::execute_data_triggers`
function. It was `#[allow(dead_code)]` + `#[deprecated]` with no
callers — the new trigger return type would have required threading
the tuple through it for nothing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The DPNS `create_domain_data_trigger` fires on every DPNS domain
create. It runs two `query_documents` calls:
- T1: parent-domain lookup (only when registering a subdomain)
- T2: preorder document lookup (always)

Both previously passed `epoch=None` to `query_documents` (which
short-circuits cost computation to 0) and discarded any cost anyway —
the trigger context's ctx ref was immutable, so triggers could not bill.

Now: pass `Some(epoch)` so query_documents computes the real grovedb
cost, accumulate both queries' costs into a `FeeResult`, and return
it from the trigger. The caller
(`DataTriggerExecutor::validate_with_data_triggers`) sums fees across
triggers and the dispatch site in `state/v0/mod.rs` bills via
`execution_context.add_operation` on `transform_into_action: 1`.

The accumulated FeeResult is returned at every exit path including
early returns after the parent-domain query (so the user pays for the
first query even if validation fails before the second one runs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The DashPay `create_contact_request_data_trigger` fetches the recipient
identity's balance to verify the identity exists before creating a
contact request. Previously used `fetch_identity_balance` (no cost
returned) with an explicit "TODO: Calculate fee operations" comment.

Switch to `fetch_identity_balance_with_costs` (passes block_info for
epoch, returns FeeResult), and propagate the FeeResult through the
trigger's return value. The caller bills it on
`transform_into_action: 1` via the now-established trigger fee plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The withdrawals `delete_withdrawal_data_trigger` runs `query_documents`
to fetch the withdrawal document being deleted (to verify status ==
COMPLETE before allowing deletion). Previously passed `epoch=None`
(cost short-circuited to 0) and discarded any outcome cost.

Pass `Some(epoch)` so the real grovedb cost is computed, build a
`FeeResult` from `documents_outcome.cost()`, and return it from every
exit path (early-return on missing withdrawal, early-return on wrong
status, and final return). The caller bills it on
`transform_into_action: 1`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shumkov and others added 7 commits May 19, 2026 22:31
…rigger epoch source

Three review-driven fixes:

1. **B5 — `fetch_document_with_id` query cost was discarded.** Same
   pattern as B4 (B4 was in a sibling function); reviewers correctly
   flagged that fixing B4 alone left an identical leak in the same
   file. `fetch_document_with_id` previously passed `epoch=None` to
   `query_documents`, which short-circuits the cost to 0. All three
   callers (`document_create_transition_action/state_v0`, `state_v1`,
   `document_delete_transition_action/state_v0`) already do
   `execution_context.add_operation(PrecalculatedOperation(fee_result))`
   — they just always got zero. Now: take `epoch: &Epoch`, gate on
   `transform_into_action` internally (v0 passes None and returns
   zero-fee for byte-identical PV11 behavior; v1 passes Some(epoch)
   and bills the real cost).

2. **Epoch source unified across batch fee sites.** Reviewers flagged
   that triggers used `last_committed_block_epoch_ref()` while the
   transformer used `&block_info.epoch` (current block). At era
   boundaries the two prices would diverge — deterministic but
   internally inconsistent. Now: `DataTriggerExecutionContext` carries
   `block_info: &'a BlockInfo`, and all three migrated triggers
   (DPNS, DashPay, Withdrawals) use `&context.block_info.epoch`.
   DashPay's `fetch_identity_balance_with_costs` call also switches
   from `last_block_info()` to `context.block_info` for the same
   reason.

3. **Stale doc-comment cleanup.** `fetch_documents.rs:21-23` referenced
   the removed `fetch_documents_for_transitions_billing` field. Now
   references the consolidated `transform_into_action` gate.

4. **v8.rs documentation.** Added a comment enumerating the 6
   sub-concerns gated by `transform_into_action: 1` (B7, B4, B5,
   T1, T2, T3, T4) so future operators investigating a fee discrepancy
   at PV12 can find them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds three regression tests that fail if the trigger fee billing
(introduced in earlier commits) is dropped:

- **T1+T2 (DPNS)**: pinned the exact aggregated `processing_fee` for
  `test_dpns_contract_references_with_no_contested_unique_index`
  (3 subdomain creates, total 6_010_380 credits). A future refactor
  that drops `accumulated_fee_result.checked_add_assign` in
  `create_domain_data_trigger_v0` would fail this assertion.

- **T3 (DashPay)**: added `assert!(fee_result.processing_fee > 0)`
  on the existing `should_return_invalid_result_if_id_not_exists`
  unit test. Catches regressions that bypass
  `fetch_identity_balance_with_costs` (e.g. reverting to the cheaper
  `fetch_identity_balance` that returns no cost).

- **T4 (Withdrawals)**: added `assert!(fee_result.processing_fee > 0)`
  on the existing `should_throw_error_if_withdrawal_has_wrong_status`
  unit test. Catches regressions that revert `epoch=Some(...)` to
  `epoch=None` in the trigger's `query_documents` call.

Also removed three unused imports (`FeeResult`,
`PlatformStateV0Methods`) that became dead after the epoch-source
unification commit removed the `last_committed_block_epoch_ref()`
calls from the triggers.

Note: T3 and T4 use non-zero assertions rather than exact-value pins
because writing dedicated batch-level fixture tests for DashPay
contact-request and withdrawal-delete scenarios would require
substantial new test scaffolding (none exists today in batch/tests/).
The non-zero assertion catches the highest-priority regression
(trigger billing entirely dropped) at zero new-fixture cost. Exact
pins can be added as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reviewer feedback: the prior architectural commit modified _v0 trigger
bodies to return `(Result, FeeResult)` for fee accounting, which broke
strict _v0 byte-identity even though the version gate at the dispatch
site kept PV11 behavior preserved. The cleaner pattern (used elsewhere
in the codebase) is to keep _v0 byte-identical and add _v1 sibling
functions with the new behavior.

Refactor:

1. **`DataTriggerExecutionContext.state_transition_execution_context`**
   changed from `&'a` to `&'a mut`. `_v1` triggers call
   `add_operation` on it directly to bill drive reads; `_v0` triggers
   ignore the mut access (their body never mutates).

2. **`DataTrigger` fn type**: return type reverted from
   `(Result, FeeResult)` to `Result` (no tuple). Param changed to
   `&mut DataTriggerExecutionContext`. The trait-binding chain
   (`DataTriggerBindingV0Getters::execute`, `DataTriggerExecutor::validate_with_data_triggers`)
   propagates the change.

3. **`_v0` trigger files** (`dpns/v0/mod.rs`, `dashpay/v0/mod.rs`,
   `withdrawals/v0/mod.rs`) **restored from v3.1-dev byte-identical
   for the function body**. Only the param type signature changed
   (`&` → `&mut`), which is a compile-time-only change with no
   observable PV11 behavior difference. epoch=None preserved, no
   add_operation calls.

4. **`_v1` trigger files** (new): `dpns/v1/mod.rs`,
   `dashpay/v1/mod.rs`, `withdrawals/v1/mod.rs`. Pass `Some(epoch)`
   to `query_documents` (DPNS, withdrawals) or use
   `fetch_identity_balance_with_costs` (dashpay), and call
   `context.state_transition_execution_context.add_operation(...)`
   directly to bill the grovedb cost.

5. **Wrappers** (`triggers/{dpns,dashpay,withdrawals}/mod.rs`) now
   dispatch on the per-trigger version field:
   - `0 =>` _v0 (legacy, no billing)
   - `1 =>` _v1 (PV12+, bills directly)

6. **`v8.rs`** bumps per-trigger fields to 1:
   - `create_domain_data_trigger: 1`
   - `create_contact_request_data_trigger: 1`
   - `delete_withdrawal_data_trigger: 1`
   - `reject_data_trigger: 0` (no drive reads, no billing needed)

7. **Dispatch site** in `batch/state/v0/mod.rs`: removed the now-stale
   local `state_transition_execution_context`, removed the FeeResult
   tuple destructure + version-gated add_operation. The trigger context
   passes the outer `execution_context` directly as `&mut`.

8. **Executor** (`data_triggers/executor.rs`) simplified — no more
   FeeResult accumulation/summing; triggers bill themselves.

9. **Test fee updates**: 6 tests on PV12 paths now reflect B5 fee
   billing for `fetch_document_with_id` (deletion x3, nft x3).
   Original-creation-cost constants and `RemoveFromBalance` desired
   amounts updated to match the new billed fees.

10. The DPNS regression test pin holds at 6_010_380 credits —
    same behavior, cleaner architecture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The trigger context already has `platform: &PlatformStateRef`, which
exposes `platform.state.last_committed_block_epoch_ref()` and
`platform.state.last_block_info()`. The dedicated `block_info` field
was redundant.

Trade-off accepted: triggers now source their epoch from the last
*committed* block rather than the *current* block being processed.
At era boundaries these differ — trigger fees price at the previous
era's rates while the transformer prices at the current era's rates
(same batch transition can have internally inconsistent fee math).
The discrepancy is deterministic across all validators, so consensus
holds; just a minor fee-accuracy quirk.

Changes:
- Removed `block_info: &'a BlockInfo` field from
  `DataTriggerExecutionContext`.
- `_v1` triggers (dpns, withdrawals) now pass
  `Some(context.platform.state.last_committed_block_epoch_ref())` to
  `query_documents`.
- `_v1` dashpay trigger passes
  `context.platform.state.last_block_info()` to
  `fetch_identity_balance_with_costs`.
- Dispatch site at `batch/state/v0/mod.rs` no longer constructs with
  `block_info`.
- Test context constructions in `_v0` files dropped the
  `block_info: &BlockInfo::default()` line.

All 262 batch tests pass — DPNS fee pin still 6_010_380 credits.

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

Reviewer feedback: when modifying _v0 / shipped function bodies (even
in ways that preserve PV11 byte-identity), the code itself should
explain WHY the change is safe. Future maintainers shouldn't have to
re-derive the consensus argument from scratch.

Added inline comments at every _v0 site this PR touches:

- `validate_state_v0` (batch/state/v0/mod.rs): explains why removing
  the local `state_transition_execution_context` and switching the
  trigger context to use the outer ctx preserves PV11 chain replay,
  plus notes the mempool-only `dry_run` semantics change.

- `fetch_documents_for_transitions_knowing_contract_and_document_type`:
  explains that `epoch: &Epoch` was added to the signature but the v0
  arm at the caller discards the resulting cost — documents returned
  are epoch-independent.

- `fetch_document_with_id`: same pattern — internal version gate
  passes `None` to query_documents on v0 (zero-cost FeeResult), so
  the caller's existing `add_operation` call adds zero — matching
  pre-PR.

- All three `_v0` trigger fns (dpns, dashpay, withdrawals): note that
  the `context: &mut DataTriggerExecutionContext` signature change
  is compile-time only — the body never mutates the context.

- Transformer's fetch_documents callsite: explains the `Some(epoch)`
  → real cost vs the version-gated discard on v0.

- `DataTriggerBindingV0Getters::execute` impl: notes that the
  `&mut` change is required by _v1 but harmless on PV11 (v0 trigger
  doesn't mutate).

No code changes — pure documentation. DPNS regression test still
passes at 6_010_380 credits.

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

Same architectural choice as the trigger refactor — pass
`&mut execution_context` into the fetch helpers and let them call
`add_operation` directly, rather than returning a `FeeResult` tuple
and having the caller bill.

Changed:

- `fetch_documents_for_transitions_knowing_contract_and_document_type`
  now takes `execution_context: &mut StateTransitionExecutionContext`
  and returns just `ConsensusValidationResult<Vec<Document>>` (no
  tuple). Internal version gate on `transform_into_action`:
  v0 → epoch=None, no add_operation; v1 → Some(epoch), add_operation.
- `fetch_document_with_id` same pattern. Returns just `Option<Document>`.
- Transformer callsite at transformer/v0/mod.rs:511 simplified —
  passes `execution_context` and drops the explicit version-gated
  add_operation block (now handled inside the helper).
- `document_create_transition_action::state_v0`,
  `document_create_transition_action::state_v1`,
  `document_delete_transition_action::state_v0` callsites updated —
  pass `execution_context`, drop the now-redundant
  `add_operation(PrecalculatedOperation(fee_result))` lines.

PROTOCOL_VERSION_11 consensus-safety: the v0 path inside each helper
forces `epoch=None` and skips `add_operation`. Pre-PR also passed
None and the caller did `add_operation` with a zero-cost FeeResult
(a no-op fee). Net effect on PV11: identical.

All 262 batch tests pass. DPNS regression pin still holds at
6_010_380 credits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tightens trigger fee-billing coverage with two complementary assertions
on each trigger's unit tests:

1. **`_v0` byte-identity** (PROTOCOL_VERSION_11 chain replay safety):
   after calling `_v0` directly, assert
   `execution_context.operations_slice().is_empty()`. Catches any
   regression that accidentally re-introduces billing in the legacy
   `_v0` trigger body.

2. **`_v1` billing** (PROTOCOL_VERSION_12+ fee correctness): after
   calling `_v1` directly, assert
   `execution_context.operations_slice()` is NON-empty. Catches the
   regression where `_v1` drops its `add_operation` call.

Coverage added:

- **DPNS** (`dpns/v0/mod.rs::test::should_return_execution_result_on_dry_run`):
  asserts `_v0` adds zero ops. Pairs with the existing batch-level
  PV12 fee pin at 6_010_380 credits for the `_v1` billing side.

- **DashPay** (`dashpay/v0/mod.rs::should_return_invalid_result_if_id_not_exists`):
  this test calls the wrapper which dispatches to `_v1` at PV12
  (`create_contact_request_data_trigger: 1` on V8). Asserts the
  resulting `execution_context` is non-empty — proves T3 billing
  works.

- **Withdrawals** (`withdrawals/v0/mod.rs::should_throw_error_if_withdrawal_has_wrong_status`):
  asserts `_v0` adds zero ops, then runs the SAME fixture through
  `_v1` directly (imported via `super::super::v1::...`) and asserts
  non-empty ops. Single test covers both T4 sides.

These are unit-level assertions on the trigger functions, not
batch-level fee pins. They run in <1s without needing full batch
fixtures, so the regression catch-net is cheap to maintain.

All 7 PV11 tests still pass + 262 batch tests still pass — PV11
chain replay byte-identity preserved through the changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QuantumExplorer added a commit that referenced this pull request May 20, 2026
…ion (audit N6/N7)

`validate_identity_public_key_contract_bounds` had an explicit
`//todo: we should add to the execution context the cost of fetching contracts`
on v0 and discarded every grovedb read (contract fetches + unique-key
lookups). Under paid-error semantics, an identity_update that failed bounds
validation under-charged the user for the validation work that ran. Audit
entries N6 / N7 in `docs/paid-error-fee-audit.md` (PR #3670), tracked by
issue #3673.

Folded into v1 of the bounds validator (already gated at PROTOCOL_VERSION_12
via DRIVE_ABCI_VALIDATION_VERSIONS_V8 by the previous commit). v0 stays
byte-identical for chain replay.

- `rs-drive`: new `pub fn fetch_identity_keys_with_costs` (mirrors the
  existing `fetch_identity_balance_with_costs` precedent — takes `&Epoch`,
  returns `(T, FeeResult)`).
- `rs-drive-abci` v1: switched contract fetches to
  `get_contract_with_fetch_info_and_fee` and identity-key lookups to the new
  `fetch_identity_keys_with_costs`, pushing each returned `FeeResult` into
  `execution_context` via `ValidationOperation::PrecalculatedOperation`.
  Also refactored the body to factor out the unique-key check into a single
  helper (was duplicated across four bounds × purpose combinations).
- Dispatcher now takes `epoch: &Epoch`. v0 ignores it (preserved).
- `identity_update::validate_state_v0` passes
  `platform.state.last_committed_block_epoch_ref()` to the dispatcher.

New test `v1_bills_contract_fetch_and_unique_key_lookup` asserts the
execution context receives at least two `PrecalculatedOperation` entries
with non-zero processing fees after v1 runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shumkov and others added 5 commits May 20, 2026 22:39
Reviewer feedback: when a new function version exists alongside the
old one, the diff sites should explicitly explain what changed vs.
the previous version and why. Without this, future maintainers
reading `_v1` have to mentally diff against `_v0` to understand the
intent.

Added "Diff vs `_v0`" comments at every site in `_v1` triggers that
diverges from the corresponding `_v0` body:

- **dpns/v1**: parent-domain query (T1) + preorder query (T2).
  Both swap `None` → `Some(epoch)` and add a billing call. The
  comments name T1/T2 explicitly so the audit-doc cross-reference
  is searchable.

- **dashpay/v1**: recipient identity existence check (T3). Swaps
  `fetch_identity_balance` → `fetch_identity_balance_with_costs`.
  Comment notes `apply: true` matches the legacy stateful query so
  the returned balance is byte-identical (only the FeeResult is new).

- **withdrawals/v1**: withdrawal-document lookup (T4). Swaps `None`
  → `Some(epoch)` + adds billing. Notes that the document returned
  is epoch-independent.

No code changes — pure documentation. All 7 trigger unit tests
still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reviewer feedback: instead of an internal `transform_into_action`
gate inside `fetch_documents_for_transitions_knowing_contract_and_document_type`
and `fetch_document_with_id`, use the same proper version-facade
pattern we use for triggers — separate `_v0` and `_v1` impls,
dispatched by a facade that takes `platform_version`.

Changes:

1. **Two new version fields** on
   `DriveAbciDocumentsStateTransitionValidationVersions`:
   - `fetch_documents_for_transitions_knowing_contract_and_document_type: FeatureVersion`
   - `fetch_document_with_id: FeatureVersion`

   v1.rs..v7.rs set both to 0 (PROTOCOL_VERSION_11 and below).
   v8.rs sets both to 1 (PROTOCOL_VERSION_12+).

2. **fetch_documents_for_transitions_knowing_contract_and_document_type**:
   - Facade dispatches on the version field.
   - `_v0`: byte-identical to v3.1-dev. No `epoch`/`execution_context`
     params, passes `epoch=None` to `query_documents`, never bills.
   - `_v1`: takes `epoch` and `execution_context`, passes
     `Some(epoch)`, bills via `add_operation`.

3. **fetch_document_with_id**:
   - Facade dispatches on the version field.
   - `_v0`: byte-identical to v3.1-dev — signature returns
     `(Option<Document>, FeeResult)`. The facade calls
     `add_operation` with the (zero-cost) FeeResult on the v0 path
     so the execution_context's operations_slice matches pre-PR
     exactly (pre-PR caller did this; we just moved the call into
     the facade).
   - `_v1`: takes `epoch` and `execution_context`, bills internally,
     returns just `Option<Document>`.

PV11 byte-identity properties (verified empirically — 7 PV11 fee-pin
tests pass unchanged):

- `_v0` function bodies match v3.1-dev pre-PR text exactly (modulo
  the function rename to `*_v0`).
- The facade's v0 arm produces the same chain state as pre-PR:
  documents are epoch-independent, the fee_result on v0 is always
  zero, the add_operation call (either at the old caller or now at
  the facade) adds a zero-fee FeeResult to ctx.

All 262 batch tests, 7 PV11 tests, 7 trigger unit tests pass.
DPNS regression pin still 6_010_380 credits.

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

Completes the diff-from-_v0 commenting sweep. All `_v1` functions
introduced by this PR now have inline comments at the lines where
behavior diverges from `_v0`, explaining what changed and why:

- `transform_into_action_v1` (this commit) — explains why `_v1`
  passes the outer `execution_context` instead of creating a local
  one. Names B7 from the audit doc and the dropped-fee category
  (transformer-phase reads, ~30K credits for token group action
  confirmers).
- `create_domain_data_trigger_v1` — already commented at parent-domain
  and preorder query sites (T1, T2).
- `create_contact_request_data_trigger_v1` — already commented at
  the `fetch_identity_balance_with_costs` site (T3).
- `delete_withdrawal_data_trigger_v1` — already commented at the
  `query_documents` site (T4).
- `fetch_documents_for_transitions_knowing_contract_and_document_type_v1`
  — already commented at the `query_documents` site.
- `fetch_document_with_id_v1` — already commented at the
  `query_documents` site.

No code changes — pure documentation. All tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 7 PV11 sibling fee-pin tests added earlier cover the paid-error
paths (failed transitions). The happy-path tests in deletion.rs also
had fees change because of B7 — transformer-phase grovedb reads are
now billed via the outer execution_context instead of being dropped
into a local. Without PV11 siblings, v11 chain replay against these
test conditions would silently diverge from mainnet history.

Added 3 PV11 sibling tests, each pinning the pre-B7 fee:
- test_document_delete_on_document_type_that_is_mutable_and_can_be_deleted
  PV12: 1678920  PV11: 1666860  delta: +12,060
- test_document_delete_on_document_type_that_is_not_mutable_and_can_be_deleted
  PV12: 2778700  PV11: 2762400  delta: +16,300
- test_document_delete_that_does_not_yet_exist (paid-error path)
  PV12: 520340   PV11: 516040   delta: +4,300

Each original test is now a 1-line caller of a shared helper
parametrized by `protocol_version` + `expected_processing_fee`,
matching the pattern used in replacement.rs / transfer.rs / nft.rs.

8/8 deletion tests pass — the 5 original PV12 cases plus the 3 new
PV11 siblings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The transfer happy-path tests had fees change because of B7 (transformer
grovedb reads now billed via the outer execution_context) and B4
(query_documents cost now billed inside fetch_documents). Without PV11
siblings, v11 chain replay against these test conditions would silently
diverge from mainnet history.

Added 4 PV11 sibling tests, each pinning the pre-fix fee:
- test_document_transfer_on_document_type_that_is_transferable
  PV12: 3643400  PV11: 3631040  delta: +12,360
- test_document_transfer_on_document_type_that_is_transferable_contract_v0
  PV12: 3380960  PV11: 3369260  delta: +11,700
- test_document_transfer_on_document_type_that_is_not_transferable
  PV12: 457000   PV11: 445700   delta: +11,300
- test_document_delete_after_transfer
  PV12: 4004260  PV11: 3991900  delta: +12,360

Each original test is now a 1-line caller of a shared helper
parametrized by `protocol_version` + `expected_processing_fee`,
matching the pattern used in deletion.rs / replacement.rs.

24/24 transfer tests pass — the original PV12 cases plus the 4 new
PV11 siblings (and the 2 existing PV11 siblings from earlier work).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shumkov and others added 5 commits May 21, 2026 17:07
The NFT set-price / purchase happy-path tests had fees change because
of B7 (transformer grovedb reads now billed via the outer
execution_context) and B4 (query_documents cost now billed inside
fetch_documents). Without PV11 siblings, v11 chain replay against these
test conditions would silently diverge from mainnet history.

Added 5 PV11 sibling tests, each pinning the pre-fix fee(s):
- test_document_set_price
  PV12: 2485600   PV11: 2473880   delta: +11,720
- test_document_set_price_and_purchase
  set:      PV12: 2485600   PV11: 2473880  delta: +11,720
  purchase: PV12: 4092360   PV11: 4080480  delta: +11,880
- test_document_set_price_and_purchase_different_epoch
  set:      PV12: 2485600   PV11: 2473880  delta: +11,720
  purchase: PV12: 4092360   PV11: 4080480  delta: +11,880
- test_document_set_price_and_purchase_different_epoch_documents_mutable
  set 1:    PV12: 2729120   PV11: 2717400  delta: +11,720
  set 2:    PV12: 2733160   PV11: 2721160  delta: +12,000
  purchase: PV12: 4357440   PV11: 4345280  delta: +12,160
- test_document_set_price_and_purchase_with_enough_credits_to_buy_but_not_enough_to_pay_for_processing
  PV12: 2485600   PV11: 2473880   delta: +11,720

Three of these tests also assert the create-step balance change via
`into_balance_change`. The create step shifts by +4,300 credits because
B7 starts billing transformer grovedb reads on Create as well, so the
helper now also takes `original_creation_cost` as a parameter:
- regular contract: 126440160 (PV12) -> 126435860 (PV11)
- mutable contract: 141238960 (PV12) -> 141234660 (PV11)
Downstream seller/buyer balance assertions are now derived from those
fee parameters, not from hardcoded sums.

Each original test is now a 1-line caller of a shared helper
parametrized by `protocol_version` + fees, matching the pattern used
in deletion.rs / replacement.rs / transfer.rs.

21/21 NFT tests pass — the original PV12 cases plus the 5 new PV11
siblings (and the 4 existing PV11 siblings from earlier work).

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

Closes a chain-replay-safety gap on the replacement.rs test file: two
of its tests had fees change because of B4+B7 (transformer-phase reads
and query_documents cost now billed via the outer execution_context).
Without PV11 siblings, v11 chain replay against these test conditions
would silently diverge from mainnet history.

Refactored each affected test into a parametrized helper + PV12 wrapper
+ PV11 sibling, matching the pattern in deletion.rs / transfer.rs / nft.rs:

- test_document_replace_on_document_type_that_is_mutable
  PV12: 1411320  PV11: 1399260  delta: +12,060
- test_document_replace_on_document_type_that_is_not_mutable_but_is_transferable
  PV12: 457660   PV11: 445700   delta: +11,960

16/16 replacement tests pass — the 12 original PV12 cases plus the 2
new PV11 siblings plus the 2 PV11 siblings that already existed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`test_token_burn_group_action_confirmer_fee_includes_transformer_reads`
is the B7-specific empirical test that documents the 30_820-credit
delta (three transformer-phase group-action reads in
`try_from_borrowed_base_transition_with_contract_lookup`). Its
docstring already mentioned both values (4_288_420 v0 vs 4_319_240
v1), but only the PV12 value was asserted — there was no PV11 sibling
to make sure the v0 path stays byte-identical.

Refactored into the same parametrized-helper pattern used elsewhere
in the PR:
- test_token_burn_group_action_confirmer_fee_includes_transformer_reads
  PV12: 4_319_240  PV11: 4_288_420  delta: +30,820

Both tests pass.

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

Final sweep of the chain-replay-safety audit caught 3 tests that lacked
PV11 siblings:

direct_selling/mod.rs (2 tests):
- test_successful_direct_purchase_single_price
  PV12: 699_868_122_220  PV11: 699_868_130_120  delta: +7,900 (B4/B7
  reads billed at PV12)
- test_direct_purchase_single_price_not_paying_full_price
  PV12: 999_987_864_860  PV11: 999_987_872_760  delta: +7,900 (same
  delta — failed-purchase bump-only path also picks up extra billed
  cost at PV12+)

dpns.rs (1 new test added by this PR):
- test_dpns_contract_references_with_no_contested_unique_index
  PV12: 6_010_380  PV11: 5_978_080  delta: +32,300 (T1 parent-domain +
  T2 preorder query costs across 3 subdomain creates, ~10,767 per
  transition, ~5,383 per query)

Each existing test is now a 1-line caller of a shared helper
parametrized by (protocol_version, expected_balance_or_fee), matching
the pattern used elsewhere in the PR. The new PV11 siblings pin the
v3.1-dev values so that v11 chain replay stays bit-for-bit
reproducible.

All 7 tests pass (4 direct_selling + 3 dpns).

Audit complete: every batch-test file with a changed fee assertion
now has PV11 sibling coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The audit doc (`docs/paid-error-fee-audit.md`) served as a planning
artifact during PR development to track Tier 0/1/2/3 fee leaks (B1–B7,
T1–T4). With the PR feature-complete and reviewed, the planning doc is
no longer needed — the in-code comments on each `_v1` function and the
PR description carry the WHY going forward.

Removed:
- docs/paid-error-fee-audit.md
- 6 references in inline code comments (state/v1, trigger/v1 files,
  deletion.rs helpers). Replaced with generic descriptions of the
  fee-billing change at each site — comments still explain WHY each
  `_v1` diverges from `_v0`, just without the audit-doc cross-ref.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shumkov shumkov marked this pull request as ready for review May 22, 2026 02:03
@shumkov shumkov requested a review from QuantumExplorer as a code owner May 22, 2026 02:03
@codecov
Copy link
Copy Markdown

codecov Bot commented May 22, 2026

Codecov Report

❌ Patch coverage is 78.59155% with 152 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.16%. Comparing base (9c6bb72) to head (62dad8b).
⚠️ Report is 1 commits behind head on v3.1-dev.

Files with missing lines Patch % Lines
...itions/batch/data_triggers/triggers/dpns/v1/mod.rs 63.50% 100 Missing ⚠️
...tate_transitions/batch/state/v0/fetch_documents.rs 89.88% 18 Missing ⚠️
...batch/data_triggers/triggers/withdrawals/v1/mod.rs 79.16% 15 Missing ⚠️
...ons/batch/data_triggers/triggers/dashpay/v1/mod.rs 85.41% 7 Missing ⚠️
...ns/batch/data_triggers/triggers/withdrawals/mod.rs 0.00% 3 Missing ⚠️
...batch/data_triggers/triggers/withdrawals/v0/mod.rs 95.34% 2 Missing ⚠️
...n/state_transitions/batch/data_triggers/context.rs 0.00% 1 Missing ⚠️
...itions/batch/data_triggers/triggers/dashpay/mod.rs 66.66% 1 Missing ⚠️
...ons/batch/data_triggers/triggers/dashpay/v0/mod.rs 96.29% 1 Missing ⚠️
...ansitions/batch/data_triggers/triggers/dpns/mod.rs 66.66% 1 Missing ⚠️
... and 3 more
Additional details and impacted files
@@            Coverage Diff             @@
##           v3.1-dev    #3670    +/-   ##
==========================================
  Coverage     87.16%   87.16%            
==========================================
  Files          2600     2603     +3     
  Lines        318021   318598   +577     
==========================================
+ Hits         277211   277719   +508     
- Misses        40810    40879    +69     
Components Coverage Δ
dpp 87.67% <ø> (+<0.01%) ⬆️
drive 85.95% <ø> (+<0.01%) ⬆️
drive-abci 89.66% <78.59%> (-0.02%) ⬇️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.17% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 49.16% <ø> (ø)
🚀 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.

@shumkov shumkov self-assigned this May 22, 2026
@shumkov shumkov moved this to In review / testing in Platform team May 22, 2026
@shumkov shumkov changed the title fix(drive-abci): bill batch transformer drive reads (B7) fix(drive-abci): bill batch transformer drive reads May 22, 2026
Co-Authored-By: Claude Opus 4.7 (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

🧹 Nitpick comments (1)
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs (1)

32-34: ⚡ Quick win

Align new PV11 test names with the test_token_* convention.

The two newly added PV11 tests don’t follow the token test naming convention used in this folder.

♻️ Proposed rename diff
-    async fn test_successful_direct_purchase_single_price_protocol_version_11() {
+    async fn test_token_successful_direct_purchase_single_price_protocol_version_11() {
         run_successful_direct_purchase_single_price_at_protocol_version(11, 699_868_130_120).await;
     }

-    async fn test_direct_purchase_single_price_not_paying_full_price_protocol_version_11() {
+    async fn test_token_direct_purchase_single_price_not_paying_full_price_protocol_version_11() {
         run_direct_purchase_single_price_not_paying_full_price_at_protocol_version(
             11,
             999_987_872_760,
         )
         .await;
     }

Based on learnings: "Enforce test function naming to use the test_token_* convention in all files under packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/."

Also applies to: 285-291

🤖 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/token/direct_selling/mod.rs`
around lines 32 - 34, Rename the newly added PV11 test functions to follow the
repository's token test naming convention (prefix with test_token_). For
example, change test_successful_direct_purchase_single_price_protocol_version_11
to test_token_successful_direct_purchase_single_price_protocol_version_11 and
similarly rename the other PV11 test referenced around lines 285-291; update any
calls or references (e.g., to
run_successful_direct_purchase_single_price_at_protocol_version) to use the new
test names so imports/refs remain consistent. Ensure the function signatures and
async/await usage remain unchanged.
🤖 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/transfer.rs`:
- Around line 233-245: The helper uses a hardcoded runtime
PlatformVersion::get(9) when creating transitions (e.g., in
run_document_transfer_on_document_type_that_is_transferable_before_creator_id_at_protocol_version
and the similar helper at 794-810), which decouples the contract test-data
version from the runtime protocol gate; change the code so the
contract/test-data still uses the fixed contract v0 version but the runtime
validation gate comes from the parameterized protocol_version passed into
TestPlatformBuilder (i.e., stop using PlatformVersion::get(9).unwrap() for
transition/processing and instead use the platform/runtime protocol version
derived from the protocol_version argument or from the TestPlatformBuilder
instance you built with with_initial_protocol_version), updating any transition
creation and processing calls that currently reference PlatformVersion::get(9)
to use the parameterized runtime version.

---

Nitpick comments:
In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs`:
- Around line 32-34: Rename the newly added PV11 test functions to follow the
repository's token test naming convention (prefix with test_token_). For
example, change test_successful_direct_purchase_single_price_protocol_version_11
to test_token_successful_direct_purchase_single_price_protocol_version_11 and
similarly rename the other PV11 test referenced around lines 285-291; update any
calls or references (e.g., to
run_successful_direct_purchase_single_price_at_protocol_version) to use the new
test names so imports/refs remain consistent. Ensure the function signatures and
async/await usage remain unchanged.
🪄 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: 3224e481-d638-49f7-a3cc-97e865a27a07

📥 Commits

Reviewing files that changed from the base of the PR and between ec1b288 and dc984c0.

📒 Files selected for processing (41)
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v1/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/executor.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/reject/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/data_triggers.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v1/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.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/tests/token/burn/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.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 with no reviewable changes (1)
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/data_triggers.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

I verified the reported trigger-billing issue against the PR head and it is real. The new PV12 data-trigger billing path uses committed-state epoch/block metadata instead of the block_info already threaded through block validation, so trigger read fees diverge from the rest of the batch validator at epoch boundaries.

🔴 1 blocking

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 carried-forward blocker from the prior dc984c09 review is STILL VALID at head 1d542c5fefc6ba080a324f4d18a3cb9967d97d2e. validate_state_v0 already has the current block_info, and the generic PV12 batch document-fetch path uses that current epoch for fee billing, but the PV12 data-trigger path still cannot access it and instead prices reads from last_committed_block_info; the latest delta only changes formatting/tests and does not address that mismatch. I did not confirm any additional latest-delta findings beyond this existing blocker.

Reviewed commit: 1d542c5

🔴 1 blocking

1 carried-forward prior finding(s)

blocking: PV12 data triggers still bill reads against last committed epoch/block metadata instead of the block being validated

packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs (line 9)

DataTriggerExecutionContext still exposes only platform, transaction, owner_id, and the outer execution context, so the current block_info never reaches PV12 triggers. In batch/state/v0/mod.rs:302-307, validate_state_v0 constructs that trigger context while block_info is already in scope, but drops it. The PV12 trigger implementations therefore continue to compute fees from committed-state accessors: DashPay calls fetch_identity_balance_with_costs(..., context.platform.state.last_block_info(), ...) at triggers/dashpay/v1/mod.rs:83-90; DPNS passes Some(context.platform.state.last_committed_block_epoch_ref()) at triggers/dpns/v1/mod.rs:265-271 and 365-371; Withdrawals does the same at triggers/withdrawals/v1/mod.rs:91-97. Those accessors are explicitly backed by last_committed_block_info in platform_types/platform_state/accessors.rs:383-392 and 489-494. This is not an accepted pattern, because the rest of the PV12 batch billing path already uses the current block epoch: fetch_documents_for_transitions_knowing_contract_and_document_type_v1 takes an epoch parameter and passes Some(epoch) to query_documents at batch/state/v0/fetch_documents.rs:144-199, and Drive derives query processing cost from the supplied epoch in rs-drive/src/drive/document/query/query_documents/v0/mod.rs:107-116; identity balance billing likewise derives fees from the supplied block_info.epoch in rs-drive/src/drive/identity/fetch/balance/fetch_identity_balance/v0/mod.rs:33-58. On the first block of a new epoch, trigger-backed reads will therefore be billed using the previous epoch's fee schedule, creating deterministic consensus-fee divergence and underbilling risk until the current BlockInfo or Epoch is threaded into DataTriggerExecutionContext and used by all _v1 trigger billing paths.

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

- [BLOCKING] In `packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs`:9-20: PV12 data triggers still bill reads against last committed epoch/block metadata instead of the block being validated
  `DataTriggerExecutionContext` still exposes only `platform`, `transaction`, `owner_id`, and the outer execution context, so the current `block_info` never reaches PV12 triggers. In `batch/state/v0/mod.rs:302-307`, `validate_state_v0` constructs that trigger context while `block_info` is already in scope, but drops it. The PV12 trigger implementations therefore continue to compute fees from committed-state accessors: DashPay calls `fetch_identity_balance_with_costs(..., context.platform.state.last_block_info(), ...)` at `triggers/dashpay/v1/mod.rs:83-90`; DPNS passes `Some(context.platform.state.last_committed_block_epoch_ref())` at `triggers/dpns/v1/mod.rs:265-271` and `365-371`; Withdrawals does the same at `triggers/withdrawals/v1/mod.rs:91-97`. Those accessors are explicitly backed by `last_committed_block_info` in `platform_types/platform_state/accessors.rs:383-392` and `489-494`. This is not an accepted pattern, because the rest of the PV12 batch billing path already uses the current block epoch: `fetch_documents_for_transitions_knowing_contract_and_document_type_v1` takes an `epoch` parameter and passes `Some(epoch)` to `query_documents` at `batch/state/v0/fetch_documents.rs:144-199`, and Drive derives query processing cost from the supplied epoch in `rs-drive/src/drive/document/query/query_documents/v0/mod.rs:107-116`; identity balance billing likewise derives fees from the supplied `block_info.epoch` in `rs-drive/src/drive/identity/fetch/balance/fetch_identity_balance/v0/mod.rs:33-58`. On the first block of a new epoch, trigger-backed reads will therefore be billed using the previous epoch's fee schedule, creating deterministic consensus-fee divergence and underbilling risk until the current `BlockInfo` or `Epoch` is threaded into `DataTriggerExecutionContext` and used by all `_v1` trigger billing paths.

CI on macOS runs `cargo clippy --workspace --all-features --tests --benches -- --no-deps -D warnings`, which surfaces lints that local `cargo check` doesn't.

- Drop unused `ValidationOperation` and `StateTransitionExecutionContextMethodsV0` imports in `state/v0/mod.rs` and `action_validation/document/document_delete_transition_action/state_v0/mod.rs` (leftover from the B7 refactor that moved billing into the v1 wrappers).
- Add `#[allow(clippy::too_many_arguments)]` to the four `fetch_documents_for_transitions_*` / `fetch_document_with_id` facade + v1 functions in `state/v0/fetch_documents.rs`. The 8-arg signature is the cost of the version-facade dispatcher pattern (drive, contract, doc_type, transitions/id, epoch, execution_context, transaction, platform_version) — splitting into a struct would obscure call sites for no clarity gain.

Co-Authored-By: Claude Opus 4.7 (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.

🧹 Nitpick comments (1)
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs (1)

127-128: 💤 Low value

Consider clarifying the TODO comment now that v1 handles billing.

This TODO ("todo: deal with cost of this operation") exists in the v0 function which intentionally passes epoch=None and doesn't bill for PV11 compatibility. Since _v1 now properly bills the cost, this comment may confuse future readers. Consider updating it to clarify the intentional no-billing behavior in v0, or simply remove it since the concern is addressed in v1.

🤖 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/state/v0/fetch_documents.rs`
around lines 127 - 128, Update the stray TODO near the drive.query_documents
call in the v0 state transition to clarify that the no-billing behavior is
intentional for PV11 compatibility: mention that this v0 path passes epoch=None
and deliberately does not bill, and note that billing is handled in the
corresponding _v1 implementation; alternatively remove the TODO entirely if you
prefer no comment. Reference the drive.query_documents call and the
documents_outcome variable when editing so future readers see the rationale tied
to the v0 path and the existence of _v1 handling billing.
🤖 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/state/v0/fetch_documents.rs`:
- Around line 127-128: Update the stray TODO near the drive.query_documents call
in the v0 state transition to clarify that the no-billing behavior is
intentional for PV11 compatibility: mention that this v0 path passes epoch=None
and deliberately does not bill, and note that billing is handled in the
corresponding _v1 implementation; alternatively remove the TODO entirely if you
prefer no comment. Reference the drive.query_documents call and the
documents_outcome variable when editing so future readers see the rationale tied
to the v0 path and the existence of _v1 handling billing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b49618e9-7c77-48ec-908f-7bb3a109f6ae

📥 Commits

Reviewing files that changed from the base of the PR and between 1d542c5 and 0fffb6f.

📒 Files selected for processing (3)
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

✅ DashSDKFFI.xcframework built for this PR.

SwiftPM (host the zip at a stable URL, then use):

.binaryTarget(
  name: "DashSDKFFI",
  url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
  checksum: "92889d5d5f41b61f42d978455393539f1cc107ca2f9111a67c980cdb4ea46a90"
)

Xcode manual integration:

  • Download 'DashSDKFFI.xcframework' artifact from the run link above.
  • Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.
  • If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.

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 checked-out worktree at 0fffb6fe2769d1b2ef828ef4ac1c149267f3a58b. The prior blocking finding from the 1d542c5f review is STILL VALID: PV12 data-trigger billing still derives fee inputs from committed-state metadata instead of the current block_info already threaded through batch validation. I did not confirm any additional actionable findings in the latest delta beyond that carried-forward consensus-fee defect.

_Note: Inline posting failed (command failed (1): python3 scripts/review_poster.py dashpay/platform 3670 0fffb6f
STDOUT:

STDERR:
Traceback (most recent call last):
File "/Users/claw/.openclaw/workspace/scripts/review_poster.py", line 138, in
result = post_review(repo, pr_number, h), so I posted the same verified findings as a top-level review body._

Reviewed commit: 0fffb6f

🔴 1 blocking

1 additional finding(s)

blocking: STILL VALID: PV12 data-trigger billing uses committed-state epoch/block metadata instead of the block being validated

packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs (line 9)

DataTriggerExecutionContext still exposes platform, transaction, owner_id, and the outer execution context, but it does not carry the current BlockInfo or Epoch. That omission is observable in the current head: validate_state_v0 receives block_info as an argument at batch/state/v0/mod.rs:46-54 and uses it throughout transition validation, but when it constructs DataTriggerExecutionContext at batch/state/v0/mod.rs:299-304 it drops block_info entirely. The new PV12 _v1 trigger implementations therefore fall back to committed-state accessors for billing inputs: DashPay passes context.platform.state.last_block_info() at triggers/dashpay/v1/mod.rs:83-90, DPNS passes Some(context.platform.state.last_committed_block_epoch_ref()) at triggers/dpns/v1/mod.rs:265-271 and 365-371, and Withdrawals does the same at triggers/withdrawals/v1/mod.rs:91-97. Those accessors are explicitly backed by last_committed_block_info in platform_types/platform_state/accessors.rs:383-392 and 489-494, not the in-scope block under validation. The fee APIs consume those supplied values when computing processing cost: fetch_identity_balance_with_costs_v0 charges against block_info.epoch at packages/rs-drive/src/drive/identity/fetch/balance/fetch_identity_balance/v0/mod.rs:38-58, and query_documents_v0 charges against the provided epoch at packages/rs-drive/src/drive/document/query/query_documents/v0/mod.rs:107-116. That means trigger-backed reads in the first block of a new epoch are still billed using the previous epoch's fee schedule. Because this is consensus-critical fee accounting, the bug remains merge-blocking until the current block context is threaded into DataTriggerExecutionContext and used by all _v1 trigger billing paths.

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

- [BLOCKING] In `packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs`:9-20: STILL VALID: PV12 data-trigger billing uses committed-state epoch/block metadata instead of the block being validated
  `DataTriggerExecutionContext` still exposes `platform`, `transaction`, `owner_id`, and the outer execution context, but it does not carry the current `BlockInfo` or `Epoch`. That omission is observable in the current head: `validate_state_v0` receives `block_info` as an argument at `batch/state/v0/mod.rs:46-54` and uses it throughout transition validation, but when it constructs `DataTriggerExecutionContext` at `batch/state/v0/mod.rs:299-304` it drops `block_info` entirely. The new PV12 `_v1` trigger implementations therefore fall back to committed-state accessors for billing inputs: DashPay passes `context.platform.state.last_block_info()` at `triggers/dashpay/v1/mod.rs:83-90`, DPNS passes `Some(context.platform.state.last_committed_block_epoch_ref())` at `triggers/dpns/v1/mod.rs:265-271` and `365-371`, and Withdrawals does the same at `triggers/withdrawals/v1/mod.rs:91-97`. Those accessors are explicitly backed by `last_committed_block_info` in `platform_types/platform_state/accessors.rs:383-392` and `489-494`, not the in-scope block under validation. The fee APIs consume those supplied values when computing processing cost: `fetch_identity_balance_with_costs_v0` charges against `block_info.epoch` at `packages/rs-drive/src/drive/identity/fetch/balance/fetch_identity_balance/v0/mod.rs:38-58`, and `query_documents_v0` charges against the provided `epoch` at `packages/rs-drive/src/drive/document/query/query_documents/v0/mod.rs:107-116`. That means trigger-backed reads in the first block of a new epoch are still billed using the previous epoch's fee schedule. Because this is consensus-critical fee accounting, the bug remains merge-blocking until the current block context is threaded into `DataTriggerExecutionContext` and used by all `_v1` trigger billing paths.

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 carried-forward prior blocking finding is STILL VALID at babab83c53854f19792439d2697cfda21b48f575. DataTriggerExecutionContext still drops the in-scope block_info, and the new PV12 _v1 trigger billing paths still price reads from committed-state metadata, so the first block of a new epoch can be charged with the previous epoch's fee schedule. I did not confirm any additional actionable findings in the latest delta, and CodeRabbit reported no inline findings to validate.

_Note: Inline posting failed (command failed (1): python3 scripts/review_poster.py dashpay/platform 3670 babab83
STDOUT:

STDERR:
Traceback (most recent call last):
File "/Users/claw/.openclaw/workspace/scripts/review_poster.py", line 138, in
result = post_review(repo, pr_number, h), so I posted the same verified findings as a top-level review body._

Reviewed commit: babab83

🔴 1 blocking

1 additional finding(s)

blocking: STILL VALID: PV12 data-trigger billing uses committed-state epoch/block metadata instead of the block being validated

packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs (line 9)

validate_state_v0 receives the current block_info, but when it builds DataTriggerExecutionContext it only stores platform, transaction, owner_id, and the outer execution context (batch/state/v0/mod.rs:299-304). That leaves every _v1 trigger without access to the block currently being validated. The new billed trigger paths therefore read fee metadata from committed-state accessors instead: DashPay passes context.platform.state.last_block_info() into fetch_identity_balance_with_costs (triggers/dashpay/v1/mod.rs:83-90), while DPNS and Withdrawals pass Some(context.platform.state.last_committed_block_epoch_ref()) into query_documents (triggers/dpns/v1/mod.rs:265-270, 365-370, triggers/withdrawals/v1/mod.rs:91-96). Those accessors are explicitly backed by last_committed_block_info (platform_state/accessors.rs:383-392, 489-494), not the current block. The downstream fee code uses exactly the supplied epoch for pricing (packages/rs-drive/src/drive/identity/fetch/balance/fetch_identity_balance/v0/mod.rs:38-58, packages/rs-drive/src/drive/document/query/query_documents/v0/mod.rs:107-116). As a result, trigger-backed reads in the first block of a new epoch are billed with the previous epoch's schedule. Because this PR changes consensus-visible fee accounting, that stale-epoch pricing remains merge-blocking until the current block context is threaded into DataTriggerExecutionContext and consumed by all billed _v1 trigger paths.

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

- [BLOCKING] In `packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs`:9-20: STILL VALID: PV12 data-trigger billing uses committed-state epoch/block metadata instead of the block being validated
  `validate_state_v0` receives the current `block_info`, but when it builds `DataTriggerExecutionContext` it only stores `platform`, `transaction`, `owner_id`, and the outer execution context (`batch/state/v0/mod.rs:299-304`). That leaves every `_v1` trigger without access to the block currently being validated. The new billed trigger paths therefore read fee metadata from committed-state accessors instead: DashPay passes `context.platform.state.last_block_info()` into `fetch_identity_balance_with_costs` (`triggers/dashpay/v1/mod.rs:83-90`), while DPNS and Withdrawals pass `Some(context.platform.state.last_committed_block_epoch_ref())` into `query_documents` (`triggers/dpns/v1/mod.rs:265-270`, `365-370`, `triggers/withdrawals/v1/mod.rs:91-96`). Those accessors are explicitly backed by `last_committed_block_info` (`platform_state/accessors.rs:383-392`, `489-494`), not the current block. The downstream fee code uses exactly the supplied epoch for pricing (`packages/rs-drive/src/drive/identity/fetch/balance/fetch_identity_balance/v0/mod.rs:38-58`, `packages/rs-drive/src/drive/document/query/query_documents/v0/mod.rs:107-116`). As a result, trigger-backed reads in the first block of a new epoch are billed with the previous epoch's schedule. Because this PR changes consensus-visible fee accounting, that stale-epoch pricing remains merge-blocking until the current block context is threaded into `DataTriggerExecutionContext` and consumed by all billed `_v1` trigger paths.

Reverts the consensus-critical part of d162df9 ("drop block_info
from DataTriggerExecutionContext"). The previous reasoning — "we have
platform.state.last_committed_block_info, no need for the current
block" — confused last-committed-block with current-block. On the
first block of a new epoch:

- the block being validated is in epoch N+1
- last_committed_block_info is in epoch N
- trigger reads were being billed at the PREVIOUS epoch's fee schedule

This would have shipped a quiet fee-pricing bug on every epoch
boundary — the inverse of the leak this PR is closing.

Changes:
- DataTriggerExecutionContext gains `block_info: &BlockInfo` (the
  current block, not last-committed).
- batch/state/v0/mod.rs threads it through from validate_state_v0's
  existing block_info parameter.
- _v1 triggers (dpns, dashpay, withdrawals) now use
  `context.block_info`/`context.block_info.epoch` instead of
  `platform.state.last_committed_block_epoch_ref()` /
  `last_block_info()`.
- _v0 triggers are unchanged — they don't read epoch from the trigger
  context.
- 6 trigger test construction sites updated to pass
  `&BlockInfo::default()`.
- Dropped the now-unused `PlatformStateV0Methods` import from each _v1
  file.

Also fixes coderabbit's transfer.rs nitpick: the
`before_creator_id_at_protocol_version` and
`contract_v0_at_protocol_version` helpers were hardcoding
`PlatformVersion::get(9)` / `PlatformVersion::latest()` as the
runtime platform_version, decoupling it from the parameterized
`protocol_version`. Now contract loading uses a separate
`contract_platform_version` (PV9 for format-v0 contract bytes) and
the runtime `platform_version` is derived from `protocol_version` —
so the assertion suite actually exercises the gate it claims to.

All 289 batch tests pass (24 transfer + 7 trigger tests checked
explicitly).

Co-Authored-By: Claude Opus 4.7 (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.

Caution

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

⚠️ Outside diff range comments (1)
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs (1)

298-305: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the legacy dry_run view for _v0 triggers.

Wiring the outer execution_context into every trigger here changes PV11 CheckTx behavior, not just billing. The legacy _v0 triggers still gate state errors on in_dry_run()—for example, DashPay v0 skips the self-contact and missing-recipient checks, and DPNS v0 skips its post-query/domain checks when dry_run is true. Before this change those paths saw a fresh default context, so they still validated during mempool checks. Please keep the old local context behavior for _v0 dispatch and only thread the outer mutable context into the _v1 paths that need to accumulate billed operations.

🤖 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/state/v0/mod.rs`
around lines 298 - 305, The change mistakenly threads the outer mutable
state_transition_execution_context into all DataTriggerExecutionContext
instances (constructed here with owner_id_value and
state_transition_execution_context: execution_context), which alters PV11
CheckTx dry_run behavior for legacy _v0 triggers; revert to creating a fresh
local/default state transition execution context for _v0 dispatch so
in_dry_run() sees the legacy view, and only pass the outer mutable
execution_context through for _v1 paths that require billing accumulation (i.e.,
construct a new local ExecutionContext/default StateTransitionExecutionContext
for the _v0 branch when building DataTriggerExecutionContext, and keep using the
existing execution_context for _v1).
🤖 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.

Outside diff comments:
In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs`:
- Around line 298-305: The change mistakenly threads the outer mutable
state_transition_execution_context into all DataTriggerExecutionContext
instances (constructed here with owner_id_value and
state_transition_execution_context: execution_context), which alters PV11
CheckTx dry_run behavior for legacy _v0 triggers; revert to creating a fresh
local/default state transition execution context for _v0 dispatch so
in_dry_run() sees the legacy view, and only pass the outer mutable
execution_context through for _v1 paths that require billing accumulation (i.e.,
construct a new local ExecutionContext/default StateTransitionExecutionContext
for the _v0 branch when building DataTriggerExecutionContext, and keep using the
existing execution_context for _v1).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d6de82a1-b536-46df-be6d-0ca039e12f89

📥 Commits

Reviewing files that changed from the base of the PR and between 0fffb6f and 62dad8b.

📒 Files selected for processing (9)
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.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

I verified the checked-out worktree at 62dad8bf88b8b4bdb40087c2ee41391a691b50b1 against the incremental range babab83c53854f19792439d2697cfda21b48f575..62dad8bf88b8b4bdb40087c2ee41391a691b50b1. The carried-forward prior blocking finding is FIXED: DataTriggerExecutionContext now stores the in-scope BlockInfo, batch state validation threads that exact block_info into trigger execution, and every billed _v1 trigger path reviewed here prices reads from context.block_info / context.block_info.epoch instead of committed-state accessors. I did not confirm any new actionable defects in the latest delta or the cumulative head.

@shumkov shumkov added the ready for final review Ready for the final review. If AI was involved in producing this PR, it has already had a reviewer. label May 22, 2026
@QuantumExplorer QuantumExplorer merged commit 6a6eb06 into v3.1-dev May 22, 2026
38 checks passed
@QuantumExplorer QuantumExplorer deleted the fix/batch-paid-error-fee-charging branch May 22, 2026 08:40
@github-project-automation github-project-automation Bot moved this from In review / testing to Done in Platform team May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for final review Ready for the final review. If AI was involved in producing this PR, it has already had a reviewer.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants