fix(drive-abci): bill batch transformer drive reads#3670
Conversation
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>
📝 WalkthroughWalkthroughIntroduce 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. ChangesData trigger billing refactoring and v1 protocol version support
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
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>
…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>
…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>
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>
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>
Codecov Report❌ Patch coverage is 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
🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
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 winAlign 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
📒 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.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v1/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v0/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/executor.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/reject/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/data_triggers.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v1/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rspackages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rspackages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rspackages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rspackages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rspackages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rspackages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rspackages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rspackages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rspackages/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
thepastaclaw
left a comment
There was a problem hiding this comment.
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
thepastaclaw
left a comment
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs (1)
127-128: 💤 Low valueConsider 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=Noneand doesn't bill for PV11 compatibility. Since_v1now 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
📒 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.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs
|
✅ 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:
|
thepastaclaw
left a comment
There was a problem hiding this comment.
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.
thepastaclaw
left a comment
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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 winPreserve the legacy
dry_runview for_v0triggers.Wiring the outer
execution_contextinto every trigger here changes PV11 CheckTx behavior, not just billing. The legacy_v0triggers still gate state errors onin_dry_run()—for example, DashPay v0 skips the self-contact and missing-recipient checks, and DPNS v0 skips its post-query/domain checks whendry_runis 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_v0dispatch and only thread the outer mutable context into the_v1paths 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
📒 Files selected for processing (9)
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rspackages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs
thepastaclaw
left a comment
There was a problem hiding this comment.
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.
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_contextwas a local that got dropped on return, silently discarding everyadd_operationcall from per-transitiontry_from_borrowed_*_with_contract_lookup(token group actions, contested document creates, etc.). This commit fixes that leak (entry B7 indocs/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_v0previously created a localStateTransitionExecutionContext, passed it intotry_into_action_v0, and dropped it on return. The outer context threaded by the processor (processor/v0/mod.rs:43) was accepted bybatch/mod.rs:57only to be ignored (_execution_context).Gated on the existing
batch_state_transition.transform_into_actionfield, bumped 0 → 1 in V8 (PROTOCOL_VERSION_12):execution_contextthrough 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_v1archive).Audit doc
docs/paid-error-fee-audit.mdcaptures the full inventory of fee-charging gaps discovered while diagnosing B7:query_documentscost — next commit)DataTriggerExecutionContextimmutability refactor)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_b7inbatch/tests/token/burn/mod.rsexercises the cleanest demonstration scenario: a group-action burn where the confirmer step triggers three drive reads insidetry_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_actionvalueprocessing_fee0(pre-fix, dropped local ctx)1(post-fix, threaded outer ctx)Existing fee-pinning tests continue to pass unchanged — verified by
test_document_replace_on_document_type_that_is_mutable(still assertsprocessing_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.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: 0arm.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: 1field on V8:execution_contextthrough the batch transformer (root-cause fix that unblocked everything else).query_documentscost infetch_documents.rs(was discarded; couldn't be fixed in isolation before B7)._with_costs).The DataTrigger refactor (return
(Result, FeeResult)so triggers can surface their costs without needing a mut ref throughDataTriggerExecutionContext) is a separate commit before the per-trigger billing commits.Follow-ups (not in this PR)
Checklist:
For repository code-owners and collaborators only
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests