fix(drive-abci)!: bill the contract fetch of a fee claim deterministically - #4954
Conversation
…cally The contract fee claim billed its contract read from the fee stored in the cached DataContractFetchInfo. That fee is only there when the cache entry was built with an epoch: the refresh after a contract create or update and the getDocuments handler cache the contract without one. A node holding such an entry billed nothing for the read, a cold node billed it, and their balances and app hashes diverged. Sending getDocuments to some nodes was enough to cause it. Bill the FeeResult the fetch returns, before anything else is checked, as the contract update and the contract user moderation do. A claim on a contract that does not exist becomes a paid refusal that bumps the contract nonce, like every other refusal of the claim. ContractFeeClaim only exists from protocol version 14, which is unreleased, so v0 is edited in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: dashpay/platform/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughContract fee claim validation now records contract fetch fees for cached and absent contracts. Claims on absent contracts return a paid refusal with a nonce-bump action. Tests check the charge and compare billing across cache states. ChangesContract Fee Claim
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to Contract claims bill consistently across the tested cache states, and absent-contract refusals are charged. No identified issue blocks merge after normal checks. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
🕓 Queued for automated review — 10th in line, estimated start in ~1.9 h (commit 10b52d2)
|
…tored fee A cache hit billed the fee its entry carried when it had one. That fee was calculated under the fee schedule active when the contract was cached, and nothing clears the contract cache on a protocol change, so a schedule that changes read costs would bill warm and cold nodes differently. The cache hit now always calculates the fee from the entry's read cost under the active schedule; the write-back that only memoized it is gone. This is consensus-neutral on every shipped version: every stored fee is calculate_fee over the entry's own cost, a read's fee depends on the epoch for nothing, and every shipped schedule (and FEE_VERSION3) shares the storage and processing costs. DataContractFetchInfo::fee becomes crate-private with a corrected doc, so no caller outside Drive can bill it again; tests use has_fee_for_tests. The fee claim tests gain a leg with a fee-carrying cache entry and check the unknown-contract refusal's nonce bump, and the book no longer calls that refusal unpaid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
📖 Book Preview built successfully. Download the preview from the workflow artifacts. Updated at 2026-09-24T00:02:42.649Z |
…tocol change Replaces the previous commit's Drive change, which recalculated every cache hit's fee in a read path all protocol versions share. The fee a cache entry carries can only go stale when the fee schedule changes, and that only happens at a protocol change, so emptying the cache there is enough. perform_events_on_first_block_of_protocol_change v2 clears the contract cache and then runs v1, whose system contract seeding has to come after the clear. Only protocol version 14's method table selects it, so upgrades into 10 to 13 keep running v1. The rs-drive fetch code is back to what it was; DataContractFetchInfo::fee stays crate-private, with a doc that now says when the cache is cleared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Issue being fixed or feature implemented
The contract fee claim (state transition type 25, protocol version 14, unreleased) billed its contract read from the fee stored inside the cached
DataContractFetchInfo:That field is
Someonly when the cache entry was built with an epoch. The cache refresh after a contract create or update, and thegetDocumentsquery handler, both cache the contract withfee: None, and the claim reads withadd_to_cache_if_pulled = false, so a cache hit never writes the fee back. The same claim therefore cost:getDocumentsfor it), andDifferent balances mean different app hashes. Anyone can cause the split, because a
getDocumentsrequest sent to some nodes fills their cache with a fee-less entry.What was done?
contract_fee_claim/state/v0transform_into_action_v0now bills theFeeResultthatget_contract_with_fetch_info_and_feereturns (.0). That fee is computed the same way on every node, whether the contract came from disk or the cache, or does not exist. It is billed before anything else is checked, and a missing fee is aCorruptedCodeExecutionerror. This is the same pattern ascontract_user_moderation/state/v0anddata_contract_update/state/v0.Behaviour change beyond the fee: a claim on a contract that does not exist is now a paid refusal. It bumps the signer's contract nonce and charges the lookup, where before it was an unpaid refusal. Reasons:
The check tx outcome is unchanged: the claim is still refused with 10400.
The contract cache is emptied on the first block of a protocol change (new
perform_events_on_first_block_of_protocol_change_v2, selected by protocol version 14's table). A cache hit bills the fee its entry carries when it has one, calculated under the fee schedule of the protocol version the contract was read in. A protocol change is the only time that schedule can change, but nothing emptied the contract cache then, so a future schedule that changes read costs would bill a node that stayed up the old fee and a node that restarted the new one, for every transition that reads a contract (fee claim, contract update, contract user moderation, key bounds). v2 clears the cache and then runs v1, whose system contract seeding must come after the clear. Upgrades into protocol versions 10 to 13 keep running v1 unchanged.DataContractFetchInfo::feeis crate-private, with its doc corrected: it claimed the cache is cleared on a protocol change, which no code did until v2. No code outside rs-drive can bill it any more. Tests that need to know whether an entry carries a fee usehas_fee_for_tests()(behindfixtures-and-mocks), which returns a bool, not a fee.The book's fee claim table (
book/src/data-model/contract-moderation.md) no longer lists the unknown-contract refusal as unpaid.Before / after
A moderator claims the moderators pot of a contract they moderate. The pot holds 1,000 credits and the claim is processed in epoch 1.
The 62,140 credits between the two "before" rows are the contract read.
A node that read a user contract under protocol version 13 and cached the fee of that read, at the first block of protocol version 14:
A claim on a contract id that does not exist:
In-place changes to shipped generations
contract_fee_claim/state/v0transform_into_action_v0(drive-abci): not shipped.ContractFeeClaimexists only from protocol version 14, which is unreleased.DRIVE_ABCI_METHOD_VERSIONS_V10: not shipped. Only protocol version 14 uses it; itsperform_events_on_first_block_of_protocol_changeslot moves from 1 to the new 2. The shipped v0 and v1 are untouched.How Has This Been Tested?
packages/rs-drive-abci/.../contract_fee_claim/tests.rs:New
should_bill_a_claim_the_same_whether_its_contract_is_cached_or_not. It processes one successful claim four times, each in a transaction that is then dropped, and asserts the fourfee_results are equal. The four cache states:getDocumentsrequest (Platform::query_documents, V0) has filled the committed cache, again fee-less (asserted);drive.cache.data_contracts.clear().Against the previous code this test fails on its first comparison: processing fee 1,635,640 as the create left the cache, against 1,697,780 cold.
should_refuse_a_claim_on_an_unknown_contract_unpaidbecomesshould_refuse_a_claim_on_an_unknown_contract_and_charge_for_the_lookup. It still checks check tx returns 10400. It now expects a paid 10400, checks the signer's balance dropped by exactly the gas charged, and checks the claimant's stored contract nonce for that contract id is the claim's nonce.New
should_empty_the_contract_cache_on_the_first_block_of_protocol_version_14(protocol change dispatcher). On a chain born at 13, a user contract is read with an epoch at 13, which caches it with its fee. Running the protocol change events of v1 leaves that entry cached; running protocol version 14's (v2) leaves the contract in neither cache.cargo test -p drive-abci --lib -- contract_fee_claim data_contract_update contract_user_moderation: 127 pass.cargo test -p drive-abci --lib -- protocol_upgrade contract_fee_claim: 74 pass.cargo test -p drive --lib -- drive::contract cache::: 480 pass.cargo test -p platform-version: pass.cargo clippy -p drive -p drive-abci -p platform-version --lib --tests -- -D warnings: clean.Upgrade strategy tests (
--test strategy_tests upgrade):run_chain_quick_version_upgradeandrun_chain_v12_to_v13_locks_in_before_activationpass. The two that upgrade into 14 (run_chain_v13_to_v14_registers_the_app_connect_contract,run_chain_reopened_drive_at_epoch_boundary_locks_in_the_same_version_as_a_warm_node) fail on v4.2-dev itself: the structure conformance check finds the moderation charters contract'selectedChartervote poll trees carryingEpochOwnedflags where the description expects none. They fail the same way with the cache clear removed.CI's "Rust workspace tests" currently stops at
moderation-charters-contract'sshould_load_the_schema_at_the_latest_platform_version, red on v4.2-dev since feat(platform)!: moderation charters system data contract #4898 and fixed by feat(platform)!: elected moderation teams moderate from their stored charter #4952 and feat(sdk): encryptedFor helpers and moderation charter readers #4953.Breaking Changes
This is a consensus change inside protocol version 14, which is unreleased: the fee claim's fee and the payment of its unknown-contract refusal both change. The contract cache clear runs only on upgrades into protocol version 14 and later. Nothing that has shipped is affected.
DataContractFetchInfo::feeis no longer public. No code outside rs-drive read it after this PR's fee claim change.Checklist:
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull requestFor repository code-owners and collaborators only
🤖 Generated with Claude Code
Summary by CodeRabbit