Skip to content

fix(executor): fetch related contracts on update_state requires(missing)#4007

Merged
iduartgomez merged 2 commits into
mainfrom
fix/update-state-requires-related-network
May 2, 2026
Merged

fix(executor): fetch related contracts on update_state requires(missing)#4007
iduartgomez merged 2 commits into
mainfrom
fix/update-state-requires-related-network

Conversation

@iduartgomez

Copy link
Copy Markdown
Collaborator

Summary

PR #4006 added network-fallback for the validate_state RequestRelated branch but missed the parallel update_state requires(missing) path. When a contract's update_state returns UpdateModification::requires(missing), bridged_upsert_contract_state still mapped the Either::Right(missing) outcome straight to MissingRelated without attempting local lookup or network fetch.

This is the actual root cause behind freenet/mail#80: the inbox contract's update_state returns requires(AFT-record) on the receiver because it hasn't seen the sender's AFT yet. The mail UI workaround inline-bundles the related state via RelatedStateAndDelta to dodge the path entirely. After this PR the runtime resolves the related contracts itself (local first, then network via start_sub_op_get when op_manager is wired), and the workaround can be removed.

Fix

Mirror the validate-side flow:

  1. On Either::Right(missing_related), walk each id with local-first / network-second fetch via fetch_related_via_network (introduced in fix(executor): escalate UPDATE related-fetch to network when op_manager is wired #4006).
  2. Append the fetched states as UpdateData::RelatedState entries to the update slice.
  3. Re-attempt the merge.
  4. If the second call still returns requires, reject (depth-1 limit, matching validate-side behavior).

Test plan

  • MockWasmRuntime gets a new UpdateOverride::RequiresRelated to drive the path
  • test_update_state_requires_related_fetches_and_retries pins the happy path: stub records the network call, second update_state succeeds with RelatedState entries, upsert resolves to Ok
  • All 16 pool_tests::related_contract_tests pass locally
  • cargo clippy --all-targets clean
  • contracts.md updated to document both branches
  • CI

E2E

E2E validated against the iso-nodes harness with this binary: alice→bob cross-node delivery completes via the runtime's own resolution path (no UI inline-bundling needed). The mail-side RelatedStateAndDelta workaround is now redundant; will follow up with a mail PR removing it once this lands.

PR #4006 added network-fallback for the validate_state RequestRelated branch but missed the parallel update_state path. When a contract's update_state returns UpdateModification::requires(missing), bridged_upsert_contract_state still mapped Either::Right straight to MissingRelated without attempting local lookup or network fetch. Cross-node UPDATE flows that only need related state at merge time (e.g. inbox.update_state needing the sender's AFT record) bounced through ResyncRequest fallback even when the related contract was reachable via local store or network GET. Surfaced in freenet/mail#80.

Mirror the validate_state path: walk each requires(missing) id with local-first / network-second fetch via fetch_related_via_network, append RelatedState entries to the update slice, re-attempt the merge. Depth limit preserved (a second requires after retry rejects). Mock executor gets a new UpdateOverride::RequiresRelated to drive the path; new test_update_state_requires_related_fetches_and_retries pins the happy path with a stubbed network fetcher.

contracts.md updated to document both branches.
@github-actions

github-actions Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Rule Review: No issues found

Rules checked: git-workflow.md, code-style.md, testing.md, contracts.md
Files reviewed: 5 (runtime.rs, mock_wasm_runtime.rs, related_contract_tests.rs, wasm_conformance_tests.rs, .claude/rules/contracts.md)

Warnings

None.

Info


Notes on what was verified:

  • fix: regression tests present — 5 new tests cover the exact bug (happy path, network failure, depth > 1 rejection, >MAX rejection, self-reference). All boundaries tested. ✓
  • No .unwrap() in production code — the new branch in runtime.rs uses let Some(c) = r.pop() else { return Err(...) } and ? propagation throughout. ✓
  • Abuse-prevention guards complete — empty list, self-reference, >MAX_RELATED_CONTRACTS_PER_REQUEST, and dedup all present in the new update-side branch, matching the validate-side guards. ✓
  • Depth=1 limit enforced — second attempt_state_update that still returns Right is rejected with MissingRelated. ✓
  • contracts.md updated — correctly documents the new update_state requires() fetch-and-retry path in the existing rules file. ✓
  • No fire-and-forget spawns, no unguarded biased;, no retry loops in changed code. ✓

Rule review against .claude/rules/. WARNING findings block merge.

Three changes addressing the rule-review:

1. update_state requires() path now applies the same abuse-prevention guards as the validate-side path: empty-list rejection, self-reference rejection, MAX_RELATED_CONTRACTS_PER_REQUEST cap (10), id dedup. Without these a misbehaving contract could fan out unbounded network GETs by returning an oversized requires() list.

2. Network-fetch failure now logs warn with related_id + error before mapping to MissingRelated. Previously the error was swallowed by Err(_).

3. Three new edge-case tests: requires+network-fail, depth>1 rejection (always-requires override), and too-many cap. Plus self-reference rejection. Mock gets a new UpdateOverride::AlwaysRequiresRelated to drive the depth and oversize paths.
@iduartgomez iduartgomez added this pull request to the merge queue May 2, 2026
Merged via the queue into main with commit 09f081f May 2, 2026
13 checks passed
@iduartgomez iduartgomez deleted the fix/update-state-requires-related-network branch May 2, 2026 19:06
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