Skip to content

feat: read-only contract inputs (integrate fuel-vm #1014)#3317

Draft
xgreenx wants to merge 3 commits into
masterfrom
feat/read-only-contract-inputs
Draft

feat: read-only contract inputs (integrate fuel-vm #1014)#3317
xgreenx wants to merge 3 commits into
masterfrom
feat/read-only-contract-inputs

Conversation

@xgreenx

@xgreenx xgreenx commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Linked Issues/PRs

Depends on fuel-vm #1014 (read-only contract inputs) — not yet merged. This PR cannot merge until that lands and is released.

Description

Integrates fuel-vm's read-only contract inputs into fuel-core. A contract input without a matching contract output is now valid and denotes a read-only contract access: the VM executes the contract's code and reads its state/balances but panics ContractIsReadOnly on any mutation. Because a read-only contract's state/balance roots cannot change, no output slot is needed to commit new roots.

Support is automatic with the new fuel-vm — there is no consensus parameter or chain-config flag to enable, and no fuel-core API surface change. Activation is coordinated by node/VM version.

⚠️ Draft — opened for review, not ready to merge. It carries a temporary [patch.crates-io] and depends on an unmerged fuel-vm PR (see Open items).

Executor audit (the substance)

fuel-core master's executor is already structurally correct for read-only inputs, so no executor logic changed. Every root-writing / output-persisting path is driven by the output list, not the input list, so a read-only input (which has no output) is simply never visited:

  • compute_state_of_not_utxo_outputs — iterates outputs, matches Output::Contract, then looks up inputs.get(output.input_index) to recompute the balance/state hashes. A read-only contract has no Output::Contract, so no root is computed or rewritten for it.
  • persist_output_utxos — same output-driven shape, so its ContractsLatestUtxo pointer is never moved.
  • compute_inputs — the only input-driven path; it writes malleable fields from storage but touches no contract storage, so it's harmless for read-only inputs.

The proven invariant is therefore: a read-only contract's persisted storage is untouched and no output root is committed for it. (Note: fuel-core does not preserve a caller-supplied input root — compute_inputs overwrites the input's root fields every block, and those fields are access-list commitments, not full contract merkle roots. The test asserts the persisted ContractsLatestUtxo is unchanged, which is the real executor-level guarantee.)

Changes

Net diff: 5 files, ~253 insertions — mostly tests. No GraphQL / schema / chain-config changes are needed, because there is no new consensus parameter to expose.

  • Temporary [patch.crates-io] pointing the fuel-vm family at the PR branch (see Open items).
  • Executor tests (below).
  • Changelog entries.

Tests

Executor unit tests (crates/fuel-core/src/executor.rs):

  • read_only_contract_input_executes_and_does_not_rewrite_root — an input-without-output CALL to a read-only contract succeeds, produces no Output::Contract, and ContractsLatestUtxo is unchanged.
  • read_only_contract_input_write_panics_and_preserves_state — a script writing to the read-only contract reverts with a ContractIsReadOnly panic; state untouched.

End-to-end integration tests (tests/tests/read_only_contract.rs) — driven through a running node via the client API:

  • read_only_contract_input__can_read_contract_state — a contract input without an output reads the contract's state and returns the stored value.
  • read_only_contract_input__can_be_read_repeatedly — the same read-only access repeats, always returning the same value (nothing mutated).
  • read_only_contract_input__rejects_state_write — a call whose contract code writes (SWW) to the read-only contract is rejected with a ContractIsReadOnly panic receipt.
  • contract_input_with_output_still_allows_state_write — the normal read-write path (contract input WITH output) still works.

All pass; the pre-existing contracts_balance_and_state_roots_* suite (7 tests) still passes unchanged.

Open items (why this is a draft)

  • Temporary [patch.crates-io] redirects the 9 fuel-vm-family crates to the fuel-vm PR branch feat/read-only-contract-inputs. Must be removed and fuel-vm-private bumped to a released version once fuel-vm Restarting node causes InvalidDatabaseVersion error #1014 merges.
  • Blocked on fuel-vm Restarting node causes InvalidDatabaseVersion error #1014 merge + release.
  • Not exercised: WASM/state-transition executor and the on-chain consensus-parameters upgrade path (audited as no-code-change; the executor takes an opaque ConsensusParameters and the validity change rides the fuel-vm bump).
  • Pre-existing, unrelated: the ReqwestExt starwars doctest fails to compile on master (CWD-relative schema path) — not touched here, flagged so it isn't mistaken for a regression.

Checklist

  • Breaking changes are clearly marked as such in the PR description and changelog
  • New behavior is reflected in tests
  • The specification matches the implemented behavior (fuel-specs update tracked on the fuel-vm PR)

Before requesting review

  • I have reviewed the code myself
  • I have created follow-up issues caused by this PR and linked them here

After merging, notify other teams

🤖 Generated with Claude Code

@cursor

cursor Bot commented Jul 17, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Consensus-breaking transaction validity change via unpinned git VM patches; coordinated network upgrade required before merge-ready release.

Overview
Integrates fuel-vm PR #1014 so a contract input without a matching contract output is valid and means read-only contract access (reads and execution allowed; mutations panic with ContractIsReadOnly). Behavior comes from the bumped VM—no new fuel-core executor logic, chain config, or GraphQL in this diff.

Adds a temporary [patch.crates-io] redirecting the fuel-vm 0.66.4 crate family to feat/read-only-contract-inputs, plus added/breaking changelog notes that nodes must upgrade together. Executor unit tests and a new read_only_contract integration test module lock in that read-only calls succeed without Output::Contract or ContractsLatestUtxo updates, writes fail with ContractIsReadOnly, and the normal input+output path still mutates.

Reviewed by Cursor Bugbot for commit 04b511d. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4f24aaf. Configure here.

Comment thread crates/client/src/client/schema/chain.rs Outdated
xgreenx and others added 2 commits July 17, 2026 19:02
Point the 9 fuel-vm workspace crates at the read-only-contract-inputs
branch of fuel-vm (PR #1014) via [patch.crates-io]. Only the 0.66.4 tree
is redirected; the legacy 0.56.0 tree is untouched, keeping the Cargo.lock
change minimal.

TEMPORARY: this whole [patch.crates-io] block MUST be removed and the
fuel-vm-private dependency bumped to a released version once fuel-vm #1014
merges and is released. fuel-core cannot merge before then.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Consume fuel-vm's read-only contract inputs (PR #1014): a contract input
without a matching contract output is now valid and denotes a read-only
contract access. Support is automatic with the new fuel-vm version — there
is no consensus parameter or chain-config flag to enable, and no fuel-core
API surface changes.

The executor already handles this correctly with no logic change: every
root-writing / output-persisting path (compute_state_of_not_utxo_outputs,
persist_output_utxos) is driven by the OUTPUT list, so a read-only input
(which has no output) is never visited — no state/balance root is computed
or rewritten for it, and its ContractsLatestUtxo pointer never moves.

Adds executor end-to-end tests:
- a read-only contract input executes and leaves ContractsLatestUtxo
  unchanged (no output / root produced);
- a script that writes to the read-only contract reverts with a
  ContractIsReadOnly panic and preserves state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@xgreenx
xgreenx force-pushed the feat/read-only-contract-inputs branch from 4f24aaf to 3621b3c Compare July 17, 2026 18:03
Drive a running node through the client API to verify read-only contract
access:
- a contract input without a matching contract output can read the
  contract's state and returns the stored value;
- the same read-only access can be repeated (nothing is mutated);
- a call whose contract code writes (SWW) to the read-only contract is
  rejected with a ContractIsReadOnly panic;
- the normal read-write path (contract input WITH output) still works.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant