bsv-overlay-engine 0.1.15

Transaction admission, lookup and GASP synchronization for BSV overlay services.
Documentation
fn memo_page(count: u32) -> (Engine, BudgetPageRemote, std::rc::Rc<MemoryStorage>) {
    let (mut engine, mut remote) = budget_page_engine(count);
    remote.node_cost_ms = 0;
    for node in std::rc::Rc::make_mut(&mut remote.nodes).values_mut() {
        node.proof = None;
    }
    engine.set_gasp_ancestor_fetcher(std::rc::Rc::new(LoadCourier {
        calls: std::rc::Rc::new(Cell::new(0)),
        bounded: false,
    }));
    let store = std::rc::Rc::new(MemoryStorage::new());
    engine.storage = Box::new(store.clone());
    virtual_peer_budget(&mut engine, &remote);
    engine.set_graph_budget(
        std::rc::Rc::new(|_| Box::pin(std::future::pending())),
        100,
        15_000,
    );
    engine.set_gasp_remote_factory(Box::new(remote.clone()));
    (engine, remote, store)
}

/// The gap between two counted absences passed by hand on every memo row
/// still counting (an unanchorable row keeps its own backoff).
async fn memo_gap_passes(store: &MemoryStorage) {
    for memo in store.fault_memos() {
        if memo.unanchorable || memo.dead {
            continue;
        }
        let mut record = memo.into_record();
        record.retry_at = 0;
        store.put_deferred_graph(&record).await.unwrap();
    }
}

/// Engine 0.1.15 (#56): absence strikes on three runs with a gap, so the
/// forty pass on the third (on 0.1.14 the rungs' absence struck at once
/// and they passed on one run, the name this witness had). Every run counts
/// all forty, none is refused, and a run inside the gap spends no request.
#[tokio::test]
async fn memo_0114_forty_absent_roots_pass_on_three_runs_with_a_gap() {
    let (engine, remote, store) = memo_page(40);
    // Run 1 counts all forty on the page; a run inside the gap asks nothing.
    let result = engine.start_gasp_sync().await.unwrap();
    assert_eq!(result.topics_synced["tm_test"].strikes_refused, 0);
    assert_eq!(result.topics_synced["tm_test"].unanchorable_graphs, 0);
    assert_eq!(store.fault_memos().len(), 40);
    assert!(store.fault_memos().iter().all(|r| r.faults == 1));
    let requests = remote.asked.borrow().len();
    engine.start_gasp_sync().await.unwrap();
    assert_eq!(
        remote.asked.borrow().len(),
        requests,
        "a remembered absence inside its gap spends no node request"
    );
    // Later runs walk the due records ahead of the page under the resumes'
    // shared allowance (100 calls), so a run counts what it reaches; none
    // counts one root twice and none is refused.
    let mut struck = 0;
    for run in 2..=8 {
        memo_gap_passes(&store).await;
        let result = engine.start_gasp_sync().await.unwrap();
        let topic = &result.topics_synced["tm_test"];
        assert_eq!(topic.strikes_refused, 0, "{topic:?}");
        assert_eq!(topic.absent_struck, topic.unanchorable_graphs);
        assert!(store
            .fault_memos()
            .iter()
            .all(|r| r.faults <= crate::gasp::UNANCHORABLE_AFTER_FAULTED_RUNS));
        struck += topic.unanchorable_graphs;
        if struck == 40 {
            break;
        }
        assert!(run < 8, "struck {struck} of 40");
    }
    assert_eq!(store.fault_memos().len(), 40);
    assert_eq!(
        store
            .get_last_interaction("https://budget.example", "tm_test")
            .await
            .unwrap(),
        400
    );
    assert!(store.deferred_graphs().is_empty());
    let requests = remote.asked.borrow().len();
    engine.start_gasp_sync().await.unwrap();
    assert_eq!(
        remote.asked.borrow().len(),
        requests,
        "remembered absence spends no node request"
    );
}

