Skip to main content

Engine

Struct Engine 

Source
pub struct Engine { /* private fields */ }
Expand description

The Overlay Services Engine.

Orchestrates topic managers, lookup services, storage, and advertisements. Does NOT include an HTTP server — that’s the job of overlay-cloudflare.

Implementations§

Source§

impl Engine

Source

pub const SYNC_RESPONSE_DEFAULT_LIMIT: u64 = 500

Page size used by /requestSyncResponse when the (public) caller omits limit. A bounded page is not lossy for a conforming initiator: ours (crate::gasp::GASPSync) keeps requesting pages for as long as the since score cursor advances (gate finding M1 — it must NOT require a “full” page to continue, because this clamp makes a full-by-the-initiator’s-limit page impossible).

Source

pub const SYNC_RESPONSE_MAX_LIMIT: u64 = 1000

Hard cap on a /requestSyncResponse page. Before bsv-low #291 an absent/huge caller-supplied limit meant an unbounded scan of the outputs table on a public route.

Source

pub fn clamp_sync_limit(requested: Option<u64>) -> u64

Clamp the public /requestSyncResponse page size: absent → the default page; anything larger than the cap → the cap. The result is ALWAYS Some-worthy — a sync response is never unbounded (bsv-low #291). Completeness relies on the INITIATOR paging while its since cursor advances (see SYNC_RESPONSE_DEFAULT_LIMIT’s doc) — the responder alone cannot page for it.

Source

pub fn new( managers: HashMap<String, Box<dyn TopicManager>>, lookup_services: HashMap<String, Box<dyn LookupService>>, storage: Box<dyn Storage>, advertiser: Option<Box<dyn Advertiser>>, config: EngineConfig, ) -> Self

Create a new Overlay Services Engine.

Source

pub fn with_chain_tracker( managers: HashMap<String, Box<dyn TopicManager>>, lookup_services: HashMap<String, Box<dyn LookupService>>, storage: Box<dyn Storage>, advertiser: Option<Box<dyn Advertiser>>, broadcaster: Option<Box<dyn Broadcaster>>, chain_tracker: Option<Box<dyn ChainTracker>>, config: EngineConfig, ) -> Self

Create a new Engine with an optional ChainTracker for SPV verification.

Source

pub fn with_all( managers: HashMap<String, Box<dyn TopicManager>>, lookup_services: HashMap<String, Box<dyn LookupService>>, storage: Box<dyn Storage>, advertiser: Option<Box<dyn Advertiser>>, broadcaster: Option<Box<dyn Broadcaster>>, arc_broadcaster: Option<Box<dyn ArcBroadcaster>>, chain_tracker: Option<Box<dyn ChainTracker>>, config: EngineConfig, ) -> Self

Create a new Engine with all optional components.

Source

pub fn set_arc_broadcaster(&mut self, arc: Box<dyn ArcBroadcaster>)

Set the ARC broadcaster for network broadcast to miners.

When set, the Engine broadcasts transactions to ARC during Phase 2 of submit() for CurrentTx submissions.

Source

pub fn set_gasp_remote_factory(&mut self, factory: Box<dyn GASPRemoteFactory>)

Set the GASP remote factory for peer communication during sync.

Platform-specific crates (like overlay-cloudflare) provide an implementation that creates HTTP-based remotes using their native fetch API.

Source

pub fn set_ship_lookup_client(&mut self, client: Box<dyn ShipLookupClient>)

Set the platform lookup transport for network SHIP discovery. Trackers come from EngineConfig::slap_trackers.

Source

pub fn set_ship_bootstrap_peers( &mut self, topic: impl Into<String>, peers: Vec<String>, )

Keep these peers in a topic’s SHIP union even when trackers fail. This leaves the existing SyncTarget serialization unchanged. Such topics run ahead of tracker imports so discovery and sync can proceed without spending the tick on importing discovery records.

Source

pub fn set_ancestor_fetcher(&mut self, fetcher: Rc<dyn AncestorFetcher>)

Set the OPT-IN chain-backed ancestor fetcher for GASP ingest.

OFF BY DEFAULT. When set, a peer’s failure to serve a needed ancestor during GASP ingest falls back to fetching that ancestor’s raw tx from chain (via the supplied fetcher), self-healing the graph and also enabling strict-BEEF finalize so missing ancestry fails loud.

This is a deliberate one-time-migration escape hatch. Production must NOT call this — when unset, GASP ingest behavior is unchanged.

Source

pub fn set_gasp_ancestor_fetcher(&mut self, fetcher: Rc<dyn AncestorFetcher>)

Supply a courier reserved for sync. Completion retains its own budget.

Source

pub fn set_landing_guard(&mut self, guard: LandingGuard)

Set the admission predicate asked BEFORE the door lands a carried predecessor (bsv-low #575, the E1D delta-2 fold, L2; Engine::land_carried). It is asked once per body the landing would submit, before that body’s submit (a dry one included) and before its reads are charged; an Err (the caller’s refusal, or its own read fault) ends the landing with nothing of that body written and the successor’s topic answers “not now”, as on 683dffd. The worker installs its eviction ledger here: an OPEN eviction of the body refuses it. Its read is not one of the submit’s 256 (it is the caller’s store, at most one per landed body). Unset (default): every carried predecessor may land. What the predicate cannot see (a row opened while the landing writes) stays the caller’s to guard after the write (MutationReport::landed_predecessors).

Source

pub fn set_peer_sync_budget(&mut self, sleep: SleepFactory, budget_ms: u64)

Set the per-peer GASP sync budget (bsv-low#302).

sleep is the platform sleep factory (ms → future); budget_ms is the wall-clock slice ONE peer’s sync may consume. A peer exceeding it is dropped (loudly logged, listed in the topic’s errors) and the loop moves on. Unset (default) = unbounded, the pre-#302 behavior.

Progress survives the deadline (bsv-low #552). With a budget set, every graph is submitted AS IT FINALIZES, inside the raced future (the reference submits inside finalizeGraph too), so what was finalized before the deadline stays admitted. At the deadline the cursor is advanced to GASPSync::completed_cursor: past the UTXOs whose graphs were completed, never past the one in flight. That graph is lost whole (nothing of it was finalized) and the next tick walks it again, down to whatever is now in storage.

The deadline never lands inside a transaction’s writes (the lens fold’s HIGH-1). It is cooperative around a finalize submit (Engine::finalize_submit_gate): a deadline that falls due while one transaction is being written waits for that transaction, and the sync is dropped at the boundary. What a dropped tick leaves of the graph being submitted is an ancestors-first prefix of WHOLE transactions, with the cursor below its UTXO. Everywhere else (a request to the peer, the walk, the anchor check) the deadline drops the sync at once, as a budget against a dead peer must. A dropped tick that finalized a graph or moved the cursor is recorded as a SUCCESSFUL attempt for the quarantine count: a peer serving a long bootstrap is not a dead peer. One with no progress is a failed attempt, as before.

The budget bounds the work of a TICK. Without a per-graph budget nothing bounds a GRAPH (parity: the reference has no node cap): one graph whose own walk outlasts the budget is dropped on every tick (deadline_dropped_graphs) and never admitted. With one (Engine::set_graph_budget, bsv-low #555) that walk is deferred with its progress kept, here too, and resumed next tick.

Source

pub fn set_graph_budget( &mut self, sleep: SleepFactory, max_calls: u32, budget_ms: u64, )

Set the per-GRAPH GASP budget and turn DEFERRAL on (bsv-low #555).

A graph whose walk makes max_calls requests (to the peer, or chain fetches) or runs budget_ms in one pass is DEFERRED: its partial walk (the nodes fetched with their proofs, the inputs still pending, the calls spent, its age in passes) is saved as ONE record of the storage (Storage::put_deferred_graph, replaced on every deferral), its UTXO is held below the cursor as a failed one is (the gap guard) and not walked again in the sync (#554), and the sync goes on to the next UTXO and topic. The next sync that is served that UTXO RESUMES the walk from the record, asking only what is pending, under the same budget; a graph so converges over passes and is then completed as any graph is (#551’s anchor check, the finalize submits, e1d’s rules). A walk cut by the per-peer deadline is deferred the same way.

Bounds: a record deferred crate::gasp::DEFERRED_GRAPH_MAX_PASSES times is dropped with its reason and the UTXO fails as before (the gap guard asks again, from the root); a record has no byte bound (bsv-low #585); at most crate::gasp::DEFERRED_GRAPHS_PER_PEER_TOPIC records per (peer, topic). Keep budget_ms below the per-peer budget so that a deep graph leaves the pass time for the UTXOs after it. Defaults offered: crate::gasp::DEFAULT_GRAPH_BUDGET_CALLS, crate::gasp::DEFAULT_GRAPH_BUDGET_MS.

Every resume of one sync shares ONE such budget (the lens fold’s M1), so the deferred UTXOs, served first, leave the pass the rest; a walk that fetched nothing keeps no record and is a failed attempt for the quarantine (H1); a walk that cannot be kept, or whose save faulted, goes on under the per-peer budget alone (L4, the delta fold’s D-L2); a FRESH walk’s error keeps no record (D-M1). With a budget, a sync the peer served work that finalized no graph and moved no cursor is YIELDLESS, and past crate::gasp::PEER_YIELDLESS_SYNCS_ALLOWED in a row AND crate::gasp::PEER_YIELDLESS_SECS_ALLOWED since the streak began each is a failed attempt (D-M2, the delta-2 fold’s D2-M1; the streak is kept by Storage::record_peer_sync_yield, keyed on the peer’s crate::gasp::peer_origin as its quarantine is). A FRESH walk that faults after half its calls or half its time keeps its record (D2-L1); a RESUMED one that faults having appended nothing crate::gasp::DEFERRED_GRAPH_MAX_IDLE_FAULTS passes in a row is dropped (D2-M2). Each record says whether its peer is configured (SyncTarget::Peers), for a storage that reserves part of its ceiling for those (the worker reserves half).

Unset (the default) nothing is deferred and the walk is the one before #555 (parity: the reference has no budget and no deferral). The storage must KEEP records (the four required deferred-graph methods of Storage): over one that refuses every save, every graph past the budget is walked from its root under the per-peer budget alone on every pass, as before #555, so do not set a budget there.

The budget has two more limbs (bsv-low #586), in force with it at their defaults: the bytes one graph may be SERVED in one pass (crate::gasp::DEFAULT_GRAPH_BUDGET_BYTES, a budget per pass only: a record has no byte bound since bsv-low #585 door 4) and the nodes it may APPEND (crate::gasp::DEFAULT_GRAPH_BUDGET_NODES, 64); Engine::set_graph_budget_limbs names others.

Source

pub fn set_graph_budget_limbs(&mut self, max_bytes_fetched: u64, max_nodes: u32)

The bytes and nodes limbs of the per-graph budget (bsv-low #586, zanaadu-v2 #377): a graph whose walk is SERVED max_bytes_fetched bytes in one pass (the hex of each node’s raw transaction and proof, as the peer sends it and a record keeps it, every answer counted), or APPENDS max_nodes nodes, is DEFERRED exactly as at the call budget (reason bytes or nodes): its record saved, its UTXO held below the cursor, the walk resumed by the next pass from what is pending. Never a drop, a refusal or a discarded graph: these are budgets PER PASS, not limits on a graph or a BEEF. A pass always makes one request whatever a node’s size; the resumes of one sync share one such budget, as they share its calls (M1); a walk that cannot be KEPT goes on under the per-peer budget alone (L4), as at the calls. They bound what one pass adds to the pending graph and to its record while the anchor check and the finalize hold it in memory. No effect until Engine::set_graph_budget turns deferral on; the reference has neither. This setter is UNCAPPED, as the worker’s GASP_GRAPH_BUDGET_BYTES var is (bsv-low #585 door 4: the engine keeps no record cap for the limb to sit under). A consumer whose STORAGE caps a record must keep the bytes limb under that cap, or a pass whose record cannot be saved walks afresh (E586-L1).

Source

pub fn set_finalize_submit_budget( &mut self, sleep: SleepFactory, budget_ms: u64, )

Bound ONE transaction’s GASP finalize submit (bsv-low #559, the delta lens’s DELTA-3). That submit is the write section no deadline drops (Engine::finalize_submit_gate), so a storage call or lookup hook that never answers inside it held the per-peer budget and any outer race for as long as it hung. With this set EACH storage call, lookup hook and topic manager call of that submit is raced against one sleep(budget_ms) of the section (the lens fold of 2026-10-07, F2): a call still unanswered when it falls due is dropped alone and is that call’s FAULT, and no call is started after it. The SUBMIT is never dropped: it runs to its end and leaves what a fault leaves (Engine::submit_with_report: nothing of the transaction, or all of it), the transaction did not land, the rest of its graph is not submitted, its UTXO fails and the cursor stays below it. The undo of a faulted submit has one more sleep(budget_ms) of its own, so a peer costs at most its budget plus twice this one. What the backend does with a statement whose caller stopped waiting is its own: a write that lands after its timeout is not seen by this submit.

DEFAULT: none (a native engine has no clock). A caller that sets a per-peer budget should set this too.

Source

pub fn finalize_submit_gate(&self) -> &SubmitGate

The gate around every GASP finalize submit (bsv-low #552, the lens fold’s HIGH-1). start_gasp_sync races each peer against its budget with crate::gasp::race_or_deadline_guarded over this gate. A caller that races start_gasp_sync ITSELF against a deadline (the worker’s scheduled step) must use the same function over the same gate: a plain race_or_deadline there can still drop a finalize submit between its writes.

Source

pub fn set_script_verification(&mut self, enabled: bool)

Turn reference-parity script verification on submit on or off. DEFAULT ON (set in every constructor).

On, every submit outside HistoricalTxNoSpv executes every unproven input’s unlocking script against its source output (and checks every merkle path against the chain tracker), exactly as the reference’s Engine.submit → tx.verify(this.chainTracker) does. Off is an ESCAPE HATCH for an operator who must admit a body the interpreter refuses (a bsv-rs interpreter defect, say) while it is fixed. It is NOT a mode: it restores the pre-2026-09-08 structural check, under which an invalid spend that no broadcaster had yet refused was admitted on BEEF structure alone. Prefer crate::builder::EngineBuilder::with_script_verification.

Source

pub fn script_verification(&self) -> bool

Whether submits execute input scripts, see Engine::set_script_verification. true by default.

Source

pub fn set_walk_budget(&mut self, budget: DoorBudget)

The budget of the submit’s own script walk (bsv-low #592, the owner’s ruling 3a of 2026-10-10). Every submit that walks (every mode but HistoricalTxNoSpv), and the GASP anchor check, read the BEEF through the door’s stream walk ([crate::script_door]) under this budget’s two limbs, the work and the memory, each estimated from the bytes before it is spent. A breach is “the walk could not run” (WalkCouldNotRun, on MutationReport::walk_could_not_run), never a refusal: the submit goes on as under HistoricalTxNoSpv. A library consumer that sets nothing gets DoorBudget::DEFAULT (64 MiB of work, 48 MiB of memory). Prefer crate::builder::EngineBuilder::with_walk_budget.

Source

pub fn walk_budget(&self) -> DoorBudget

The budget of the submit’s own script walk, see Engine::set_walk_budget.

Source

pub async fn verify_scripts_only( &self, beef_bytes: &[u8], subject_txid: &str, ) -> Result<WalkStats, EngineError>

The reference’s tx.verify('scripts only') over a BEEF: every unproven transaction from subject_txid down executes every input’s unlocking script against its source output and obeys the value rule, while a transaction that carries a merkle path is TRUSTED AS-IS — no root is computed and no chain tracker is consulted (the ts-sdk’s own shape when chainTracker === 'scripts only': a proven tx is added to the verified set and the walk stops there).

Built for an admission path whose bar is the NETWORK, not a proof (bsv-low’s broadcast-gated, register row D1): the overlay broadcasts and admits on network evidence, so a lagging tracker must never read as a refused transaction — but a spend the interpreter refuses is one the network will refuse too, and executing it at the door saves the broadcast and names the fault. Independent of Engine::set_script_verification (that switch governs submit; this is an explicit ask) and of Engine::submit’s mode (HistoricalTxNoSpv still skips, as the reference does).

Runs under the engine’s budget (Engine::set_walk_budget, the submit’s own; DoorBudget::DEFAULT unless set: one budget per engine, the E592 lens fold’s NOTE-2): the work is bounded statically before anything executes, and the interpreter’s stack memory limit is the budget’s element limit. The body is read through the streaming reader ([crate::script_door], bsv-low #585): no bound on its size, its transactions or a transaction’s inputs or bytes; the budget’s two limbs are the work and the memory beside the body.

Errors: EngineError::ScriptVerificationFailed is the INTERPRETER’s verdict and the only refusal; EngineError::ScriptWalkInconclusive is a structural fault (a transaction or source missing from the BEEF, a parse fault, the value rule) and EngineError::ScriptWalkOverBudget the door’s own bound (the static budget or the memory limit) — both name whether the SUBJECT was judged before the fault, and a caller with a stronger bar behind it does not refuse on either.

Source

pub async fn verify_scripts_only_under( &self, beef_bytes: &[u8], subject_txid: &str, budget: DoorBudget, ) -> Result<WalkStats, EngineError>

Engine::verify_scripts_only under a budget the caller names (the pins measure the door with a limb lifted; a caller with another platform’s ceiling sets its own).

Source

pub async fn submit_validate_only( &self, tagged_beef: &TaggedBEEF, mode: SubmitMode, ) -> Result<Steak, EngineError>

Validate a tagged BEEF without applying mutations.

Runs Phase 1 (topic validation) and Phase 2 (broadcast) but NOT Phase 3 (storage mutations). Returns the Steak (admittance decisions) that submit() would return.

Used by the onSteakReady pattern to return results to clients before mutations are applied via a queue consumer or ctx.wait_until().

The topic managers are called with dry_run: true (TopicAdmittanceContext::DRY_RUN): nothing is admitted on this call, so a manager that writes on admission leaves no trace of it.

Source

pub async fn submit( &self, tagged_beef: &TaggedBEEF, mode: SubmitMode, ) -> Result<Steak, EngineError>

Submit a transaction for processing by the overlay.

Three phases:

  1. VALIDATE — check topics, dedup, find previous coins, call topic managers
  2. BROADCAST — broadcast to network (if mode is CurrentTx)
  3. MUTATE — mark spent, delete stale, insert new outputs, notify lookup services

Returns a STEAK mapping each topic to its admittance instructions.

Phase-3 faults are reported through Engine::submit_with_report; this convenience keeps the historical signature and only LOGS the report’s summary. A caller that must not ack an admission whose writes did not land (the /submit route — S2) uses the report form.

Source

pub async fn submit_with_report( &self, tagged_beef: &TaggedBEEF, mode: SubmitMode, ) -> Result<(Steak, MutationReport), EngineError>

Engine::submit plus the Phase-3 durability report (S2 queue-durable admission, bsv-low 2026-08-29).

The STEAK is the admission DECISION; the report says whether the decision was DURABLY WRITTEN. They are returned together because they are answered by different things: Phase 1+2 decide, Phase 3 lands, and only the caller knows whether an undurable landing may be acked (never, on the money path — re-queue it instead).

Every write failure is still logged AND still non-fatal to the other topics (a faulted topic does not un-admit its siblings); what changed is that the failure is also VISIBLE, and that the faulted topic is not recorded as applied — so a replay of the same bytes is re-validated and re-written (every backend write is idempotent: INSERT OR IGNORE / OR REPLACE / UPDATE).

What a fault leaves (bsv-low #559 and its lens fold of 2026-10-07): a topic’s previous coins are deleted once EVERY admitted output of the transaction is inserted, and only then. A fault before that (a validation read, the spent mark of a coin the manager retains, an insert) leaves the previous coins held and nothing of the transaction: what did get inserted is taken out again, so the replay is the same judgement and no successor can spend half of it. A fault after it (a lookup notification, the consumed-by update, the applied row) leaves the transaction’s outputs and no previous coin. A fault before any delete was started (the record of the spent coin’s transaction, the read of the coin) is the first case if the coins are read back still held; once a delete was started its fault never undoes, answered or not (it may land yet): the outputs stay beside the stale coin, marked spent. A submit that deletes a stale coin first records that coin’s transaction as applied, so a replay of it after its successor is a dupe. So a non-retaining chain never has two unspent heads and never none, whatever single call faults. The applied row is withheld on ANY fault. And a successor that was judged without the coin an unlanded predecessor has yet to leave is reported as a fault (predecessor_not_landed), not recorded as applied.

A walk that could not run within the engine’s budget is “not now” here (WalkBreachPolicy::NotNow, EngineError::WalkCouldNotRun): never an admission. Engine::submit_with_report_under names the policy.

Source

pub async fn submit_with_report_under( &self, tagged_beef: &TaggedBEEF, mode: SubmitMode, policy: WalkBreachPolicy, ) -> Result<(Steak, MutationReport), EngineError>

Engine::submit_with_report under an explicit WalkBreachPolicy (the E592 lens fold, H1): what a walk that could not run within the budget does. NetworkAccepted is for a caller that holds the network’s accept of these very bytes (the queue’s replay of a gated submission) and for nothing else: it admits a breached body with its scripts unrun.

Source

pub async fn lookup( &self, question: &LookupQuestion, history_selector: Option<HistorySelector>, ) -> Result<LookupAnswer, EngineError>

Answer a lookup query.

Delegates to the appropriate LookupService, then hydrates results with BEEF.

When history_selector is provided, each output is hydrated with its ancestor spend chain (via get_utxo_history) before building the response. This matches the TS Engine behavior when a history selector is configured.

Source

pub async fn lookup_with_txids( &self, question: &LookupQuestion, history_selector: Option<HistorySelector>, ) -> Result<(LookupAnswer, Vec<String>), EngineError>

Same as Engine::lookup, additionally returning each hydrated output’s txid, aligned index-for-index with the OutputList items (empty for pre-formed Answers).

The txid IS the storage primary key the row was just fetched by — handing it to the caller lets the aggregated wire serializer write it directly instead of re-deriving it with a full BEEF parse plus a double-SHA256 per output (bsv-low #289).

Source

pub async fn get_utxo_history( &self, output: &Output, history_selector: Option<HistorySelector>, ) -> Result<Option<Output>, EngineError>

Traverse and return the history of a UTXO.

If no history_selector is provided, returns the output as-is. If a depth selector is provided, includes up to N levels of ancestor transactions by embedding them as source_transactions in the BEEF.

Source

pub async fn list_topic_managers(&self) -> HashMap<String, ServiceMetadata>

Source

pub async fn list_lookup_service_providers( &self, ) -> HashMap<String, ServiceMetadata>

List all registered lookup services with their metadata.

Source

pub async fn get_documentation_for_topic_manager(&self, name: &str) -> String

Get documentation for a specific topic manager.

Source

pub async fn get_documentation_for_lookup_service(&self, name: &str) -> String

Get documentation for a specific lookup service.

Source

pub async fn provide_foreign_sync_response( &self, request: &GASPInitialRequest, topic: &str, ) -> Result<GASPInitialResponse, EngineError>

Respond to a GASP initial sync request.

Returns UTXOs for the given topic since the requested score.

The caller-supplied limit is CLAMPED to Engine::SYNC_RESPONSE_MAX_LIMIT (and defaulted when absent): /requestSyncResponse is a PUBLIC route, and an absent limit used to mean an unbounded scan of the outputs table (bsv-low #291). Bounding is lossless for sync correctness because the initiator (crate::gasp::GASPSync) pages for as long as its since score cursor advances — a truncated page is fetched by its NEXT request within the same run (gate finding M1: pagination must not demand a full page, which this clamp can make impossible).

Source

pub async fn provide_foreign_gasp_node( &self, graph_id: &str, txid: &str, output_index: u32, ) -> Result<GASPNode, EngineError>

Provide a GASPNode for a specific transaction within a graph.

Searches the BEEF tree of the root output (identified by graphID) for the requested txid. If not found in the BEEF tree, recurses through outputsConsumed in storage.

Source

pub async fn handle_new_merkle_proof( &self, txid: &str, proof_hex: &str, block_height: Option<u32>, ) -> Result<(), EngineError>

Handle a new merkle proof for a transaction.

When a transaction gets mined, ARC calls back with the merkle proof. This method:

  1. Finds all outputs for the txid in storage
  2. Parses the BEEF, updates the merkle path in the transaction tree
  3. Serializes updated BEEF back to storage
  4. Recursively updates the consumedBy chain (outputs that spent this one)
  5. Updates blockHeight on the outputs
Source

pub async fn complete_missing_proofs( &self, limit: u64, min_age_secs: u64, ) -> Result<ProofCompletionSummary, EngineError>

Complete missing merkle proofs for stored, confirmed transactions.

The /submit-admitted ingest path stores a proofless BEEF for each transaction (the submitter does not yet have a merkle proof at submit time). When such a transaction later mines, nothing in the overlay re-fetches its proof, so its stored BEEF stays proofless forever — and a frontend that trims inputBEEF to {source tx + its BUMP} for a P2PKH payment-import spend has no BUMP to trim to, hanging the import (#130).

This pass closes that gap from chain (the one blessed server-side WoC use). It:

  1. Pulls a bounded page (limit) of stored (txid, beef) from storage.
  2. Parses each BEEF and keeps only those whose target tx still lacks a merkle proof (Beef::find_txid(txid).has_proof() == false).
  3. For each proofless candidate, fetches its BUMP via the configured AncestorFetcher. If a proof comes back, it calls Self::handle_new_merkle_proof, which stitches the BUMP into the stored BEEF (D1 + R2 mirror), updates output block height, and recurses the consumedBy chain.
  4. Skips (does NOT error) txs the fetcher can’t prove yet (still unconfirmed) — those are simply retried on a later tick.

No-op when no [AncestorFetcher] is configured — production-safe default (the same opt-in switch as GASP ancestor hydration). Bounded by limit (a per-tick budget, like the platform fetcher’s own budget).

min_age_secs is the PUSH-PRIMARY BACKSTOP gate (bsv-low #228 / arcade#259): since the Arcade MINED webhook (/arc-ingest) pushes a verified proof ~150 ms after a tx mines, this poll pass is a BACKSTOP, not the primary source — rows stored less than min_age_secs ago are skipped entirely (their push is still expected). Pass 0 to disable the gate (poll everything — the pre-#228 behaviour, and the degradation mode if pushes stop: an old-enough row is ALWAYS polled, so webhook loss degrades to polling, never to nothing). Unknown-age rows are always eligible (fail-safe — see the storage-trait doc).

Source

pub async fn complete_missing_proofs_with( &self, fetcher: &dyn AncestorFetcher, limit: u64, min_age_secs: u64, ) -> Result<ProofCompletionSummary, EngineError>

Complete stored transactions using the caller’s dedicated allowance. The engine’s other passes retain their configured ancestor courier.

Source

pub fn stitch_proof_into_stored_beef( stored: &[u8], txid: &str, proof: &MerklePath, ) -> Option<Vec<u8>>

Recursively update merkle paths in a transaction’s input tree.

If the transaction’s id matches txid, set its merkle_path. Otherwise, recurse into sourceTransactions of each input. Stitch a verified BUMP for txid into a STORED BEEF, preserving every transaction and every other proof in it (#284).

The old path round-tripped the stored bytes through Transaction::from_beef(bytes, None) -> to_beef(true), and from_beef(_, None) picks txs.last() — it ignores the atomic subject pointer entirely. Whenever the subject was not the last tx in wire order, a PARENT was picked, to_beef(true) then serialized that parent’s world, and the rewrite dropped the subject transaction from its own stored BEEF — observed live on every pf head-spend (list / reprice / delist / buy) the moment it mined, breaking the NEXT spend on that name (zanaadu#284). update_input_proofs’ “already has a proof — update it” arm could even stamp the child’s bump onto that mis-picked parent. This is the same destroy-the-stored-BEEF class the #4 fix note above (hydrate_utxo_history) already documents for lookup hydration.

Operating on Beef directly has none of those failure modes: merge_bump dedupes against existing bumps by (height, root) and assigns the bump ONLY to transactions that appear as flagged txid leaves, so stitching tx X’s proof can never mislabel tx Y, and the tx set is never rebuilt at all.

Returns None when the bytes do not parse or txid is not in this BEEF (nothing to stitch — e.g. a consuming row whose ancestry got trimmed), so callers skip the write instead of writing garbage.

bsv-low M19 R2 round 3 (review HIGH-1): a same-height DIFFERENT-root proof (the 2026-09-07 reorg shape) does NOT combine in merge_bump (the roots differ) — it is pushed as a SECOND bump, and update_bump_indices assigns it only to txs whose bump_index is None, so the reorged subject keeps its index on the STALE bump. Left alone, has_proof/proofHeight and every find_txid(txid) .bump_index() reader then keep pointing at the orphan bump, and the row refutes again next sweep pass forever. So after the merge, FORCE the subject onto the bump that actually proves it, then drop any bump no transaction references (the orphan’s, now unreferenced).

Source

pub async fn sync_advertisements( &self, ) -> Result<SyncAdvertisementsReport, EngineError>

Sync SHIP/SLAP advertisements with configured managers and services.

Creates missing advertisements and revokes stale ones. Returns a SyncAdvertisementsReport so a create/submit failure is VISIBLE to the caller (bsv-low #320 defect 3a): this method used to swallow both into error! and return Ok(()), so /admin/syncAdvertisements reported success while zero advertisements were admitted locally — a caller could not tell a converged no-op from a silent failure, and every cycle re-created the full ad set.

Source

pub async fn renotify_admitted( &self, txid: &str, topic: &str, ) -> Result<RenotifyReport, EngineError>

Evict a specific outpoint from the overlay.

  1. Deletes the output from storage for the given topic.
  2. Notifies all lookup services via output_evicted().

If no topic is specified, finds all outputs for the txid and evicts the matching outputIndex across every topic.

Matches TS OverlayExpress /admin/evictOutpoint (lines 974-1004). bsv-low PLAN-PRE-LOOP4 §H4 (2026-09-06): RE-NOTIFY every lookup service of the outputs the engine ALREADY admitted for (txid, topic).

Phase-1 dedup means a (txid, topic) once recorded in applied_transactions never re-enters through /submit, so a lookup service that missed — or mis-parsed — its admit notification could never be told again (the loop-2 subject trap: the pot index’s admit hook shape-checked a wire-last hop and wrote no record while the engine had admitted the pot). This replays output_admitted_by_topic from the engine’s OWN stored outputs (with their BEEF) and the subject-NAMED body exactly as submit_with_report sends it. It validates nothing anew (the topic manager judged these outputs at admit), writes none of the engine’s tables, and is idempotent for every lookup service whose writes are (the pot index’s store_record is insert-if-absent for the spend fields — re-admission never regresses spend state). outputs == 0 means the engine never admitted this txid on this topic: nothing to re-notify — the caller re-SUBMITS instead.

Source

pub async fn forget_phantom_applied( &self, txid: &str, topic: &str, ) -> Result<bool, EngineError>

bsv-low PLAN-PRE-LOOP4 §H4 (2026-09-06): FORGET a PHANTOM applied row — (txid, topic) recorded in applied_transactions while the topic holds NO output of that txid (the engine judged the tx and admitted nothing: the loop-2 JOIN f9e85aab… under the pre-D5 subject rule). Such a row does nothing but block a correct re-submit through the Phase-1 dedup (/admin/readmit on beta answered deduped: ["tm_pot_beta"] and the pot stayed known:false). A row with ANY stored output on the topic is a real admission and is never touched (returns false, as does a missing row). The forgotten bytes are then re-VALIDATED by the topic manager like any first submit — nothing is admitted here.

Source

pub async fn evict_output( &self, txid: &str, output_index: u32, topic: Option<&str>, ) -> Result<(), EngineError>

Source

pub async fn start_gasp_sync(&self) -> Result<GASPSyncResult, EngineError>

Start GASP synchronization with peers for all configured topics.

For each topic in sync_configuration:

  • SyncTarget::Ship: unions local SHIP records, network discovery via configured SLAP trackers, and the topic’s bootstrap peers.
  • SyncTarget::Peers(urls) — uses the provided peer URLs directly.
  • SyncTarget::Disabled — skips the topic.

Filters out our own hosting_url to avoid self-sync.

Returns a GASPSyncResult summarizing discovered peers per topic.

Discovers peers for each configured topic and runs GASP sync with each.

When a GASPRemoteFactory is set (via set_gasp_remote_factory), this method creates OverlayGASPStorage + GASPRemote instances per (topic, peer) pair and runs GASPSync::sync() to exchange UTXOs.

Without a factory, peer discovery still runs but no actual sync occurs (backwards-compatible with the previous discovery-only behavior).

Source

pub async fn rerequest_held_proofs( &self, candidates: &[HeldOutput], progress: &Cell<PeerProofSummary>, )

Union local records, tracker answers, SLAP-named providers and bootstrap. Reference: overlay-services@480d77a src/Engine.ts:839-876 and ts-stack@edf6e03 packages/sdk/src/overlay-tools/LookupResolver.ts:1976. Ask the configured topic peers for held outputs, using the same SHIP discovery as sync. Only a header-verified proof can change stored bytes.

Source

pub fn storage(&self) -> &dyn Storage

Get a reference to the storage backend.

Source

pub fn config(&self) -> &EngineConfig

Get the engine configuration.

Auto Trait Implementations§

§

impl !Freeze for Engine

§

impl !RefUnwindSafe for Engine

§

impl !Send for Engine

§

impl !Sync for Engine

§

impl !UnwindSafe for Engine

§

impl Unpin for Engine

§

impl UnsafeUnpin for Engine

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more