From 93a13ebeebca5cb02d30285f4266d28e40623654 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 19 May 2026 17:58:46 +0700 Subject: [PATCH 01/20] fix(drive-abci): bill batch transformer drive reads (B7) 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) --- docs/paid-error-fee-audit.md | 301 ++++++++++++++++++ .../state_transitions/batch/mod.rs | 17 +- .../state_transitions/batch/state/v0/mod.rs | 62 +++- .../batch/tests/token/burn/mod.rs | 189 +++++++++++ .../drive_abci_validation_versions/v8.rs | 8 +- 5 files changed, 563 insertions(+), 14 deletions(-) create mode 100644 docs/paid-error-fee-audit.md diff --git a/docs/paid-error-fee-audit.md b/docs/paid-error-fee-audit.md new file mode 100644 index 00000000000..77b379633c3 --- /dev/null +++ b/docs/paid-error-fee-audit.md @@ -0,0 +1,301 @@ +# Paid-Error Fee Charging Audit + +**Branch:** `fix/batch-paid-error-fee-charging` +**Context:** PR #3616 (commit `dfb6d846f9`) flipped invalid batch transitions from `UnpaidConsensusError` to `PaidConsensusError` on `PROTOCOL_VERSION_12+`. Under the new semantics, a failed batch lands as a `BumpIdentityDataContractNonce` action — the user pays for the validation work that ran. But validation does drive reads (contract fetches, document queries, identity lookups) whose cost is sometimes dropped on the floor. Result: the user is under-charged. + +Non-batch state transitions already had paid-error semantics before #3616, so the same class of under-charging has been latent there for some time. + +## ⚠️ CRITICAL CONSTRAINT — consensus-affecting; new function version required + +**Changing the fee a user pays is consensus-affecting.** The chain must reproduce historical block validation bit-for-bit on every protocol version that has shipped. `LATEST_PLATFORM_VERSION = PLATFORM_V12` (`rs-platform-version/src/version/protocol_version.rs:68`) — meaning PV11 and PV12 are frozen. + +**Rule:** every fix below must ship as a **new function version**, gated by a new field on `PlatformVersion`, with v0/v1 behavior preserved verbatim for replay. The target protocol version is **the next one** (current candidate: `PROTOCOL_VERSION_13` — confirm with platform-version policy before implementing). + +**What this looks like in practice** (modeled on the `failed_per_transition_action` versioning that #3616 introduced): + +1. Add a new version field on the relevant struct in `rs-platform-version/src/version/dpp_versions/` or `drive_abci_versions/`. Example: + ```rust + // drive_abci_validation_versions/mod.rs + pub struct BatchStateTransitionValidationVersions { + // ...existing fields... + pub transform_into_action_billing: FeatureVersion, // 0 = pre-fix, 1 = bill transformer reads + } + ``` +2. In `v11.rs`/`v12.rs` set `transform_into_action_billing: 0`. In the new `v13.rs` (or wherever the next bump lands) set `transform_into_action_billing: 1`. +3. In the call site, dispatch on the field: + ```rust + match platform_version.drive_abci.validation_and_processing + .state_transitions.batch_state_transition.transform_into_action_billing + { + 0 => /* old behavior: drop fees */, + 1 => /* new behavior: bill fees */, + v => return Err(UnknownVersionMismatch { ... }), + } + ``` +4. **Never modify** an existing `_v0` or `_v1` function body that has shipped. If the new behavior needs different code, create `_v1` / `_v2` alongside and dispatch. + +**Caveat on file naming** (per the comment at `transformer/v0/mod.rs:1-22`): the `_v0` suffix on batch transformer functions has been retained across behavior changes when the change is finer-grained than a full transformer rewrite — instead, protocol behavior is gated at the *version-field* granularity inside the function. That pattern is preferred when adding billing because duplicating the ~1100-line transformer body for one fee fix would be silly. **Bottom line:** new version *field* + branch inside the existing function, not a new file. Reserve a new `_vN` file only if the change is large enough to make in-function branching unwieldy. + +## Fee plumbing — how a drive read becomes a charge + +1. Validator calls `execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee_result))` after each billable drive op. +2. After validation, `execute_event/v0/mod.rs:61` calls `ValidationOperation::add_many_to_fee_result(&execution_operations, &mut individual_fee_result, ...)`, adding accumulated validation cost on top of the action's drive-op fee. +3. The combined fee is what the user pays — works the same for successful actions and `BumpIdentityDataContractNonce` paid-error actions. + +**Implication:** the canonical fix for every site below is "compute the FeeResult, push it into `execution_context`" — except for the data-trigger sites where the context ref is immutable (architectural blocker — see Tier 4). + +## Severity scale + +- **HIGH** — runs on every transition of its kind, discards measurable cost, hits the paid-error or successful-action path +- **MEDIUM** — runs in many but not all transitions OR smaller per-op cost +- **LOW** — dead code / billed elsewhere via a sibling path +- **NONE** — stale TODO, not actually a fee gap + +--- + +## Tier 0 — Pipeline-level fee leak (discovered while verifying B1/B2) + +### B7 — Transformer's execution_context is a dropped local + +**Severity: HIGH+ (likely dwarfs all individual leaks below)** + +The outer `state_transition_execution_context` created by the processor (`processor/v0/mod.rs:43`, threaded as `&mut` into every other phase) **is not threaded into the batch transformer**. Instead: + +**`batch/mod.rs:57-84`** — `transform_into_action`: +```rust +fn transform_into_action( + &self, /* ... */ + _execution_context: &mut StateTransitionExecutionContext, // <-- parameter ignored (underscore-prefixed) + tx: TransactionArg, +) -> Result<...> { + /* ... */ + 0 => self.transform_into_action_v0(&platform.into(), block_info, validation_mode, tx), + // ^^^ outer ctx NOT passed through +} +``` + +**`state/v0/mod.rs:323-344`** — `transform_into_action_v0`: +```rust +fn transform_into_action_v0(&self, /* ... no ctx param ... */) -> Result<...> { + let mut execution_context = // <-- LOCAL ctx created + StateTransitionExecutionContext::default_for_platform_version(platform_version)?; + + let validation_result = self.try_into_action_v0(/* ... */, &mut execution_context)?; + // ^^^ transformer add_operation calls go here + + Ok(validation_result.map(Into::into)) // <-- LOCAL ctx dropped, all ops discarded +} +``` + +**Consequence:** every `execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee_result))` inside the batch transformer is silently discarded. That includes: + +- **Token base transition group-action lookups** in `try_from_borrowed_base_transition_with_contract_lookup` (`rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_base_transition_action/v0/transformer.rs:289-340`): `fetch_action_is_closed`, `fetch_action_id_signers_power_and_add_operations`, `fetch_active_action_info_and_add_operations`. These accumulate real `LowLevelDriveOperation` entries that get converted via `Drive::calculate_fee(...)` into the returned `FeeResult` — which is then added to the dropped local ctx. +- **All `try_from_borrowed_token_*_with_contract_lookup`** fee_results: `transformer/v0/mod.rs:586, 596, 606, 619, 629, 639, 649, 659, 669, 679, 689`. +- **All `try_from_document_borrowed_*_transition_with_contract_lookup`** fee_results: `transformer/v0/mod.rs:748, 819, 829, 894, 959`. + +**Severity assessment:** +- For token group actions: confirmed real cost loss — `validate_state` performs different reads (`fetch_identity_token_balance`) than the transformer's group-action reads, so the costs don't overlap. +- For non-group tokens: the `None` arm at `token_base_transition_action/v0/transformer.rs:244-245` skips the group reads — actual loss is minimal. +- For document creates/replaces: depends on what `try_from_document_borrowed_*` actually reads vs. what `validate_state` re-reads. Requires per-transition-kind audit. + +**B1, B2, B3 are subsumed by B7.** Even if you switched `epoch=None` → `epoch=Some(...)` to make those fetches produce a `FeeResult`, that result would still flow into the local ctx and be dropped. + +**Fix approach** (gated by new protocol version per the constraint above): +- Thread `&mut execution_context` from `batch/mod.rs:65` through `transform_into_action_v0` down into `try_into_action_v0`. The outer ctx already exists at the processor; the local-ctx wrapper just needs to be removed (or repurposed as a no-op pre-PV-bump for replay). +- Verify the existing v0 behavior is preserved by reading via the version field — current behavior is "discard ops", new behavior is "thread through". + +**Open question for archaeology pass:** is the local-ctx pattern intentional ("transformer reads are free by design") or a latent bug? Initial git-blame shows the pattern has existed since `8884df7f58` (the squashed commit that introduced batch state validation). No comment justifying it. Probably an oversight, but worth a definitive answer before treating as a bug. + +--- + +## Tier 1 — Batch path (new bugs surfaced by #3616) + +### Contract fetches in batch transformer + +| # | file:line | what's discarded | severity | +|---|---|---|---| +| B1 | `state_transitions/batch/transformer/v0/mod.rs:365` | Token contract fetch cost — `get_contract_with_fetch_info_and_fee(...)` called with `epoch=None`, so `FeeResult` is `None` by design. Runs on every batch with token transitions. | **SUBSUMED BY B7** — even with `epoch=Some(...)`, the resulting fee would flow into the dropped local ctx. Verified: not billed upstream (basic structure does no reads; nonce validation bills its own reads only; signature/advanced-structure phases don't refetch the contract; per-action `validate_state` reads contract from `data_contract_fetch_info_ref()` of the action, not a fresh fetch). | +| B2 | `state_transitions/batch/transformer/v0/mod.rs:418` | Document contract fetch cost — same pattern as B1. Runs on every batch with document transitions. | **SUBSUMED BY B7** | +| B3 | `state_transitions/batch/state/v0/fetch_documents.rs:91` | Internal contract fetch cost in `fetch_documents_for_transitions_knowing_contract_id_and_document_type_name`. Function is `#[allow(dead_code)]` but reachable via other paths to audit. | LOW | + +**B1/B2 resolved:** verified there is no upstream billing site. The contract is fetched exactly once during the batch pipeline (in the transformer), with `epoch=None` deliberately suppressing fee computation, into a context that gets dropped. Three independent reasons it's unbilled — fixing any one of them is necessary but not sufficient without also fixing B7. + +### Document query in batch transformer + +| # | file:line | what's discarded | severity | +|---|---|---|---| +| B4 | `state_transitions/batch/state/v0/fetch_documents.rs:168` | `drive.query_documents(...)` cost from `fetch_documents_for_transitions_knowing_contract_and_document_type`. Documented as `// todo: deal with cost of this operation`. Used by transformer/v0/mod.rs:511 on every batch with replace/transfer/purchase/update-price. | **HIGH** — this is the headliner | +| B5 | `state_transitions/batch/state/v0/fetch_documents.rs:211` | `drive.query_documents(...)` cost from `fetch_document_with_id` — function returns `FeeResult` separately, billing is caller-dependent. | MEDIUM | + +### Failed-batch path + +| # | file:line | what's discarded | severity | +|---|---|---|---| +| B6 | `transformer/v0/mod.rs:1119` (`failed_per_transition_action`) | When validation fails after the document query at line 511 succeeded, the query cost was never added to `execution_context` (see B4). User pays only `BumpIdentityDataContractNonce` cost. | HIGH (duplicate of B4 — fixing B4 fixes this) | + +--- + +## Tier 2 — Data triggers (new bugs surfaced by #3616 + architectural blocker) + +**Architectural blocker:** `DataTriggerExecutionContext` holds an immutable `&'a StateTransitionExecutionContext`. Triggers cannot call `add_operation()` because that method requires `&mut self`. Fixing the trigger fee leaks requires either: +- Refactoring `DataTriggerExecutionContext` to hold `&'a mut StateTransitionExecutionContext`, OR +- Returning a `FeeResult` from each trigger fn and having the caller bill it. + +The second option is less invasive but spreads fee accounting across the trigger dispatcher. + +| # | file:line | trigger | what's discarded | severity | +|---|---|---|---|---| +| T1 | `data_triggers/triggers/dpns/v0/mod.rs:246` | dpns: `create_domain_data_trigger_v0` | `drive.query_documents(...)` cost — parent domain lookup. Runs when creating a subdomain. | HIGH | +| T2 | `data_triggers/triggers/dpns/v0/mod.rs:337` | dpns: `create_domain_data_trigger_v0` | `drive.query_documents(...)` cost — preorder lookup. Runs on every DPNS domain create. | HIGH | +| T3 | `data_triggers/triggers/dashpay/v0/mod.rs:77` | dashpay: `create_contact_request_data_trigger_v0` | `drive.fetch_identity_balance(...)` cost — recipient identity check. Runs on every contact request. (Line 74 has `// TODO: Calculate fee operations` placeholder.) | HIGH | +| T4 | `data_triggers/triggers/withdrawals/v0/mod.rs:79` | withdrawals: `delete_withdrawal_data_trigger_v0` | `drive.query_documents(...)` cost — withdrawal document fetch. Runs on every withdrawal delete. | HIGH | +| T5 | `data_triggers/triggers/reject/v0/mod.rs` | reject | (no drive ops — pure validation) | NONE | + +--- + +## Tier 3 — Non-batch state transitions (pre-existing bugs, predate #3616) + +### identity_credit_transfer + +| # | file:line | what's discarded | severity | +|---|---|---|---| +| N1 | `state_transitions/identity_credit_transfer/state/v0/mod.rs:38` | `drive.fetch_identity_balance(self.identity_id(), ...)` — sender balance fetch. Runs every transition. | HIGH | +| N2 | `state_transitions/identity_credit_transfer/state/v0/mod.rs:61` | `drive.fetch_identity_balance(self.recipient_id(), ...)` — recipient balance fetch. Runs every transition. | HIGH | + +### identity_create / identity_create_from_addresses + +| # | file:line | what's discarded | severity | +|---|---|---|---| +| N3 | `state_transitions/identity_create/state/v0/mod.rs:75` | `drive.fetch_identity_balance(identity_id, ...)` — existence check. | HIGH | +| N4 | `state_transitions/identity_create_from_addresses/state/v0/mod.rs:56` | `drive.fetch_identity_balance(identity_id, ...)` — existence check. | HIGH | + +### masternode_vote + +| # | file:line | what's discarded | severity | +|---|---|---|---| +| N5 | `state_transitions/masternode_vote/transform_into_action/v0/mod.rs:53` | `drive.fetch_identity_contested_resource_vote(...)` — existing vote lookup. | MEDIUM | + +### Shared validators in `common/` + +These are called from multiple transition kinds, so a single fix benefits many sites. + +| # | file:line | what's discarded | severity | +|---|---|---|---| +| N6 | `common/validate_identity_public_key_contract_bounds/v0/mod.rs:61` | Has explicit `//todo: we should add to the execution context the cost of fetching contracts`. All contract fetches and key lookups inside this function are unbilled. Called on every identity_update with contract-bounded keys. | HIGH | +| N7 | `common/validate_identity_public_key_contract_bounds/v0/mod.rs:67,111,154,185,234,269` | Six internal `get_contract_with_fetch_info` / `fetch_identity_keys` calls — each runs on different bounds patterns. | MEDIUM (covered by N6 root-cause fix) | +| N8 | `common/validate_identity_public_key_ids_dont_exist_in_state/v0/mod.rs:39` | `drive.fetch_identity_keys::(...)` — key-ID existence check. Called on identity_update adding keys and identity_create. | HIGH | +| N9 | `common/validate_identity_public_key_ids_exist_in_state/v0/mod.rs:34` | `drive.fetch_identity_keys::(...)` — key fetch for disable/remove. Called on identity_update disabling keys. | HIGH | +| N10 | `common/validate_unique_identity_public_key_hashes_in_state/v0/mod.rs:35` | `drive.has_any_of_unique_public_key_hashes(...)` — global key-hash uniqueness. Called on identity_create, identity_create_from_addresses, identity_update with added keys. | HIGH | +| N11 | `common/asset_lock/proof/verify_is_not_spent/v0/mod.rs:30` | `drive.fetch_asset_lock_outpoint_info(...)` — asset-lock spent check. Called on identity_create, identity_top_up, address_funding_from_asset_lock, shield_from_asset_lock. | HIGH | +| N12 | `common/asset_lock/proof/validate/instant/mod.rs:57` | Local instant-lock signature verification — has `// TODO: Shouldn't we add an operation for fees?` | MEDIUM | + +--- + +## Severity rollup + +| Tier | HIGH count | MEDIUM count | LOW/NONE count | +|---|---|---|---| +| **Tier 0 (pipeline)** | **1 (B7 — likely dominates all batch leaks)** | | | +| Tier 1 (batch) | 1 standalone (B4) — B1/B2 subsumed by B7 | 1 (B5) | 1 (B3) | +| Tier 2 (triggers) | 4 (T1, T2, T3, T4) | 0 | 1 (T5) | +| Tier 3 (non-batch) | 7 (N1–N4, N6, N8–N11) | 4 (N5, N7, N12, partially N6 sub-sites) | 0 | +| **Total HIGH** | **13 + B7 (pipeline-level)** | | | + +## Open questions to resolve in implementation phase + +1. ~~**B1/B2 — is the batch transformer's contract fetch already billed upstream?**~~ **RESOLVED** — no, and B1/B2 are subsumed by B7. Fixing B7 (threading the outer ctx through the transformer) is necessary but not sufficient to bill the contract fetch — the `epoch=None` parameter must also become `Some(...)` so a `FeeResult` is actually computed. +2. **B7 — confirm the local-ctx drop is unintentional, not a deliberate "transformer reads are free" policy.** Git-blame to `8884df7f58` (squashed commit) shows no comment justifying it. Recommend posting to PR description as a discovery rather than treating as confirmed bug until reviewed. +3. **Trigger architecture (T1–T4) — refactor context to `&mut` vs. return FeeResult?** The first is cleaner but touches more code. +4. **PR splitting:** B7 + Tier 1 + Tier 2 ship together (one PV13 gate covering all batch-path billing). Tier 3 is pre-existing under-billing in other transition kinds — could be the same PV13 bump or a separate PR. Decision affects diff size and review surface. +5. **Test strategy:** for each HIGH site, write a regression test that runs the same input under both `PLATFORM_V12` (old: under-billed) and `PLATFORM_V13` (new: correctly billed), asserting the fee delta. Pattern after the existing `replayed_failed_replace_with_consumed_nonce_must_be_rejected_at_check_tx` test in `batch/tests/document/replacement.rs`, which already uses the dual-version pattern from #3616. + +## Next steps + +Walk through each entry, in this order: +1. **B7 first** (it dominates the batch leak and gates how everything else gets fixed) — write a regression test that captures the dropped transformer-phase fee, confirm red on V12, design the version-field plumbing, implement. +2. Then **T1–T4** (triggers) — needs the architectural decision on plumbing first. +3. Then **N1–N12** (non-batch) — can proceed in parallel once the version-gate pattern is established. + +Per CLAUDE.md TDD discipline: every fix gets a test that goes red on V12 → green on V13 in the same commit. + +--- + +## Implementation approach (B7 + B4) + +**User directives (2026-05-19):** +- Target = **PV12 re-cut** (V8 amended in place; not a new platform version). Confirms PV12 hasn't shipped to mainnet yet. +- **Reuse existing version field** `batch_state_transition.transform_into_action` (bump 0 → 1) rather than adding a new field. +- **Separate tests and separate commits** for B7 and B4 — so each leak is *independently observable*. If B4 were fixed first or bundled, fixing B7 wouldn't move any fee number ("B4 can hide B7"). Order matters: B7 first, then B4. + +### Two-commit shape + +| Commit | Code change | Test added | Demonstrates | +|---|---|---|---| +| 1 — **B7 fix** | Thread outer `execution_context` through `batch/mod.rs` and `state/v0/mod.rs`. Gate behavior on `transform_into_action: 0 / 1`. Bump V8's value to 1. | New test using a scenario with non-zero transformer-phase reads (token group action OR contested document create). Asserts the post-fix fee value. | B7 is real and isolatable: only group-action / contested-doc scenarios change fee. Non-group simple replaces (B4-affected) remain unchanged. | +| 2 — **B4 fix** | Change `fetch_documents_for_transitions_knowing_contract_and_document_type` to return cost (or accept ctx); bill at callsite in transformer/v0/mod.rs:511. Same `transform_into_action: 1` gate. | New test using a simple successful document replace. Asserts the post-fix fee value, with the delta = query_documents cost. | B4 is a SEPARATE bug from B7 — even after B7's fix, simple replaces still leaked the query cost. | + +**Why this order matters:** B4's fix has no user-visible effect without B7 — `fetch_documents.rs:168` is called from inside the transformer, whose `execution_context` is the dropped local. If we did B4 first, fixing the query cost would add it to a context that gets discarded → no fee change → can't tell whether B4 is real or fixed. So **B7 must land first, then B4 builds on top**. + +### Original combined approach (kept for reference) + +These two fix together because they touch the same call chain: +- **B7** is the structural fix — thread the outer `execution_context` through `batch/mod.rs:57-84` and `state/v0/mod.rs:323-344` so the transformer's `add_operation` calls actually land in the per-transition fee. +- **B4** is the leaf fix — `fetch_documents_for_transitions_knowing_contract_and_document_type` at `state/v0/fetch_documents.rs:128` calls `drive.query_documents(...)` and discards `documents_outcome.cost()`. After B7, this cost has a home; before B7, fixing B4 in isolation does nothing. + +### Version-field plumbing + +Following the `failed_per_transition_action` model from #3616: + +1. Add a new field on `DriveAbciDocumentsStateTransitionValidationVersions` (in `rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs`): + ```rust + pub batch_transformer_billing: FeatureVersion, + ``` +2. Existing v1.rs through v8.rs (covering PV1–PV12): explicitly `batch_transformer_billing: 0`. +3. New v9.rs (for PV13): `batch_transformer_billing: 1`. +4. New v13.rs in `rs-platform-version/src/version/`: wires `DRIVE_ABCI_VALIDATION_VERSIONS_V9` into `PLATFORM_V13`. +5. `protocol_version.rs:68` `LATEST_PLATFORM_VERSION = &PLATFORM_V13`. + +### Code-level branch points + +Per the file-header comment at `transformer/v0/mod.rs:1-22`, we **do not bump the `_v0` suffix**; we branch *inside* the existing function based on the version field. + +| Branch site | v0 behavior (PV11–PV12, frozen) | v1 behavior (PV13+, new) | +|---|---|---| +| `batch/mod.rs:57-84` `transform_into_action` | `_execution_context` ignored; call wrapper without ctx | `execution_context` passed to wrapper | +| `state/v0/mod.rs:323-344` `transform_into_action_v0` | Create local ctx, drop on return | Accept ctx parameter, use outer; no local | +| `state/v0/fetch_documents.rs:128` `fetch_documents_for_transitions_knowing_contract_and_document_type` | Discard `documents_outcome.cost()` | Wrap return type to `(Vec, FeeResult)`; caller adds to ctx | +| `transformer/v0/mod.rs:511` (callsite for ↑) | No fee accounting | After call, `execution_context.add_operation(PrecalculatedOperation(fee))` | +| Transformer add_operation calls (lines 586, 596, 606, 619, 629, 639, 649, 659, 669, 679, 689, 748, 819, 829, 894, 959) | Land in local ctx (dropped) | Land in outer ctx (billed) — no code change, behavior changes via ctx threading alone | + +The function signatures changing means PV11/PV12 callers need a way to invoke the old behavior. Simplest pattern: make the new signature additive (`Option<&mut StateTransitionExecutionContext>` or similar) and branch on the version field internally. + +### Test scenarios + +Two test scenarios cover both leaks: + +1. **`test_successful_document_replace_fee_protocol_version_12` (new, pins V12 baseline)** — exercises B4. Successful Replace of a mutable profile document. V12 fee captured as the under-billing baseline. Asserts current `aggregated_fees().processing_fee` value. +2. **`test_successful_document_replace_fee_protocol_version_13` (new, pins V13 fix)** — same scenario, but on PV13. Asserts the fee is higher by exactly the `query_documents` cost. Δ ≈ the cost of fetching one document from the documents subtree. + +Optional follow-up tests for B7-only (token-group-action and contested-document-create scenarios) can land in the same PR if time permits. + +### Files to touch (B7 + B4 first PR) + +``` +packages/rs-platform-version/src/version/ + drive_abci_versions/drive_abci_validation_versions/mod.rs # +field on struct + drive_abci_versions/drive_abci_validation_versions/v1.rs..v8.rs # +field = 0 (8 files) + drive_abci_versions/drive_abci_validation_versions/v9.rs # NEW: field = 1 + v13.rs # NEW + protocol_version.rs # +PLATFORM_V13 to list, bump LATEST + +packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/ + mod.rs # thread ctx in transform_into_action + state/v0/mod.rs # thread ctx in transform_into_action_v0; gate local-ctx creation + state/v0/fetch_documents.rs # add fee return + version gate + transformer/v0/mod.rs # bill the query cost at callsite (gated) + tests/document/replacement.rs # add 2 new tests (V12 baseline + V13 fix) +``` + +### Pre-flight checks + +Before starting code: confirm `LATEST_PLATFORM_VERSION` is V12, no test pins fees on V13 (none exist yet), and `protocol_version.rs` is the only place that needs `LATEST_PLATFORM_VERSION` updated. Also: search for any explicit `.latest()` / `PV13` references that would need staging. diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs index 8c0ba510d5e..287e30a19ea 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs @@ -62,11 +62,16 @@ impl StateTransitionActionTransformer for BatchTransition { BTreeMap, >, validation_mode: ValidationMode, - _execution_context: &mut StateTransitionExecutionContext, + execution_context: &mut StateTransitionExecutionContext, tx: TransactionArg, ) -> Result, Error> { let platform_version = platform.state.current_platform_version()?; + // Dispatch always calls `transform_into_action_v0` (the only function + // version that exists); the version field is interpreted *inside* that + // function to decide whether the outer `execution_context` is threaded + // through to the transformer or whether a dropped-on-return local ctx + // is used (v11-and-below behavior). See B7 in docs/paid-error-fee-audit.md. match platform_version .drive_abci .validation_and_processing @@ -74,10 +79,16 @@ impl StateTransitionActionTransformer for BatchTransition { .batch_state_transition .transform_into_action { - 0 => self.transform_into_action_v0(&platform.into(), block_info, validation_mode, tx), + 0 | 1 => self.transform_into_action_v0( + &platform.into(), + block_info, + validation_mode, + execution_context, + tx, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "documents batch transition: transform_into_action".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index 5c75b5e3c6b..62b4ba57c24 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -59,6 +59,7 @@ pub(in crate::execution::validation::state_transition::state_transitions::batch) platform: &PlatformStateRef, block_info: &BlockInfo, validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, tx: TransactionArg, ) -> Result, Error>; } @@ -325,21 +326,62 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { platform: &PlatformStateRef, block_info: &BlockInfo, validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, tx: TransactionArg, ) -> Result, Error> { let platform_version = platform.state.current_platform_version()?; - let mut execution_context = - StateTransitionExecutionContext::default_for_platform_version(platform_version)?; + // The `transform_into_action` field gates whether transformer-phase + // `add_operation` calls are billed back to the user. + // + // - v0 (PROTOCOL_VERSION_11 and below): the transformer accumulates + // into a local `StateTransitionExecutionContext` that is dropped on + // return — every per-transition fee_result added by + // `try_from_borrowed_*_with_contract_lookup` is discarded. Preserved + // verbatim for chain replay. + // - v1 (PROTOCOL_VERSION_12+): the outer `execution_context` is + // threaded into `try_into_action_v0` so the transformer's + // add_operation calls actually reach the user's bill (issue B7 in + // docs/paid-error-fee-audit.md). + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .transform_into_action + { + 0 => { + let mut local_execution_context = + StateTransitionExecutionContext::default_for_platform_version( + platform_version, + )?; - let validation_result = self.try_into_action_v0( - platform, - block_info, - validation_mode.should_validate_batch_valid_against_state(), - tx, - &mut execution_context, - )?; + let validation_result = self.try_into_action_v0( + platform, + block_info, + validation_mode.should_validate_batch_valid_against_state(), + tx, + &mut local_execution_context, + )?; - Ok(validation_result.map(Into::into)) + Ok(validation_result.map(Into::into)) + } + 1 => { + let validation_result = self.try_into_action_v0( + platform, + block_info, + validation_mode.should_validate_batch_valid_against_state(), + tx, + execution_context, + )?; + + Ok(validation_result.map(Into::into)) + } + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "documents batch transition: transform_into_action_v0".to_string(), + known_versions: vec![0, 1], + received: version, + })), + } } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs index b55b7e13523..3219e8f3e3a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs @@ -3666,4 +3666,193 @@ mod token_burn_tests { .expect("expected to fetch total supply"); assert_eq!(total_supply, Some(103000)); } + + /// Regression test for B7 (paid-error-fee-audit.md): the batch + /// transformer's `execution_context` was previously 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 are the cleanest demonstration site: the + /// **confirmer** step (action_is_proposer=false) 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` — accumulated into a + /// `FeeResult` that was then added to the dropped local ctx. + /// + /// This test pins the post-B7-fix fee (PROTOCOL_VERSION_12+, where + /// `transform_into_action` field bumped 0 → 1 in V8). The same scenario + /// run with `transform_into_action: 0` would produce a lower fee equal + /// to the difference of the three dropped group reads — see the audit + /// doc for the diagnostic procedure. + #[tokio::test] + async fn test_token_burn_group_action_confirmer_fee_b7() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + + let mut rng = StdRng::seed_from_u64(49853); + let platform_state = platform.state.load(); + + let (identity1, signer1, key1) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (identity2, signer2, key2) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity1.id(), + Some(|token_configuration: &mut TokenConfiguration| { + token_configuration.set_manual_burning_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::Group(0), + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + }), + None, + Some( + [( + 0, + Group::V0(GroupV0 { + members: [(identity1.id(), 1), (identity2.id(), 1)].into(), + required_power: 2, + }), + )] + .into(), + ), + None, + platform_version, + ); + + add_tokens_to_identity(&platform, token_id, identity1.id(), 100000); + + // Step 1: identity1 proposes the burn — action_is_proposer=true, no + // transformer-phase group reads, no B7-affected fee. + let propose_transition = BatchTransition::new_token_burn_transition( + token_id, + identity1.id(), + contract.id(), + 0, + 100000, + None, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), + &key1, + 2, + 0, + &signer1, + platform_version, + None, + ) + .await + .expect("expected to create proposer burn transition"); + + let propose_serialized = propose_transition + .serialize_to_bytes() + .expect("expected to serialize proposer burn"); + + let transaction = platform.drive.grove.start_transaction(); + let proposer_result = platform + .platform + .process_raw_state_transitions( + &[propose_serialized], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process proposer burn"); + assert_eq!(proposer_result.valid_count(), 1); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit proposer burn"); + + // Step 2: identity2 confirms the burn — action_is_proposer=false. + // The confirmer's `try_from_borrowed_base_transition_with_contract_lookup` + // does the three group-action drive reads whose cost B7 now bills. + let action_id = TokenBurnTransition::calculate_action_id_with_fields( + token_id.as_bytes(), + identity1.id().as_bytes(), + 2, + 100000, + ); + + let confirm_transition = BatchTransition::new_token_burn_transition( + token_id, + identity2.id(), + contract.id(), + 0, + 100000, + None, + Some( + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: 0, + action_id, + action_is_proposer: false, + }, + ), + ), + &key2, + 2, + 0, + &signer2, + platform_version, + None, + ) + .await + .expect("expected to create confirmer burn transition"); + + let confirm_serialized = confirm_transition + .serialize_to_bytes() + .expect("expected to serialize confirmer burn"); + + let transaction = platform.drive.grove.start_transaction(); + let confirmer_result = platform + .platform + .process_raw_state_transitions( + &[confirm_serialized], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process confirmer burn"); + assert_eq!(confirmer_result.valid_count(), 1); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit confirmer burn"); + + // B7 assertion: pin the confirmer's fee, which now includes the + // three group-action read costs that B7 previously dropped via the + // local execution_context. + // + // Empirical values captured during B7 development: + // * `transform_into_action: 0` (pre-B7, dropped local ctx): 4_288_420 + // * `transform_into_action: 1` (post-B7, threaded outer ctx): 4_319_240 + // * delta = 30_820 credits = the three transformer-phase reads + // (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. + assert_eq!( + confirmer_result.aggregated_fees().processing_fee, + 4_319_240, + "B7: confirmer step must bill the three group-action drive reads" + ); + } } diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index f7e83c01ee8..757fdce896d 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -120,7 +120,13 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = advanced_structure: 0, state: 0, revision: 0, - transform_into_action: 0, + // PROTOCOL_VERSION_12 (v3.1 hard fork): the outer + // `execution_context` is threaded through the batch transformer + // so per-transition fee_results accumulated inside + // `try_into_action_v0` are billed to the user. v0 preserves the + // legacy dropped-local-ctx behavior for PROTOCOL_VERSION_11 + // chain replay. See B7 in docs/paid-error-fee-audit.md. + transform_into_action: 1, // PROTOCOL_VERSION_12 (v3.1 hard fork): per-transition // failure paths in `transform_document_transition` now emit // a `BumpIdentityDataContractNonce` action so the user pays From 7a5634692747006d93639327be9ac040fc6b3042 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 19 May 2026 18:34:39 +0700 Subject: [PATCH 02/20] refactor(drive-abci): split _v0/_v1 for transform_into_action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/paid-error-fee-audit.md | 50 +++++----- .../state_transitions/batch/mod.rs | 14 +-- .../state_transitions/batch/state/v0/mod.rs | 92 +++++++++---------- 3 files changed, 76 insertions(+), 80 deletions(-) diff --git a/docs/paid-error-fee-audit.md b/docs/paid-error-fee-audit.md index 77b379633c3..ef62ec14a72 100644 --- a/docs/paid-error-fee-audit.md +++ b/docs/paid-error-fee-audit.md @@ -13,28 +13,22 @@ Non-batch state transitions already had paid-error semantics before #3616, so th **What this looks like in practice** (modeled on the `failed_per_transition_action` versioning that #3616 introduced): -1. Add a new version field on the relevant struct in `rs-platform-version/src/version/dpp_versions/` or `drive_abci_versions/`. Example: - ```rust - // drive_abci_validation_versions/mod.rs - pub struct BatchStateTransitionValidationVersions { - // ...existing fields... - pub transform_into_action_billing: FeatureVersion, // 0 = pre-fix, 1 = bill transformer reads - } - ``` -2. In `v11.rs`/`v12.rs` set `transform_into_action_billing: 0`. In the new `v13.rs` (or wherever the next bump lands) set `transform_into_action_billing: 1`. -3. In the call site, dispatch on the field: +1. Bump (or add) a version field on the relevant struct in `rs-platform-version/src/version/dpp_versions/` or `drive_abci_versions/`. Existing pattern reuses the dedicated field that already gates the function — e.g. `batch_state_transition.transform_into_action: 0 → 1`. +2. In `v11.rs` (or earlier) the field stays at its old value; in `v12.rs` (or the targeted version) the field gets the new value. +3. **Add a new function version** `_v1` alongside the existing `_v0` with the new behavior. `_v0` stays **byte-identical** to the version that shipped. +4. Dispatch on the field at the call site — the version branch happens **outside** the function, not inside: ```rust match platform_version.drive_abci.validation_and_processing - .state_transitions.batch_state_transition.transform_into_action_billing + .state_transitions.batch_state_transition.transform_into_action { - 0 => /* old behavior: drop fees */, - 1 => /* new behavior: bill fees */, + 0 => self.transform_into_action_v0(/* old signature */), + 1 => self.transform_into_action_v1(/* new signature, threaded ctx */), v => return Err(UnknownVersionMismatch { ... }), } ``` -4. **Never modify** an existing `_v0` or `_v1` function body that has shipped. If the new behavior needs different code, create `_v1` / `_v2` alongside and dispatch. +5. **Never modify** an existing `_v0` (or any shipped `_vN`) function body. If a new field value needs different behavior, add `_v(N+1)` and route to it. -**Caveat on file naming** (per the comment at `transformer/v0/mod.rs:1-22`): the `_v0` suffix on batch transformer functions has been retained across behavior changes when the change is finer-grained than a full transformer rewrite — instead, protocol behavior is gated at the *version-field* granularity inside the function. That pattern is preferred when adding billing because duplicating the ~1100-line transformer body for one fee fix would be silly. **Bottom line:** new version *field* + branch inside the existing function, not a new file. Reserve a new `_vN` file only if the change is large enough to make in-function branching unwieldy. +**Exception for the ~1100-line transformer body** (per the comment at `transformer/v0/mod.rs:1-22`): the `try_into_action_v0` / `transform_document_transition_v0` / etc. functions in that file are intentionally kept at `_v0` and gated at *finer* version-field granularity inside them (e.g. `failed_per_transition_action`, `flatten`, `merge_many`). Bumping their suffix would force copy-pasting the entire file as a v0 archive — exactly the regression #3616 set out to avoid. The B7 fix follows this pattern: `try_into_action_v0` is unchanged and continues to be the single transformer entry-point; only the *outer wrapper* `transform_into_action_v0` got a `_v1` sibling to thread the ctx through. ## Fee plumbing — how a drive read becomes a charge @@ -255,19 +249,27 @@ Following the `failed_per_transition_action` model from #3616: 4. New v13.rs in `rs-platform-version/src/version/`: wires `DRIVE_ABCI_VALIDATION_VERSIONS_V9` into `PLATFORM_V13`. 5. `protocol_version.rs:68` `LATEST_PLATFORM_VERSION = &PLATFORM_V13`. -### Code-level branch points +### Code-level branch points (B7, as shipped) + +Two function versions side-by-side; dispatch outside. + +| Branch site | v0 behavior (PV11, frozen — `transform_into_action: 0`) | v1 behavior (PV12, new — `transform_into_action: 1`) | +|---|---|---| +| `batch/mod.rs:57-84` `transform_into_action` | Match arm `0` → calls `transform_into_action_v0(...)` (no ctx) | Match arm `1` → calls `transform_into_action_v1(..., execution_context, ...)` | +| `state/v0/mod.rs::transform_into_action_v0` | **Byte-identical to v3.1-dev original.** Creates local ctx, calls `try_into_action_v0`, drops local on return | Untouched | +| `state/v0/mod.rs::transform_into_action_v1` | N/A | New function. Takes the outer `execution_context` and threads it into `try_into_action_v0` | +| Transformer add_operation calls inside `try_into_action_v0` (lines 586, 596, 606, 619, 629, 639, 649, 659, 669, 679, 689, 748, 819, 829, 894, 959) | Land in the local ctx that `_v0` drops | Land in the outer ctx that `_v1` threads → billed to the user | + +The transformer body (`try_into_action_v0` and all its helpers in `transformer/v0/mod.rs`) is **unchanged** — same single function, called by both wrappers. Only the wrapper's choice of which ctx to pass differs. -Per the file-header comment at `transformer/v0/mod.rs:1-22`, we **do not bump the `_v0` suffix**; we branch *inside* the existing function based on the version field. +### Code-level branch points (B4, next commit) -| Branch site | v0 behavior (PV11–PV12, frozen) | v1 behavior (PV13+, new) | +| Branch site | v0 behavior (PV11, frozen) | v1 behavior (PV12, new) | |---|---|---| -| `batch/mod.rs:57-84` `transform_into_action` | `_execution_context` ignored; call wrapper without ctx | `execution_context` passed to wrapper | -| `state/v0/mod.rs:323-344` `transform_into_action_v0` | Create local ctx, drop on return | Accept ctx parameter, use outer; no local | -| `state/v0/fetch_documents.rs:128` `fetch_documents_for_transitions_knowing_contract_and_document_type` | Discard `documents_outcome.cost()` | Wrap return type to `(Vec, FeeResult)`; caller adds to ctx | -| `transformer/v0/mod.rs:511` (callsite for ↑) | No fee accounting | After call, `execution_context.add_operation(PrecalculatedOperation(fee))` | -| Transformer add_operation calls (lines 586, 596, 606, 619, 629, 639, 649, 659, 669, 679, 689, 748, 819, 829, 894, 959) | Land in local ctx (dropped) | Land in outer ctx (billed) — no code change, behavior changes via ctx threading alone | +| `state/v0/fetch_documents.rs::fetch_documents_for_transitions_knowing_contract_and_document_type` | Discards `documents_outcome.cost()` (keeps current signature) | New function variant returns `(Vec, FeeResult)` | +| `transformer/v0/mod.rs:511` (callsite for ↑) | No fee accounting (kept for v0 transformer wrapper, called only from `_v0` path) | After call, `execution_context.add_operation(PrecalculatedOperation(fee))` | -The function signatures changing means PV11/PV12 callers need a way to invoke the old behavior. Simplest pattern: make the new signature additive (`Option<&mut StateTransitionExecutionContext>` or similar) and branch on the version field internally. +To preserve the `try_into_action_v0` single-entry-point invariant, B4 will likely thread cost via a return-value channel that the existing function can ignore — see B4 implementation notes when that commit lands. ### Test scenarios diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs index 287e30a19ea..eacc78eb2a9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs @@ -67,11 +67,6 @@ impl StateTransitionActionTransformer for BatchTransition { ) -> Result, Error> { let platform_version = platform.state.current_platform_version()?; - // Dispatch always calls `transform_into_action_v0` (the only function - // version that exists); the version field is interpreted *inside* that - // function to decide whether the outer `execution_context` is threaded - // through to the transformer or whether a dropped-on-return local ctx - // is used (v11-and-below behavior). See B7 in docs/paid-error-fee-audit.md. match platform_version .drive_abci .validation_and_processing @@ -79,7 +74,14 @@ impl StateTransitionActionTransformer for BatchTransition { .batch_state_transition .transform_into_action { - 0 | 1 => self.transform_into_action_v0( + // PROTOCOL_VERSION_11 and below: legacy `_v0` drops every + // transformer-phase fee_result via a local execution_context. + // Preserved verbatim for chain replay. + 0 => self.transform_into_action_v0(&platform.into(), block_info, validation_mode, tx), + // PROTOCOL_VERSION_12+: `_v1` threads the outer execution_context + // into the transformer so per-transition fees are billed. See B7 + // in docs/paid-error-fee-audit.md. + 1 => self.transform_into_action_v1( &platform.into(), block_info, validation_mode, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index 62b4ba57c24..c9dcdf7181d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -55,6 +55,20 @@ pub(in crate::execution::validation::state_transition::state_transitions::batch) ) -> Result, Error>; fn transform_into_action_v0( + &self, + platform: &PlatformStateRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + tx: TransactionArg, + ) -> Result, Error>; + + /// PROTOCOL_VERSION_12+: same as v0 but threads the caller's + /// `execution_context` into `try_into_action_v0` so per-transition + /// fee_results accumulated by the transformer + /// (`try_from_borrowed_*_with_contract_lookup`) are billed to the + /// user instead of being dropped via a local ctx. See B7 in + /// docs/paid-error-fee-audit.md. + fn transform_into_action_v1( &self, platform: &PlatformStateRef, block_info: &BlockInfo, @@ -326,62 +340,40 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { platform: &PlatformStateRef, block_info: &BlockInfo, validation_mode: ValidationMode, - execution_context: &mut StateTransitionExecutionContext, tx: TransactionArg, ) -> Result, Error> { let platform_version = platform.state.current_platform_version()?; - // The `transform_into_action` field gates whether transformer-phase - // `add_operation` calls are billed back to the user. - // - // - v0 (PROTOCOL_VERSION_11 and below): the transformer accumulates - // into a local `StateTransitionExecutionContext` that is dropped on - // return — every per-transition fee_result added by - // `try_from_borrowed_*_with_contract_lookup` is discarded. Preserved - // verbatim for chain replay. - // - v1 (PROTOCOL_VERSION_12+): the outer `execution_context` is - // threaded into `try_into_action_v0` so the transformer's - // add_operation calls actually reach the user's bill (issue B7 in - // docs/paid-error-fee-audit.md). - match platform_version - .drive_abci - .validation_and_processing - .state_transitions - .batch_state_transition - .transform_into_action - { - 0 => { - let mut local_execution_context = - StateTransitionExecutionContext::default_for_platform_version( - platform_version, - )?; + let mut execution_context = + StateTransitionExecutionContext::default_for_platform_version(platform_version)?; - let validation_result = self.try_into_action_v0( - platform, - block_info, - validation_mode.should_validate_batch_valid_against_state(), - tx, - &mut local_execution_context, - )?; + let validation_result = self.try_into_action_v0( + platform, + block_info, + validation_mode.should_validate_batch_valid_against_state(), + tx, + &mut execution_context, + )?; - Ok(validation_result.map(Into::into)) - } - 1 => { - let validation_result = self.try_into_action_v0( - platform, - block_info, - validation_mode.should_validate_batch_valid_against_state(), - tx, - execution_context, - )?; + Ok(validation_result.map(Into::into)) + } - Ok(validation_result.map(Into::into)) - } - version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { - method: "documents batch transition: transform_into_action_v0".to_string(), - known_versions: vec![0, 1], - received: version, - })), - } + fn transform_into_action_v1( + &self, + platform: &PlatformStateRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + tx: TransactionArg, + ) -> Result, Error> { + let validation_result = self.try_into_action_v0( + platform, + block_info, + validation_mode.should_validate_batch_valid_against_state(), + tx, + execution_context, + )?; + + Ok(validation_result.map(Into::into)) } } From 446110ed63002cc359c1593c297ce16bb718bb40 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 19 May 2026 18:42:42 +0700 Subject: [PATCH 03/20] refactor(drive-abci): move transform_into_action_v1 to its own v1 module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/paid-error-fee-audit.md | 8 ++- .../state_transitions/batch/mod.rs | 1 + .../state_transitions/batch/state/mod.rs | 1 + .../state_transitions/batch/state/v0/mod.rs | 34 ------------ .../state_transitions/batch/state/v1/mod.rs | 55 +++++++++++++++++++ 5 files changed, 62 insertions(+), 37 deletions(-) create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v1/mod.rs diff --git a/docs/paid-error-fee-audit.md b/docs/paid-error-fee-audit.md index ef62ec14a72..a4f6c45df6b 100644 --- a/docs/paid-error-fee-audit.md +++ b/docs/paid-error-fee-audit.md @@ -255,11 +255,13 @@ Two function versions side-by-side; dispatch outside. | Branch site | v0 behavior (PV11, frozen — `transform_into_action: 0`) | v1 behavior (PV12, new — `transform_into_action: 1`) | |---|---|---| -| `batch/mod.rs:57-84` `transform_into_action` | Match arm `0` → calls `transform_into_action_v0(...)` (no ctx) | Match arm `1` → calls `transform_into_action_v1(..., execution_context, ...)` | -| `state/v0/mod.rs::transform_into_action_v0` | **Byte-identical to v3.1-dev original.** Creates local ctx, calls `try_into_action_v0`, drops local on return | Untouched | -| `state/v0/mod.rs::transform_into_action_v1` | N/A | New function. Takes the outer `execution_context` and threads it into `try_into_action_v0` | +| `batch/mod.rs::transform_into_action` (dispatcher) | Match arm `0` → calls `transform_into_action_v0(...)` (no ctx) | Match arm `1` → calls `transform_into_action_v1(..., execution_context, ...)` | +| `batch/state/v0/mod.rs` — `DocumentsBatchStateTransitionStateValidationV0::transform_into_action_v0` | **Byte-identical to v3.1-dev original.** Creates local ctx, calls `try_into_action_v0`, drops local on return | Untouched | +| `batch/state/v1/mod.rs` — `DocumentsBatchStateTransitionStateValidationV1::transform_into_action_v1` | N/A (new file) | New trait + impl. Takes the outer `execution_context` and threads it into `try_into_action_v0` | | Transformer add_operation calls inside `try_into_action_v0` (lines 586, 596, 606, 619, 629, 639, 649, 659, 669, 679, 689, 748, 819, 829, 894, 959) | Land in the local ctx that `_v0` drops | Land in the outer ctx that `_v1` threads → billed to the user | +Each function version lives in its own directory (`state/v0/`, `state/v1/`) with its own trait — matches the pattern used by other versioned state-transition functions (e.g., `address_funding_from_asset_lock/transform_into_action/v0/`). + The transformer body (`try_into_action_v0` and all its helpers in `transformer/v0/mod.rs`) is **unchanged** — same single function, called by both wrappers. Only the wrapper's choice of which ctx to pass differs. ### Code-level branch points (B4, next commit) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs index eacc78eb2a9..9a81f62b914 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs @@ -33,6 +33,7 @@ use crate::rpc::core::CoreRPCLike; use crate::execution::validation::state_transition::batch::advanced_structure::v0::DocumentsBatchStateTransitionStructureValidationV0; use crate::execution::validation::state_transition::batch::identity_contract_nonce::v0::DocumentsBatchStateTransitionIdentityContractNonceV0; use crate::execution::validation::state_transition::batch::state::v0::DocumentsBatchStateTransitionStateValidationV0; +use crate::execution::validation::state_transition::batch::state::v1::DocumentsBatchStateTransitionStateValidationV1; use crate::execution::validation::state_transition::processor::advanced_structure_with_state::StateTransitionStructureKnownInStateValidationV0; use crate::execution::validation::state_transition::processor::basic_structure::StateTransitionBasicStructureValidationV0; use crate::execution::validation::state_transition::processor::identity_nonces::StateTransitionIdentityNonceValidationV0; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs index 9a1925de7fc..008be12cc67 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/mod.rs @@ -1 +1,2 @@ pub(crate) mod v0; +pub(crate) mod v1; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index c9dcdf7181d..5c75b5e3c6b 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -61,21 +61,6 @@ pub(in crate::execution::validation::state_transition::state_transitions::batch) validation_mode: ValidationMode, tx: TransactionArg, ) -> Result, Error>; - - /// PROTOCOL_VERSION_12+: same as v0 but threads the caller's - /// `execution_context` into `try_into_action_v0` so per-transition - /// fee_results accumulated by the transformer - /// (`try_from_borrowed_*_with_contract_lookup`) are billed to the - /// user instead of being dropped via a local ctx. See B7 in - /// docs/paid-error-fee-audit.md. - fn transform_into_action_v1( - &self, - platform: &PlatformStateRef, - block_info: &BlockInfo, - validation_mode: ValidationMode, - execution_context: &mut StateTransitionExecutionContext, - tx: TransactionArg, - ) -> Result, Error>; } impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { @@ -357,23 +342,4 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { Ok(validation_result.map(Into::into)) } - - fn transform_into_action_v1( - &self, - platform: &PlatformStateRef, - block_info: &BlockInfo, - validation_mode: ValidationMode, - execution_context: &mut StateTransitionExecutionContext, - tx: TransactionArg, - ) -> Result, Error> { - let validation_result = self.try_into_action_v0( - platform, - block_info, - validation_mode.should_validate_batch_valid_against_state(), - tx, - execution_context, - )?; - - Ok(validation_result.map(Into::into)) - } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v1/mod.rs new file mode 100644 index 00000000000..cdc824af4f5 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v1/mod.rs @@ -0,0 +1,55 @@ +use dpp::block::block_info::BlockInfo; +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::batch_transition::BatchTransition; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::StateTransitionAction; + +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::state_transitions::batch::transformer::v0::BatchTransitionTransformerV0; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; + +/// PROTOCOL_VERSION_12+: like v0, but threads the caller's +/// `execution_context` into `try_into_action_v0` so per-transition +/// fee_results accumulated by the transformer +/// (`try_from_borrowed_*_with_contract_lookup`) are billed to the user +/// instead of being dropped via a local ctx. See B7 in +/// `docs/paid-error-fee-audit.md`. +/// +/// The transformer body (`try_into_action_v0` and all helpers in +/// `transformer/v0/mod.rs`) is intentionally still at `_v0` per the +/// file-header comment there — both `transform_into_action_v0` and this +/// `_v1` wrapper share the same single transformer entry point. +pub(in crate::execution::validation::state_transition::state_transitions::batch) trait DocumentsBatchStateTransitionStateValidationV1 +{ + fn transform_into_action_v1( + &self, + platform: &PlatformStateRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + tx: TransactionArg, + ) -> Result, Error>; +} + +impl DocumentsBatchStateTransitionStateValidationV1 for BatchTransition { + fn transform_into_action_v1( + &self, + platform: &PlatformStateRef, + block_info: &BlockInfo, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + tx: TransactionArg, + ) -> Result, Error> { + let validation_result = self.try_into_action_v0( + platform, + block_info, + validation_mode.should_validate_batch_valid_against_state(), + tx, + execution_context, + )?; + + Ok(validation_result.map(Into::into)) + } +} From 201a2e3c1f43f66f31407076560bab70b1326fbd Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 19 May 2026 19:07:12 +0700 Subject: [PATCH 04/20] chore(drive-abci): remove audit-doc references from code 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) --- .../state_transitions/batch/mod.rs | 5 +- .../state_transitions/batch/state/v1/mod.rs | 3 +- .../batch/tests/token/burn/mod.rs | 53 ++++++++++--------- .../drive_abci_validation_versions/v8.rs | 2 +- 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs index 9a81f62b914..c1fef559bd1 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs @@ -80,8 +80,9 @@ impl StateTransitionActionTransformer for BatchTransition { // Preserved verbatim for chain replay. 0 => self.transform_into_action_v0(&platform.into(), block_info, validation_mode, tx), // PROTOCOL_VERSION_12+: `_v1` threads the outer execution_context - // into the transformer so per-transition fees are billed. See B7 - // in docs/paid-error-fee-audit.md. + // into the transformer so per-transition fees accumulated by + // `try_from_borrowed_*_with_contract_lookup` are billed to the + // user instead of being dropped via a local ctx. 1 => self.transform_into_action_v1( &platform.into(), block_info, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v1/mod.rs index cdc824af4f5..d5cc4e73ced 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v1/mod.rs @@ -14,8 +14,7 @@ use crate::platform_types::platform::PlatformStateRef; /// `execution_context` into `try_into_action_v0` so per-transition /// fee_results accumulated by the transformer /// (`try_from_borrowed_*_with_contract_lookup`) are billed to the user -/// instead of being dropped via a local ctx. See B7 in -/// `docs/paid-error-fee-audit.md`. +/// instead of being dropped via a local ctx. /// /// The transformer body (`try_into_action_v0` and all helpers in /// `transformer/v0/mod.rs`) is intentionally still at `_v0` per the diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs index 3219e8f3e3a..154ed6bbd3c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/burn/mod.rs @@ -3667,25 +3667,24 @@ mod token_burn_tests { assert_eq!(total_supply, Some(103000)); } - /// Regression test for B7 (paid-error-fee-audit.md): the batch - /// transformer's `execution_context` was previously a local that got - /// dropped on return, silently discarding every `add_operation` call - /// from per-transition `try_from_borrowed_*_with_contract_lookup`. + /// Pins the confirmer-step processing fee for a token group burn. /// - /// Token group actions are the cleanest demonstration site: the - /// **confirmer** step (action_is_proposer=false) 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` — accumulated into a - /// `FeeResult` that was then added to the dropped local ctx. + /// The confirmer (action_is_proposer=false) 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`. Their cost is + /// accumulated into a `FeeResult` and added to the + /// `execution_context`. /// - /// This test pins the post-B7-fix fee (PROTOCOL_VERSION_12+, where - /// `transform_into_action` field bumped 0 → 1 in V8). The same scenario - /// run with `transform_into_action: 0` would produce a lower fee equal - /// to the difference of the three dropped group reads — see the audit - /// doc for the diagnostic procedure. + /// Under `transform_into_action: 1` (PROTOCOL_VERSION_12+) the outer + /// `execution_context` is threaded through the transformer, so this + /// fee_result reaches the user's bill. Under v0 (PROTOCOL_VERSION_11 + /// and below) the fee_result lands in a dropped local ctx — verified + /// empirically by toggling the version field and re-running this test + /// (see commit message of the version bump for the recorded delta). #[tokio::test] - async fn test_token_burn_group_action_confirmer_fee_b7() { + async fn test_token_burn_group_action_confirmer_fee_includes_transformer_reads() { let platform_version = PlatformVersion::latest(); let mut platform = TestPlatformBuilder::new() .with_latest_protocol_version() @@ -3731,8 +3730,10 @@ mod token_burn_tests { add_tokens_to_identity(&platform, token_id, identity1.id(), 100000); - // Step 1: identity1 proposes the burn — action_is_proposer=true, no - // transformer-phase group reads, no B7-affected fee. + // Step 1: identity1 proposes the burn — action_is_proposer=true, so + // `try_from_borrowed_base_transition_with_contract_lookup` skips the + // group-action drive reads (the only path that adds non-empty + // fee_results inside the transformer). let propose_transition = BatchTransition::new_token_burn_transition( token_id, identity1.id(), @@ -3778,7 +3779,7 @@ mod token_burn_tests { // Step 2: identity2 confirms the burn — action_is_proposer=false. // The confirmer's `try_from_borrowed_base_transition_with_contract_lookup` - // does the three group-action drive reads whose cost B7 now bills. + // does the three group-action drive reads whose cost we now bill. let action_id = TokenBurnTransition::calculate_action_id_with_fields( token_id.as_bytes(), identity1.id().as_bytes(), @@ -3837,13 +3838,13 @@ mod token_burn_tests { .unwrap() .expect("expected to commit confirmer burn"); - // B7 assertion: pin the confirmer's fee, which now includes the - // three group-action read costs that B7 previously dropped via the - // local execution_context. + // Pin the confirmer's fee, which now includes the three + // group-action read costs previously dropped via the local + // execution_context. // - // Empirical values captured during B7 development: - // * `transform_into_action: 0` (pre-B7, dropped local ctx): 4_288_420 - // * `transform_into_action: 1` (post-B7, threaded outer ctx): 4_319_240 + // Empirical values captured during development: + // * `transform_into_action: 0` (legacy, dropped local ctx): 4_288_420 + // * `transform_into_action: 1` (current, threaded outer ctx): 4_319_240 // * delta = 30_820 credits = the three transformer-phase reads // (fetch_action_is_closed + // fetch_action_id_signers_power_and_add_operations + @@ -3852,7 +3853,7 @@ mod token_burn_tests { assert_eq!( confirmer_result.aggregated_fees().processing_fee, 4_319_240, - "B7: confirmer step must bill the three group-action drive reads" + "confirmer step must bill the three group-action drive reads" ); } } diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index 757fdce896d..06ac68a0c46 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -125,7 +125,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = // so per-transition fee_results accumulated inside // `try_into_action_v0` are billed to the user. v0 preserves the // legacy dropped-local-ctx behavior for PROTOCOL_VERSION_11 - // chain replay. See B7 in docs/paid-error-fee-audit.md. + // chain replay. transform_into_action: 1, // PROTOCOL_VERSION_12 (v3.1 hard fork): per-transition // failure paths in `transform_document_transition` now emit From 7a93acb1fbb3d190c3c2fa79108533617a416dad Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 19 May 2026 20:13:48 +0700 Subject: [PATCH 05/20] fix(drive-abci): bill query_documents cost in batch transformer (B4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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) --- .../batch/state/v0/fetch_documents.rs | 58 ++++++++++++++----- .../batch/tests/document/nft.rs | 40 ++++++------- .../batch/tests/document/replacement.rs | 13 +++-- .../batch/tests/document/transfer.rs | 14 ++--- .../batch/tests/token/direct_selling/mod.rs | 4 +- .../batch/transformer/v0/mod.rs | 36 +++++++++++- .../drive_abci_validation_versions/mod.rs | 12 ++++ .../drive_abci_validation_versions/v1.rs | 1 + .../drive_abci_validation_versions/v2.rs | 1 + .../drive_abci_validation_versions/v3.rs | 1 + .../drive_abci_validation_versions/v4.rs | 1 + .../drive_abci_validation_versions/v5.rs | 1 + .../drive_abci_validation_versions/v6.rs | 1 + .../drive_abci_validation_versions/v7.rs | 1 + .../drive_abci_validation_versions/v8.rs | 8 +++ 15 files changed, 142 insertions(+), 50 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs index 58583fb5455..3310e01ec86 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs @@ -35,6 +35,7 @@ use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperat pub(crate) fn fetch_documents_for_transitions( platform: &PlatformStateRef, document_transitions: &[&DocumentTransition], + epoch: &Epoch, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result>, Error> { @@ -63,6 +64,7 @@ pub(crate) fn fetch_documents_for_transitions( contract_id, document_type_name, transitions.as_slice(), + epoch, transaction, platform_version, ) @@ -81,6 +83,7 @@ pub(crate) fn fetch_documents_for_transitions_knowing_contract_id_and_document_t contract_id: &Identifier, document_type_name: &str, transitions: &[&DocumentTransition], + epoch: &Epoch, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result>, Error> { @@ -115,26 +118,46 @@ pub(crate) fn fetch_documents_for_transitions_knowing_contract_id_and_document_t .into(), )); }; - fetch_documents_for_transitions_knowing_contract_and_document_type( - drive, - &contract_fetch_info.contract, - document_type, - transitions, - transaction, - platform_version, - ) + // This caller is `#[allow(dead_code)]`; the FeeResult is discarded + // here because it is currently unused. If this function gets revived, + // the caller will need to propagate the fee through the existing + // billing version-gate. + let (validation_result, _fee_result) = + fetch_documents_for_transitions_knowing_contract_and_document_type( + drive, + &contract_fetch_info.contract, + document_type, + transitions, + epoch, + transaction, + platform_version, + )?; + Ok(validation_result) } +/// Returns the fetched documents plus the `FeeResult` for the underlying +/// `query_documents` operation. The caller decides whether to bill the +/// `FeeResult` to the `StateTransitionExecutionContext` — gated by the +/// `fetch_documents_for_transitions_billing` field on +/// `DriveAbciDocumentsStateTransitionValidationVersions`. +/// +/// `query_documents` only computes a non-zero cost when an `Epoch` is +/// provided; the legacy `None` epoch resulted in a hard-coded zero cost +/// that was discarded anyway. pub(crate) fn fetch_documents_for_transitions_knowing_contract_and_document_type( drive: &Drive, contract: &DataContract, document_type: DocumentTypeRef, transitions: &[&DocumentTransition], + epoch: &Epoch, transaction: TransactionArg, platform_version: &PlatformVersion, -) -> Result>, Error> { +) -> Result<(ConsensusValidationResult>, FeeResult), Error> { if transitions.is_empty() { - return Ok(ConsensusValidationResult::new_with_data(vec![])); + return Ok(( + ConsensusValidationResult::new_with_data(vec![]), + FeeResult::default(), + )); } let ids: Vec = transitions @@ -164,17 +187,24 @@ pub(crate) fn fetch_documents_for_transitions_knowing_contract_and_document_type block_time_ms: None, }; - // todo: deal with cost of this operation let documents_outcome = drive.query_documents( drive_query, - None, + Some(epoch), false, transaction, Some(platform_version.protocol_version), )?; - Ok(ConsensusValidationResult::new_with_data( - documents_outcome.documents_owned(), + let fee_result = FeeResult { + storage_fee: 0, + processing_fee: documents_outcome.cost(), + fee_refunds: Default::default(), + removed_bytes_from_system: 0, + }; + + Ok(( + ConsensusValidationResult::new_with_data(documents_outcome.documents_owned()), + fee_result, )) } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs index 9498b00ed25..0a63c02a4d6 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs @@ -346,7 +346,7 @@ mod nft_tests { assert_eq!(processing_result.valid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 2473880); + assert_eq!(processing_result.aggregated_fees().processing_fee, 2485600); let query_sender_results = platform .drive @@ -602,7 +602,7 @@ mod nft_tests { None ); - assert_eq!(processing_result.aggregated_fees().processing_fee, 2473880); + assert_eq!(processing_result.aggregated_fees().processing_fee, 2485600); let seller_balance = platform .drive @@ -613,7 +613,7 @@ mod nft_tests { // the seller should have received 0.1 and already had 0.1 minus the processing fee and storage fee assert_eq!( seller_balance, - dash_to_credits!(0.1) - original_creation_cost - 2689880 + dash_to_credits!(0.1) - original_creation_cost - 2701600 ); let query_sender_results = platform @@ -709,7 +709,7 @@ mod nft_tests { assert_eq!(processing_result.aggregated_fees().storage_fee, 64611000); - assert_eq!(processing_result.aggregated_fees().processing_fee, 4080480); + assert_eq!(processing_result.aggregated_fees().processing_fee, 4092360); assert_eq!( processing_result @@ -743,7 +743,7 @@ mod nft_tests { // the seller should have received 0.1 and already had 0.1 minus the processing fee and storage fee assert_eq!( seller_balance, - dash_to_credits!(0.2) - original_creation_cost + 20014623 + dash_to_credits!(0.2) - original_creation_cost + 20002903 ); let buyers_balance = platform @@ -753,7 +753,7 @@ mod nft_tests { .expect("expected that purchaser exists"); // the buyer paid 0.1, but also storage and processing fees - assert_eq!(buyers_balance, dash_to_credits!(0.9) - 68691480); + assert_eq!(buyers_balance, dash_to_credits!(0.9) - 68703360); } #[tokio::test] @@ -1017,7 +1017,7 @@ mod nft_tests { None ); - assert_eq!(processing_result.aggregated_fees().processing_fee, 2717400); + assert_eq!(processing_result.aggregated_fees().processing_fee, 2729120); let seller_balance = platform .drive @@ -1028,7 +1028,7 @@ mod nft_tests { // the seller should have received 0.1 and already had 0.1 minus the processing fee and storage fee assert_eq!( seller_balance, - dash_to_credits!(0.1) - original_creation_cost - 2717400 - 378000 + dash_to_credits!(0.1) - original_creation_cost - 2729120 - 378000 ); // now let's update price, but first go to next epoch @@ -1106,7 +1106,7 @@ mod nft_tests { None ); - assert_eq!(processing_result.aggregated_fees().processing_fee, 2721160); + assert_eq!(processing_result.aggregated_fees().processing_fee, 2733160); let seller_balance = platform .drive @@ -1117,7 +1117,7 @@ mod nft_tests { // the seller should have received 0.1 and already had 0.1 minus the processing fee and storage fee assert_eq!( seller_balance, - dash_to_credits!(0.1) - original_creation_cost - 2717400 - 378000 - 2721160 - 216000 + dash_to_credits!(0.1) - original_creation_cost - 2729120 - 378000 - 2733160 - 216000 ); let query_sender_results = platform @@ -1230,7 +1230,7 @@ mod nft_tests { assert_eq!(processing_result.aggregated_fees().storage_fee, 64611000); - assert_eq!(processing_result.aggregated_fees().processing_fee, 4345280); + assert_eq!(processing_result.aggregated_fees().processing_fee, 4357440); assert_eq!( processing_result @@ -1264,7 +1264,7 @@ mod nft_tests { // the seller should have received 0.1 and already had 0.1 minus the processing fee and storage fee assert_eq!( seller_balance, - dash_to_credits!(0.2) - original_creation_cost + 46955162 + dash_to_credits!(0.2) - original_creation_cost + 46931442 ); let buyers_balance = platform @@ -1274,7 +1274,7 @@ mod nft_tests { .expect("expected that purchaser exists"); // the buyer paid 0.1, but also storage and processing fees - assert_eq!(buyers_balance, dash_to_credits!(0.9) - 68956280); + assert_eq!(buyers_balance, dash_to_credits!(0.9) - 68968440); } #[tokio::test] @@ -1506,7 +1506,7 @@ mod nft_tests { None ); - assert_eq!(processing_result.aggregated_fees().processing_fee, 2473880); + assert_eq!(processing_result.aggregated_fees().processing_fee, 2485600); let seller_balance = platform .drive @@ -1517,7 +1517,7 @@ mod nft_tests { // the seller should have received 0.1 and already had 0.1 minus the processing fee and storage fee assert_eq!( seller_balance, - dash_to_credits!(0.1) - original_creation_cost - 2689880 + dash_to_credits!(0.1) - original_creation_cost - 2701600 ); let query_sender_results = platform @@ -1615,7 +1615,7 @@ mod nft_tests { assert_eq!(processing_result.aggregated_fees().storage_fee, 64611000); - assert_eq!(processing_result.aggregated_fees().processing_fee, 4080480); + assert_eq!(processing_result.aggregated_fees().processing_fee, 4092360); assert_eq!( processing_result @@ -1649,7 +1649,7 @@ mod nft_tests { // the seller should have received 0.1 and already had 0.1 minus the processing fee and storage fee assert_eq!( seller_balance, - dash_to_credits!(0.2) - original_creation_cost + 20014623 + dash_to_credits!(0.2) - original_creation_cost + 20002903 ); let buyers_balance = platform @@ -1659,7 +1659,7 @@ mod nft_tests { .expect("expected that purchaser exists"); // the buyer paid 0.1, but also storage and processing fees - assert_eq!(buyers_balance, dash_to_credits!(0.9) - 68691480); + assert_eq!(buyers_balance, dash_to_credits!(0.9) - 68703360); } /// Helper for the paired Purchase-at-wrong-price test. Same scenario at @@ -2613,7 +2613,7 @@ mod nft_tests { assert_eq!(processing_result.valid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 2473880); + assert_eq!(processing_result.aggregated_fees().processing_fee, 2485600); let query_sender_results = platform .drive @@ -2922,7 +2922,7 @@ mod nft_tests { async fn test_document_set_price_on_not_owned_document() { run_document_set_price_on_not_owned_document_at_protocol_version( PlatformVersion::latest().protocol_version, - 571240, + 582960, ) .await; } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs index 8a16213a46b..b4bfcf1f3b6 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs @@ -146,7 +146,7 @@ mod replacement_tests { assert_eq!(processing_result.valid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 1399260); + assert_eq!(processing_result.aggregated_fees().processing_fee, 1411320); let issues = platform .drive @@ -671,7 +671,7 @@ mod replacement_tests { async fn test_document_replace_on_document_type_that_is_not_mutable() { run_document_replace_on_document_type_that_is_not_mutable_at_protocol_version( PlatformVersion::latest().protocol_version, - 445700, + 460920, ) .await; } @@ -1094,7 +1094,7 @@ mod replacement_tests { assert_eq!(processing_result.valid_count(), 0); - assert_eq!(processing_result.aggregated_fees().processing_fee, 445700); + assert_eq!(processing_result.aggregated_fees().processing_fee, 457660); let query_sender_results = platform .drive @@ -1224,13 +1224,14 @@ mod replacement_tests { ); } - /// PROTOCOL_VERSION_12+ — same fee as v11 because the bump emission for - /// this specific path is unconditional (pre-existing legacy behavior). + /// PROTOCOL_VERSION_12+ — bump emission for this specific path is + /// unconditional (pre-existing legacy behavior), but the document + /// query now bills its cost on top of v11's bump-only fee. #[tokio::test] async fn test_document_replace_that_does_not_yet_exist() { run_document_replace_that_does_not_yet_exist_at_protocol_version( PlatformVersion::latest().protocol_version, - 516040, + 520340, ) .await; } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs index 2df43222264..70cbf0d5fd8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs @@ -160,7 +160,7 @@ mod transfer_tests { assert_eq!(processing_result.aggregated_fees().storage_fee, 0); // There is no storage fee, as there are no indexes that will change - assert_eq!(processing_result.aggregated_fees().processing_fee, 1985420); + assert_eq!(processing_result.aggregated_fees().processing_fee, 1997120); let issues = platform .drive @@ -391,7 +391,7 @@ mod transfer_tests { Some(14992395) ); - assert_eq!(processing_result.aggregated_fees().processing_fee, 3369260); + assert_eq!(processing_result.aggregated_fees().processing_fee, 3380960); let query_sender_results = platform .drive @@ -639,7 +639,7 @@ mod transfer_tests { Some(14992395) ); - assert_eq!(processing_result.aggregated_fees().processing_fee, 3631040); + assert_eq!(processing_result.aggregated_fees().processing_fee, 3643400); let query_sender_results = platform .drive @@ -893,7 +893,7 @@ mod transfer_tests { Some(14992395) ); - assert_eq!(processing_result.aggregated_fees().processing_fee, 3369260); + assert_eq!(processing_result.aggregated_fees().processing_fee, 3380960); let query_sender_results = platform .drive @@ -1105,7 +1105,7 @@ mod transfer_tests { assert_eq!(processing_result.valid_count(), 0); - assert_eq!(processing_result.aggregated_fees().processing_fee, 445700); + assert_eq!(processing_result.aggregated_fees().processing_fee, 457000); let query_sender_results = platform .drive @@ -1292,7 +1292,7 @@ mod transfer_tests { async fn test_document_transfer_that_does_not_yet_exist() { run_document_transfer_that_does_not_yet_exist_at_protocol_version( PlatformVersion::latest().protocol_version, - 517400, + 521700, ) .await; } @@ -1484,7 +1484,7 @@ mod transfer_tests { assert_eq!(processing_result.valid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 3991900); + assert_eq!(processing_result.aggregated_fees().processing_fee, 4004260); let query_sender_results = platform .drive diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs index 20ec6ed5377..d53177d1f5a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs @@ -155,7 +155,7 @@ mod token_selling_tests { .drive .fetch_identity_balance(buyer.id().to_buffer(), None, platform_version) .expect("expected to fetch credit balance"); - assert_eq!(buyer_credit_balance, Some(699_868_130_120)); // 10.0 - 3.0 spent - fees =~ 7 dash left + assert_eq!(buyer_credit_balance, Some(699_868_122_220)); // 10.0 - 3.0 spent - fees =~ 7 dash left } #[tokio::test] @@ -362,7 +362,7 @@ mod token_selling_tests { .drive .fetch_identity_balance(buyer.id().to_buffer(), None, platform_version) .expect("expected to fetch credit balance"); - assert_eq!(buyer_credit_balance, Some(999_987_872_760)); // 10.0 - bump action fees + assert_eq!(buyer_credit_balance, Some(999_987_864_860)); // 10.0 - bump action fees } #[tokio::test] diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs index b22235fdb29..b06d4c88d32 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs @@ -508,16 +508,50 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { // Below we also perform state validation for replace and transfer transitions only // other transitions are validated in their validate_state functions // TODO: Think more about this architecture - let fetched_documents_validation_result = + let (fetched_documents_validation_result, fetch_documents_fee_result) = fetch_documents_for_transitions_knowing_contract_and_document_type( platform.drive, data_contract, document_type, replace_and_transfer_transitions.as_slice(), + &block_info.epoch, transaction, platform_version, )?; + // The `fetch_documents_for_transitions_billing` field gates whether + // the `query_documents` cost is billed to the user. v0 + // (PROTOCOL_VERSION_11 and below) preserves the legacy + // discard-cost behavior for chain replay; v1 (PROTOCOL_VERSION_12+) + // adds the fee to the execution_context (which on v1 of + // `transform_into_action` is the outer ctx that reaches the + // user's bill). + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .fetch_documents_for_transitions_billing + { + 0 => {} + 1 => { + execution_context.add_operation(ValidationOperation::PrecalculatedOperation( + fetch_documents_fee_result, + )); + } + version => { + return Err(Error::Execution( + crate::error::execution::ExecutionError::UnknownVersionMismatch { + method: + "documents batch transition: fetch_documents_for_transitions_billing" + .to_string(), + known_versions: vec![0, 1], + received: version, + }, + )); + } + } + if !fetched_documents_validation_result.is_valid() { return Ok(ConsensusValidationResult::new_with_errors( fetched_documents_validation_result.errors, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index 8b3b18edcae..c8c92ba9214 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -142,6 +142,18 @@ pub struct DriveAbciDocumentsStateTransitionValidationVersions { /// /// [`transform_document_transition`]: crate pub failed_per_transition_action: FeatureVersion, + /// Versions the billing of `query_documents` cost performed by + /// `fetch_documents_for_transitions_knowing_contract_and_document_type` + /// inside the batch transformer (run on every batch with + /// replace/transfer/purchase/update-price transitions). + /// + /// - `0` (PROTOCOL_VERSION_11 and below): the query cost is computed + /// and discarded — the user does not pay for the document fetch. + /// - `1` (PROTOCOL_VERSION_12+): the query cost is added to the + /// `StateTransitionExecutionContext` so it reaches the user's bill. + /// Pairs with `transform_into_action: 1`, which is what ensures the + /// billed ctx is not the dropped local from the legacy wrapper. + pub fetch_documents_for_transitions_billing: FeatureVersion, pub data_triggers: DriveAbciValidationDataTriggerAndBindingVersions, pub is_allowed: FeatureVersion, pub document_create_transition_structure_validation: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs index fce75c16330..36a387322bd 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs @@ -107,6 +107,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_billing: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs index ab2d160f2a3..b6787a37ebb 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs @@ -107,6 +107,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_billing: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs index c80ed9f6e0d..3624caf49b3 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs @@ -107,6 +107,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_billing: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs index a986d603a1a..4d4370d48cd 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs @@ -110,6 +110,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_billing: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs index bb9673de70b..003aa6d97bc 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs @@ -111,6 +111,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_billing: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs index 21838220e8f..10a42812686 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs @@ -114,6 +114,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_billing: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs index 5e23882714d..fdcae23e168 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs @@ -108,6 +108,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_billing: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index 06ac68a0c46..aae65a7878b 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -134,6 +134,14 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = // ownership/revision check). v0 stays for chain // reproducibility on PROTOCOL_VERSION_11 and below. failed_per_transition_action: 1, + // PROTOCOL_VERSION_12 (v3.1 hard fork): the + // `query_documents` cost from + // `fetch_documents_for_transitions_knowing_contract_and_document_type` + // is now billed to the user via `execution_context` + // instead of being discarded inside the helper. Pairs + // with `transform_into_action: 1` so the billed ctx + // reaches the user's bill. + fetch_documents_for_transitions_billing: 1, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { From fefba84ae6fbc09b8482083ec73bc3c9c71f0576 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 19 May 2026 20:29:14 +0700 Subject: [PATCH 06/20] chore(drive-abci): delete dead fetch_documents wrappers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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) --- .../batch/state/v0/fetch_documents.rs | 116 ------------------ 1 file changed, 116 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs index 3310e01ec86..bc5ef82c769 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs @@ -1,23 +1,13 @@ use crate::error::Error; -use crate::platform_types::platform::PlatformStateRef; use dpp::block::epoch::Epoch; -use dpp::consensus::basic::document::{DataContractNotPresentError, InvalidDocumentTypeError}; -use dpp::consensus::basic::BasicError; -use dpp::data_contract::accessors::v0::DataContractV0Getters; -use std::collections::btree_map::Entry; -use std::collections::BTreeMap; - use dpp::data_contract::document_type::DocumentTypeRef; use dpp::data_contract::DataContract; - -use crate::platform_types::platform_state::PlatformStateV0Methods; use dpp::document::Document; use dpp::fee::fee_result::FeeResult; use dpp::platform_value::{Identifier, Value}; use dpp::state_transition::batch_transition::batched_transition::document_transition::{ DocumentTransition, DocumentTransitionV0Methods, }; -use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; use dpp::validation::ConsensusValidationResult; use dpp::version::PlatformVersion; use drive::drive::document::query::query_contested_documents_storage::QueryContestedDocumentsOutcomeV0Methods; @@ -29,112 +19,6 @@ use drive::query::drive_contested_document_query::{ }; use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperator}; -#[allow(dead_code)] -#[deprecated(note = "This function is marked as unused.")] -#[allow(deprecated)] -pub(crate) fn fetch_documents_for_transitions( - platform: &PlatformStateRef, - document_transitions: &[&DocumentTransition], - epoch: &Epoch, - transaction: TransactionArg, - platform_version: &PlatformVersion, -) -> Result>, Error> { - let mut transitions_by_contracts_and_types: BTreeMap< - (&Identifier, &String), - Vec<&DocumentTransition>, - > = BTreeMap::new(); - - for document_transition in document_transitions { - let document_type = document_transition.base().document_type_name(); - let data_contract_id = document_transition.base().data_contract_id_ref(); - - match transitions_by_contracts_and_types.entry((data_contract_id, document_type)) { - Entry::Vacant(v) => { - v.insert(vec![document_transition]); - } - Entry::Occupied(mut o) => o.get_mut().push(document_transition), - } - } - - let validation_results_of_documents = transitions_by_contracts_and_types - .into_iter() - .map(|((contract_id, document_type_name), transitions)| { - fetch_documents_for_transitions_knowing_contract_id_and_document_type_name( - platform, - contract_id, - document_type_name, - transitions.as_slice(), - epoch, - transaction, - platform_version, - ) - }) - .collect::>>, Error>>()?; - - let validation_result = - ConsensusValidationResult::flatten(validation_results_of_documents, platform_version)?; - - Ok(validation_result) -} - -#[allow(dead_code)] -pub(crate) fn fetch_documents_for_transitions_knowing_contract_id_and_document_type_name( - platform: &PlatformStateRef, - contract_id: &Identifier, - document_type_name: &str, - transitions: &[&DocumentTransition], - epoch: &Epoch, - transaction: TransactionArg, - platform_version: &PlatformVersion, -) -> Result>, Error> { - let drive = platform.drive; - //todo: deal with fee result - //we only want to add to the cache if we are validating in a transaction - let add_to_cache_if_pulled = transaction.is_some(); - let (_, contract_fetch_info) = drive.get_contract_with_fetch_info_and_fee( - contract_id.to_buffer(), - Some(platform.state.last_committed_block_epoch_ref()), - add_to_cache_if_pulled, - transaction, - platform_version, - )?; - - let Some(contract_fetch_info) = contract_fetch_info else { - return Ok(ConsensusValidationResult::new_with_error( - BasicError::DataContractNotPresentError(DataContractNotPresentError::new(*contract_id)) - .into(), - )); - }; - - let Some(document_type) = contract_fetch_info - .contract - .document_type_optional_for_name(document_type_name) - else { - return Ok(ConsensusValidationResult::new_with_error( - BasicError::InvalidDocumentTypeError(InvalidDocumentTypeError::new( - document_type_name.to_string(), - *contract_id, - )) - .into(), - )); - }; - // This caller is `#[allow(dead_code)]`; the FeeResult is discarded - // here because it is currently unused. If this function gets revived, - // the caller will need to propagate the fee through the existing - // billing version-gate. - let (validation_result, _fee_result) = - fetch_documents_for_transitions_knowing_contract_and_document_type( - drive, - &contract_fetch_info.contract, - document_type, - transitions, - epoch, - transaction, - platform_version, - )?; - Ok(validation_result) -} - /// Returns the fetched documents plus the `FeeResult` for the underlying /// `query_documents` operation. The caller decides whether to bill the /// `FeeResult` to the `StateTransitionExecutionContext` — gated by the From 2f1dca8212b9224b3fb8277d9875a107a3c6064f Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 19 May 2026 20:36:49 +0700 Subject: [PATCH 07/20] refactor(drive-abci): consolidate B4 billing under transform_into_action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../batch/transformer/v0/mod.rs | 19 ++++++++----------- .../drive_abci_validation_versions/mod.rs | 12 ------------ .../drive_abci_validation_versions/v1.rs | 1 - .../drive_abci_validation_versions/v2.rs | 1 - .../drive_abci_validation_versions/v3.rs | 1 - .../drive_abci_validation_versions/v4.rs | 1 - .../drive_abci_validation_versions/v5.rs | 1 - .../drive_abci_validation_versions/v6.rs | 1 - .../drive_abci_validation_versions/v7.rs | 1 - .../drive_abci_validation_versions/v8.rs | 8 -------- 10 files changed, 8 insertions(+), 38 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs index b06d4c88d32..9f3ed2ad032 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs @@ -519,19 +519,17 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { platform_version, )?; - // The `fetch_documents_for_transitions_billing` field gates whether - // the `query_documents` cost is billed to the user. v0 - // (PROTOCOL_VERSION_11 and below) preserves the legacy - // discard-cost behavior for chain replay; v1 (PROTOCOL_VERSION_12+) - // adds the fee to the execution_context (which on v1 of - // `transform_into_action` is the outer ctx that reaches the - // user's bill). + // Reuse the `transform_into_action` field that already gates whether + // this transformer's execution_context is the outer (threaded) one + // or a dropped-on-return local. On v0 the document-query cost would + // land in the dropped local — billing it would be wasted work, so we + // skip. On v1 the ctx reaches the user's bill, so we bill the cost. match platform_version .drive_abci .validation_and_processing .state_transitions .batch_state_transition - .fetch_documents_for_transitions_billing + .transform_into_action { 0 => {} 1 => { @@ -542,9 +540,8 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { version => { return Err(Error::Execution( crate::error::execution::ExecutionError::UnknownVersionMismatch { - method: - "documents batch transition: fetch_documents_for_transitions_billing" - .to_string(), + method: "documents batch transition: fetch_documents query billing" + .to_string(), known_versions: vec![0, 1], received: version, }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index c8c92ba9214..8b3b18edcae 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -142,18 +142,6 @@ pub struct DriveAbciDocumentsStateTransitionValidationVersions { /// /// [`transform_document_transition`]: crate pub failed_per_transition_action: FeatureVersion, - /// Versions the billing of `query_documents` cost performed by - /// `fetch_documents_for_transitions_knowing_contract_and_document_type` - /// inside the batch transformer (run on every batch with - /// replace/transfer/purchase/update-price transitions). - /// - /// - `0` (PROTOCOL_VERSION_11 and below): the query cost is computed - /// and discarded — the user does not pay for the document fetch. - /// - `1` (PROTOCOL_VERSION_12+): the query cost is added to the - /// `StateTransitionExecutionContext` so it reaches the user's bill. - /// Pairs with `transform_into_action: 1`, which is what ensures the - /// billed ctx is not the dropped local from the legacy wrapper. - pub fetch_documents_for_transitions_billing: FeatureVersion, pub data_triggers: DriveAbciValidationDataTriggerAndBindingVersions, pub is_allowed: FeatureVersion, pub document_create_transition_structure_validation: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs index 36a387322bd..fce75c16330 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs @@ -107,7 +107,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, - fetch_documents_for_transitions_billing: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs index b6787a37ebb..ab2d160f2a3 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs @@ -107,7 +107,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, - fetch_documents_for_transitions_billing: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs index 3624caf49b3..c80ed9f6e0d 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs @@ -107,7 +107,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, - fetch_documents_for_transitions_billing: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs index 4d4370d48cd..a986d603a1a 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs @@ -110,7 +110,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, - fetch_documents_for_transitions_billing: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs index 003aa6d97bc..bb9673de70b 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs @@ -111,7 +111,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, - fetch_documents_for_transitions_billing: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs index 10a42812686..21838220e8f 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs @@ -114,7 +114,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, - fetch_documents_for_transitions_billing: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs index fdcae23e168..5e23882714d 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs @@ -108,7 +108,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, - fetch_documents_for_transitions_billing: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index aae65a7878b..06ac68a0c46 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -134,14 +134,6 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = // ownership/revision check). v0 stays for chain // reproducibility on PROTOCOL_VERSION_11 and below. failed_per_transition_action: 1, - // PROTOCOL_VERSION_12 (v3.1 hard fork): the - // `query_documents` cost from - // `fetch_documents_for_transitions_knowing_contract_and_document_type` - // is now billed to the user via `execution_context` - // instead of being discarded inside the helper. Pairs - // with `transform_into_action: 1` so the billed ctx - // reaches the user's bill. - fetch_documents_for_transitions_billing: 1, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { From 082fae947f72c6cf69881e983f4b94defd14cc19 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 19 May 2026 21:35:05 +0700 Subject: [PATCH 08/20] refactor(drive-abci): data triggers return FeeResult for caller to bill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../bindings/data_trigger_binding/mod.rs | 3 +- .../bindings/data_trigger_binding/v0/mod.rs | 5 ++- .../batch/data_triggers/executor.rs | 20 ++++++--- .../batch/data_triggers/mod.rs | 11 ++++- .../data_triggers/triggers/dashpay/mod.rs | 3 +- .../data_triggers/triggers/dashpay/v0/mod.rs | 14 +++---- .../batch/data_triggers/triggers/dpns/mod.rs | 3 +- .../data_triggers/triggers/dpns/v0/mod.rs | 14 +++---- .../data_triggers/triggers/reject/mod.rs | 6 ++- .../data_triggers/triggers/withdrawals/mod.rs | 3 +- .../triggers/withdrawals/v0/mod.rs | 10 ++--- .../batch/state/v0/data_triggers.rs | 30 ------------- .../state_transitions/batch/state/v0/mod.rs | 42 +++++++++++++++++-- 13 files changed, 96 insertions(+), 68 deletions(-) delete mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/data_triggers.rs diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/mod.rs index 9c06e6b4294..798e3e195b6 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/mod.rs @@ -2,6 +2,7 @@ use crate::execution::validation::state_transition::batch::data_triggers::{ DataTriggerExecutionContext, DataTriggerExecutionResult, }; use derive_more::From; +use dpp::fee::fee_result::FeeResult; use dpp::identifier::Identifier; use dpp::version::PlatformVersion; use drive::state_transition_action::batch::batched_transition::document_transition::{ @@ -24,7 +25,7 @@ impl DataTriggerBindingV0Getters for DataTriggerBinding { document_transition: &DocumentTransitionAction, context: &DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, - ) -> Result { + ) -> Result<(DataTriggerExecutionResult, FeeResult), Error> { match self { DataTriggerBinding::V0(binding) => { binding.execute(document_transition, context, platform_version) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs index 39e3c871c66..3f49ca89a20 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs @@ -2,6 +2,7 @@ use crate::error::Error; use crate::execution::validation::state_transition::batch::data_triggers::{ DataTrigger, DataTriggerExecutionContext, DataTriggerExecutionResult, }; +use dpp::fee::fee_result::FeeResult; use dpp::identifier::Identifier; use dpp::version::PlatformVersion; use drive::state_transition_action::batch::batched_transition::document_transition::{ @@ -48,7 +49,7 @@ pub trait DataTriggerBindingV0Getters { document_transition: &DocumentTransitionAction, context: &DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, - ) -> Result; + ) -> Result<(DataTriggerExecutionResult, FeeResult), Error>; /// Checks whether the data trigger matches the specified data contract ID, document type, and action. /// @@ -78,7 +79,7 @@ impl DataTriggerBindingV0Getters for DataTriggerBindingV0 { document_transition: &DocumentTransitionAction, context: &DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, - ) -> Result { + ) -> Result<(DataTriggerExecutionResult, FeeResult), Error> { (self.data_trigger)(document_transition, context, platform_version) } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/executor.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/executor.rs index a85f1b6febb..f30ea8c6e0f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/executor.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/executor.rs @@ -3,6 +3,7 @@ use crate::execution::validation::state_transition::batch::data_triggers::{ }; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; +use dpp::fee::fee_result::FeeResult; use dpp::state_transition::batch_transition::batched_transition::document_transition_action_type::DocumentTransitionActionTypeGetter; use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; use dpp::version::PlatformVersion; @@ -16,7 +17,7 @@ pub trait DataTriggerExecutor { data_trigger_bindings: &[DataTriggerBinding], context: &DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, - ) -> Result; + ) -> Result<(DataTriggerExecutionResult, FeeResult), Error>; } impl DataTriggerExecutor for DocumentTransitionAction { @@ -25,13 +26,17 @@ impl DataTriggerExecutor for DocumentTransitionAction { data_trigger_bindings: &[DataTriggerBinding], context: &DataTriggerExecutionContext, platform_version: &PlatformVersion, - ) -> Result { + ) -> Result<(DataTriggerExecutionResult, FeeResult), Error> { let data_contract_id = self.base().data_contract_id(); let document_type_name = self.base().document_type_name(); let transition_action = self.action_type(); + let mut aggregated_fee_result = FeeResult::default(); + // Match data triggers by action type, contract ID and document type name - // and then execute matched triggers until one of them returns invalid result + // and then execute matched triggers until one of them returns invalid result. + // Fees from every trigger that actually executed (including the one that + // returned invalid) are accumulated so the user pays for the work done. for data_trigger_binding in data_trigger_bindings { if !data_trigger_binding.is_matching( &data_contract_id, @@ -41,13 +46,16 @@ impl DataTriggerExecutor for DocumentTransitionAction { continue; } - let result = data_trigger_binding.execute(self, context, platform_version)?; + let (result, fee_result) = + data_trigger_binding.execute(self, context, platform_version)?; + + aggregated_fee_result.checked_add_assign(fee_result)?; if !result.is_valid() { - return Ok(result); + return Ok((result, aggregated_fee_result)); } } - Ok(DataTriggerExecutionResult::default()) + Ok((DataTriggerExecutionResult::default(), aggregated_fee_result)) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/mod.rs index 21bdb917740..c8f6034d6da 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/mod.rs @@ -6,6 +6,7 @@ use drive::state_transition_action::batch::batched_transition::document_transiti use crate::error::Error; use dpp::consensus::state::data_trigger::DataTriggerError; +use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; pub(super) use bindings::list::data_trigger_bindings_list; @@ -17,11 +18,19 @@ mod context; mod executor; mod triggers; +/// Data trigger function pointer. +/// +/// Returns the validation result and the `FeeResult` for whatever drive +/// reads (`query_documents`, `fetch_identity_balance`, etc.) the trigger +/// performed. The caller (`DataTriggerExecutor::validate_with_data_triggers`) +/// sums fees across all executed triggers and the outer batch-state +/// validator decides whether to bill them via the `transform_into_action` +/// version gate. type DataTrigger = fn( &DocumentTransitionAction, &DataTriggerExecutionContext<'_>, &PlatformVersion, -) -> Result; +) -> Result<(DataTriggerExecutionResult, FeeResult), Error>; /// A type alias for a [SimpleValidationResult] with a [DataTriggerError] as the error type. /// diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/mod.rs index 61c486c13ad..06207d598ad 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/mod.rs @@ -4,6 +4,7 @@ use crate::execution::validation::state_transition::batch::data_triggers::trigge use crate::execution::validation::state_transition::batch::data_triggers::{ DataTriggerExecutionContext, DataTriggerExecutionResult, }; +use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; @@ -13,7 +14,7 @@ pub fn create_contact_request_data_trigger( document_transition: &DocumentTransitionAction, context: &DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, -) -> Result { +) -> Result<(DataTriggerExecutionResult, FeeResult), Error> { match platform_version .drive_abci .validation_and_processing diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs index 3adefc5de0c..f448bbe54a9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs @@ -35,7 +35,7 @@ pub(super) fn create_contact_request_data_trigger_v0( document_transition: &DocumentTransitionAction, context: &DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, -) -> Result { +) -> Result<(DataTriggerExecutionResult, dpp::fee::fee_result::FeeResult), Error> { let data_contract_fetch_info = document_transition.base().data_contract_fetch_info(); let data_contract = &data_contract_fetch_info.contract; let mut result = DataTriggerExecutionResult::default(); @@ -68,7 +68,7 @@ pub(super) fn create_contact_request_data_trigger_v0( result.add_error(err); - return Ok(result); + return Ok((result, dpp::fee::fee_result::FeeResult::default())); } // TODO: Calculate fee operations @@ -89,10 +89,10 @@ pub(super) fn create_contact_request_data_trigger_v0( result.add_error(err); - return Ok(result); + return Ok((result, dpp::fee::fee_result::FeeResult::default())); } - Ok(result) + Ok((result, dpp::fee::fee_result::FeeResult::default())) } #[cfg(test)] @@ -184,7 +184,7 @@ mod test { transaction: None, }; - let result = create_contact_request_data_trigger( + let (result, _fee_result) = create_contact_request_data_trigger( &DocumentCreateTransitionAction::try_from_document_borrowed_create_transition_with_contract_lookup(&platform.drive, *owner_id, None, document_create_transition, &BlockInfo::default(), 0, |_identifier| { Ok(Arc::new(DataContractFetchInfo::dashpay_contract_fixture(protocol_version))) }, platform_version).expect("expected to create action").0.into_data().expect("expected to be a valid transition").as_document_action().expect("expected document action"), @@ -307,7 +307,7 @@ mod test { let _dashpay_identity_id = data_trigger_context.owner_id.to_owned(); - let result = create_contact_request_data_trigger( + let (result, _fee_result) = create_contact_request_data_trigger( &DocumentCreateTransitionAction::try_from_document_borrowed_create_transition_with_contract_lookup(&platform.drive, owner_id, None, document_create_transition, &BlockInfo::default(), 0, |_identifier| { Ok(Arc::new(DataContractFetchInfo::dashpay_contract_fixture(protocol_version))) }, platform_version).expect("expected to create action").0.into_data().expect("expected to be a valid transition").as_document_action().expect("expected document action"), @@ -425,7 +425,7 @@ mod test { let _dashpay_identity_id = data_trigger_context.owner_id.to_owned(); - let result = create_contact_request_data_trigger( + let (result, _fee_result) = create_contact_request_data_trigger( &DocumentCreateTransitionAction::try_from_document_borrowed_create_transition_with_contract_lookup(&platform.drive, owner_id, None, document_create_transition, &BlockInfo::default(), 0, |_identifier| { Ok(Arc::new(DataContractFetchInfo::dashpay_contract_fixture(protocol_version))) }, platform_version).expect("expected to create action").0.into_data().expect("expected to be a valid transition").as_document_action().expect("expected document action"), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/mod.rs index 42159810ddf..0f4942d2bdc 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/mod.rs @@ -4,6 +4,7 @@ use crate::execution::validation::state_transition::batch::data_triggers::trigge use crate::execution::validation::state_transition::batch::data_triggers::{ DataTriggerExecutionContext, DataTriggerExecutionResult, }; +use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; @@ -13,7 +14,7 @@ pub fn create_domain_data_trigger( document_transition: &DocumentTransitionAction, context: &DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, -) -> Result { +) -> Result<(DataTriggerExecutionResult, FeeResult), Error> { match platform_version .drive_abci .validation_and_processing diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs index 6aff34de018..ed008cf6203 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs @@ -49,7 +49,7 @@ pub(super) fn create_domain_data_trigger_v0( document_transition: &DocumentTransitionAction, context: &DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, -) -> Result { +) -> Result<(DataTriggerExecutionResult, dpp::fee::fee_result::FeeResult), Error> { let data_contract_fetch_info = document_transition.base().data_contract_fetch_info(); let data_contract = &data_contract_fetch_info.contract; let is_dry_run = context.state_transition_execution_context.in_dry_run(); @@ -266,7 +266,7 @@ pub(super) fn create_domain_data_trigger_v0( result.add_error(err); - return Ok(result); + return Ok((result, dpp::fee::fee_result::FeeResult::default())); } let parent_domain = &documents[0]; @@ -279,7 +279,7 @@ pub(super) fn create_domain_data_trigger_v0( result.add_error(err); - return Ok(result); + return Ok((result, dpp::fee::fee_result::FeeResult::default())); } if (!parent_domain @@ -296,7 +296,7 @@ pub(super) fn create_domain_data_trigger_v0( result.add_error(err); - return Ok(result); + return Ok((result, dpp::fee::fee_result::FeeResult::default())); } } } @@ -348,7 +348,7 @@ pub(super) fn create_domain_data_trigger_v0( .documents_owned(); if is_dry_run { - return Ok(result); + return Ok((result, dpp::fee::fee_result::FeeResult::default())); } if preorder_documents.is_empty() { @@ -363,7 +363,7 @@ pub(super) fn create_domain_data_trigger_v0( result.add_error(err) } - Ok(result) + Ok((result, dpp::fee::fee_result::FeeResult::default())) } #[cfg(test)] @@ -446,7 +446,7 @@ mod test { transaction: None, }; - let result = create_domain_data_trigger_v0( + let (result, _fee_result) = create_domain_data_trigger_v0( &DocumentCreateTransitionAction::try_from_document_borrowed_create_transition_with_contract_lookup(&platform.drive, owner_id, None, document_create_transition, &BlockInfo::default(), 0, |_identifier| { Ok(Arc::new(DataContractFetchInfo::dpns_contract_fixture(platform_version.protocol_version))) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/reject/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/reject/mod.rs index 7c540836cad..c69b8c81f5e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/reject/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/reject/mod.rs @@ -4,6 +4,7 @@ use crate::execution::validation::state_transition::batch::data_triggers::trigge use crate::execution::validation::state_transition::batch::data_triggers::{ DataTriggerExecutionContext, DataTriggerExecutionResult, }; +use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; @@ -13,7 +14,7 @@ pub fn reject_data_trigger( document_transition: &DocumentTransitionAction, _context: &DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, -) -> Result { +) -> Result<(DataTriggerExecutionResult, FeeResult), Error> { match platform_version .drive_abci .validation_and_processing @@ -23,7 +24,8 @@ pub fn reject_data_trigger( .triggers .reject_data_trigger { - 0 => reject_data_trigger_v0(document_transition), + // Reject performs no drive reads — FeeResult is always default. + 0 => Ok((reject_data_trigger_v0(document_transition)?, FeeResult::default())), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "reject_data_trigger".to_string(), known_versions: vec![0], diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/mod.rs index 967adaba8c8..a695d474b6a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/mod.rs @@ -4,6 +4,7 @@ use crate::execution::validation::state_transition::batch::data_triggers::{ DataTriggerExecutionContext, DataTriggerExecutionResult, }; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; +use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; use crate::execution::validation::state_transition::batch::data_triggers::triggers::withdrawals::v0::delete_withdrawal_data_trigger_v0; @@ -13,7 +14,7 @@ pub fn delete_withdrawal_data_trigger( document_transition: &DocumentTransitionAction, context: &DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, -) -> Result { +) -> Result<(DataTriggerExecutionResult, FeeResult), Error> { match platform_version .drive_abci .validation_and_processing diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs index 791ebaeb5fe..cef3e3baf70 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs @@ -38,7 +38,7 @@ pub(super) fn delete_withdrawal_data_trigger_v0( document_transition: &DocumentTransitionAction, context: &DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, -) -> Result { +) -> Result<(DataTriggerExecutionResult, dpp::fee::fee_result::FeeResult), Error> { let data_contract_fetch_info = document_transition.base().data_contract_fetch_info(); let data_contract = &data_contract_fetch_info.contract; let mut result = DataTriggerExecutionResult::default(); @@ -98,7 +98,7 @@ pub(super) fn delete_withdrawal_data_trigger_v0( result.add_error(err); - return Ok(result); + return Ok((result, dpp::fee::fee_result::FeeResult::default())); }; let status: u8 = withdrawal @@ -115,10 +115,10 @@ pub(super) fn delete_withdrawal_data_trigger_v0( result.add_error(err); - return Ok(result); + return Ok((result, dpp::fee::fee_result::FeeResult::default())); } - Ok(result) + Ok((result, dpp::fee::fee_result::FeeResult::default())) } #[cfg(test)] @@ -326,7 +326,7 @@ mod tests { state_transition_execution_context: &transition_execution_context, transaction: None, }; - let result = delete_withdrawal_data_trigger_v0( + let (result, _fee_result) = delete_withdrawal_data_trigger_v0( &document_transition, &data_trigger_context, platform_version, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/data_triggers.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/data_triggers.rs deleted file mode 100644 index 554fac68077..00000000000 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/data_triggers.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::error::Error; -use crate::execution::validation::state_transition::batch::data_triggers::{ - data_trigger_bindings_list, DataTriggerExecutionContext, DataTriggerExecutionResult, - DataTriggerExecutor, -}; -use dpp::version::PlatformVersion; - -use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; - -#[allow(dead_code)] -#[deprecated(note = "This function is marked as unused.")] -#[allow(deprecated)] -pub(super) fn execute_data_triggers( - document_transition_actions: &Vec, - context: &DataTriggerExecutionContext, - platform_version: &PlatformVersion, -) -> Result { - let data_trigger_bindings = data_trigger_bindings_list(platform_version)?; - - for document_transition_action in document_transition_actions { - let data_trigger_execution_result = document_transition_action - .validate_with_data_triggers(&data_trigger_bindings, context, platform_version)?; - - if !data_trigger_execution_result.is_valid() { - return Ok(data_trigger_execution_result); - } - } - - Ok(DataTriggerExecutionResult::default()) -} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index 5c75b5e3c6b..67640569622 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -14,7 +14,10 @@ use drive::state_transition_action::batch::BatchTransitionAction; use drive::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; use crate::error::Error; use crate::error::execution::ExecutionError; -use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, +}; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::DocumentCreateTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::DocumentDeleteTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_purchase_transition_action::DocumentPurchaseTransitionActionValidation; @@ -38,7 +41,6 @@ use crate::execution::validation::state_transition::state_transitions::batch::tr use crate::execution::validation::state_transition::ValidationMode; use crate::platform_types::platform_state::PlatformStateV0Methods; -mod data_triggers; pub mod fetch_contender; pub mod fetch_documents; @@ -278,13 +280,45 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { owner_id: &self.owner_id(), state_transition_execution_context: &state_transition_execution_context, }; - let data_trigger_execution_result = document_transition - .validate_with_data_triggers( + let (data_trigger_execution_result, data_trigger_fee_result) = + document_transition.validate_with_data_triggers( &data_trigger_bindings, &data_trigger_execution_context, platform_version, )?; + // Bill the accumulated trigger drive-read cost on the + // bumped `transform_into_action` gate, matching the same + // gate used by the transformer-phase fee fixes. On v0 the + // cost is discarded for chain replay reproducibility. + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .transform_into_action + { + 0 => {} + 1 => { + execution_context.add_operation( + ValidationOperation::PrecalculatedOperation( + data_trigger_fee_result, + ), + ); + } + version => { + return Err(Error::Execution( + ExecutionError::UnknownVersionMismatch { + method: + "documents batch transition: data trigger fee billing" + .to_string(), + known_versions: vec![0, 1], + received: version, + }, + )); + } + } + if !data_trigger_execution_result.is_valid() { // If a state transition isn't valid because of data triggers we still need // to bump the identity data contract nonce From e5fda5366924255d853d68a1dfbddc354e8fc0be Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 19 May 2026 21:38:13 +0700 Subject: [PATCH 09/20] fix(drive-abci): bill DPNS data trigger query_documents costs (T1, T2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../data_triggers/triggers/dpns/v0/mod.rs | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs index ed008cf6203..5ca1d37f96b 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs @@ -22,10 +22,13 @@ use dpp::system_data_contracts::dpns_contract; use dpp::system_data_contracts::dpns_contract::v1::document_types::domain::properties::{ALLOW_SUBDOMAINS, DASH_ALIAS_IDENTITY_ID, DASH_UNIQUE_IDENTITY_ID, LABEL, NORMALIZED_LABEL, NORMALIZED_PARENT_DOMAIN_NAME, PREORDER_SALT, RECORDS}; use dpp::util::strings::convert_to_homograph_safe_chars; +use dpp::block::epoch::Epoch; +use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperator}; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; +use crate::platform_types::platform_state::PlatformStateV0Methods; pub const MAX_PRINTABLE_DOMAIN_NAME_LENGTH: usize = 253; @@ -103,6 +106,8 @@ pub(super) fn create_domain_data_trigger_v0( }; let mut result = DataTriggerExecutionResult::default(); + let mut accumulated_fee_result = FeeResult::default(); + let epoch: &Epoch = context.platform.state.last_committed_block_epoch_ref(); if !is_dry_run { if full_domain_name.len() > MAX_PRINTABLE_DOMAIN_NAME_LENGTH { @@ -243,18 +248,22 @@ pub(super) fn create_domain_data_trigger_v0( block_time_ms: None, }; - // todo: deal with cost of this operation - let documents = context - .platform - .drive - .query_documents( - drive_query, - None, - is_dry_run, - context.transaction, - Some(platform_version.protocol_version), - )? - .documents_owned(); + // Pass `Some(epoch)` so query_documents computes the real cost + // (with `None` it short-circuits to 0). The cost is added to the + // accumulated FeeResult that the caller bills on + // `transform_into_action: 1`. + let parent_domain_outcome = context.platform.drive.query_documents( + drive_query, + Some(epoch), + is_dry_run, + context.transaction, + Some(platform_version.protocol_version), + )?; + accumulated_fee_result.checked_add_assign(FeeResult { + processing_fee: parent_domain_outcome.cost(), + ..Default::default() + })?; + let documents = parent_domain_outcome.documents_owned(); if !is_dry_run { if documents.is_empty() { @@ -266,7 +275,7 @@ pub(super) fn create_domain_data_trigger_v0( result.add_error(err); - return Ok((result, dpp::fee::fee_result::FeeResult::default())); + return Ok((result, accumulated_fee_result)); } let parent_domain = &documents[0]; @@ -279,7 +288,7 @@ pub(super) fn create_domain_data_trigger_v0( result.add_error(err); - return Ok((result, dpp::fee::fee_result::FeeResult::default())); + return Ok((result, accumulated_fee_result)); } if (!parent_domain @@ -296,7 +305,7 @@ pub(super) fn create_domain_data_trigger_v0( result.add_error(err); - return Ok((result, dpp::fee::fee_result::FeeResult::default())); + return Ok((result, accumulated_fee_result)); } } } @@ -334,21 +343,23 @@ pub(super) fn create_domain_data_trigger_v0( block_time_ms: None, }; - // todo: deal with cost of this operation - let preorder_documents = context - .platform - .drive - .query_documents( - drive_query, - None, - is_dry_run, - context.transaction, - Some(platform_version.protocol_version), - )? - .documents_owned(); + // Same pattern as the parent-domain query above — capture cost + // for the caller to bill. + let preorder_outcome = context.platform.drive.query_documents( + drive_query, + Some(epoch), + is_dry_run, + context.transaction, + Some(platform_version.protocol_version), + )?; + accumulated_fee_result.checked_add_assign(FeeResult { + processing_fee: preorder_outcome.cost(), + ..Default::default() + })?; + let preorder_documents = preorder_outcome.documents_owned(); if is_dry_run { - return Ok((result, dpp::fee::fee_result::FeeResult::default())); + return Ok((result, accumulated_fee_result)); } if preorder_documents.is_empty() { @@ -363,7 +374,7 @@ pub(super) fn create_domain_data_trigger_v0( result.add_error(err) } - Ok((result, dpp::fee::fee_result::FeeResult::default())) + Ok((result, accumulated_fee_result)) } #[cfg(test)] From 33aa35c05c8db61ca164ad9df71952702224c8d8 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 19 May 2026 21:41:14 +0700 Subject: [PATCH 10/20] fix(drive-abci): bill DashPay data trigger identity balance fetch (T3) 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) --- .../data_triggers/triggers/dashpay/v0/mod.rs | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs index f448bbe54a9..d02b9046902 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs @@ -11,9 +11,11 @@ use drive::state_transition_action::batch::batched_transition::document_transiti use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionActionAccessorsV0; use dpp::system_data_contracts::dashpay_contract::v1::document_types::contact_request::properties ::{TO_USER_ID}; +use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; use crate::execution::validation::state_transition::batch::data_triggers::{DataTriggerExecutionContext, DataTriggerExecutionResult}; +use crate::platform_types::platform_state::PlatformStateV0Methods; /// Creates a data trigger for handling contact request documents. /// @@ -71,14 +73,19 @@ pub(super) fn create_contact_request_data_trigger_v0( return Ok((result, dpp::fee::fee_result::FeeResult::default())); } - // TODO: Calculate fee operations - - // Recipient identity must exist - let to_identity = context.platform.drive.fetch_identity_balance( - to_user_id.to_buffer(), - context.transaction, - platform_version, - )?; + // Recipient identity must exist. + // + // Use `fetch_identity_balance_with_costs` so the grovedb cost is + // returned alongside the balance, then surface it via the returned + // FeeResult so the caller bills it on `transform_into_action: 1`. + let (to_identity, balance_fee_result) = + context.platform.drive.fetch_identity_balance_with_costs( + to_user_id.to_buffer(), + context.platform.state.last_block_info(), + true, + context.transaction, + platform_version, + )?; if !is_dry_run && to_identity.is_none() { let err = DataTriggerConditionError::new( @@ -89,10 +96,10 @@ pub(super) fn create_contact_request_data_trigger_v0( result.add_error(err); - return Ok((result, dpp::fee::fee_result::FeeResult::default())); + return Ok((result, balance_fee_result)); } - Ok((result, dpp::fee::fee_result::FeeResult::default())) + Ok((result, balance_fee_result)) } #[cfg(test)] From cb18189c91a166fbe249167ff3e8dd974bca17f3 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 19 May 2026 21:44:47 +0700 Subject: [PATCH 11/20] fix(drive-abci): bill withdrawals data trigger query_documents cost (T4) 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) --- .../triggers/withdrawals/v0/mod.rs | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs index cef3e3baf70..0a4224b4b3d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs @@ -5,6 +5,8 @@ use crate::error::Error; use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; use dpp::platform_value::Value; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; +use dpp::block::epoch::Epoch; +use dpp::fee::fee_result::FeeResult; use dpp::system_data_contracts::withdrawals_contract; use dpp::version::PlatformVersion; use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperator}; @@ -18,6 +20,7 @@ use drive::state_transition_action::batch::batched_transition::document_transiti use dpp::system_data_contracts::withdrawals_contract::v1::document_types::withdrawal; use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; use crate::execution::validation::state_transition::batch::data_triggers::{DataTriggerExecutionContext, DataTriggerExecutionResult}; +use crate::platform_types::platform_state::PlatformStateV0Methods; /// Creates a data trigger for handling deletion of withdrawal documents. /// @@ -76,18 +79,22 @@ pub(super) fn delete_withdrawal_data_trigger_v0( block_time_ms: None, }; - // todo: deal with cost of this operation - let withdrawals = context - .platform - .drive - .query_documents( - drive_query, - None, - false, - context.transaction, - Some(platform_version.protocol_version), - )? - .documents_owned(); + // Pass `Some(epoch)` so query_documents computes the real cost + // (with `None` it short-circuits to 0). Surface it via the returned + // FeeResult so the caller bills it on `transform_into_action: 1`. + let epoch: &Epoch = context.platform.state.last_committed_block_epoch_ref(); + let withdrawals_outcome = context.platform.drive.query_documents( + drive_query, + Some(epoch), + false, + context.transaction, + Some(platform_version.protocol_version), + )?; + let query_fee_result = FeeResult { + processing_fee: withdrawals_outcome.cost(), + ..Default::default() + }; + let withdrawals = withdrawals_outcome.documents_owned(); let Some(withdrawal) = withdrawals.first() else { let err = DataTriggerConditionError::new( @@ -98,7 +105,7 @@ pub(super) fn delete_withdrawal_data_trigger_v0( result.add_error(err); - return Ok((result, dpp::fee::fee_result::FeeResult::default())); + return Ok((result, query_fee_result)); }; let status: u8 = withdrawal @@ -115,10 +122,10 @@ pub(super) fn delete_withdrawal_data_trigger_v0( result.add_error(err); - return Ok((result, dpp::fee::fee_result::FeeResult::default())); + return Ok((result, query_fee_result)); } - Ok((result, dpp::fee::fee_result::FeeResult::default())) + Ok((result, query_fee_result)) } #[cfg(test)] From b5d4cecdc453a926047c7af9893c686277295c1e Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 19 May 2026 22:31:39 +0700 Subject: [PATCH 12/20] fix(drive-abci): bill fetch_document_with_id query cost (B5), unify trigger epoch source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../state_v0/mod.rs | 1 + .../state_v1/mod.rs | 1 + .../state_v0/mod.rs | 1 + .../batch/data_triggers/context.rs | 8 ++++ .../data_triggers/triggers/dashpay/v0/mod.rs | 5 ++- .../data_triggers/triggers/dpns/v0/mod.rs | 3 +- .../triggers/withdrawals/v0/mod.rs | 4 +- .../batch/state/v0/fetch_documents.rs | 44 ++++++++++++++++--- .../state_transitions/batch/state/v0/mod.rs | 1 + .../drive_abci_validation_versions/v8.rs | 29 +++++++++--- 10 files changed, 82 insertions(+), 15 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v0/mod.rs index 5b016084d44..b644f4564d8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v0/mod.rs @@ -64,6 +64,7 @@ impl DocumentCreateTransitionActionStateValidationV0 for DocumentCreateTransitio contract, document_type, self.base().id(), + &block_info.epoch, transaction, platform_version, )?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v1/mod.rs index 58f8e8b8204..430d349e7d3 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v1/mod.rs @@ -80,6 +80,7 @@ impl DocumentCreateTransitionActionStateValidationV1 for DocumentCreateTransitio contract, document_type, self.base().id(), + &block_info.epoch, transaction, platform_version, )?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v0/mod.rs index 7b386543d41..08fa45db9b4 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v0/mod.rs @@ -74,6 +74,7 @@ impl DocumentDeleteTransitionActionStateValidationV0 for DocumentDeleteTransitio contract, document_type, self.base().id(), + &block_info.epoch, transaction, platform_version, )?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs index 3060e541601..7423dd05ba5 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs @@ -1,5 +1,6 @@ use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; use dpp::prelude::*; use drive::grovedb::TransactionArg; use std::fmt::{Debug, Formatter}; @@ -14,6 +15,13 @@ pub struct DataTriggerExecutionContext<'a> { pub transaction: TransactionArg<'a, 'a>, /// The identifier of the owner of the data contract that the trigger is associated with. pub owner_id: &'a Identifier, + /// The current block info, used as the source of `epoch` for trigger + /// fee computations. Triggers must use `block_info.epoch` (matching + /// the batch transformer's epoch source) rather than + /// `platform.state.last_committed_block_epoch_ref()` so the per-batch + /// fee accounting is consistent across all sites that bill on + /// `transform_into_action: 1`. + pub block_info: &'a BlockInfo, /// A reference to the execution context for the state transition that triggered the data trigger. pub state_transition_execution_context: &'a StateTransitionExecutionContext, } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs index d02b9046902..0f81b021176 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs @@ -81,7 +81,7 @@ pub(super) fn create_contact_request_data_trigger_v0( let (to_identity, balance_fee_result) = context.platform.drive.fetch_identity_balance_with_costs( to_user_id.to_buffer(), - context.platform.state.last_block_info(), + context.block_info, true, context.transaction, platform_version, @@ -187,6 +187,7 @@ mod test { let data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id, + block_info: &BlockInfo::default(), state_transition_execution_context: &transition_execution_context, transaction: None, }; @@ -308,6 +309,7 @@ mod test { let data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id: &owner_id, + block_info: &BlockInfo::default(), state_transition_execution_context: &transition_execution_context, transaction: None, }; @@ -426,6 +428,7 @@ mod test { let data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id: &owner_id, + block_info: &BlockInfo::default(), state_transition_execution_context: &transition_execution_context, transaction: None, }; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs index 5ca1d37f96b..151db897a1e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs @@ -107,7 +107,7 @@ pub(super) fn create_domain_data_trigger_v0( let mut result = DataTriggerExecutionResult::default(); let mut accumulated_fee_result = FeeResult::default(); - let epoch: &Epoch = context.platform.state.last_committed_block_epoch_ref(); + let epoch: &Epoch = &context.block_info.epoch; if !is_dry_run { if full_domain_name.len() > MAX_PRINTABLE_DOMAIN_NAME_LENGTH { @@ -453,6 +453,7 @@ mod test { let data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id: &owner_id, + block_info: &BlockInfo::default(), state_transition_execution_context: &transition_execution_context, transaction: None, }; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs index 0a4224b4b3d..1f0cb5ab5a5 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs @@ -82,7 +82,7 @@ pub(super) fn delete_withdrawal_data_trigger_v0( // Pass `Some(epoch)` so query_documents computes the real cost // (with `None` it short-circuits to 0). Surface it via the returned // FeeResult so the caller bills it on `transform_into_action: 1`. - let epoch: &Epoch = context.platform.state.last_committed_block_epoch_ref(); + let epoch: &Epoch = &context.block_info.epoch; let withdrawals_outcome = context.platform.drive.query_documents( drive_query, Some(epoch), @@ -191,6 +191,7 @@ mod tests { let data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id: &owner_id, + block_info: &BlockInfo::default(), state_transition_execution_context: &StateTransitionExecutionContext::V0( transition_execution_context, ), @@ -330,6 +331,7 @@ mod tests { let data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id: &owner_id, + block_info: &BlockInfo::default(), state_transition_execution_context: &transition_execution_context, transaction: None, }; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs index bc5ef82c769..822b7624ba8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs @@ -22,8 +22,9 @@ use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperat /// Returns the fetched documents plus the `FeeResult` for the underlying /// `query_documents` operation. The caller decides whether to bill the /// `FeeResult` to the `StateTransitionExecutionContext` — gated by the -/// `fetch_documents_for_transitions_billing` field on -/// `DriveAbciDocumentsStateTransitionValidationVersions`. +/// `transform_into_action` field on +/// `DriveAbciDocumentsStateTransitionValidationVersions` (`0` discards +/// the cost for PROTOCOL_VERSION_11 chain replay, `1` bills it). /// /// `query_documents` only computes a non-zero cost when an `Epoch` is /// provided; the legacy `None` epoch resulted in a hard-coded zero cost @@ -92,11 +93,24 @@ pub(crate) fn fetch_documents_for_transitions_knowing_contract_and_document_type )) } +/// Returns the document (if any) plus the `FeeResult` for the underlying +/// `query_documents` operation. +/// +/// The cost computation is gated by `transform_into_action` on +/// `DriveAbciDocumentsStateTransitionValidationVersions`: +/// - `0` (PROTOCOL_VERSION_11 and below): pass `epoch=None` to +/// `query_documents`, which hard-codes the cost to 0. The returned +/// `FeeResult` has `processing_fee=0` and callers' `add_operation` +/// becomes a no-op-fee. Byte-identical to pre-PR behavior on v11. +/// - `1` (PROTOCOL_VERSION_12+): pass `Some(epoch)` so the real grovedb +/// cost is computed and returned. Callers bill it via the existing +/// `execution_context.add_operation` call site. pub(crate) fn fetch_document_with_id( drive: &Drive, contract: &DataContract, document_type: DocumentTypeRef, id: Identifier, + epoch: &Epoch, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result<(Option, FeeResult), Error> { @@ -122,19 +136,37 @@ pub(crate) fn fetch_document_with_id( block_time_ms: None, }; - // todo: deal with cost of this operation + let epoch_arg = match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .transform_into_action + { + 0 => None, + 1 => Some(epoch), + version => { + return Err(Error::Execution( + crate::error::execution::ExecutionError::UnknownVersionMismatch { + method: "fetch_document_with_id: transform_into_action gate".to_string(), + known_versions: vec![0, 1], + received: version, + }, + )); + } + }; + let documents_outcome = drive.query_documents( drive_query, - None, + epoch_arg, false, transaction, Some(platform_version.protocol_version), )?; - let fee = documents_outcome.cost(); let fee_result = FeeResult { storage_fee: 0, - processing_fee: fee, + processing_fee: documents_outcome.cost(), fee_refunds: Default::default(), removed_bytes_from_system: 0, }; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index 67640569622..356831570f2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -278,6 +278,7 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { platform, transaction, owner_id: &self.owner_id(), + block_info, state_transition_execution_context: &state_transition_execution_context, }; let (data_trigger_execution_result, data_trigger_fee_result) = diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index 06ac68a0c46..ae01ab8b991 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -120,12 +120,29 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = advanced_structure: 0, state: 0, revision: 0, - // PROTOCOL_VERSION_12 (v3.1 hard fork): the outer - // `execution_context` is threaded through the batch transformer - // so per-transition fee_results accumulated inside - // `try_into_action_v0` are billed to the user. v0 preserves the - // legacy dropped-local-ctx behavior for PROTOCOL_VERSION_11 - // chain replay. + // PROTOCOL_VERSION_12 (v3.1 hard fork): batch state transition + // fee accounting fixes. This single field gates multiple + // related billing changes so they all activate together at + // the same hard fork. On v0 every behavior below is the + // legacy under-billing, preserved verbatim for + // PROTOCOL_VERSION_11 chain replay. + // + // Gated by `transform_into_action: 1`: + // * B7 — outer `execution_context` is threaded through the + // batch transformer (was a dropped local) so per- + // transition fee_results in `try_into_action_v0` are + // billed. + // * B4 — `query_documents` cost in + // `fetch_documents_for_transitions_knowing_contract_and_document_type` + // is added to `execution_context`. + // * B5 — `query_documents` cost in `fetch_document_with_id` + // is added to `execution_context`. + // * T1 — DPNS data trigger parent-domain + // `query_documents` cost. + // * T2 — DPNS data trigger preorder `query_documents` cost. + // * T3 — DashPay data trigger recipient identity-balance + // fetch cost (switched to `fetch_identity_balance_with_costs`). + // * T4 — withdrawals data trigger `query_documents` cost. transform_into_action: 1, // PROTOCOL_VERSION_12 (v3.1 hard fork): per-transition // failure paths in `transform_document_transition` now emit From f42497d252ddc68c05611ad3bbf439507914d6fa Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 19 May 2026 22:48:08 +0700 Subject: [PATCH 13/20] =?UTF-8?q?test(drive-abci):=20pin=20trigger=20fee?= =?UTF-8?q?=20billing=20=E2=80=94=20T1/T2/T3/T4=20regression=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../batch/data_triggers/triggers/dashpay/v0/mod.rs | 12 +++++++++--- .../batch/data_triggers/triggers/dpns/v0/mod.rs | 1 - .../data_triggers/triggers/withdrawals/v0/mod.rs | 12 ++++++++++-- .../state_transitions/batch/tests/document/dpns.rs | 10 ++++++++++ 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs index 0f81b021176..5c2f29edf70 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs @@ -11,11 +11,9 @@ use drive::state_transition_action::batch::batched_transition::document_transiti use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionActionAccessorsV0; use dpp::system_data_contracts::dashpay_contract::v1::document_types::contact_request::properties ::{TO_USER_ID}; -use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; use crate::execution::validation::state_transition::batch::data_triggers::{DataTriggerExecutionContext, DataTriggerExecutionResult}; -use crate::platform_types::platform_state::PlatformStateV0Methods; /// Creates a data trigger for handling contact request documents. /// @@ -435,7 +433,7 @@ mod test { let _dashpay_identity_id = data_trigger_context.owner_id.to_owned(); - let (result, _fee_result) = create_contact_request_data_trigger( + let (result, fee_result) = create_contact_request_data_trigger( &DocumentCreateTransitionAction::try_from_document_borrowed_create_transition_with_contract_lookup(&platform.drive, owner_id, None, document_create_transition, &BlockInfo::default(), 0, |_identifier| { Ok(Arc::new(DataContractFetchInfo::dashpay_contract_fixture(protocol_version))) }, platform_version).expect("expected to create action").0.into_data().expect("expected to be a valid transition").as_document_action().expect("expected document action"), @@ -447,6 +445,14 @@ mod test { assert!(!result.is_valid()); let data_trigger_error = &result.errors[0]; + // T3 regression pin: the trigger ran `fetch_identity_balance_with_costs` + // to check recipient existence — that fetch must produce a non-zero + // FeeResult so the caller can bill it on transform_into_action: 1. + assert!( + fee_result.processing_fee > 0, + "T3: fetch_identity_balance_with_costs must surface non-zero cost" + ); + assert!(matches!( data_trigger_error, DataTriggerError::DataTriggerConditionError(e) if { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs index 151db897a1e..9ec0f7bfe4c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs @@ -28,7 +28,6 @@ use dpp::version::PlatformVersion; use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperator}; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; -use crate::platform_types::platform_state::PlatformStateV0Methods; pub const MAX_PRINTABLE_DOMAIN_NAME_LENGTH: usize = 253; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs index 1f0cb5ab5a5..357df97fec8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs @@ -20,7 +20,6 @@ use drive::state_transition_action::batch::batched_transition::document_transiti use dpp::system_data_contracts::withdrawals_contract::v1::document_types::withdrawal; use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; use crate::execution::validation::state_transition::batch::data_triggers::{DataTriggerExecutionContext, DataTriggerExecutionResult}; -use crate::platform_types::platform_state::PlatformStateV0Methods; /// Creates a data trigger for handling deletion of withdrawal documents. /// @@ -335,7 +334,7 @@ mod tests { state_transition_execution_context: &transition_execution_context, transaction: None, }; - let (result, _fee_result) = delete_withdrawal_data_trigger_v0( + let (result, fee_result) = delete_withdrawal_data_trigger_v0( &document_transition, &data_trigger_context, platform_version, @@ -350,5 +349,14 @@ mod tests { error.to_string(), "withdrawal deletion is allowed only for COMPLETE statuses" ); + + // T4 regression pin: the trigger ran `query_documents` to fetch the + // withdrawal — that query must surface a non-zero cost via the + // returned FeeResult so the caller can bill it on + // `transform_into_action: 1`. + assert!( + fee_result.processing_fee > 0, + "T4: query_documents must surface non-zero cost" + ); } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs index 36433c4df60..1ee2390bf75 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs @@ -385,6 +385,16 @@ mod dpns_tests { assert_eq!(processing_result.valid_count(), 3); + // T1/T2 regression pin: the DPNS `create_domain_data_trigger` + // runs two `query_documents` calls per transition (parent-domain + // + preorder). The accumulated cost is billed via the trigger's + // returned `FeeResult` on `transform_into_action: 1`. + assert_eq!( + processing_result.aggregated_fees().processing_fee, + 6_010_380, // 3 subdomain creates: T1 + T2 query costs billed per transition + "DPNS domain create fee must include T1 parent-domain + T2 preorder query costs" + ); + let mut order_by = IndexMap::new(); order_by.insert( From e9f239f95083af1f97164b1cd2c39536adb817c9 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 20 May 2026 00:43:17 +0700 Subject: [PATCH 14/20] refactor(drive-abci): strict _v1 sibling pattern for trigger fee billing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../bindings/data_trigger_binding/mod.rs | 5 +- .../bindings/data_trigger_binding/v0/mod.rs | 9 +- .../batch/data_triggers/context.rs | 15 +- .../batch/data_triggers/executor.rs | 29 +- .../batch/data_triggers/mod.rs | 15 +- .../data_triggers/triggers/dashpay/mod.rs | 10 +- .../data_triggers/triggers/dashpay/v0/mod.rs | 67 ++-- .../data_triggers/triggers/dashpay/v1/mod.rs | 95 +++++ .../batch/data_triggers/triggers/dpns/mod.rs | 13 +- .../data_triggers/triggers/dpns/v0/mod.rs | 80 ++-- .../data_triggers/triggers/dpns/v1/mod.rs | 379 ++++++++++++++++++ .../data_triggers/triggers/reject/mod.rs | 9 +- .../data_triggers/triggers/withdrawals/mod.rs | 10 +- .../triggers/withdrawals/v0/mod.rs | 71 ++-- .../triggers/withdrawals/v1/mod.rs | 124 ++++++ .../state_transitions/batch/state/v0/mod.rs | 54 +-- .../batch/tests/document/deletion.rs | 6 +- .../batch/tests/document/nft.rs | 12 +- .../drive_abci_validation_versions/v8.rs | 12 +- 19 files changed, 774 insertions(+), 241 deletions(-) create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/mod.rs index 798e3e195b6..0468c93a7e8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/mod.rs @@ -2,7 +2,6 @@ use crate::execution::validation::state_transition::batch::data_triggers::{ DataTriggerExecutionContext, DataTriggerExecutionResult, }; use derive_more::From; -use dpp::fee::fee_result::FeeResult; use dpp::identifier::Identifier; use dpp::version::PlatformVersion; use drive::state_transition_action::batch::batched_transition::document_transition::{ @@ -23,9 +22,9 @@ impl DataTriggerBindingV0Getters for DataTriggerBinding { fn execute( &self, document_transition: &DocumentTransitionAction, - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, - ) -> Result<(DataTriggerExecutionResult, FeeResult), Error> { + ) -> Result { match self { DataTriggerBinding::V0(binding) => { binding.execute(document_transition, context, platform_version) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs index 3f49ca89a20..2e0d0293eaf 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs @@ -2,7 +2,6 @@ use crate::error::Error; use crate::execution::validation::state_transition::batch::data_triggers::{ DataTrigger, DataTriggerExecutionContext, DataTriggerExecutionResult, }; -use dpp::fee::fee_result::FeeResult; use dpp::identifier::Identifier; use dpp::version::PlatformVersion; use drive::state_transition_action::batch::batched_transition::document_transition::{ @@ -47,9 +46,9 @@ pub trait DataTriggerBindingV0Getters { fn execute( &self, document_transition: &DocumentTransitionAction, - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, - ) -> Result<(DataTriggerExecutionResult, FeeResult), Error>; + ) -> Result; /// Checks whether the data trigger matches the specified data contract ID, document type, and action. /// @@ -77,9 +76,9 @@ impl DataTriggerBindingV0Getters for DataTriggerBindingV0 { fn execute( &self, document_transition: &DocumentTransitionAction, - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, - ) -> Result<(DataTriggerExecutionResult, FeeResult), Error> { + ) -> Result { (self.data_trigger)(document_transition, context, platform_version) } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs index 7423dd05ba5..f80329b50ef 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs @@ -7,7 +7,6 @@ use std::fmt::{Debug, Formatter}; /// DataTriggerExecutionContext represents the context in which a data trigger is executed. /// It contains references to relevant state and transaction data needed for the trigger to perform its actions. -#[derive(Clone)] pub struct DataTriggerExecutionContext<'a> { /// A reference to the platform state, which contains information about the current blockchain environment. pub platform: &'a PlatformStateRef<'a>, @@ -16,14 +15,14 @@ pub struct DataTriggerExecutionContext<'a> { /// The identifier of the owner of the data contract that the trigger is associated with. pub owner_id: &'a Identifier, /// The current block info, used as the source of `epoch` for trigger - /// fee computations. Triggers must use `block_info.epoch` (matching - /// the batch transformer's epoch source) rather than - /// `platform.state.last_committed_block_epoch_ref()` so the per-batch - /// fee accounting is consistent across all sites that bill on - /// `transform_into_action: 1`. + /// fee computations on PROTOCOL_VERSION_12+. `_v1` triggers use + /// `block_info.epoch` to match the batch transformer's epoch source. pub block_info: &'a BlockInfo, - /// A reference to the execution context for the state transition that triggered the data trigger. - pub state_transition_execution_context: &'a StateTransitionExecutionContext, + /// Mutable reference to the outer execution context — `_v1` triggers + /// call `add_operation` on this to bill their drive reads directly. + /// `_v0` triggers ignore it (preserving PROTOCOL_VERSION_11 chain + /// replay byte-identity at the behavior level). + pub state_transition_execution_context: &'a mut StateTransitionExecutionContext, } impl Debug for DataTriggerExecutionContext<'_> { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/executor.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/executor.rs index f30ea8c6e0f..e34d6341cfa 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/executor.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/executor.rs @@ -3,7 +3,6 @@ use crate::execution::validation::state_transition::batch::data_triggers::{ }; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; -use dpp::fee::fee_result::FeeResult; use dpp::state_transition::batch_transition::batched_transition::document_transition_action_type::DocumentTransitionActionTypeGetter; use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; use dpp::version::PlatformVersion; @@ -15,28 +14,27 @@ pub trait DataTriggerExecutor { fn validate_with_data_triggers( &self, data_trigger_bindings: &[DataTriggerBinding], - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, - ) -> Result<(DataTriggerExecutionResult, FeeResult), Error>; + ) -> Result; } impl DataTriggerExecutor for DocumentTransitionAction { fn validate_with_data_triggers( &self, data_trigger_bindings: &[DataTriggerBinding], - context: &DataTriggerExecutionContext, + context: &mut DataTriggerExecutionContext, platform_version: &PlatformVersion, - ) -> Result<(DataTriggerExecutionResult, FeeResult), Error> { + ) -> Result { let data_contract_id = self.base().data_contract_id(); let document_type_name = self.base().document_type_name(); let transition_action = self.action_type(); - let mut aggregated_fee_result = FeeResult::default(); - - // Match data triggers by action type, contract ID and document type name - // and then execute matched triggers until one of them returns invalid result. - // Fees from every trigger that actually executed (including the one that - // returned invalid) are accumulated so the user pays for the work done. + // Match data triggers by action type, contract ID and document + // type name, then execute matched triggers until one returns + // invalid. `_v1` triggers bill their own drive reads directly + // via `context.state_transition_execution_context.add_operation`; + // `_v0` triggers don't bill (PROTOCOL_VERSION_11 chain replay). for data_trigger_binding in data_trigger_bindings { if !data_trigger_binding.is_matching( &data_contract_id, @@ -46,16 +44,13 @@ impl DataTriggerExecutor for DocumentTransitionAction { continue; } - let (result, fee_result) = - data_trigger_binding.execute(self, context, platform_version)?; - - aggregated_fee_result.checked_add_assign(fee_result)?; + let result = data_trigger_binding.execute(self, context, platform_version)?; if !result.is_valid() { - return Ok((result, aggregated_fee_result)); + return Ok(result); } } - Ok((DataTriggerExecutionResult::default(), aggregated_fee_result)) + Ok(DataTriggerExecutionResult::default()) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/mod.rs index c8f6034d6da..ac4267d7040 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/mod.rs @@ -6,7 +6,6 @@ use drive::state_transition_action::batch::batched_transition::document_transiti use crate::error::Error; use dpp::consensus::state::data_trigger::DataTriggerError; -use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; pub(super) use bindings::list::data_trigger_bindings_list; @@ -20,17 +19,15 @@ mod triggers; /// Data trigger function pointer. /// -/// Returns the validation result and the `FeeResult` for whatever drive -/// reads (`query_documents`, `fetch_identity_balance`, etc.) the trigger -/// performed. The caller (`DataTriggerExecutor::validate_with_data_triggers`) -/// sums fees across all executed triggers and the outer batch-state -/// validator decides whether to bill them via the `transform_into_action` -/// version gate. +/// Takes a mutable `DataTriggerExecutionContext` so `_v1` triggers can +/// call `context.state_transition_execution_context.add_operation(...)` +/// directly to bill their drive reads. `_v0` triggers don't call +/// `add_operation` (preserving PROTOCOL_VERSION_11 chain replay). type DataTrigger = fn( &DocumentTransitionAction, - &DataTriggerExecutionContext<'_>, + &mut DataTriggerExecutionContext<'_>, &PlatformVersion, -) -> Result<(DataTriggerExecutionResult, FeeResult), Error>; +) -> Result; /// A type alias for a [SimpleValidationResult] with a [DataTriggerError] as the error type. /// diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/mod.rs index 06207d598ad..c782ac77381 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/mod.rs @@ -1,20 +1,21 @@ use crate::error::execution::ExecutionError; use crate::error::Error; use crate::execution::validation::state_transition::batch::data_triggers::triggers::dashpay::v0::create_contact_request_data_trigger_v0; +use crate::execution::validation::state_transition::batch::data_triggers::triggers::dashpay::v1::create_contact_request_data_trigger_v1; use crate::execution::validation::state_transition::batch::data_triggers::{ DataTriggerExecutionContext, DataTriggerExecutionResult, }; -use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; mod v0; +mod v1; pub fn create_contact_request_data_trigger( document_transition: &DocumentTransitionAction, - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, -) -> Result<(DataTriggerExecutionResult, FeeResult), Error> { +) -> Result { match platform_version .drive_abci .validation_and_processing @@ -25,9 +26,10 @@ pub fn create_contact_request_data_trigger( .create_contact_request_data_trigger { 0 => create_contact_request_data_trigger_v0(document_transition, context, platform_version), + 1 => create_contact_request_data_trigger_v1(document_transition, context, platform_version), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "create_contact_request_data_trigger".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs index 5c2f29edf70..066f02938c0 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs @@ -33,9 +33,9 @@ use crate::execution::validation::state_transition::batch::data_triggers::{DataT #[inline(always)] pub(super) fn create_contact_request_data_trigger_v0( document_transition: &DocumentTransitionAction, - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, -) -> Result<(DataTriggerExecutionResult, dpp::fee::fee_result::FeeResult), Error> { +) -> Result { let data_contract_fetch_info = document_transition.base().data_contract_fetch_info(); let data_contract = &data_contract_fetch_info.contract; let mut result = DataTriggerExecutionResult::default(); @@ -68,22 +68,17 @@ pub(super) fn create_contact_request_data_trigger_v0( result.add_error(err); - return Ok((result, dpp::fee::fee_result::FeeResult::default())); + return Ok(result); } - // Recipient identity must exist. - // - // Use `fetch_identity_balance_with_costs` so the grovedb cost is - // returned alongside the balance, then surface it via the returned - // FeeResult so the caller bills it on `transform_into_action: 1`. - let (to_identity, balance_fee_result) = - context.platform.drive.fetch_identity_balance_with_costs( - to_user_id.to_buffer(), - context.block_info, - true, - context.transaction, - platform_version, - )?; + // TODO: Calculate fee operations + + // Recipient identity must exist + let to_identity = context.platform.drive.fetch_identity_balance( + to_user_id.to_buffer(), + context.transaction, + platform_version, + )?; if !is_dry_run && to_identity.is_none() { let err = DataTriggerConditionError::new( @@ -94,10 +89,10 @@ pub(super) fn create_contact_request_data_trigger_v0( result.add_error(err); - return Ok((result, balance_fee_result)); + return Ok(result); } - Ok((result, balance_fee_result)) + Ok(result) } #[cfg(test)] @@ -182,19 +177,19 @@ mod test { transition_execution_context.enable_dry_run(); - let data_trigger_context = DataTriggerExecutionContext { + let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id, block_info: &BlockInfo::default(), - state_transition_execution_context: &transition_execution_context, + state_transition_execution_context: &mut transition_execution_context, transaction: None, }; - let (result, _fee_result) = create_contact_request_data_trigger( + let result = create_contact_request_data_trigger( // dispatches to _v0 (per-trigger version field = 0 at this test's default PV) &DocumentCreateTransitionAction::try_from_document_borrowed_create_transition_with_contract_lookup(&platform.drive, *owner_id, None, document_create_transition, &BlockInfo::default(), 0, |_identifier| { Ok(Arc::new(DataContractFetchInfo::dashpay_contract_fixture(protocol_version))) }, platform_version).expect("expected to create action").0.into_data().expect("expected to be a valid transition").as_document_action().expect("expected document action"), - &data_trigger_context, + &mut data_trigger_context, platform_version, ) .expect("the execution result should be returned"); @@ -285,7 +280,7 @@ mod test { .as_transition_create() .expect("expected a document create transition"); - let transition_execution_context = + let mut transition_execution_context = StateTransitionExecutionContext::default_for_platform_version(platform_version) .unwrap(); let identity_fixture = @@ -304,21 +299,21 @@ mod test { ) .expect("expected to insert identity"); - let data_trigger_context = DataTriggerExecutionContext { + let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id: &owner_id, block_info: &BlockInfo::default(), - state_transition_execution_context: &transition_execution_context, + state_transition_execution_context: &mut transition_execution_context, transaction: None, }; let _dashpay_identity_id = data_trigger_context.owner_id.to_owned(); - let (result, _fee_result) = create_contact_request_data_trigger( + let result = create_contact_request_data_trigger( // dispatches to _v0 (per-trigger version field = 0 at this test's default PV) &DocumentCreateTransitionAction::try_from_document_borrowed_create_transition_with_contract_lookup(&platform.drive, owner_id, None, document_create_transition, &BlockInfo::default(), 0, |_identifier| { Ok(Arc::new(DataContractFetchInfo::dashpay_contract_fixture(protocol_version))) }, platform_version).expect("expected to create action").0.into_data().expect("expected to be a valid transition").as_document_action().expect("expected document action"), - &data_trigger_context, + &mut data_trigger_context, platform_version, ) .expect("data trigger result should be returned"); @@ -419,25 +414,25 @@ mod test { .as_transition_create() .expect("expected a document create transition"); - let transition_execution_context = + let mut transition_execution_context = StateTransitionExecutionContext::default_for_platform_version(platform_version) .unwrap(); - let data_trigger_context = DataTriggerExecutionContext { + let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id: &owner_id, block_info: &BlockInfo::default(), - state_transition_execution_context: &transition_execution_context, + state_transition_execution_context: &mut transition_execution_context, transaction: None, }; let _dashpay_identity_id = data_trigger_context.owner_id.to_owned(); - let (result, fee_result) = create_contact_request_data_trigger( + let result = create_contact_request_data_trigger( // dispatches to _v0 (per-trigger version field = 0 at this test's default PV) &DocumentCreateTransitionAction::try_from_document_borrowed_create_transition_with_contract_lookup(&platform.drive, owner_id, None, document_create_transition, &BlockInfo::default(), 0, |_identifier| { Ok(Arc::new(DataContractFetchInfo::dashpay_contract_fixture(protocol_version))) }, platform_version).expect("expected to create action").0.into_data().expect("expected to be a valid transition").as_document_action().expect("expected document action"), - &data_trigger_context, + &mut data_trigger_context, platform_version, ) .expect("data trigger result should be returned"); @@ -445,14 +440,6 @@ mod test { assert!(!result.is_valid()); let data_trigger_error = &result.errors[0]; - // T3 regression pin: the trigger ran `fetch_identity_balance_with_costs` - // to check recipient existence — that fetch must produce a non-zero - // FeeResult so the caller can bill it on transform_into_action: 1. - assert!( - fee_result.processing_fee > 0, - "T3: fetch_identity_balance_with_costs must surface non-zero cost" - ); - assert!(matches!( data_trigger_error, DataTriggerError::DataTriggerConditionError(e) if { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs new file mode 100644 index 00000000000..bf76a40d7ad --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs @@ -0,0 +1,95 @@ +//! PROTOCOL_VERSION_12+ version of the DashPay contact-request trigger. +//! +//! Mirrors `create_contact_request_data_trigger_v0` but uses +//! `fetch_identity_balance_with_costs` (instead of `fetch_identity_balance`) +//! and bills the returned `FeeResult` via +//! `context.state_transition_execution_context.add_operation(...)`. + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; +use crate::execution::validation::state_transition::batch::data_triggers::{ + DataTriggerExecutionContext, DataTriggerExecutionResult, +}; +use dpp::consensus::state::data_trigger::data_trigger_condition_error::DataTriggerConditionError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; +use dpp::system_data_contracts::dashpay_contract::v1::document_types::contact_request::properties::TO_USER_ID; +use dpp::version::PlatformVersion; +use dpp::ProtocolError; +use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; + +#[inline(always)] +pub(super) fn create_contact_request_data_trigger_v1( + document_transition: &DocumentTransitionAction, + context: &mut DataTriggerExecutionContext<'_>, + platform_version: &PlatformVersion, +) -> Result { + let data_contract_fetch_info = document_transition.base().data_contract_fetch_info(); + let data_contract = &data_contract_fetch_info.contract; + let mut result = DataTriggerExecutionResult::default(); + let is_dry_run = context.state_transition_execution_context.in_dry_run(); + let owner_id = context.owner_id; + + let DocumentTransitionAction::CreateAction(document_create_transition) = document_transition + else { + return Err(Error::Execution(ExecutionError::DataTriggerExecutionError( + format!( + "the Document Transition {} isn't 'CREATE", + document_transition.base().id() + ), + ))); + }; + + let data = document_create_transition.data(); + + let to_user_id = data + .get_identifier(TO_USER_ID) + .map_err(ProtocolError::ValueError)?; + + // You shouldn't create a contract request to yourself + if !is_dry_run && owner_id == &to_user_id { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + format!("Identity {to_user_id} must not be equal to owner id"), + ); + + result.add_error(err); + + return Ok(result); + } + + // Recipient identity must exist. Bill the balance-fetch cost via + // add_operation on the outer execution_context. + let (to_identity, balance_fee_result) = + context.platform.drive.fetch_identity_balance_with_costs( + to_user_id.to_buffer(), + context.block_info, + true, + context.transaction, + platform_version, + )?; + context + .state_transition_execution_context + .add_operation(ValidationOperation::PrecalculatedOperation( + balance_fee_result, + )); + + if !is_dry_run && to_identity.is_none() { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_create_transition.base().id(), + format!("Identity {to_user_id} doesn't exist"), + ); + + result.add_error(err); + + return Ok(result); + } + + Ok(result) +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/mod.rs index 0f4942d2bdc..2ef56404c59 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/mod.rs @@ -1,20 +1,21 @@ use crate::error::execution::ExecutionError; use crate::error::Error; use crate::execution::validation::state_transition::batch::data_triggers::triggers::dpns::v0::create_domain_data_trigger_v0; +use crate::execution::validation::state_transition::batch::data_triggers::triggers::dpns::v1::create_domain_data_trigger_v1; use crate::execution::validation::state_transition::batch::data_triggers::{ DataTriggerExecutionContext, DataTriggerExecutionResult, }; -use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; mod v0; +mod v1; pub fn create_domain_data_trigger( document_transition: &DocumentTransitionAction, - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, -) -> Result<(DataTriggerExecutionResult, FeeResult), Error> { +) -> Result { match platform_version .drive_abci .validation_and_processing @@ -24,10 +25,14 @@ pub fn create_domain_data_trigger( .triggers .create_domain_data_trigger { + // PROTOCOL_VERSION_11 and below: trigger doesn't bill drive reads. 0 => create_domain_data_trigger_v0(document_transition, context, platform_version), + // PROTOCOL_VERSION_12+: trigger bills via add_operation on the + // outer execution_context (threaded through the mutable context). + 1 => create_domain_data_trigger_v1(document_transition, context, platform_version), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "create_domain_data_trigger".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs index 9ec0f7bfe4c..61afdaa5794 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs @@ -22,8 +22,6 @@ use dpp::system_data_contracts::dpns_contract; use dpp::system_data_contracts::dpns_contract::v1::document_types::domain::properties::{ALLOW_SUBDOMAINS, DASH_ALIAS_IDENTITY_ID, DASH_UNIQUE_IDENTITY_ID, LABEL, NORMALIZED_LABEL, NORMALIZED_PARENT_DOMAIN_NAME, PREORDER_SALT, RECORDS}; use dpp::util::strings::convert_to_homograph_safe_chars; -use dpp::block::epoch::Epoch; -use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperator}; @@ -49,9 +47,9 @@ pub const MAX_PRINTABLE_DOMAIN_NAME_LENGTH: usize = 253; #[inline(always)] pub(super) fn create_domain_data_trigger_v0( document_transition: &DocumentTransitionAction, - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, -) -> Result<(DataTriggerExecutionResult, dpp::fee::fee_result::FeeResult), Error> { +) -> Result { let data_contract_fetch_info = document_transition.base().data_contract_fetch_info(); let data_contract = &data_contract_fetch_info.contract; let is_dry_run = context.state_transition_execution_context.in_dry_run(); @@ -105,8 +103,6 @@ pub(super) fn create_domain_data_trigger_v0( }; let mut result = DataTriggerExecutionResult::default(); - let mut accumulated_fee_result = FeeResult::default(); - let epoch: &Epoch = &context.block_info.epoch; if !is_dry_run { if full_domain_name.len() > MAX_PRINTABLE_DOMAIN_NAME_LENGTH { @@ -247,22 +243,18 @@ pub(super) fn create_domain_data_trigger_v0( block_time_ms: None, }; - // Pass `Some(epoch)` so query_documents computes the real cost - // (with `None` it short-circuits to 0). The cost is added to the - // accumulated FeeResult that the caller bills on - // `transform_into_action: 1`. - let parent_domain_outcome = context.platform.drive.query_documents( - drive_query, - Some(epoch), - is_dry_run, - context.transaction, - Some(platform_version.protocol_version), - )?; - accumulated_fee_result.checked_add_assign(FeeResult { - processing_fee: parent_domain_outcome.cost(), - ..Default::default() - })?; - let documents = parent_domain_outcome.documents_owned(); + // todo: deal with cost of this operation + let documents = context + .platform + .drive + .query_documents( + drive_query, + None, + is_dry_run, + context.transaction, + Some(platform_version.protocol_version), + )? + .documents_owned(); if !is_dry_run { if documents.is_empty() { @@ -274,7 +266,7 @@ pub(super) fn create_domain_data_trigger_v0( result.add_error(err); - return Ok((result, accumulated_fee_result)); + return Ok(result); } let parent_domain = &documents[0]; @@ -287,7 +279,7 @@ pub(super) fn create_domain_data_trigger_v0( result.add_error(err); - return Ok((result, accumulated_fee_result)); + return Ok(result); } if (!parent_domain @@ -304,7 +296,7 @@ pub(super) fn create_domain_data_trigger_v0( result.add_error(err); - return Ok((result, accumulated_fee_result)); + return Ok(result); } } } @@ -342,23 +334,21 @@ pub(super) fn create_domain_data_trigger_v0( block_time_ms: None, }; - // Same pattern as the parent-domain query above — capture cost - // for the caller to bill. - let preorder_outcome = context.platform.drive.query_documents( - drive_query, - Some(epoch), - is_dry_run, - context.transaction, - Some(platform_version.protocol_version), - )?; - accumulated_fee_result.checked_add_assign(FeeResult { - processing_fee: preorder_outcome.cost(), - ..Default::default() - })?; - let preorder_documents = preorder_outcome.documents_owned(); + // todo: deal with cost of this operation + let preorder_documents = context + .platform + .drive + .query_documents( + drive_query, + None, + is_dry_run, + context.transaction, + Some(platform_version.protocol_version), + )? + .documents_owned(); if is_dry_run { - return Ok((result, accumulated_fee_result)); + return Ok(result); } if preorder_documents.is_empty() { @@ -373,7 +363,7 @@ pub(super) fn create_domain_data_trigger_v0( result.add_error(err) } - Ok((result, accumulated_fee_result)) + Ok(result) } #[cfg(test)] @@ -449,20 +439,20 @@ mod test { transition_execution_context.enable_dry_run(); - let data_trigger_context = DataTriggerExecutionContext { + let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id: &owner_id, block_info: &BlockInfo::default(), - state_transition_execution_context: &transition_execution_context, + state_transition_execution_context: &mut transition_execution_context, transaction: None, }; - let (result, _fee_result) = create_domain_data_trigger_v0( + let result = create_domain_data_trigger_v0( &DocumentCreateTransitionAction::try_from_document_borrowed_create_transition_with_contract_lookup(&platform.drive, owner_id, None, document_create_transition, &BlockInfo::default(), 0, |_identifier| { Ok(Arc::new(DataContractFetchInfo::dpns_contract_fixture(platform_version.protocol_version))) }, platform_version).expect("expected to create action").0.into_data().expect("expected to be a valid transition").as_document_action().expect("expected document action"), - &data_trigger_context, + &mut data_trigger_context, platform_version, ) .expect("the execution result should be returned"); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs new file mode 100644 index 00000000000..388ccb52b71 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs @@ -0,0 +1,379 @@ +use dpp::consensus::state::data_trigger::data_trigger_condition_error::DataTriggerConditionError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contracts::dpns_contract::v1::document_types::domain::properties::PARENT_DOMAIN_NAME; +/// The `dpns_triggers` module contains data triggers specific to the DPNS data contract. +use dpp::util::hash::hash_double; +use std::collections::BTreeMap; + +use crate::error::execution::ExecutionError; +use crate::error::Error; + +use crate::execution::validation::state_transition::batch::data_triggers::{ + DataTriggerExecutionContext, DataTriggerExecutionResult, +}; +use dpp::document::DocumentV0Getters; +use dpp::platform_value::btreemap_extensions::{BTreeValueMapHelper, BTreeValueMapPathHelper}; +use dpp::platform_value::Value; +use dpp::ProtocolError; +use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; +use dpp::system_data_contracts::dpns_contract; +use dpp::system_data_contracts::dpns_contract::v1::document_types::domain::properties::{ALLOW_SUBDOMAINS, + DASH_ALIAS_IDENTITY_ID, DASH_UNIQUE_IDENTITY_ID, LABEL, NORMALIZED_LABEL, NORMALIZED_PARENT_DOMAIN_NAME, PREORDER_SALT, RECORDS}; +use dpp::util::strings::convert_to_homograph_safe_chars; +use dpp::version::PlatformVersion; +use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; +use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperator}; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; + +pub const MAX_PRINTABLE_DOMAIN_NAME_LENGTH: usize = 253; + +/// Creates a data trigger for handling domain documents. +/// +/// The trigger is executed whenever a new domain document is created on the blockchain. +/// It performs various actions depending on the state of the document and the context in which it was created. +/// +/// # Arguments +/// +/// * `document_transition` - A reference to the document transition that triggered the data trigger. +/// * `context` - A reference to the data trigger execution context. +/// * `dpns_contract::OWNER_ID` - An optional identifier for the top-level identity associated with the domain +/// document (if one exists). +/// +/// # Returns +/// +/// A `DataTriggerExecutionResult` indicating the success or failure of the trigger execution. +#[inline(always)] +pub(super) fn create_domain_data_trigger_v1( + document_transition: &DocumentTransitionAction, + context: &mut DataTriggerExecutionContext<'_>, + platform_version: &PlatformVersion, +) -> Result { + let data_contract_fetch_info = document_transition.base().data_contract_fetch_info(); + let data_contract = &data_contract_fetch_info.contract; + let is_dry_run = context.state_transition_execution_context.in_dry_run(); + let document_create_transition = match document_transition { + DocumentTransitionAction::CreateAction(d) => d, + _ => { + return Err(Error::Execution(ExecutionError::DataTriggerExecutionError( + format!( + "the Document Transition {} isn't 'CREATE", + document_transition.base().id() + ), + ))) + } + }; + + let data = document_create_transition.data(); + + let owner_id = context.owner_id; + let label = data.get_string(LABEL).map_err(ProtocolError::ValueError)?; + let normalized_label = data + .get_str(NORMALIZED_LABEL) + .map_err(ProtocolError::ValueError)?; + + let parent_domain_name = data + .get_string(PARENT_DOMAIN_NAME) + .map_err(ProtocolError::ValueError)?; + let normalized_parent_domain_name = data + .get_string(NORMALIZED_PARENT_DOMAIN_NAME) + .map_err(ProtocolError::ValueError)?; + + let preorder_salt = data + .get_hash256_bytes(PREORDER_SALT) + .map_err(ProtocolError::ValueError)?; + let records = data + .get(RECORDS) + .ok_or(ExecutionError::DataTriggerExecutionError(format!( + "property '{}' doesn't exist", + RECORDS + )))? + .to_btree_ref_string_map() + .map_err(ProtocolError::ValueError)?; + + let rule_allow_subdomains = data + .get_bool_at_path(ALLOW_SUBDOMAINS) + .map_err(ProtocolError::ValueError)?; + + let full_domain_name = if parent_domain_name.is_empty() { + label.to_string() + } else { + format!("{normalized_label}.{parent_domain_name}") + }; + + let mut result = DataTriggerExecutionResult::default(); + + if !is_dry_run { + if full_domain_name.len() > MAX_PRINTABLE_DOMAIN_NAME_LENGTH { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + format!( + "Full domain name length can not be more than {} characters long but got {}", + MAX_PRINTABLE_DOMAIN_NAME_LENGTH, + full_domain_name.len() + ), + ); + + result.add_error(err) + } + + if normalized_label != convert_to_homograph_safe_chars(label.as_str()) { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + format!( + "Normalized label doesn't match label: {} != {}", + normalized_label, label + ), + ); + + result.add_error(err); + } + + if normalized_parent_domain_name + != convert_to_homograph_safe_chars(parent_domain_name.as_str()) + { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + format!( + "Normalized parent domain name doesn't match parent domain name: {} != {}", + normalized_parent_domain_name, parent_domain_name + ), + ); + + result.add_error(err); + } + + if let Some(id) = records + .get_optional_identifier(DASH_UNIQUE_IDENTITY_ID) + .map_err(ProtocolError::ValueError)? + { + if id != owner_id { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + format!( + "ownerId {} doesn't match {} {}", + owner_id, DASH_UNIQUE_IDENTITY_ID, id + ), + ); + + result.add_error(err); + } + } + + if let Some(id) = records + .get_optional_identifier(DASH_ALIAS_IDENTITY_ID) + .map_err(ProtocolError::ValueError)? + { + if id != owner_id { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + format!( + "ownerId {} doesn't match {} {}", + owner_id, DASH_ALIAS_IDENTITY_ID, id + ), + ); + + result.add_error(err); + } + } + + if normalized_parent_domain_name.is_empty() && context.owner_id != &dpns_contract::OWNER_ID + { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + "Can't create top level domain for this identity".to_string(), + ); + + result.add_error(err); + } + } + + if !normalized_parent_domain_name.is_empty() { + //? What is the `normalized_parent_name`. Are we sure the content is a valid dot-separated data + let mut parent_domain_segments = normalized_parent_domain_name.split('.'); + let parent_domain_label = parent_domain_segments.next().unwrap().to_string(); + let grand_parent_domain_name = parent_domain_segments.collect::>().join("."); + + let document_type = data_contract.document_type_for_name( + document_create_transition + .base() + .document_type_name() + .as_str(), + )?; + + let drive_query = DriveDocumentQuery { + contract: data_contract, + document_type, + internal_clauses: InternalClauses { + primary_key_in_clause: None, + primary_key_equal_clause: None, + in_clause: None, + range_clause: None, + equal_clauses: BTreeMap::from([ + ( + "normalizedParentDomainName".to_string(), + WhereClause { + field: "normalizedParentDomainName".to_string(), + operator: WhereOperator::Equal, + value: Value::Text(grand_parent_domain_name), + }, + ), + ( + "normalizedLabel".to_string(), + WhereClause { + field: "normalizedLabel".to_string(), + operator: WhereOperator::Equal, + value: Value::Text(parent_domain_label), + }, + ), + ]), + }, + offset: None, + limit: None, + order_by: Default::default(), + start_at: None, + start_at_included: false, + block_time_ms: None, + }; + + // Pass Some(epoch) so the real grovedb cost is computed; bill + // it via add_operation on the outer execution_context. + let parent_domain_outcome = context.platform.drive.query_documents( + drive_query, + Some(&context.block_info.epoch), + is_dry_run, + context.transaction, + Some(platform_version.protocol_version), + )?; + context.state_transition_execution_context.add_operation( + crate::execution::types::execution_operation::ValidationOperation::PrecalculatedOperation( + dpp::fee::fee_result::FeeResult { + processing_fee: parent_domain_outcome.cost(), + ..Default::default() + }, + ), + ); + let documents = parent_domain_outcome.documents_owned(); + + if !is_dry_run { + if documents.is_empty() { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + "Parent domain is not present".to_string(), + ); + + result.add_error(err); + + return Ok(result); + } + let parent_domain = &documents[0]; + + if rule_allow_subdomains { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + "Allowing subdomains registration is forbidden for this domain".to_string(), + ); + + result.add_error(err); + + return Ok(result); + } + + if (!parent_domain + .properties() + .get_bool_at_path(ALLOW_SUBDOMAINS) + .map_err(ProtocolError::ValueError)?) + && context.owner_id != &parent_domain.owner_id() + { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + "The subdomain can be created only by the parent domain owner".to_string(), + ); + + result.add_error(err); + + return Ok(result); + } + } + } + + let mut salted_domain_buffer: Vec = vec![]; + salted_domain_buffer.extend(preorder_salt); + salted_domain_buffer.extend(full_domain_name.as_bytes()); + + let salted_domain_hash = hash_double(salted_domain_buffer); + + let document_type = data_contract.document_type_for_name("preorder")?; + + let drive_query = DriveDocumentQuery { + contract: data_contract, + document_type, + internal_clauses: InternalClauses { + primary_key_in_clause: None, + primary_key_equal_clause: None, + in_clause: None, + range_clause: None, + equal_clauses: BTreeMap::from([( + "saltedDomainHash".to_string(), + WhereClause { + field: "saltedDomainHash".to_string(), + operator: WhereOperator::Equal, + value: Value::Bytes32(salted_domain_hash), + }, + )]), + }, + offset: None, + limit: None, + order_by: Default::default(), + start_at: None, + start_at_included: false, + block_time_ms: None, + }; + + // Same pattern as the parent-domain query above — bill the cost. + let preorder_outcome = context.platform.drive.query_documents( + drive_query, + Some(&context.block_info.epoch), + is_dry_run, + context.transaction, + Some(platform_version.protocol_version), + )?; + context.state_transition_execution_context.add_operation( + crate::execution::types::execution_operation::ValidationOperation::PrecalculatedOperation( + dpp::fee::fee_result::FeeResult { + processing_fee: preorder_outcome.cost(), + ..Default::default() + }, + ), + ); + let preorder_documents = preorder_outcome.documents_owned(); + + if is_dry_run { + return Ok(result); + } + + if preorder_documents.is_empty() { + let err = DataTriggerConditionError::new( + data_contract.id(), + document_transition.base().id(), + format!( + "preorderDocument was not found with a salted domain hash of {}", + hex::encode(salted_domain_hash) + ), + ); + result.add_error(err) + } + + Ok(result) +} + diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/reject/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/reject/mod.rs index c69b8c81f5e..a626e8e6908 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/reject/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/reject/mod.rs @@ -4,7 +4,6 @@ use crate::execution::validation::state_transition::batch::data_triggers::trigge use crate::execution::validation::state_transition::batch::data_triggers::{ DataTriggerExecutionContext, DataTriggerExecutionResult, }; -use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; @@ -12,9 +11,9 @@ mod v0; pub fn reject_data_trigger( document_transition: &DocumentTransitionAction, - _context: &DataTriggerExecutionContext<'_>, + _context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, -) -> Result<(DataTriggerExecutionResult, FeeResult), Error> { +) -> Result { match platform_version .drive_abci .validation_and_processing @@ -24,8 +23,8 @@ pub fn reject_data_trigger( .triggers .reject_data_trigger { - // Reject performs no drive reads — FeeResult is always default. - 0 => Ok((reject_data_trigger_v0(document_transition)?, FeeResult::default())), + // Reject performs no drive reads — never bills. + 0 => reject_data_trigger_v0(document_transition), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "reject_data_trigger".to_string(), known_versions: vec![0], diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/mod.rs index a695d474b6a..55886df78f7 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/mod.rs @@ -4,17 +4,18 @@ use crate::execution::validation::state_transition::batch::data_triggers::{ DataTriggerExecutionContext, DataTriggerExecutionResult, }; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; -use dpp::fee::fee_result::FeeResult; use dpp::version::PlatformVersion; use crate::execution::validation::state_transition::batch::data_triggers::triggers::withdrawals::v0::delete_withdrawal_data_trigger_v0; +use crate::execution::validation::state_transition::batch::data_triggers::triggers::withdrawals::v1::delete_withdrawal_data_trigger_v1; mod v0; +mod v1; pub fn delete_withdrawal_data_trigger( document_transition: &DocumentTransitionAction, - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, -) -> Result<(DataTriggerExecutionResult, FeeResult), Error> { +) -> Result { match platform_version .drive_abci .validation_and_processing @@ -25,9 +26,10 @@ pub fn delete_withdrawal_data_trigger( .delete_withdrawal_data_trigger { 0 => delete_withdrawal_data_trigger_v0(document_transition, context, platform_version), + 1 => delete_withdrawal_data_trigger_v1(document_transition, context, platform_version), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "delete_withdrawal_data_trigger".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs index 357df97fec8..419d253b7af 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs @@ -5,8 +5,6 @@ use crate::error::Error; use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; use dpp::platform_value::Value; use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; -use dpp::block::epoch::Epoch; -use dpp::fee::fee_result::FeeResult; use dpp::system_data_contracts::withdrawals_contract; use dpp::version::PlatformVersion; use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperator}; @@ -38,9 +36,9 @@ use crate::execution::validation::state_transition::batch::data_triggers::{DataT #[inline(always)] pub(super) fn delete_withdrawal_data_trigger_v0( document_transition: &DocumentTransitionAction, - context: &DataTriggerExecutionContext<'_>, + context: &mut DataTriggerExecutionContext<'_>, platform_version: &PlatformVersion, -) -> Result<(DataTriggerExecutionResult, dpp::fee::fee_result::FeeResult), Error> { +) -> Result { let data_contract_fetch_info = document_transition.base().data_contract_fetch_info(); let data_contract = &data_contract_fetch_info.contract; let mut result = DataTriggerExecutionResult::default(); @@ -78,22 +76,18 @@ pub(super) fn delete_withdrawal_data_trigger_v0( block_time_ms: None, }; - // Pass `Some(epoch)` so query_documents computes the real cost - // (with `None` it short-circuits to 0). Surface it via the returned - // FeeResult so the caller bills it on `transform_into_action: 1`. - let epoch: &Epoch = &context.block_info.epoch; - let withdrawals_outcome = context.platform.drive.query_documents( - drive_query, - Some(epoch), - false, - context.transaction, - Some(platform_version.protocol_version), - )?; - let query_fee_result = FeeResult { - processing_fee: withdrawals_outcome.cost(), - ..Default::default() - }; - let withdrawals = withdrawals_outcome.documents_owned(); + // todo: deal with cost of this operation + let withdrawals = context + .platform + .drive + .query_documents( + drive_query, + None, + false, + context.transaction, + Some(platform_version.protocol_version), + )? + .documents_owned(); let Some(withdrawal) = withdrawals.first() else { let err = DataTriggerConditionError::new( @@ -104,7 +98,7 @@ pub(super) fn delete_withdrawal_data_trigger_v0( result.add_error(err); - return Ok((result, query_fee_result)); + return Ok(result); }; let status: u8 = withdrawal @@ -121,10 +115,10 @@ pub(super) fn delete_withdrawal_data_trigger_v0( result.add_error(err); - return Ok((result, query_fee_result)); + return Ok(result); } - Ok((result, query_fee_result)) + Ok(result) } #[cfg(test)] @@ -166,7 +160,7 @@ mod tests { }; let platform_version = state_read_guard.current_platform_version().unwrap(); - let transition_execution_context = StateTransitionExecutionContextV0::default(); + let mut transition_execution_context = StateTransitionExecutionContextV0::default(); let data_contract = get_data_contract_fixture(None, 0, platform_version.protocol_version) .data_contract_owned(); let owner_id = data_contract.owner_id(); @@ -187,19 +181,19 @@ mod tests { .into(); let document_transition = DocumentTransitionAction::DeleteAction(delete_transition); - let data_trigger_context = DataTriggerExecutionContext { + let mut state_transition_execution_context_outer = + StateTransitionExecutionContext::V0(transition_execution_context); + let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id: &owner_id, block_info: &BlockInfo::default(), - state_transition_execution_context: &StateTransitionExecutionContext::V0( - transition_execution_context, - ), + state_transition_execution_context: &mut state_transition_execution_context_outer, transaction: None, }; let result = delete_withdrawal_data_trigger_v0( &document_transition, - &data_trigger_context, + &mut data_trigger_context, platform_version, ) .expect_err("the execution result should be returned"); @@ -259,7 +253,7 @@ mod tests { config: &platform.config, }; - let transition_execution_context = + let mut transition_execution_context = StateTransitionExecutionContext::V0(StateTransitionExecutionContextV0::default()); let platform_version = state_read_guard @@ -327,16 +321,16 @@ mod tests { }), ); - let data_trigger_context = DataTriggerExecutionContext { + let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id: &owner_id, block_info: &BlockInfo::default(), - state_transition_execution_context: &transition_execution_context, + state_transition_execution_context: &mut transition_execution_context, transaction: None, }; - let (result, fee_result) = delete_withdrawal_data_trigger_v0( + let result = delete_withdrawal_data_trigger_v0( &document_transition, - &data_trigger_context, + &mut data_trigger_context, platform_version, ) .expect("the execution result should be returned"); @@ -349,14 +343,5 @@ mod tests { error.to_string(), "withdrawal deletion is allowed only for COMPLETE statuses" ); - - // T4 regression pin: the trigger ran `query_documents` to fetch the - // withdrawal — that query must surface a non-zero cost via the - // returned FeeResult so the caller can bill it on - // `transform_into_action: 1`. - assert!( - fee_result.processing_fee > 0, - "T4: query_documents must surface non-zero cost" - ); } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs new file mode 100644 index 00000000000..260eb053f00 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs @@ -0,0 +1,124 @@ +//! PROTOCOL_VERSION_12+ version of the withdrawals delete trigger. +//! +//! Mirrors `delete_withdrawal_data_trigger_v0` but passes `Some(epoch)` +//! to `query_documents` so the real grovedb cost is computed, and +//! bills it via +//! `context.state_transition_execution_context.add_operation(...)` so +//! the user pays for the read. + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; +use crate::execution::validation::state_transition::batch::data_triggers::{ + DataTriggerExecutionContext, DataTriggerExecutionResult, +}; +use dpp::consensus::state::data_trigger::data_trigger_condition_error::DataTriggerConditionError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::document::DocumentV0Getters; +use dpp::fee::fee_result::FeeResult; +use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; +use dpp::platform_value::Value; +use dpp::system_data_contracts::withdrawals_contract; +use dpp::system_data_contracts::withdrawals_contract::v1::document_types::withdrawal; +use dpp::version::PlatformVersion; +use dpp::{document, ProtocolError}; +use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; +use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperator}; +use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::v0::DocumentDeleteTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction; +use std::collections::BTreeMap; + +#[inline(always)] +pub(super) fn delete_withdrawal_data_trigger_v1( + document_transition: &DocumentTransitionAction, + context: &mut DataTriggerExecutionContext<'_>, + platform_version: &PlatformVersion, +) -> Result { + let data_contract_fetch_info = document_transition.base().data_contract_fetch_info(); + let data_contract = &data_contract_fetch_info.contract; + let mut result = DataTriggerExecutionResult::default(); + + let DocumentTransitionAction::DeleteAction(dt_delete) = document_transition else { + return Err(Error::Execution(ExecutionError::DataTriggerExecutionError( + format!( + "the Document Transition {} isn't 'DELETE", + document_transition.base().id() + ), + ))); + }; + + let document_type = data_contract.document_type_for_name(withdrawal::NAME)?; + + let drive_query = DriveDocumentQuery { + contract: data_contract, + document_type, + internal_clauses: InternalClauses { + primary_key_in_clause: None, + primary_key_equal_clause: Some(WhereClause { + field: document::property_names::ID.to_string(), + operator: WhereOperator::Equal, + value: Value::Identifier(dt_delete.base().id().to_buffer()), + }), + in_clause: None, + range_clause: None, + equal_clauses: BTreeMap::default(), + }, + offset: None, + limit: Some(100), + order_by: Default::default(), + start_at: None, + start_at_included: false, + block_time_ms: None, + }; + + // Pass `Some(epoch)` so the grovedb cost is computed; then bill via + // add_operation on the outer execution_context. + let withdrawals_outcome = context.platform.drive.query_documents( + drive_query, + Some(&context.block_info.epoch), + false, + context.transaction, + Some(platform_version.protocol_version), + )?; + let query_fee = FeeResult { + processing_fee: withdrawals_outcome.cost(), + ..Default::default() + }; + context + .state_transition_execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(query_fee)); + let withdrawals = withdrawals_outcome.documents_owned(); + + let Some(withdrawal) = withdrawals.first() else { + let err = DataTriggerConditionError::new( + data_contract.id(), + dt_delete.base().id(), + "Withdrawal document was not found".to_string(), + ); + + result.add_error(err); + + return Ok(result); + }; + + let status: u8 = withdrawal + .properties() + .get_integer("status") + .map_err(ProtocolError::ValueError)?; + + if status != withdrawals_contract::WithdrawalStatus::COMPLETE as u8 { + let err = DataTriggerConditionError::new( + data_contract.id(), + dt_delete.base().id(), + "withdrawal deletion is allowed only for COMPLETE statuses".to_string(), + ); + + result.add_error(err); + + return Ok(result); + } + + Ok(result) +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index 356831570f2..2e2dbb28f9d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -77,9 +77,6 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { ) -> Result, Error> { let mut validation_result = ConsensusValidationResult::::new(); - let state_transition_execution_context = - StateTransitionExecutionContext::default_for_platform_version(platform_version)?; - let owner_id = state_transition_action.owner_id(); let mut validated_transitions = vec![]; @@ -273,53 +270,26 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { )); } else if platform.config.execution.use_document_triggers { if let BatchedTransitionAction::DocumentAction(document_transition) = &transition { - // we should also validate document triggers - let data_trigger_execution_context = DataTriggerExecutionContext { + // Triggers receive a &mut DataTriggerExecutionContext + // wrapping the outer execution_context — `_v1` triggers + // call add_operation on it to bill their drive reads + // directly. `_v0` triggers don't bill (per-trigger + // version field stays at 0 on PROTOCOL_VERSION_11). + let owner_id_value = self.owner_id(); + let mut data_trigger_execution_context = DataTriggerExecutionContext { platform, transaction, - owner_id: &self.owner_id(), + owner_id: &owner_id_value, block_info, - state_transition_execution_context: &state_transition_execution_context, + state_transition_execution_context: execution_context, }; - let (data_trigger_execution_result, data_trigger_fee_result) = - document_transition.validate_with_data_triggers( + let data_trigger_execution_result = document_transition + .validate_with_data_triggers( &data_trigger_bindings, - &data_trigger_execution_context, + &mut data_trigger_execution_context, platform_version, )?; - // Bill the accumulated trigger drive-read cost on the - // bumped `transform_into_action` gate, matching the same - // gate used by the transformer-phase fee fixes. On v0 the - // cost is discarded for chain replay reproducibility. - match platform_version - .drive_abci - .validation_and_processing - .state_transitions - .batch_state_transition - .transform_into_action - { - 0 => {} - 1 => { - execution_context.add_operation( - ValidationOperation::PrecalculatedOperation( - data_trigger_fee_result, - ), - ); - } - version => { - return Err(Error::Execution( - ExecutionError::UnknownVersionMismatch { - method: - "documents batch transition: data trigger fee billing" - .to_string(), - known_versions: vec![0, 1], - received: version, - }, - )); - } - } - if !data_trigger_execution_result.is_valid() { // If a state transition isn't valid because of data triggers we still need // to bump the identity data contract nonce diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs index f48ffe27f0d..a657b46ffce 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/deletion.rs @@ -143,7 +143,7 @@ mod deletion_tests { assert_eq!(processing_result.valid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 1666860); + assert_eq!(processing_result.aggregated_fees().processing_fee, 1678920); let issues = platform .drive @@ -488,7 +488,7 @@ mod deletion_tests { assert_eq!(processing_result.valid_count(), 1); - assert_eq!(processing_result.aggregated_fees().processing_fee, 2762400); + assert_eq!(processing_result.aggregated_fees().processing_fee, 2778700); let issues = platform .drive @@ -744,7 +744,7 @@ mod deletion_tests { assert_eq!(processing_result.valid_count(), 0); - assert_eq!(processing_result.aggregated_fees().processing_fee, 516040); + assert_eq!(processing_result.aggregated_fees().processing_fee, 520340); } #[tokio::test] async fn test_document_deletion_that_needs_a_token() { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs index 0a63c02a4d6..4702bc6eb08 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs @@ -469,11 +469,11 @@ mod nft_tests { .change(), &BalanceChange::RemoveFromBalance { required_removed_balance: 123579000, - desired_removed_balance: 126435860, + desired_removed_balance: 126440160, } ); - let original_creation_cost = 126435860; + let original_creation_cost = 126440160; platform .drive @@ -864,11 +864,11 @@ mod nft_tests { .change(), &BalanceChange::RemoveFromBalance { required_removed_balance: 138159000, - desired_removed_balance: 141234660, + desired_removed_balance: 141238960, } ); - let original_creation_cost = 141234660; + let original_creation_cost = 141238960; platform .drive @@ -1369,11 +1369,11 @@ mod nft_tests { .change(), &BalanceChange::RemoveFromBalance { required_removed_balance: 123579000, - desired_removed_balance: 126435860, + desired_removed_balance: 126440160, } ); - let original_creation_cost = 126435860; + let original_creation_cost = 126440160; platform .drive diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index ae01ab8b991..e805b9ab540 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -154,12 +154,18 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { - create_contact_request_data_trigger: 0, - create_domain_data_trigger: 0, + // PROTOCOL_VERSION_12 (v3.1 hard fork): triggers + // that perform drive reads now have `_v1` versions + // that bill the cost via add_operation on the + // outer execution_context. v0 versions remain + // byte-identical to PV11 (don't bill). + create_contact_request_data_trigger: 1, + create_domain_data_trigger: 1, create_identity_data_trigger: 0, create_feature_flag_data_trigger: 0, create_masternode_reward_shares_data_trigger: 0, - delete_withdrawal_data_trigger: 0, + delete_withdrawal_data_trigger: 1, + // Reject does no drive reads — stays at v0. reject_data_trigger: 0, }, }, From d162df9968859b9aeb75d00e4fd15210b097ce3b Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 20 May 2026 01:19:23 +0700 Subject: [PATCH 15/20] refactor(drive-abci): drop block_info from DataTriggerExecutionContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../state_transitions/batch/data_triggers/context.rs | 5 ----- .../batch/data_triggers/triggers/dashpay/v0/mod.rs | 3 --- .../batch/data_triggers/triggers/dashpay/v1/mod.rs | 3 ++- .../batch/data_triggers/triggers/dpns/v0/mod.rs | 1 - .../batch/data_triggers/triggers/dpns/v1/mod.rs | 5 +++-- .../batch/data_triggers/triggers/withdrawals/v0/mod.rs | 2 -- .../batch/data_triggers/triggers/withdrawals/v1/mod.rs | 3 ++- .../state_transition/state_transitions/batch/state/v0/mod.rs | 1 - 8 files changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs index f80329b50ef..4f033dc4c86 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/context.rs @@ -1,6 +1,5 @@ use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::platform_types::platform::PlatformStateRef; -use dpp::block::block_info::BlockInfo; use dpp::prelude::*; use drive::grovedb::TransactionArg; use std::fmt::{Debug, Formatter}; @@ -14,10 +13,6 @@ pub struct DataTriggerExecutionContext<'a> { pub transaction: TransactionArg<'a, 'a>, /// The identifier of the owner of the data contract that the trigger is associated with. pub owner_id: &'a Identifier, - /// The current block info, used as the source of `epoch` for trigger - /// fee computations on PROTOCOL_VERSION_12+. `_v1` triggers use - /// `block_info.epoch` to match the batch transformer's epoch source. - pub block_info: &'a BlockInfo, /// Mutable reference to the outer execution context — `_v1` triggers /// call `add_operation` on this to bill their drive reads directly. /// `_v0` triggers ignore it (preserving PROTOCOL_VERSION_11 chain diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs index 066f02938c0..0276cdd1ae0 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs @@ -180,7 +180,6 @@ mod test { let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id, - block_info: &BlockInfo::default(), state_transition_execution_context: &mut transition_execution_context, transaction: None, }; @@ -302,7 +301,6 @@ mod test { let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id: &owner_id, - block_info: &BlockInfo::default(), state_transition_execution_context: &mut transition_execution_context, transaction: None, }; @@ -421,7 +419,6 @@ mod test { let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id: &owner_id, - block_info: &BlockInfo::default(), state_transition_execution_context: &mut transition_execution_context, transaction: None, }; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs index bf76a40d7ad..233ae341767 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs @@ -12,6 +12,7 @@ use crate::execution::types::state_transition_execution_context::StateTransition use crate::execution::validation::state_transition::batch::data_triggers::{ DataTriggerExecutionContext, DataTriggerExecutionResult, }; +use crate::platform_types::platform_state::PlatformStateV0Methods; use dpp::consensus::state::data_trigger::data_trigger_condition_error::DataTriggerConditionError; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; @@ -68,7 +69,7 @@ pub(super) fn create_contact_request_data_trigger_v1( let (to_identity, balance_fee_result) = context.platform.drive.fetch_identity_balance_with_costs( to_user_id.to_buffer(), - context.block_info, + context.platform.state.last_block_info(), true, context.transaction, platform_version, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs index 61afdaa5794..54319998d39 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs @@ -442,7 +442,6 @@ mod test { let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id: &owner_id, - block_info: &BlockInfo::default(), state_transition_execution_context: &mut transition_execution_context, transaction: None, }; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs index 388ccb52b71..7c2f2075c5d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs @@ -26,6 +26,7 @@ use dpp::version::PlatformVersion; use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperator}; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; +use crate::platform_types::platform_state::PlatformStateV0Methods; pub const MAX_PRINTABLE_DOMAIN_NAME_LENGTH: usize = 253; @@ -247,7 +248,7 @@ pub(super) fn create_domain_data_trigger_v1( // it via add_operation on the outer execution_context. let parent_domain_outcome = context.platform.drive.query_documents( drive_query, - Some(&context.block_info.epoch), + Some(context.platform.state.last_committed_block_epoch_ref()), is_dry_run, context.transaction, Some(platform_version.protocol_version), @@ -343,7 +344,7 @@ pub(super) fn create_domain_data_trigger_v1( // Same pattern as the parent-domain query above — bill the cost. let preorder_outcome = context.platform.drive.query_documents( drive_query, - Some(&context.block_info.epoch), + Some(context.platform.state.last_committed_block_epoch_ref()), is_dry_run, context.transaction, Some(platform_version.protocol_version), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs index 419d253b7af..30ed9788c49 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs @@ -186,7 +186,6 @@ mod tests { let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id: &owner_id, - block_info: &BlockInfo::default(), state_transition_execution_context: &mut state_transition_execution_context_outer, transaction: None, }; @@ -324,7 +323,6 @@ mod tests { let mut data_trigger_context = DataTriggerExecutionContext { platform: &platform_ref, owner_id: &owner_id, - block_info: &BlockInfo::default(), state_transition_execution_context: &mut transition_execution_context, transaction: None, }; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs index 260eb053f00..0537aec58b8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs @@ -13,6 +13,7 @@ use crate::execution::types::state_transition_execution_context::StateTransition use crate::execution::validation::state_transition::batch::data_triggers::{ DataTriggerExecutionContext, DataTriggerExecutionResult, }; +use crate::platform_types::platform_state::PlatformStateV0Methods; use dpp::consensus::state::data_trigger::data_trigger_condition_error::DataTriggerConditionError; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::document::DocumentV0Getters; @@ -77,7 +78,7 @@ pub(super) fn delete_withdrawal_data_trigger_v1( // add_operation on the outer execution_context. let withdrawals_outcome = context.platform.drive.query_documents( drive_query, - Some(&context.block_info.epoch), + Some(context.platform.state.last_committed_block_epoch_ref()), false, context.transaction, Some(platform_version.protocol_version), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index 2e2dbb28f9d..5695ee104a3 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -280,7 +280,6 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { platform, transaction, owner_id: &owner_id_value, - block_info, state_transition_execution_context: execution_context, }; let data_trigger_execution_result = document_transition From 9fa24e1a336e10a164b5c52dd3e485c38ee49575 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 20 May 2026 01:43:44 +0700 Subject: [PATCH 16/20] docs(drive-abci): add PV11 consensus-safety comments at modified _v0 sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../bindings/data_trigger_binding/v0/mod.rs | 5 +++ .../data_triggers/triggers/dashpay/v0/mod.rs | 3 ++ .../data_triggers/triggers/dpns/v0/mod.rs | 7 ++++ .../triggers/withdrawals/v0/mod.rs | 3 ++ .../batch/state/v0/fetch_documents.rs | 17 ++++++++++ .../state_transitions/batch/state/v0/mod.rs | 33 ++++++++++++++++--- .../batch/transformer/v0/mod.rs | 13 ++++++++ 7 files changed, 76 insertions(+), 5 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs index 2e0d0293eaf..8b722617c84 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/bindings/data_trigger_binding/v0/mod.rs @@ -73,6 +73,11 @@ pub trait DataTriggerBindingV0Getters { } impl DataTriggerBindingV0Getters for DataTriggerBindingV0 { + // PROTOCOL_VERSION_11 consensus-safety: `execute` now takes + // `&mut DataTriggerExecutionContext` instead of `&...` so that + // `_v1` triggers can call `add_operation` on the context. On PV11 + // the dispatched trigger is `_v0`, which never mutates the + // context, so the chain state is identical to pre-PR. fn execute( &self, document_transition: &DocumentTransitionAction, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs index 0276cdd1ae0..5e44257a4cc 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs @@ -30,6 +30,9 @@ use crate::execution::validation::state_transition::batch::data_triggers::{DataT /// # Returns /// /// A `DataTriggerExecutionResult` indicating the success or failure of the trigger execution. +// PROTOCOL_VERSION_11 consensus-safety: body byte-identical to +// v3.1-dev. Only the `context` param type changed from `&` to `&mut` +// (compile-time only — the body never mutates the context). #[inline(always)] pub(super) fn create_contact_request_data_trigger_v0( document_transition: &DocumentTransitionAction, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs index 54319998d39..18f3c15e1b6 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs @@ -44,6 +44,13 @@ pub const MAX_PRINTABLE_DOMAIN_NAME_LENGTH: usize = 253; /// # Returns /// /// A `DataTriggerExecutionResult` indicating the success or failure of the trigger execution. +// PROTOCOL_VERSION_11 consensus-safety: the body of this function is +// byte-identical to v3.1-dev. The only signature change is the +// `context` parameter: pre-PR `&DataTriggerExecutionContext`, now +// `&mut DataTriggerExecutionContext` (required by the new +// `DataTrigger` fn type that `_v1` triggers need). The body never +// mutates the context (no `add_operation` calls, only reads via +// `in_dry_run()` and field accesses), so PV11 behavior is identical. #[inline(always)] pub(super) fn create_domain_data_trigger_v0( document_transition: &DocumentTransitionAction, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs index 30ed9788c49..dc63e02c311 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs @@ -33,6 +33,9 @@ use crate::execution::validation::state_transition::batch::data_triggers::{DataT /// # Returns /// /// A `DataTriggerExecutionResult` indicating the success or failure of the trigger execution. +// PROTOCOL_VERSION_11 consensus-safety: body byte-identical to +// v3.1-dev. Only the `context` param type changed from `&` to `&mut` +// (compile-time only — the body never mutates the context). #[inline(always)] pub(super) fn delete_withdrawal_data_trigger_v0( document_transition: &DocumentTransitionAction, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs index 822b7624ba8..2d585a8bd28 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs @@ -26,6 +26,13 @@ use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperat /// `DriveAbciDocumentsStateTransitionValidationVersions` (`0` discards /// the cost for PROTOCOL_VERSION_11 chain replay, `1` bills it). /// +/// PROTOCOL_VERSION_11 consensus-safety: the function signature +/// changed from pre-PR (added `epoch: &Epoch`, return type now a +/// tuple) but the DOCUMENTS returned are unchanged — `query_documents` +/// is epoch-independent for the documents/skipped fields, only the +/// `cost` field varies. The cost is discarded on `transform_into_action: 0` +/// at the caller, so net PV11 user-visible behavior matches pre-PR. +/// /// `query_documents` only computes a non-zero cost when an `Epoch` is /// provided; the legacy `None` epoch resulted in a hard-coded zero cost /// that was discarded anyway. @@ -105,6 +112,16 @@ pub(crate) fn fetch_documents_for_transitions_knowing_contract_and_document_type /// - `1` (PROTOCOL_VERSION_12+): pass `Some(epoch)` so the real grovedb /// cost is computed and returned. Callers bill it via the existing /// `execution_context.add_operation` call site. +/// +/// PROTOCOL_VERSION_11 consensus-safety: signature changed from pre-PR +/// (added `epoch: &Epoch` parameter) but the v0 arm forces epoch=None +/// inside `query_documents`, producing the exact same zero-cost +/// `FeeResult` that pre-PR produced. The DOCUMENT returned is +/// epoch-independent. Callers (`document_create_transition_action`, +/// `document_delete_transition_action`) always called +/// `add_operation(PrecalculatedOperation(fee_result))` pre-PR — that +/// call survives unchanged but receives a zero-cost FeeResult on PV11, +/// same net effect (no fees added). pub(crate) fn fetch_document_with_id( drive: &Drive, contract: &DataContract, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index 5695ee104a3..16c774cf67e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -270,11 +270,34 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { )); } else if platform.config.execution.use_document_triggers { if let BatchedTransitionAction::DocumentAction(document_transition) = &transition { - // Triggers receive a &mut DataTriggerExecutionContext - // wrapping the outer execution_context — `_v1` triggers - // call add_operation on it to bill their drive reads - // directly. `_v0` triggers don't bill (per-trigger - // version field stays at 0 on PROTOCOL_VERSION_11). + // Pre-PR this site allocated a default-initialized local + // `StateTransitionExecutionContext` and passed `&local` to + // the trigger context. The local was dropped on return and + // all of its add_operation calls were silently discarded. + // `_v1` triggers need to actually bill (call add_operation), + // so the trigger context now references the OUTER mutable + // execution_context that the processor threaded in. + // + // PROTOCOL_VERSION_11 consensus-safety: on PV11 the + // per-trigger version fields stay at 0, so wrappers + // dispatch to `_v0` triggers whose bodies are + // byte-identical to v3.1-dev (only their param signature + // gained `&mut`, the body never mutates). _v0 triggers + // do not call `add_operation`, so the outer + // execution_context is read-only from the trigger's + // perspective on PV11 — same chain state as pre-PR. + // + // Non-consensus side-effect on PV11 mempool: the trigger + // now sees the outer ctx's real `dry_run` flag instead of + // the previous-default `false`. During CheckTx with + // `dry_run: true`, _v0 triggers short-circuit their + // `query_documents` (via `query_documents_v0`'s internal + // dry-run guard) and skip the post-query validation. + // Doesn't affect block validation (Validator mode is + // always `dry_run: false`), so chain replay matches + // pre-PR byte-for-byte. Pre-PR also did the query but + // ignored its result during dry-run validation, so the + // net mempool outcome is the same. let owner_id_value = self.owner_id(); let mut data_trigger_execution_context = DataTriggerExecutionContext { platform, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs index 9f3ed2ad032..45cd6efa4e3 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs @@ -508,6 +508,17 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { // Below we also perform state validation for replace and transfer transitions only // other transitions are validated in their validate_state functions // TODO: Think more about this architecture + // + // PROTOCOL_VERSION_11 consensus-safety: this callsite now passes + // `&block_info.epoch` so `query_documents` computes a non-zero + // cost. Pre-PR the function passed `epoch=None` and the cost was + // hard-coded to 0. The DOCUMENTS returned by `query_documents` are + // epoch-independent (see `query_documents_v0`: only the `cost` + // field reads epoch) so the validation outcome on PV11 is + // unchanged. The cost itself only reaches the user's bill when + // we explicitly add it via `add_operation` below, which is gated + // on `transform_into_action: 1` — on v0 we discard the cost, + // identical to pre-PR. let (fetched_documents_validation_result, fetch_documents_fee_result) = fetch_documents_for_transitions_knowing_contract_and_document_type( platform.drive, @@ -531,6 +542,8 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { .batch_state_transition .transform_into_action { + // PROTOCOL_VERSION_11: discard the fee. Same end state as + // pre-PR where the cost was always zero (epoch was None). 0 => {} 1 => { execution_context.add_operation(ValidationOperation::PrecalculatedOperation( From 2564e4e0260c1a64d318a3ffde9201e0be53049b Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 20 May 2026 02:23:57 +0700 Subject: [PATCH 17/20] refactor(drive-abci): fetch_documents helpers bill internally via &mut ctx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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>` (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`. - 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) --- .../state_v0/mod.rs | 12 +- .../state_v1/mod.rs | 7 +- .../state_v0/mod.rs | 8 +- .../batch/state/v0/fetch_documents.rs | 143 ++++++++++-------- .../batch/transformer/v0/mod.rs | 57 ++----- 5 files changed, 111 insertions(+), 116 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v0/mod.rs index b644f4564d8..9da8abd7843 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v0/mod.rs @@ -58,19 +58,23 @@ impl DocumentCreateTransitionActionStateValidationV0 for DocumentCreateTransitio }; // TODO: Use multi get https://github.com/facebook/rocksdb/wiki/MultiGet-Performance - // We should check to see if a document already exists in the state - let (already_existing_document, fee_result) = fetch_document_with_id( + // We should check to see if a document already exists in the state. + // `fetch_document_with_id` bills the query cost internally via + // execution_context on `transform_into_action: 1` (PROTOCOL_VERSION_12+); + // on v0 it forces epoch=None and skips billing — identical to + // the pre-PR pattern where this site explicitly added a + // zero-cost FeeResult. + let already_existing_document = fetch_document_with_id( platform.drive, contract, document_type, self.base().id(), &block_info.epoch, + execution_context, transaction, platform_version, )?; - execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - if already_existing_document.is_some() { return Ok(ConsensusValidationResult::new_with_error( ConsensusError::StateError(StateError::DocumentAlreadyPresentError( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v1/mod.rs index 430d349e7d3..67177456e68 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/state_v1/mod.rs @@ -75,18 +75,19 @@ impl DocumentCreateTransitionActionStateValidationV1 for DocumentCreateTransitio // TODO: Use multi get https://github.com/facebook/rocksdb/wiki/MultiGet-Performance // We should check to see if a document already exists in the state - let (already_existing_document, fee_result) = fetch_document_with_id( + // `fetch_document_with_id` bills the query cost internally via + // execution_context on transform_into_action: 1+. + let already_existing_document = fetch_document_with_id( platform.drive, contract, document_type, self.base().id(), &block_info.epoch, + execution_context, transaction, platform_version, )?; - execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - if already_existing_document.is_some() { return Ok(ConsensusValidationResult::new_with_error( ConsensusError::StateError(StateError::DocumentAlreadyPresentError( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v0/mod.rs index 08fa45db9b4..22a20f50fa7 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_delete_transition_action/state_v0/mod.rs @@ -69,18 +69,20 @@ impl DocumentDeleteTransitionActionStateValidationV0 for DocumentDeleteTransitio }; // TODO: Use multi get https://github.com/facebook/rocksdb/wiki/MultiGet-Performance - let (original_document, fee) = fetch_document_with_id( + // `fetch_document_with_id` bills internally on transform_into_action: 1+. + // PV11 byte-safe: v0 forces epoch=None inside, no add_operation, + // same net effect as pre-PR's explicit zero-fee add_operation call. + let original_document = fetch_document_with_id( platform.drive, contract, document_type, self.base().id(), &block_info.epoch, + execution_context, transaction, platform_version, )?; - execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee)); - let Some(document) = original_document else { return Ok(ConsensusValidationResult::new_with_error( ConsensusError::StateError(StateError::DocumentNotFoundError( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs index 2d585a8bd28..977d1896f64 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs @@ -1,4 +1,8 @@ use crate::error::Error; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, +}; use dpp::block::epoch::Epoch; use dpp::data_contract::document_type::DocumentTypeRef; use dpp::data_contract::DataContract; @@ -19,37 +23,32 @@ use drive::query::drive_contested_document_query::{ }; use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperator}; -/// Returns the fetched documents plus the `FeeResult` for the underlying -/// `query_documents` operation. The caller decides whether to bill the -/// `FeeResult` to the `StateTransitionExecutionContext` — gated by the -/// `transform_into_action` field on -/// `DriveAbciDocumentsStateTransitionValidationVersions` (`0` discards -/// the cost for PROTOCOL_VERSION_11 chain replay, `1` bills it). +/// Fetches the documents and bills the `query_documents` cost directly +/// to the passed-in `execution_context` (gated by `transform_into_action`). /// -/// PROTOCOL_VERSION_11 consensus-safety: the function signature -/// changed from pre-PR (added `epoch: &Epoch`, return type now a -/// tuple) but the DOCUMENTS returned are unchanged — `query_documents` -/// is epoch-independent for the documents/skipped fields, only the -/// `cost` field varies. The cost is discarded on `transform_into_action: 0` -/// at the caller, so net PV11 user-visible behavior matches pre-PR. +/// PROTOCOL_VERSION_11 consensus-safety: +/// - On `transform_into_action: 0` the function passes `epoch=None` to +/// `query_documents` (cost hard-coded to 0) and skips `add_operation`. +/// Identical to pre-PR — pre-PR also passed `None` and never billed. +/// - On `transform_into_action: 1` (PROTOCOL_VERSION_12+) it passes +/// `Some(epoch)` to get the real grovedb cost and adds it via +/// `add_operation`. /// -/// `query_documents` only computes a non-zero cost when an `Epoch` is -/// provided; the legacy `None` epoch resulted in a hard-coded zero cost -/// that was discarded anyway. +/// Either way the DOCUMENTS returned are unchanged — `query_documents` +/// is epoch-independent for the documents/skipped fields, only the +/// `cost` field varies. pub(crate) fn fetch_documents_for_transitions_knowing_contract_and_document_type( drive: &Drive, contract: &DataContract, document_type: DocumentTypeRef, transitions: &[&DocumentTransition], epoch: &Epoch, + execution_context: &mut StateTransitionExecutionContext, transaction: TransactionArg, platform_version: &PlatformVersion, -) -> Result<(ConsensusValidationResult>, FeeResult), Error> { +) -> Result>, Error> { if transitions.is_empty() { - return Ok(( - ConsensusValidationResult::new_with_data(vec![]), - FeeResult::default(), - )); + return Ok(ConsensusValidationResult::new_with_data(vec![])); } let ids: Vec = transitions @@ -79,58 +78,77 @@ pub(crate) fn fetch_documents_for_transitions_knowing_contract_and_document_type block_time_ms: None, }; + let epoch_arg = match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .transform_into_action + { + 0 => None, + 1 => Some(epoch), + version => { + return Err(Error::Execution( + crate::error::execution::ExecutionError::UnknownVersionMismatch { + method: + "fetch_documents_for_transitions_knowing_contract_and_document_type: \ + transform_into_action gate" + .to_string(), + known_versions: vec![0, 1], + received: version, + }, + )); + } + }; + let documents_outcome = drive.query_documents( drive_query, - Some(epoch), + epoch_arg, false, transaction, Some(platform_version.protocol_version), )?; - let fee_result = FeeResult { - storage_fee: 0, - processing_fee: documents_outcome.cost(), - fee_refunds: Default::default(), - removed_bytes_from_system: 0, - }; + // Bill only on v1. On v0 the cost is 0 anyway (epoch was None), but + // we still skip the add_operation call so the v0 path is also a + // syntactic no-op for the execution_context — matching pre-PR. + if epoch_arg.is_some() { + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(FeeResult { + storage_fee: 0, + processing_fee: documents_outcome.cost(), + fee_refunds: Default::default(), + removed_bytes_from_system: 0, + })); + } - Ok(( - ConsensusValidationResult::new_with_data(documents_outcome.documents_owned()), - fee_result, + Ok(ConsensusValidationResult::new_with_data( + documents_outcome.documents_owned(), )) } -/// Returns the document (if any) plus the `FeeResult` for the underlying -/// `query_documents` operation. -/// -/// The cost computation is gated by `transform_into_action` on -/// `DriveAbciDocumentsStateTransitionValidationVersions`: -/// - `0` (PROTOCOL_VERSION_11 and below): pass `epoch=None` to -/// `query_documents`, which hard-codes the cost to 0. The returned -/// `FeeResult` has `processing_fee=0` and callers' `add_operation` -/// becomes a no-op-fee. Byte-identical to pre-PR behavior on v11. -/// - `1` (PROTOCOL_VERSION_12+): pass `Some(epoch)` so the real grovedb -/// cost is computed and returned. Callers bill it via the existing -/// `execution_context.add_operation` call site. +/// Returns the document (if any) and bills the `query_documents` cost +/// directly to the passed-in `execution_context` (gated by +/// `transform_into_action`). /// -/// PROTOCOL_VERSION_11 consensus-safety: signature changed from pre-PR -/// (added `epoch: &Epoch` parameter) but the v0 arm forces epoch=None -/// inside `query_documents`, producing the exact same zero-cost -/// `FeeResult` that pre-PR produced. The DOCUMENT returned is -/// epoch-independent. Callers (`document_create_transition_action`, -/// `document_delete_transition_action`) always called -/// `add_operation(PrecalculatedOperation(fee_result))` pre-PR — that -/// call survives unchanged but receives a zero-cost FeeResult on PV11, -/// same net effect (no fees added). +/// PROTOCOL_VERSION_11 consensus-safety: +/// - On `transform_into_action: 0` the function passes `epoch=None` +/// (cost hard-coded to 0) and skips `add_operation`. Pre-PR called +/// `query_documents` with `None` too and the caller did +/// `add_operation` with a zero `FeeResult` (no-op-fee). Net effect: +/// identical to pre-PR. +/// - On `transform_into_action: 1` (PROTOCOL_VERSION_12+) it passes +/// `Some(epoch)` for the real grovedb cost and adds it via +/// `add_operation`. pub(crate) fn fetch_document_with_id( drive: &Drive, contract: &DataContract, document_type: DocumentTypeRef, id: Identifier, epoch: &Epoch, + execution_context: &mut StateTransitionExecutionContext, transaction: TransactionArg, platform_version: &PlatformVersion, -) -> Result<(Option, FeeResult), Error> { +) -> Result, Error> { let drive_query = DriveDocumentQuery { contract, document_type, @@ -181,18 +199,23 @@ pub(crate) fn fetch_document_with_id( Some(platform_version.protocol_version), )?; - let fee_result = FeeResult { - storage_fee: 0, - processing_fee: documents_outcome.cost(), - fee_refunds: Default::default(), - removed_bytes_from_system: 0, - }; + // Bill only on v1. Same reasoning as + // `fetch_documents_for_transitions_knowing_contract_and_document_type`. + if epoch_arg.is_some() { + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(FeeResult { + storage_fee: 0, + processing_fee: documents_outcome.cost(), + fee_refunds: Default::default(), + removed_bytes_from_system: 0, + })); + } + let mut documents = documents_outcome.documents_owned(); if documents.is_empty() { - Ok((None, fee_result)) + Ok(None) } else { - Ok((Some(documents.remove(0)), fee_result)) + Ok(Some(documents.remove(0))) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs index 45cd6efa4e3..8220f9f6a02 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs @@ -504,64 +504,29 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { .collect::>(); // We fetch documents only for replace and transfer transitions - // since we need them to create transition actions - // Below we also perform state validation for replace and transfer transitions only - // other transitions are validated in their validate_state functions + // since we need them to create transition actions. Below we also + // perform state validation for replace and transfer transitions + // only; other transitions are validated in their validate_state + // functions. // TODO: Think more about this architecture // - // PROTOCOL_VERSION_11 consensus-safety: this callsite now passes - // `&block_info.epoch` so `query_documents` computes a non-zero - // cost. Pre-PR the function passed `epoch=None` and the cost was - // hard-coded to 0. The DOCUMENTS returned by `query_documents` are - // epoch-independent (see `query_documents_v0`: only the `cost` - // field reads epoch) so the validation outcome on PV11 is - // unchanged. The cost itself only reaches the user's bill when - // we explicitly add it via `add_operation` below, which is gated - // on `transform_into_action: 1` — on v0 we discard the cost, - // identical to pre-PR. - let (fetched_documents_validation_result, fetch_documents_fee_result) = + // PROTOCOL_VERSION_11 consensus-safety: the fetch fn now takes + // `&mut execution_context` and bills the query cost internally + // — but only when `transform_into_action: 1`. On v0 it forces + // `epoch=None` (zero cost) and skips `add_operation`, matching + // pre-PR exactly. + let fetched_documents_validation_result = fetch_documents_for_transitions_knowing_contract_and_document_type( platform.drive, data_contract, document_type, replace_and_transfer_transitions.as_slice(), &block_info.epoch, + execution_context, transaction, platform_version, )?; - // Reuse the `transform_into_action` field that already gates whether - // this transformer's execution_context is the outer (threaded) one - // or a dropped-on-return local. On v0 the document-query cost would - // land in the dropped local — billing it would be wasted work, so we - // skip. On v1 the ctx reaches the user's bill, so we bill the cost. - match platform_version - .drive_abci - .validation_and_processing - .state_transitions - .batch_state_transition - .transform_into_action - { - // PROTOCOL_VERSION_11: discard the fee. Same end state as - // pre-PR where the cost was always zero (epoch was None). - 0 => {} - 1 => { - execution_context.add_operation(ValidationOperation::PrecalculatedOperation( - fetch_documents_fee_result, - )); - } - version => { - return Err(Error::Execution( - crate::error::execution::ExecutionError::UnknownVersionMismatch { - method: "documents batch transition: fetch_documents query billing" - .to_string(), - known_versions: vec![0, 1], - received: version, - }, - )); - } - } - if !fetched_documents_validation_result.is_valid() { return Ok(ConsensusValidationResult::new_with_errors( fetched_documents_validation_result.errors, From db1836f24c218d67456d43ef76f9e32fb17e0f2e Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 20 May 2026 02:45:17 +0700 Subject: [PATCH 18/20] test(drive-abci): per-trigger v0 byte-identity + v1 billing assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../data_triggers/triggers/dashpay/v0/mod.rs | 14 ++++++ .../data_triggers/triggers/dpns/v0/mod.rs | 12 ++++++ .../triggers/withdrawals/v0/mod.rs | 43 ++++++++++++++++++- 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs index 5e44257a4cc..32a8ba96602 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v0/mod.rs @@ -446,5 +446,19 @@ mod test { e.message() == format!("Identity {contract_request_to_user_id} doesn't exist") } )); + + // T3 PROTOCOL_VERSION_12+ billing assertion: this test runs at + // `PlatformVersion::latest()` where + // `create_contact_request_data_trigger: 1` dispatches to `_v1`. + // `_v1` must surface the `fetch_identity_balance_with_costs` + // cost via `add_operation`. If a regression drops the + // `add_operation` call in `_v1`, this assertion fails. + let ops = data_trigger_context + .state_transition_execution_context + .operations_slice(); + assert!( + !ops.is_empty(), + "T3: _v1 must add operations to execution_context (caught zero ops)" + ); } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs index 18f3c15e1b6..56edf001a47 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs @@ -462,6 +462,18 @@ mod test { platform_version, ) .expect("the execution result should be returned"); + + // PROTOCOL_VERSION_11 byte-identity assertion: _v0 must NOT add + // any operations to the execution_context. If a future refactor + // accidentally re-introduces billing in _v0, this assertion + // fails and PV11 chain replay would diverge. + assert!( + data_trigger_context + .state_transition_execution_context + .operations_slice() + .is_empty(), + "create_domain_data_trigger_v0 must not add operations (PV11 byte-identity)" + ); assert!(result.is_valid()); } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs index dc63e02c311..3e54b7e8d7b 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs @@ -144,7 +144,9 @@ mod tests { use dpp::version::PlatformVersion; use drive::util::object_size_info::DocumentInfo::DocumentRefInfo; use drive::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; - use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; + use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, + }; use dpp::withdrawal::Pooling; use drive::drive::contract::DataContractFetchInfo; use crate::execution::types::state_transition_execution_context::v0::StateTransitionExecutionContextV0; @@ -344,5 +346,44 @@ mod tests { error.to_string(), "withdrawal deletion is allowed only for COMPLETE statuses" ); + + // PROTOCOL_VERSION_11 byte-identity assertion: _v0 must NOT add + // any operations to the execution_context. Catches any regression + // that accidentally re-introduces billing in _v0. + assert!( + data_trigger_context + .state_transition_execution_context + .operations_slice() + .is_empty(), + "delete_withdrawal_data_trigger_v0 must not add operations (PV11 byte-identity)" + ); + + // T4 PROTOCOL_VERSION_12+ billing assertion: run the same + // scenario through `_v1` directly and verify it DID add an + // operation. Same fixture, different code path — catches the + // regression where `_v1` drops the `add_operation` call. + let mut transition_execution_context_v1 = + StateTransitionExecutionContext::V0(StateTransitionExecutionContextV0::default()); + let mut data_trigger_context_v1 = DataTriggerExecutionContext { + platform: &platform_ref, + owner_id: &owner_id, + state_transition_execution_context: &mut transition_execution_context_v1, + transaction: None, + }; + use super::super::v1::delete_withdrawal_data_trigger_v1; + let result_v1 = delete_withdrawal_data_trigger_v1( + &document_transition, + &mut data_trigger_context_v1, + platform_version, + ) + .expect("the execution result should be returned (v1)"); + assert!(!result_v1.is_valid()); + assert!( + !data_trigger_context_v1 + .state_transition_execution_context + .operations_slice() + .is_empty(), + "T4: delete_withdrawal_data_trigger_v1 must add operations to execution_context" + ); } } From ae932acdd91cb7e36398d35528aabbd455f445d5 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 20 May 2026 22:39:06 +0700 Subject: [PATCH 19/20] docs(drive-abci): explain _v1 diffs from _v0 in trigger files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../data_triggers/triggers/dashpay/v1/mod.rs | 19 ++++++++++++-- .../data_triggers/triggers/dpns/v1/mod.rs | 26 ++++++++++++++++--- .../triggers/withdrawals/v1/mod.rs | 16 ++++++++++-- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs index 233ae341767..4184d8a3cb4 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dashpay/v1/mod.rs @@ -64,8 +64,23 @@ pub(super) fn create_contact_request_data_trigger_v1( return Ok(result); } - // Recipient identity must exist. Bill the balance-fetch cost via - // add_operation on the outer execution_context. + // Diff vs `_v0` (recipient identity existence check): + // + // _v0: `.fetch_identity_balance(to_user_id, transaction, pv)?` + // — returns just the balance (no cost info). No way to + // bill the grovedb read; the explicit `TODO: Calculate fee + // operations` comment marked the gap. + // + // _v1: `.fetch_identity_balance_with_costs(to_user_id, + // block_info, apply=true, transaction, pv)?` — returns + // `(Option, FeeResult)`. Bill the FeeResult via + // `add_operation`. + // + // Why the change: closes T3 from `docs/paid-error-fee-audit.md`. + // Contact-request creates would do a grovedb identity-balance + // lookup for free on PV11. `apply: true` matches `_v0`'s stateful + // query (not the stateless estimated-cost path) so the balance + // value returned is byte-identical to `_v0`. let (to_identity, balance_fee_result) = context.platform.drive.fetch_identity_balance_with_costs( to_user_id.to_buffer(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs index 7c2f2075c5d..520facec949 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs @@ -244,8 +244,24 @@ pub(super) fn create_domain_data_trigger_v1( block_time_ms: None, }; - // Pass Some(epoch) so the real grovedb cost is computed; bill - // it via add_operation on the outer execution_context. + // Diff vs `_v0` (parent-domain query): + // + // _v0: `.query_documents(drive_query, None, is_dry_run, ...)` + // — `epoch=None` short-circuits the cost computation to + // 0 inside `query_documents_v0`. The outcome's documents + // are used; the cost is implicitly zero and discarded. + // Net effect: trigger does the read but never charges + // the user for it. + // + // _v1: `.query_documents(drive_query, Some(epoch), ...)` and + // immediately `add_operation(PrecalculatedOperation(...))` + // with the real cost. The user now pays for the trigger's + // grovedb read on the outer execution_context. + // + // Why the change: closes T1 from `docs/paid-error-fee-audit.md` + // — DPNS subdomain registrations were a free DoS surface on + // PROTOCOL_VERSION_11 because the trigger's parent-domain + // lookup ran on the chain but the user paid nothing for it. let parent_domain_outcome = context.platform.drive.query_documents( drive_query, Some(context.platform.state.last_committed_block_epoch_ref()), @@ -341,7 +357,11 @@ pub(super) fn create_domain_data_trigger_v1( block_time_ms: None, }; - // Same pattern as the parent-domain query above — bill the cost. + // Diff vs `_v0` (preorder query): same change as above. `_v0` + // passes `epoch=None` (zero cost, discarded); `_v1` passes + // `Some(epoch)` and bills via `add_operation`. Closes T2 — the + // preorder lookup runs on every DPNS domain create (not just + // subdomain), so the unbilled cost compounded faster than T1. let preorder_outcome = context.platform.drive.query_documents( drive_query, Some(context.platform.state.last_committed_block_epoch_ref()), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs index 0537aec58b8..c61491f636a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs @@ -74,8 +74,20 @@ pub(super) fn delete_withdrawal_data_trigger_v1( block_time_ms: None, }; - // Pass `Some(epoch)` so the grovedb cost is computed; then bill via - // add_operation on the outer execution_context. + // Diff vs `_v0` (withdrawal-document lookup): + // + // _v0: `.query_documents(drive_query, None, false, ...)` — + // `epoch=None` short-circuits cost to 0. The outcome's + // documents drive the status-check; the cost is discarded. + // + // _v1: `.query_documents(drive_query, Some(epoch), ...)` and + // immediately bill via `add_operation`. + // + // Why the change: closes T4 from `docs/paid-error-fee-audit.md`. + // Every withdrawal-document delete ran a grovedb lookup on PV11 + // that the user didn't pay for. The fetched document is + // epoch-independent so changing `None` to `Some(epoch)` only + // affects the cost field — not the validation outcome. let withdrawals_outcome = context.platform.drive.query_documents( drive_query, Some(context.platform.state.last_committed_block_epoch_ref()), From ab9f6b603988c1f785d7a5f8e469b8aa8b8e1641 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 21 May 2026 01:29:53 +0700 Subject: [PATCH 20/20] refactor(drive-abci): version-facade pattern for fetch_documents helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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, 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`. 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) --- .../batch/state/v0/fetch_documents.rs | 356 +++++++++++++----- .../drive_abci_validation_versions/mod.rs | 11 + .../drive_abci_validation_versions/v1.rs | 2 + .../drive_abci_validation_versions/v2.rs | 2 + .../drive_abci_validation_versions/v3.rs | 2 + .../drive_abci_validation_versions/v4.rs | 2 + .../drive_abci_validation_versions/v5.rs | 2 + .../drive_abci_validation_versions/v6.rs | 2 + .../drive_abci_validation_versions/v7.rs | 2 + .../drive_abci_validation_versions/v8.rs | 7 + 10 files changed, 299 insertions(+), 89 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs index 977d1896f64..97bffb4dd01 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs @@ -23,20 +23,18 @@ use drive::query::drive_contested_document_query::{ }; use drive::query::{DriveDocumentQuery, InternalClauses, WhereClause, WhereOperator}; -/// Fetches the documents and bills the `query_documents` cost directly -/// to the passed-in `execution_context` (gated by `transform_into_action`). -/// -/// PROTOCOL_VERSION_11 consensus-safety: -/// - On `transform_into_action: 0` the function passes `epoch=None` to -/// `query_documents` (cost hard-coded to 0) and skips `add_operation`. -/// Identical to pre-PR — pre-PR also passed `None` and never billed. -/// - On `transform_into_action: 1` (PROTOCOL_VERSION_12+) it passes -/// `Some(epoch)` to get the real grovedb cost and adds it via -/// `add_operation`. +// ============================================================================ +// fetch_documents_for_transitions_knowing_contract_and_document_type +// ============================================================================ + +/// Versioned facade for `fetch_documents_for_transitions_knowing_contract_and_document_type`. /// -/// Either way the DOCUMENTS returned are unchanged — `query_documents` -/// is epoch-independent for the documents/skipped fields, only the -/// `cost` field varies. +/// Dispatches on the per-helper version field on +/// `DriveAbciDocumentsStateTransitionValidationVersions`: +/// - v0 (PROTOCOL_VERSION_11 and below) — byte-identical to pre-PR. +/// `epoch` and `execution_context` arguments are unused. No fees billed. +/// - v1 (PROTOCOL_VERSION_12+) — passes `Some(epoch)` to `query_documents` +/// and bills the cost via `execution_context.add_operation`. pub(crate) fn fetch_documents_for_transitions_knowing_contract_and_document_type( drive: &Drive, contract: &DataContract, @@ -46,6 +44,54 @@ pub(crate) fn fetch_documents_for_transitions_knowing_contract_and_document_type execution_context: &mut StateTransitionExecutionContext, transaction: TransactionArg, platform_version: &PlatformVersion, +) -> Result>, Error> { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .fetch_documents_for_transitions_knowing_contract_and_document_type + { + 0 => fetch_documents_for_transitions_knowing_contract_and_document_type_v0( + drive, + contract, + document_type, + transitions, + transaction, + platform_version, + ), + 1 => fetch_documents_for_transitions_knowing_contract_and_document_type_v1( + drive, + contract, + document_type, + transitions, + epoch, + execution_context, + transaction, + platform_version, + ), + version => Err(Error::Execution( + crate::error::execution::ExecutionError::UnknownVersionMismatch { + method: + "fetch_documents_for_transitions_knowing_contract_and_document_type" + .to_string(), + known_versions: vec![0, 1], + received: version, + }, + )), + } +} + +/// PROTOCOL_VERSION_11 byte-identical implementation. Passes `epoch=None` +/// to `query_documents`, ignores `execution_context`, never bills. +/// Body matches pre-PR (v3.1-dev). +fn fetch_documents_for_transitions_knowing_contract_and_document_type_v0( + drive: &Drive, + contract: &DataContract, + document_type: DocumentTypeRef, + transitions: &[&DocumentTransition], + transaction: TransactionArg, + platform_version: &PlatformVersion, ) -> Result>, Error> { if transitions.is_empty() { return Ok(ConsensusValidationResult::new_with_data(vec![])); @@ -78,67 +124,100 @@ pub(crate) fn fetch_documents_for_transitions_knowing_contract_and_document_type block_time_ms: None, }; - let epoch_arg = match platform_version - .drive_abci - .validation_and_processing - .state_transitions - .batch_state_transition - .transform_into_action - { - 0 => None, - 1 => Some(epoch), - version => { - return Err(Error::Execution( - crate::error::execution::ExecutionError::UnknownVersionMismatch { - method: - "fetch_documents_for_transitions_knowing_contract_and_document_type: \ - transform_into_action gate" - .to_string(), - known_versions: vec![0, 1], - received: version, - }, - )); - } - }; - + // todo: deal with cost of this operation let documents_outcome = drive.query_documents( drive_query, - epoch_arg, + None, false, transaction, Some(platform_version.protocol_version), )?; - // Bill only on v1. On v0 the cost is 0 anyway (epoch was None), but - // we still skip the add_operation call so the v0 path is also a - // syntactic no-op for the execution_context — matching pre-PR. - if epoch_arg.is_some() { - execution_context.add_operation(ValidationOperation::PrecalculatedOperation(FeeResult { - storage_fee: 0, - processing_fee: documents_outcome.cost(), - fee_refunds: Default::default(), - removed_bytes_from_system: 0, - })); + Ok(ConsensusValidationResult::new_with_data( + documents_outcome.documents_owned(), + )) +} + +/// PROTOCOL_VERSION_12+ implementation. Passes `Some(epoch)` to +/// `query_documents` so the real grovedb cost is computed; bills it via +/// `execution_context.add_operation`. Documents returned are +/// epoch-independent — same as v0. +fn fetch_documents_for_transitions_knowing_contract_and_document_type_v1( + drive: &Drive, + contract: &DataContract, + document_type: DocumentTypeRef, + transitions: &[&DocumentTransition], + epoch: &Epoch, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result>, Error> { + if transitions.is_empty() { + return Ok(ConsensusValidationResult::new_with_data(vec![])); } + let ids: Vec = transitions + .iter() + .map(|dt| Value::Identifier(dt.get_id().to_buffer())) + .collect(); + + let drive_query = DriveDocumentQuery { + contract, + document_type, + internal_clauses: InternalClauses { + primary_key_in_clause: Some(WhereClause { + field: "$id".to_string(), + operator: WhereOperator::In, + value: Value::Array(ids), + }), + primary_key_equal_clause: None, + in_clause: None, + range_clause: None, + equal_clauses: Default::default(), + }, + offset: None, + limit: Some(transitions.len() as u16), + order_by: Default::default(), + start_at: None, + start_at_included: false, + block_time_ms: None, + }; + + // Diff vs `_v0`: epoch is `Some(...)` and the cost is billed via + // add_operation on the outer execution_context. + let documents_outcome = drive.query_documents( + drive_query, + Some(epoch), + false, + transaction, + Some(platform_version.protocol_version), + )?; + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(FeeResult { + storage_fee: 0, + processing_fee: documents_outcome.cost(), + fee_refunds: Default::default(), + removed_bytes_from_system: 0, + })); + Ok(ConsensusValidationResult::new_with_data( documents_outcome.documents_owned(), )) } -/// Returns the document (if any) and bills the `query_documents` cost -/// directly to the passed-in `execution_context` (gated by -/// `transform_into_action`). +// ============================================================================ +// fetch_document_with_id +// ============================================================================ + +/// Versioned facade for `fetch_document_with_id`. /// -/// PROTOCOL_VERSION_11 consensus-safety: -/// - On `transform_into_action: 0` the function passes `epoch=None` -/// (cost hard-coded to 0) and skips `add_operation`. Pre-PR called -/// `query_documents` with `None` too and the caller did -/// `add_operation` with a zero `FeeResult` (no-op-fee). Net effect: -/// identical to pre-PR. -/// - On `transform_into_action: 1` (PROTOCOL_VERSION_12+) it passes -/// `Some(epoch)` for the real grovedb cost and adds it via -/// `add_operation`. +/// Dispatches on the per-helper version field on +/// `DriveAbciDocumentsStateTransitionValidationVersions`: +/// - v0 (PROTOCOL_VERSION_11 and below) — byte-identical to pre-PR. +/// Calls `_v0` (returns `(Option, FeeResult)` with a zero-cost +/// FeeResult), adds the zero-fee FeeResult to `execution_context` via +/// `add_operation` — matching the pre-PR caller's behavior exactly. +/// - v1 (PROTOCOL_VERSION_12+) — calls `_v1` which passes `Some(epoch)` +/// for the real grovedb cost and bills internally. pub(crate) fn fetch_document_with_id( drive: &Drive, contract: &DataContract, @@ -149,6 +228,63 @@ pub(crate) fn fetch_document_with_id( transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .fetch_document_with_id + { + 0 => { + // Preserve pre-PR caller semantics: the caller used to call + // `add_operation(PrecalculatedOperation(fee_result))` with + // the (always-zero) FeeResult returned by the old fn. We + // emulate that here so the operations_slice on v0 has the + // same shape as pre-PR. + let (document, fee_result) = fetch_document_with_id_v0( + drive, + contract, + document_type, + id, + transaction, + platform_version, + )?; + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + Ok(document) + } + 1 => fetch_document_with_id_v1( + drive, + contract, + document_type, + id, + epoch, + execution_context, + transaction, + platform_version, + ), + version => Err(Error::Execution( + crate::error::execution::ExecutionError::UnknownVersionMismatch { + method: "fetch_document_with_id".to_string(), + known_versions: vec![0, 1], + received: version, + }, + )), + } +} + +/// PROTOCOL_VERSION_11 byte-identical implementation. Passes `epoch=None` +/// to `query_documents` (cost hard-coded to 0). Returns `(Option, +/// FeeResult)` with `processing_fee=0` — matches pre-PR (v3.1-dev) signature +/// and behavior exactly. +fn fetch_document_with_id_v0( + drive: &Drive, + contract: &DataContract, + document_type: DocumentTypeRef, + id: Identifier, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result<(Option, FeeResult), Error> { let drive_query = DriveDocumentQuery { contract, document_type, @@ -171,44 +307,82 @@ pub(crate) fn fetch_document_with_id( block_time_ms: None, }; - let epoch_arg = match platform_version - .drive_abci - .validation_and_processing - .state_transitions - .batch_state_transition - .transform_into_action - { - 0 => None, - 1 => Some(epoch), - version => { - return Err(Error::Execution( - crate::error::execution::ExecutionError::UnknownVersionMismatch { - method: "fetch_document_with_id: transform_into_action gate".to_string(), - known_versions: vec![0, 1], - received: version, - }, - )); - } - }; - + // todo: deal with cost of this operation let documents_outcome = drive.query_documents( drive_query, - epoch_arg, + None, false, transaction, Some(platform_version.protocol_version), )?; - // Bill only on v1. Same reasoning as - // `fetch_documents_for_transitions_knowing_contract_and_document_type`. - if epoch_arg.is_some() { - execution_context.add_operation(ValidationOperation::PrecalculatedOperation(FeeResult { - storage_fee: 0, - processing_fee: documents_outcome.cost(), - fee_refunds: Default::default(), - removed_bytes_from_system: 0, - })); + let fee = documents_outcome.cost(); + let fee_result = FeeResult { + storage_fee: 0, + processing_fee: fee, + fee_refunds: Default::default(), + removed_bytes_from_system: 0, + }; + let mut documents = documents_outcome.documents_owned(); + + if documents.is_empty() { + Ok((None, fee_result)) + } else { + Ok((Some(documents.remove(0)), fee_result)) } +} + +/// PROTOCOL_VERSION_12+ implementation. Passes `Some(epoch)` to +/// `query_documents` for the real grovedb cost; bills it via +/// `execution_context.add_operation`. Document returned is +/// epoch-independent — same as v0. +fn fetch_document_with_id_v1( + drive: &Drive, + contract: &DataContract, + document_type: DocumentTypeRef, + id: Identifier, + epoch: &Epoch, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result, Error> { + let drive_query = DriveDocumentQuery { + contract, + document_type, + internal_clauses: InternalClauses { + primary_key_in_clause: None, + primary_key_equal_clause: Some(WhereClause { + field: "$id".to_string(), + operator: WhereOperator::Equal, + value: Value::Identifier(id.to_buffer()), + }), + in_clause: None, + range_clause: None, + equal_clauses: Default::default(), + }, + offset: None, + limit: Some(1), + order_by: Default::default(), + start_at: None, + start_at_included: false, + block_time_ms: None, + }; + + // Diff vs `_v0`: epoch is `Some(...)` and the cost is billed via + // add_operation on the outer execution_context. + let documents_outcome = drive.query_documents( + drive_query, + Some(epoch), + false, + transaction, + Some(platform_version.protocol_version), + )?; + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(FeeResult { + storage_fee: 0, + processing_fee: documents_outcome.cost(), + fee_refunds: Default::default(), + removed_bytes_from_system: 0, + })); let mut documents = documents_outcome.documents_owned(); @@ -219,6 +393,10 @@ pub(crate) fn fetch_document_with_id( } } +// ============================================================================ +// has_contested_document_with_document_id — unchanged +// ============================================================================ + pub(crate) fn has_contested_document_with_document_id<'a>( drive: &Drive, contract: &'a DataContract, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index 8b3b18edcae..75a72eb680d 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -142,6 +142,17 @@ pub struct DriveAbciDocumentsStateTransitionValidationVersions { /// /// [`transform_document_transition`]: crate pub failed_per_transition_action: FeatureVersion, + /// Versions the + /// `fetch_documents_for_transitions_knowing_contract_and_document_type` + /// helper. v0 (PROTOCOL_VERSION_11 and below) passes `epoch=None` + /// to `query_documents` and doesn't bill the cost. v1 + /// (PROTOCOL_VERSION_12+) passes `Some(epoch)` and bills via + /// `execution_context.add_operation`. + pub fetch_documents_for_transitions_knowing_contract_and_document_type: FeatureVersion, + /// Versions the `fetch_document_with_id` helper. Same v0 vs v1 + /// semantics as + /// `fetch_documents_for_transitions_knowing_contract_and_document_type`. + pub fetch_document_with_id: FeatureVersion, pub data_triggers: DriveAbciValidationDataTriggerAndBindingVersions, pub is_allowed: FeatureVersion, pub document_create_transition_structure_validation: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs index fce75c16330..1ff25e46d71 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs @@ -107,6 +107,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs index ab2d160f2a3..b50a183fa5e 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs @@ -107,6 +107,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs index c80ed9f6e0d..db1b6eb9c6f 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs @@ -107,6 +107,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs index a986d603a1a..1731709db20 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs @@ -110,6 +110,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs index bb9673de70b..825e41c20df 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs @@ -111,6 +111,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs index 21838220e8f..f0f2307635c 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs @@ -114,6 +114,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs index 5e23882714d..eb2518ff40d 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs @@ -108,6 +108,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = revision: 0, transform_into_action: 0, failed_per_transition_action: 0, + fetch_documents_for_transitions_knowing_contract_and_document_type: 0, + fetch_document_with_id: 0, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions { diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index e805b9ab540..2194b742f31 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -151,6 +151,13 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = // ownership/revision check). v0 stays for chain // reproducibility on PROTOCOL_VERSION_11 and below. failed_per_transition_action: 1, + // PROTOCOL_VERSION_12 (v3.1 hard fork): fetch_documents + // helpers bumped to v1 which bill the grovedb cost of + // their query_documents calls. v0 stays for PV11 chain + // replay (the v0 helpers pass epoch=None and never call + // add_operation — byte-identical to pre-PR behavior). + fetch_documents_for_transitions_knowing_contract_and_document_type: 1, + fetch_document_with_id: 1, data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { bindings: 0, triggers: DriveAbciValidationDataTriggerVersions {