#[tokio::test]
async fn memo_0114_walk_admission_does_not_wait_for_a_push() {
    let f = unanchorable_fixture();
    f.serve.set(true);
    f.engine.start_gasp_sync().await.unwrap();
    let root = f.root.trim_end_matches(".0");
    let candidates = f
        .store
        .find_transactions_for_proof_check(100, 1800)
        .await
        .unwrap();
    assert!(
        candidates.iter().any(|c| c.txid == root),
        "a walk-admitted row has no callback coming"
    );
    let courier = StubFetcher {
        proof: Some(single_leaf_bump_hex(root, 901)),
        calls: Cell::new(0),
    };
    let result = f
        .engine
        .complete_missing_proofs_with(&courier, 100, 1800)
        .await
        .unwrap();
    assert!(result.early_gasp > 0);
    assert_eq!(result.completed, 1);
    assert_eq!(courier.calls.get(), 1);
    let stored = f
        .store
        .find_output(root, 0, Some("tm_test"), None, true)
        .await
        .unwrap()
        .unwrap();
    assert!(
        crate::beef_limits::own_proof(stored.beef.as_ref().unwrap(), root)
            .unwrap()
            .is_some()
    );
}

#[tokio::test]
async fn memo_0114_folds_legacy_facts_once_and_keeps_partial_graphs() {
    let (engine, remote, store) = memo_page(40);
    let later = (current_timestamp_ms() / 1000.0) as u64 + 3600;
    for output in remote.outputs.iter() {
        let mut r = bare_record(0, output.score as u64, "unanchorable:not_found", true);
        r.outpoint = format!("{}.0", output.txid);
        r.faults = 1;
        r.retry_at = later;
        r.unanchorable_retries = 2;
        store.seed_legacy_fault(r);
    }
    let result = engine.start_gasp_sync().await.unwrap();
    assert_eq!(result.topics_synced["tm_test"].memo_folded, 40);
    assert!(remote.asked.borrow().is_empty());
    assert_eq!(store.fault_memos().len(), 40);
    assert!(store
        .fault_memos()
        .iter()
        .all(|r| r.retries == 2 && r.next_at == later));
    assert!(store.deferred_graphs().is_empty());
    let result = engine.start_gasp_sync().await.unwrap();
    assert_eq!(result.topics_synced["tm_test"].memo_folded, 0);
    assert!(remote.asked.borrow().is_empty());
}

#[tokio::test]
async fn memo_0114_bound_evicts_old_facts_without_refusing_strikes() {
    let store = MemoryStorage::new();
    let limit = crate::fault_memo::FAULT_MEMO_MAX_ROWS;
    for n in 0..limit + 2 {
        let mut r = bare_record(n as u32, n as u64, "unanchorable:not_found", true);
        r.faults = 1;
        assert_eq!(
            store.put_deferred_graph(&r).await.unwrap(),
            crate::gasp::DeferredGraphSave::Saved
        );
    }
    assert_eq!(store.fault_memos().len(), limit);
    assert!(store.deferred_graphs().is_empty());
    let old = bare_record(0, 0, "", false);
    assert!(store
        .get_deferred_graph(&old.peer, &old.topic, &old.outpoint)
        .await
        .unwrap()
        .is_none());
    let due = store
        .read_fault_memos(&old.peer, &old.topic, None)
        .await
        .unwrap();
    assert_eq!(due.records.len(), crate::fault_memo::FAULT_MEMO_DUE_LIMIT);
}

#[tokio::test]
async fn memo_0114_full_partial_store_does_not_limit_fault_facts() {
    let (engine, _, store) = memo_page(40);
    store.set_deferred_graph_ceiling(Some(0));
    let result = engine.start_gasp_sync().await.unwrap();
    // Engine 0.1.15 (#56): one run counts forty absences, not forty strikes
    // (`unanchorable_graphs` 40 on 0.1.14); the facts are the memo's.
    assert_eq!(store.fault_memos().len(), 40);
    assert_eq!(result.topics_synced["tm_test"].strikes_refused, 0);
}