Skip to content

feat: add delegate contract PUT, UPDATE, and SUBSCRIBE capabilities (#2829, #2831, #2830)#2885

Closed
sanity wants to merge 9 commits intomainfrom
fix-2829-delegate-put
Closed

feat: add delegate contract PUT, UPDATE, and SUBSCRIBE capabilities (#2829, #2831, #2830)#2885
sanity wants to merge 9 commits intomainfrom
fix-2829-delegate-put

Conversation

@sanity
Copy link
Copy Markdown
Collaborator

@sanity sanity commented Feb 7, 2026

Summary

Implements three delegate-contract capabilities from issue #2827:

  • Delegate capability: Contract PUT #2829 PUT: Delegates can store contract state via PutContractRequest (fire-and-forget, uses upsert_contract_state)
  • Delegate capability: Contract UPDATE #2831 UPDATE: Delegates can update contract state via UpdateContractRequest (fire-and-forget, converts UpdateData to state/delta for upsert)
  • Delegate capability: Contract SUBSCRIBE #2830 SUBSCRIBE (partial): Delegates can request subscription via SubscribeContractRequest. Registration is acknowledged, but notification delivery to the delegate is deferred to Nacho's async delegate v2 API (the current API has no mechanism to push notifications to delegates)

All three follow the same fire-and-forget pattern: delegate emits an outbound request, the runtime passes it to the executor, and the response is sent back as an inbound message.

Status of all #2827 capabilities

Capability Issue Status
GET #2828 Done (merged previously)
PUT #2829 Done (this PR)
UPDATE #2831 Done (this PR)
SUBSCRIBE #2830 Partial (this PR) - types + registration, notification delivery deferred to v2
Register delegate #2832 Nacho's scope
Unregister delegate #2833 Nacho's scope

Dependencies

Requires freenet-stdlib PR freenet/freenet-stdlib#48 which adds the new types (UpdateContractRequest, UpdateContractResponse, SubscribeContractRequest, SubscribeContractResponse).

Test plan

  • 17 unit tests in test-delegate-capabilities (GET: 8, PUT: 3, UPDATE: 3, SUBSCRIBE: 3)
  • 18 integration tests in wasm_runtime::delegate::test (existing + new types compile and run)
  • cargo clippy clean
  • cargo fmt clean

[AI-assisted - Claude]

sanity and others added 2 commits February 6, 2026 23:36
Delegates can now emit PutContractRequest to store contracts via the
runtime. The request is handled fire-and-forget: the runtime upserts
the contract state and sends a PutContractResponse back to the delegate.

Changes:
- delegate.rs: Handle PutContractRequest in process_outbound() and
  PutContractResponse in inbound_app_message()
- contract/mod.rs: Extend handle_delegate_with_contract_requests()
  to process PutContractRequest via upsert_contract_state()
- test-delegate-capabilities: Add PutContractState command, response
  handling, and unit tests

Requires freenet-stdlib feat-delegate-contract-put branch which adds
PutContractRequest/PutContractResponse types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…2830)

Extends PR #2885 with two more delegate-contract capabilities:
- UPDATE: delegates can update contract state via UpdateContractRequest
  (fire-and-forget, converts UpdateData to state/delta for upsert)
- SUBSCRIBE: delegates can request subscription via SubscribeContractRequest
  (registration acknowledged, notification delivery deferred to async v2 API)

Both follow the same pattern as GET/PUT: outbound request from delegate,
runtime passes to executor, response sent back as inbound message.

Adds 6 new unit tests in test-delegate-capabilities covering request
emission and response handling (success + error) for both capabilities.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sanity sanity changed the title feat: add delegate contract PUT capability (#2829) feat: add delegate contract PUT, UPDATE, and SUBSCRIBE capabilities (#2829, #2831, #2830) Feb 7, 2026
- Collapse 6 identical match arms in inbound_app_message into unified loop
- Extract DelegateState::for_app() and app_id() helpers in test delegate
- Extract build_contract_response() shared helper for PUT/UPDATE/SUBSCRIBE

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sanity sanity requested a review from iduartgomez February 7, 2026 16:10
- SUBSCRIBE now returns Err (not yet implemented) instead of misleading Ok
- Fix stale error message "GetContractRequest" -> "contract request"
- Add WASM runtime integration tests for PUT, UPDATE, and SUBSCRIBE
  (verifies full round-trip through WASM serialization boundary)
- Document local-only semantics: PUT/UPDATE call upsert_contract_state
  directly and do NOT propagate to the network via operation state machine
- Document which UpdateData variants are supported (State, Delta) and
  why compound variants are rejected (missing related contract context)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

[dependencies]
freenet-stdlib = { version = "0.1.34", features = ["contract"] }
freenet-stdlib = { git = "https://github.com/freenet/freenet-stdlib.git", branch = "feat-delegate-contract-put", features = ["contract"] }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

will have to publish first the changes in stdlib, then update this before merging

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Acknowledged — will update to published crate version once freenet-stdlib PR #48 is merged and released. [AI-assisted - Claude]

Comment thread crates/core/src/contract/mod.rs Outdated
/// 3. Repeats until no more GetContractRequest messages
/// When a delegate emits contract request messages, this function:
/// 1. For GET: Fetches the contract state and sends GetContractResponse back to the delegate
/// 2. For PUT: Upserts the contract state (fire-and-forget, sends PutContractResponse back)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@claude all this operations mutate contract state, are they propagating properly from the contract actor to network ops?

Copy link
Copy Markdown

@claude claude Bot Feb 7, 2026

Choose a reason for hiding this comment

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

Claude finished @iduartgomez's task in 1m 7s —— View job


Analyzing state propagation

Todo List

  • Read the changed contract/mod.rs file to understand delegate operation implementation
  • Trace how upsert_contract_state works
  • Check if state mutations propagate to network operations
  • Provide analysis and answer

Analysis

I've traced through the code to understand how delegate operations (PUT, UPDATE) propagate state changes to the network. Here's what I found:

Current Implementation

The delegate operations in this PR call upsert_contract_state directly (lines 164-172 for PUT, lines 297-305 for UPDATE in crates/core/src/contract/mod.rs). This function:

  1. Stores state locally via the state store
  2. Does emit network propagation events - specifically NodeEvent::BroadcastStateChange

Where Network Propagation Happens

Looking at crates/core/src/contract/executor/runtime.rs:

For NEW contracts (PUT) - Line 808-836:

// Notify network peers of new contract state (automatic propagation)
if let Some(op_manager) = &self.op_manager {
    if let Err(err) = op_manager
        .notify_node_event(
            crate::message::NodeEvent::BroadcastStateChange {
                key,
                new_state: incoming_state.clone(),
            },
        )
        .await
    { /* ... */ }
}

For existing contracts (UPDATE) - Line 1762-1778 in attempt_state_update():

// Notify network peers of state change (automatic propagation)
// Echo-back prevention is handled by summary comparison in p2p_protoc
if let Some(op_manager) = &self.op_manager {
    if let Err(err) = op_manager
        .notify_node_event(crate::message::NodeEvent::BroadcastStateChange {
            key: *key,
            new_state: new_state.clone(),
        })
        .await
    { /* ... */ }
}

The Critical Difference

However, there's an important architectural distinction mentioned in the comments at lines 149-152:

"Note: This stores the state locally via upsert_contract_state but does NOT propagate to the network via the PUT operation state machine. Network propagation would require going through the full ContractHandlerEvent::PutQuery path."

What this means:

  • ✅ State IS propagated via BroadcastStateChange events (passive broadcast to peers)
  • ❌ State does NOT go through the full PUT/UPDATE operation state machines

The difference is:

  • Direct upsert path (used by delegates): Emits BroadcastStateChange → peers receive notifications
  • Full operation path (used by normal operations): Goes through the complete operation state machine with retries, peer selection, caching strategies, etc.

Answer

Yes, state mutations ARE propagating to network operations, but through a simplified path:

  1. Delegate operations call upsert_contract_state
  2. upsert_contract_state stores locally and emits BroadcastStateChange events
  3. The OpManager receives these events and broadcasts to network peers

The comments in the code are slightly misleading - they say "does NOT propagate to the network" but what they really mean is "does not use the full operation state machine for propagation." The state changes DO reach the network via BroadcastStateChange events.

Is this correct? Based on the code, yes. The op_manager field is populated in production (it's only None in some test scenarios), so network propagation should work. The main limitation is that it uses a simpler broadcast mechanism rather than the sophisticated peer-selection and retry logic of the full operation state machines.


Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@sanity shjouldn't this propagate throught the network? and use proper operations for that? rev-iew throughtfully this is actually the case and we are not taking shortcuts or hacking it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Traced this thoroughly — it does propagate properly. Both the normal client path and the delegate path converge at upsert_contract_state(), which emits BroadcastStateChange automatically. This event is handled in p2p_protoc.rs:1771+ and creates proper UpdateMsg::BroadcastTo operations sent to network peers.

The earlier "local-only" comments I added were incorrect and are now fixed. See the detailed analysis in the PR-level comment above.

[AI-assisted - Claude]

Copy link
Copy Markdown
Collaborator

@iduartgomez iduartgomez left a comment

Choose a reason for hiding this comment

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

check network propagation from ops origianted by delegates

@iduartgomez
Copy link
Copy Markdown
Collaborator

iduartgomez commented Feb 7, 2026

this needs more tests at the integration level ; (using a real contract+delegate so we can test E2E) either modify freenet-ping-app to use delegates or use river in the 6 peer test to initiate changes and verify them (or both)

@sanity
Copy link
Copy Markdown
Collaborator Author

sanity commented Feb 7, 2026

Re: network propagation concern from @iduartgomez

I traced the propagation path thoroughly and the delegate operations DO propagate to the network correctly. Here's the chain:

  1. Delegate PUT/UPDATE calls executor.upsert_contract_state() — the exact same function called by the normal client PUT/UPDATE path (via ContractHandlerEvent::PutQuery / UpdateQuery)

  2. upsert_contract_state() in executor/runtime.rs:

    • For new contracts: stores state, then emits NodeEvent::BroadcastStateChange (line ~818)
    • For updates: calls attempt_state_update(), which emits BroadcastStateChange when state actually changes (line ~1766)
  3. BroadcastStateChange is handled in p2p_protoc.rs:1771+:

    • Finds broadcast targets via op_manager.get_broadcast_targets_update()
    • Creates proper UpdateMsg::BroadcastTo operations
    • Sends to network peers through the UPDATE operation state machine

The normal client PUT path is: client → PUT operation state machine → ContractHandlerEvent::PutQueryupsert_contract_state()BroadcastStateChange

The delegate PUT path is: delegate → handle_delegate_with_contract_requests()upsert_contract_state()BroadcastStateChange

Both paths converge at upsert_contract_state with the same parameters (including Some(contract) for the contract code). The propagation mechanism is identical.

I had earlier added incorrect comments saying "local-only / does NOT propagate to the network" — those are now fixed to accurately reflect that propagation IS automatic via BroadcastStateChange.

Re: E2E integration tests — working on adding tests with real contract+delegate as requested.

[AI-assisted - Claude]

sanity and others added 2 commits February 7, 2026 13:26
The comments incorrectly stated that PUT/UPDATE from delegates were
"local-only" and didn't propagate to the network. In fact,
upsert_contract_state automatically emits BroadcastStateChange which
propagates updates via the same mechanism as normal client operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add two new integration tests using real WASM modules to verify the
full delegate→contract capability pipeline end-to-end:

- test_delegate_contract_put_and_update: Registers a delegate, has it
  issue PutContractRequest and UpdateContractRequest to a real contract,
  then verifies state via direct GET. Tests the complete flow through
  the WASM runtime, executor, and contract handler.

- test_delegate_contract_get: PUTs a contract directly, then has a
  delegate issue GetContractRequest and verifies the state is returned
  correctly through the delegate response pipeline.

Both tests use test-delegate-capabilities (WASM delegate) and
test-contract-integration (WASM contract with TodoList state model).

Addresses review feedback requesting E2E tests with real
contract+delegate pairs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sanity
Copy link
Copy Markdown
Collaborator Author

sanity commented Feb 7, 2026

Added two E2E integration tests using real WASM modules (test-delegate-capabilities + test-contract-integration) that exercise the full delegate→contract pipeline:

test_delegate_contract_put_and_update — Registers a real delegate, has it issue PutContractRequest and UpdateContractRequest to a real contract (TodoList), then verifies state via direct GET. Covers:

  • Delegate registration → ApplicationMessage → PutContractRequest → upsert_contract_state → PutContractResponse → delegate output
  • Same flow for UpdateContractRequest (UpdateData::State path)
  • State verification via direct client GET

test_delegate_contract_get — PUTs a contract directly, then has a delegate issue GetContractRequest and verifies the correct state is returned through the delegate response pipeline.

Both tests use #[freenet_test] with real nodes, real WASM runtime (not mocks), and real contract validation. All 18 operations tests pass.

[AI-assisted - Claude]

@sanity sanity enabled auto-merge February 7, 2026 23:49
@sanity sanity force-pushed the fix-2829-delegate-put branch from 824b9f2 to 136c999 Compare February 8, 2026 17:22
@sanity sanity closed this Feb 8, 2026
auto-merge was automatically disabled February 8, 2026 17:23

Pull request was closed

@sanity sanity reopened this Feb 8, 2026
The feat-delegate-contract-put branch was merged and 0.1.35 published
to crates.io. Update workspace and test-delegate-capabilities deps
from git branch references to the published crate version. Also fix
clippy clone_on_copy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sanity sanity force-pushed the fix-2829-delegate-put branch from 136c999 to e2fd946 Compare February 8, 2026 17:25
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sanity
Copy link
Copy Markdown
Collaborator Author

sanity commented Feb 8, 2026

Closing to re-create PR — GitHub Actions stopped triggering CI after a close/reopen cycle. [AI-assisted - Claude]

@sanity sanity closed this Feb 8, 2026
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.

2 participants