freenet 0.2.87

Freenet core software
Documentation
//! Tests for the cheap state-change-detector that backs the read-only
//! `summarize_contract_state` / `get_contract_state_delta` caches.
//!
//! The optimization replaces the per-call full-state load + `DefaultHasher`
//! pass (used only to build the summary/delta cache key) with a cheap
//! per-contract change-detector kept in `StateStore`. The detector is
//! invalidated by every state write and repopulated from a freshly-loaded
//! state, so an unchanged contract summarizes/deltas without reloading or
//! rehashing the state, or re-running WASM — while a changed contract is always
//! recomputed.
//!
//! The stake is divergence: a stale summary OR delta served against changed
//! state would hand a peer the wrong bytes. The correctness tests below assert
//! freshness after an update and would FAIL if the detector missed a write.

use either::Either;
use freenet_stdlib::prelude::*;

use crate::contract::executor::mock_wasm_runtime::MockWasmRuntime;
use crate::contract::executor::{ContractExecutor, Executor};
use crate::wasm_runtime::MockStateStorage;

use super::super::mock_runtime::test::create_test_contract as test_contract;

async fn cached_executor(storage: MockStateStorage) -> Executor<MockWasmRuntime, MockStateStorage> {
    Executor::new_mock_wasm("summarize_delta_cache", storage, None, None)
        .await
        .expect("create cached MockWasmRuntime executor")
}

// =========================================================================
// CORRECTNESS (the guard against divergence)
// =========================================================================

/// PUT a contract, summarize, UPDATE it, summarize again → the second summarize
/// MUST reflect the updated state, never the stale cached summary. This fails if
/// the detector misses the write (e.g. if a write path stopped invalidating it).
#[tokio::test(flavor = "current_thread")]
async fn summarize_is_fresh_after_update_not_stale_cache() {
    let storage = MockStateStorage::new();
    let mut exec = cached_executor(storage).await;

    let contract = test_contract(b"summarize_fresh_after_update");
    let key = contract.key();
    let state_a = WrappedState::new(vec![1, 2, 3]);
    let state_b = WrappedState::new(vec![9, 8, 7, 6]);

    // Initial PUT of state A.
    exec.upsert_contract_state(
        key,
        Either::Left(state_a.clone()),
        RelatedContracts::default(),
        Some(contract.clone()),
    )
    .await
    .expect("PUT A");

    let summary_a = exec
        .summarize_contract_state(key)
        .await
        .expect("summarize A");
    assert_eq!(
        summary_a.as_ref(),
        blake3::hash(state_a.as_ref()).as_bytes(),
        "summary of A must be blake3(A)"
    );

    // Repeat on unchanged state: still A (exercises the fast path).
    let summary_a2 = exec
        .summarize_contract_state(key)
        .await
        .expect("summarize A again");
    assert_eq!(summary_a2.as_ref(), summary_a.as_ref());

    // UPDATE to state B.
    exec.upsert_contract_state(
        key,
        Either::Left(state_b.clone()),
        RelatedContracts::default(),
        None,
    )
    .await
    .expect("UPDATE B");

    // Summarize again: MUST be fresh (B), not the stale cached A.
    let summary_b = exec
        .summarize_contract_state(key)
        .await
        .expect("summarize B");
    assert_eq!(
        summary_b.as_ref(),
        blake3::hash(state_b.as_ref()).as_bytes(),
        "summary must reflect updated state B, not stale cached A"
    );
    assert_ne!(
        summary_b.as_ref(),
        summary_a.as_ref(),
        "summary must change after the state changed"
    );
}

/// The delta sibling of the test above: a delta computed against a fixed peer
/// summary MUST be recomputed after the state changes, never served stale.
/// `MockWasmRuntime::get_state_delta` returns the full state as the delta, so a
/// fresh delta equals the new state bytes and a stale one would equal the old.
#[tokio::test(flavor = "current_thread")]
async fn delta_is_fresh_after_update_not_stale_cache() {
    let storage = MockStateStorage::new();
    let mut exec = cached_executor(storage).await;

    let contract = test_contract(b"delta_fresh_after_update");
    let key = contract.key();
    let state_a = WrappedState::new(vec![1, 2, 3]);
    let state_b = WrappedState::new(vec![9, 8, 7, 6]);
    // A fixed peer summary: the same value is used before and after the update so
    // ONLY the state component of the delta cache key changes.
    let peer_summary = StateSummary::from(vec![42u8; 4]);

    exec.upsert_contract_state(
        key,
        Either::Left(state_a.clone()),
        RelatedContracts::default(),
        Some(contract.clone()),
    )
    .await
    .expect("PUT A");

    let delta_a = exec
        .get_contract_state_delta(key, peer_summary.clone())
        .await
        .expect("delta A");
    assert_eq!(
        delta_a.as_ref(),
        state_a.as_ref(),
        "mock delta is the full state A"
    );

    // Repeat on unchanged state + same peer summary: cached, identical delta.
    let delta_a2 = exec
        .get_contract_state_delta(key, peer_summary.clone())
        .await
        .expect("delta A again");
    assert_eq!(delta_a2.as_ref(), delta_a.as_ref());

    // UPDATE to state B.
    exec.upsert_contract_state(
        key,
        Either::Left(state_b.clone()),
        RelatedContracts::default(),
        None,
    )
    .await
    .expect("UPDATE B");

    // Delta against the SAME peer summary: MUST be fresh (B), not stale (A).
    let delta_b = exec
        .get_contract_state_delta(key, peer_summary.clone())
        .await
        .expect("delta B");
    assert_eq!(
        delta_b.as_ref(),
        state_b.as_ref(),
        "delta must reflect updated state B, not stale cached A"
    );
    assert_ne!(
        delta_b.as_ref(),
        delta_a.as_ref(),
        "delta must change after the state changed"
    );
}

// =========================================================================
// OPTIMIZATION (the fast path skips the state load + rehash + WASM)
// =========================================================================

/// On an UNCHANGED contract, repeated summarize must NOT reload the state from
/// storage (and therefore must not re-hash it or re-run WASM). Uses an uncached
/// `StateStore` so every load is observable via `MockStateStorage::get_count`.
#[tokio::test(flavor = "current_thread")]
async fn summarize_unchanged_skips_state_load() {
    let storage = MockStateStorage::new();
    let mut exec = Executor::new_mock_wasm_uncached("summarize_opt", storage.clone())
        .await
        .expect("create uncached executor");

    let contract = test_contract(b"summarize_opt");
    let key = contract.key();
    let state = WrappedState::new(vec![1, 2, 3, 4, 5]);

    exec.upsert_contract_state(
        key,
        Either::Left(state.clone()),
        RelatedContracts::default(),
        Some(contract.clone()),
    )
    .await
    .expect("PUT");

    // First summarize: cold detector → slow path, loads the state.
    let _ = exec
        .summarize_contract_state(key)
        .await
        .expect("summarize 1");
    let gets_after_first = storage.get_count();
    assert!(
        gets_after_first >= 1,
        "the cold first summarize must load the state at least once"
    );

    // Second + third summarize on unchanged state: fast path, NO extra loads.
    let _ = exec
        .summarize_contract_state(key)
        .await
        .expect("summarize 2");
    let _ = exec
        .summarize_contract_state(key)
        .await
        .expect("summarize 3");
    assert_eq!(
        storage.get_count(),
        gets_after_first,
        "repeated unchanged summarize must not reload the state (fast path)"
    );

    // After an UPDATE the detector is invalidated → the next summarize reloads.
    let new_state = WrappedState::new(vec![6, 7, 8, 9, 10, 11]);
    exec.upsert_contract_state(
        key,
        Either::Left(new_state.clone()),
        RelatedContracts::default(),
        None,
    )
    .await
    .expect("UPDATE");
    let gets_before_post_update = storage.get_count();
    let _ = exec
        .summarize_contract_state(key)
        .await
        .expect("summarize after update");
    assert!(
        storage.get_count() > gets_before_post_update,
        "summarize after an update must reload the changed state"
    );
}

/// Delta sibling: repeated delta on an UNCHANGED contract against the same peer
/// summary must NOT reload the state.
#[tokio::test(flavor = "current_thread")]
async fn delta_unchanged_skips_state_load() {
    let storage = MockStateStorage::new();
    let mut exec = Executor::new_mock_wasm_uncached("delta_opt", storage.clone())
        .await
        .expect("create uncached executor");

    let contract = test_contract(b"delta_opt");
    let key = contract.key();
    let state = WrappedState::new(vec![5, 4, 3, 2, 1]);
    let peer_summary = StateSummary::from(vec![7u8; 3]);

    exec.upsert_contract_state(
        key,
        Either::Left(state.clone()),
        RelatedContracts::default(),
        Some(contract.clone()),
    )
    .await
    .expect("PUT");

    let _ = exec
        .get_contract_state_delta(key, peer_summary.clone())
        .await
        .expect("delta 1");
    let gets_after_first = storage.get_count();
    assert!(
        gets_after_first >= 1,
        "cold first delta must load the state"
    );

    let _ = exec
        .get_contract_state_delta(key, peer_summary.clone())
        .await
        .expect("delta 2");
    let _ = exec
        .get_contract_state_delta(key, peer_summary.clone())
        .await
        .expect("delta 3");
    assert_eq!(
        storage.get_count(),
        gets_after_first,
        "repeated unchanged delta must not reload the state (fast path)"
    );

    // After an UPDATE the detector is invalidated → the next delta against the
    // SAME peer summary must reload the changed state (symmetry with
    // `summarize_unchanged_skips_state_load`).
    let new_state = WrappedState::new(vec![9, 8, 7, 6, 5, 4]);
    exec.upsert_contract_state(
        key,
        Either::Left(new_state.clone()),
        RelatedContracts::default(),
        None,
    )
    .await
    .expect("UPDATE");
    let gets_before_post_update = storage.get_count();
    let _ = exec
        .get_contract_state_delta(key, peer_summary.clone())
        .await
        .expect("delta after update");
    assert!(
        storage.get_count() > gets_before_post_update,
        "delta after an update must reload the changed state"
    );
}

// =========================================================================
// EDGE CASES
// =========================================================================

/// State-absent: summarizing a contract with no stored state must error (the
/// #4610 state-presence behavior), never fast-path a phantom summary.
#[tokio::test(flavor = "current_thread")]
async fn summarize_absent_contract_errors() {
    let storage = MockStateStorage::new();
    let mut exec = cached_executor(storage).await;
    let contract = test_contract(b"summarize_absent");
    let key = contract.key();

    let result = exec.summarize_contract_state(key).await;
    assert!(
        result.is_err(),
        "summarize of an absent contract must error, not return a phantom summary"
    );
}

/// Cold caches (simulating a restart or a detector/LRU eviction): a second
/// executor that shares the backing storage but has cold detector + summary
/// caches must recompute the correct summary from the on-disk state, not error
/// or return a phantom.
#[tokio::test(flavor = "current_thread")]
async fn summarize_with_cold_caches_recomputes_correctly() {
    let storage = MockStateStorage::new();
    let contract = test_contract(b"summarize_restart");
    let key = contract.key();
    let state = WrappedState::new(vec![3, 1, 4, 1, 5]);

    // Executor 1 PUTs and summarizes (warming its caches and the backing store).
    let mut e1 = cached_executor(storage.clone()).await;
    e1.upsert_contract_state(
        key,
        Either::Left(state.clone()),
        RelatedContracts::default(),
        Some(contract.clone()),
    )
    .await
    .expect("PUT");
    let s1 = e1
        .summarize_contract_state(key)
        .await
        .expect("summarize e1");
    assert_eq!(s1.as_ref(), blake3::hash(state.as_ref()).as_bytes());

    // Executor 2 shares the backing storage but its detector + summary caches are
    // cold (the StateStore detector is per-StateStore-instance). It must still
    // recompute the correct summary from disk.
    let mut e2 = cached_executor(storage.clone()).await;
    let s2 = e2
        .summarize_contract_state(key)
        .await
        .expect("summarize e2");
    assert_eq!(
        s2.as_ref(),
        blake3::hash(state.as_ref()).as_bytes(),
        "cold-cache executor must recompute the correct summary from disk"
    );
}