Skip to content

fix: client-initiated PUT requests not cached locally when forwarded to target peer #2010

@sanity

Description

@sanity

Client-initiated PUT requests not cached locally when forwarded to target peer

Problem

When a client publishes a contract update via fdev publish, the local node fails to cache the new state if it determines another peer should be the primary holder. This causes the publishing node to continue serving stale cached state even after successfully initiating a PUT operation.

Steps to Reproduce

  1. Run Freenet node in network mode with at least one peer connection
  2. Publish a contract update via fdev publish for a contract that routes to a remote peer (contract location != local peer location)
  3. Observe PUT operation is forwarded to remote peer
  4. Access the contract via HTTP gateway immediately after publishing

Expected: Local node serves the newly published state
Actual: Local node serves old cached state from before the PUT

Observed Behavior

From logs during cargo make publish-river (contract BcfxyjCH4snaknrBoCiqhYc9UFvmiJvhsp5d4L5DuvRa):

[00:01:18] PUT request initiated by client
[00:01:18] Determined PUT routing target: v6MWKgqHiBMNcGtG (@ 0.923911415153386)
[00:01:18] Forwarding PUT to target peer
[00:01:18] Sending outbound message to peer
[00:02:23] Transaction timed out: 01K8M1WZYG2NCWGQR8JDN0MH01

After timeout, HTTP gateway still serves old state:

[00:02:23] fetched contract state, contract: BcfxyjCH4snaknrBoCiqhYc9UFvmiJvhsp5d4L5DuvRa,
            state_size: 2134337  // OLD STATE

New state size should be: 2218761 bytes

Root Cause

In crates/core/src/operations/put.rs, the request_put() function at lines ~955-1130:

// Find the optimal target for this contract
let target = op_manager
    .ring
    .closest_potentially_caching(&key, [&own_location.peer].as_slice());

if target.is_none() {
    // NO OTHER PEERS: Store locally then broadcast
    let updated_value = put_contract(op_manager, key, value, ...).await?;
    // ... broadcast to subscribers
} else {
    // PEER FOUND: Forward WITHOUT storing locally
    let target_peer = target.unwrap();

    put_op.state = Some(PutState::AwaitingResponse { ... });

    let msg = PutMsg::RequestPut {
        id,
        sender: own_location,
        contract,
        related_contracts,
        value,
        htl,
        target: target_peer,
    };

    // DIRECTLY FORWARDS - NO LOCAL CACHE UPDATE
    op_manager.notify_op_change(NetMessage::from(msg), OpEnum::Put(put_op)).await?;
}

The else branch forwards the PUT without calling put_contract(), so the local node never caches the new state.

Relationship to PR #1996

PR #1996 fixed a similar issue for incoming PUT requests (when receiving from network), ensuring upsert_contract_state() persists merged state after validation.

However, that fix only applies to the process_put_request() code path. The request_put() function (client-initiated PUTs) has a separate code path that was not addressed.

Suggested Fix

The request_put() function should always call put_contract() to update the local cache before forwarding to the target peer:

if target.is_none() {
    // No other peers - handle locally
    let updated_value = put_contract(op_manager, key, value, ...).await?;
    // ... broadcast logic
} else {
    // Peer found - cache locally THEN forward
    let target_peer = target.unwrap();

    // NEW: Cache locally first
    let updated_value = put_contract(op_manager, key, value.clone(),
                                     related_contracts.clone(), &contract).await?;

    // THEN forward to target peer
    put_op.state = Some(PutState::AwaitingResponse {
        key,
        upstream: None,
        contract: contract.clone(),
        state: updated_value.clone(),  // Use cached value
        subscribe,
    });

    let msg = PutMsg::RequestPut { ... };
    op_manager.notify_op_change(...).await?;
}

This ensures:

Impact

Affected scenarios:

  • Contract publishing via fdev publish when running in network mode
  • Any client-initiated PUT that routes to a non-local peer
  • Particularly impacts webapp contracts where publishers expect immediate availability

Not affected:

Environment

Additional Context

This was discovered while publishing River webapp updates. The build timestamp embedded in the webapp confirmed the local node was serving stale state even though the PUT operation had been initiated successfully.

Related: #1995, PR #1996

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-networkingArea: Networking, ring protocol, peer discoveryE-mediumExperience needed to fix/implement: Medium / intermediateP-highHigh priorityT-bugType: Something is broken

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions