Skip to content

feat: read-only contract inputs (contract input without contract output) - #1014

Open
xgreenx wants to merge 1 commit into
masterfrom
feat/read-only-contract-inputs
Open

xgreenx wants to merge 1 commit into
masterfrom
feat/read-only-contract-inputs

Conversation

@xgreenx

@xgreenx xgreenx commented Jul 16, 2026 •

Copy link
Copy Markdown
Collaborator

Groundwork for concurrent execution of read-heavy contract transactions (fuel-core's lane-scheduler txpool integration derives Read access for contract inputs without a matching output).

Description

Relaxes transaction validity so that a contract input without the corresponding contract output is valid and denotes a read-only contract access. The contract's code can be executed and its state and balances read, but any modification panics. Since a read-only contract's state root and balance root cannot change, no output slot is needed to commit new roots — which lets block producers/schedulers run transactions that only read a shared contract (oracles, config registries) concurrently. Simulation on read-heavy traffic measured up to ~7x throughput once such inputs exist.

This is automatic on the new version — it is not gated behind a consensus parameter. Every transaction on a node running this version supports read-only contract inputs; activation is coordinated by node/VM version, not by an on-chain flag.

fuel-tx

  • A contract input may now have zero or one associated contract output. Zero = read-only; one = the usual read-write case whose new roots are committed to that output. More than one is still rejected (InputContractAssociatedOutputContract).
  • The output-side check (OutputContractInputIndex) is unchanged: a contract output must still point at a contract input.
  • Input::check / Input::check_without_signature and the TypeScript check_input binding keep their original signatures — no consensus-parameter plumbing.

fuel-vm

  • The interpreter computes read_only_contracts at init: contract inputs with no entry in the input→output index map.
  • Every state/balance mutation path panics with the new PanicReason::ContractIsReadOnly = 0x43 when targeting such a contract:
    • storage writes and clears (SWW, SWWQ, SWRD/SWRI, SUPD/SUPI, SCWQ) — guarded at the two central chokepoints storage_write_slot / storage_clear_slot_range;
    • MINT / BURN with a non-zero amount (zero-amount is a storage no-op and stays allowed);
    • TR / TRO debits and credits;
    • SMO with coins;
    • CALL with forwarded coins (coin-less calls work, so read-only contracts remain fully executable).
  • GTF InputContractOutputIndex panics with InputNotFound for a read-only input (no mapping exists), as the map-based lookup already did.

Audit notes

The VM does not otherwise rely on every contract input having an output: the input→output index map is consumed only by the GTF opcode above, and all ContractsState/ContractsAssets writers route through the guarded paths (the only unguarded writers are Create-tx deployment of the newly created contract and test/diff-revert helpers).

Breaking changes

  • Consensus-breaking transaction-validity change: a contract input without a contract output is now accepted (previously InputContractAssociatedOutputContract). Active for every transaction on this version — not gated behind a consensus parameter.
  • New PanicReason variant ContractIsReadOnly = 0x43 (PanicReason is #[non_exhaustive]).

Checklist

  • Breaking changes are clearly marked as such in the PR description and changelog
  • New behavior is reflected in tests
  • If performance characteristic of an instruction change, update gas costs as well or make a follow-up PR for that (no gas changes; guards are set lookups)
  • The specification matches the implemented behavior (spec update PR needed — follow-up)

Before requesting review

  • I have reviewed the code myself
  • I have created follow-up issues caused by this PR and linked them here (fuel-specs update; fuel-core integration)

After merging, notify other teams

🤖 Generated with Claude Code

@xgreenx
xgreenx requested a review from Voxelot as a code owner July 16, 2026 23:49
@xgreenx
xgreenx force-pushed the feat/read-only-contract-inputs branch from 8fb7e01 to e2f8cb8 Compare July 16, 2026 23:49
@cursor

cursor Bot commented Jul 16, 2026 •

Copy link
Copy Markdown

PR Summary

High Risk
Consensus-breaking transaction validity plus new VM panic paths across storage, balances, transfers, and calls; incorrect read-only classification could allow silent writes or block valid txs.

Overview
Consensus-breaking: A contract input with no matching contract output is now valid (read-only access). Duplicate outputs for the same input are still rejected.

The VM tracks those contracts in read_only_contracts at init and panics with PanicReason::ContractIsReadOnly (0x43) on state/balance mutations: storage writes/clears, non-zero mint/burn, transfers (including read-only destinations), SMO with coins, and CALL with forwarded coins. Zero-amount mint/burn stays allowed; reads and coin-less calls still work. GTF InputContractOutputIndex still fails for read-only inputs (InputNotFound).

Behavior is always on in this version (no consensus flag). Changelog and tests cover validity and VM paths.

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

Relax transaction validity so a contract input without the corresponding
contract output is valid, denoting a READ-ONLY contract access. This is
automatic on this version and is NOT gated behind a consensus parameter:
every transaction on the new version supports it.

Such an input lets the VM execute the contract's code and read its state
and balances, but panics with the new PanicReason::ContractIsReadOnly on
any mutation (storage writes and clears, non-zero MINT/BURN, TR/TRO debits
or credits, SMO with coins, CALL with forwarded coins). Because a read-only
contract's state root and balance root cannot change, no output slot is
needed to commit new roots, so block executors can schedule transactions
that only read a shared contract concurrently.

- fuel-tx: a contract input may now have zero or one associated contract
  output (more than one is still InputContractAssociatedOutputContract).
  Input::check/check_without_signature and the TypeScript check_input
  binding keep their original signatures (no consensus-parameter plumbing).
- fuel-asm: new PanicReason::ContractIsReadOnly = 0x43.
- fuel-vm: interpreter tracks read_only_contracts (contract inputs without
  an output slot) and guards every state/balance mutation path; GTF
  InputContractOutputIndex panics with InputNotFound for read-only inputs
  (no mapping exists).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@xgreenx
xgreenx force-pushed the feat/read-only-contract-inputs branch from e2f8cb8 to 6f91dfa Compare July 17, 2026 17:49
@xgreenx xgreenx changed the title feat: allow read-only contract inputs (contract input without contract output) feat: read-only contract inputs (contract input without contract output) Jul 17, 2026

This branch has not been deployed

No deployments
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