Skip to content
Open
10 changes: 8 additions & 2 deletions bpa/CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion bpa/docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,6 @@ Items the external reviews of the filter subsystem (2026-09-30, and its 2026-10-

**No test observes the filter counters.** `bpa.filter.filtered` and `bpa.filter.modified` are incremented as documented, but the bpa tests install no metrics recorder, so nothing pins them. A `metrics_util::debugging::DebuggingRecorder` (a dev-dependency) around the engine's inline tests would assert each, by hook.

**No block-count cap.** The in-memory `Block` map costs ~774 B per wire block, so 262,146 tiny extension blocks in a 3.8 MB bundle take ~53× the wire size in heap before any filter runs, and the work, though linear, is synchronous (2.5 s measured, inside an async task with no yield point). Each editing Rewriter adds a full wire-form copy per attempt — a factor streamed egress shrinks to the header region (`streaming_pipeline_design.md` Phase B). A configurable maximum block count at the pre-drain gate, beside `max_bundle_size` and the RFC 9171 gates, would cap both; whether a cap conflicts with any conformance requirement is unchecked.
**No block-count cap.** The in-memory `Block` map costs ~774 B per wire block, so 262,146 tiny extension blocks in a 3.8 MB bundle take ~53× the wire size in heap before any filter runs, and the work, though linear, is synchronous (2.5 s measured, inside an async task with no yield point). Each editing Rewriter adds a snapshot of every block header per attempt — proportional to the block count, not the wire size: the output stages edit one editor over the resident bytes and rebuild once, and nothing on the egress path flattens the bundle. A configurable maximum block count at the pre-drain gate, beside `max_bundle_size` and the RFC 9171 gates, would cap both; whether a cap conflicts with any conformance requirement is unchecked.

**The post-registration poll can re-attempt a fresh park.** `register_service` spawns `poll_service_waiting` asynchronously; landing after a bundle parks `WaitingForService`, it re-dispatches the bundle at once — one redundant attempt (failing the same way if the failure is deterministic), and a non-determinism tests must synchronise around (`bpa/tests/filter_dispositions.rs` waits on the park swap itself). The RIB's Waiting poller skips stale wakes since `Rib::request_poll`; the service poll has no equivalent.
6 changes: 4 additions & 2 deletions bpa/docs/component_test_plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,13 @@ The unit testing strategy focuses on isolating complex logic from the async runt

### Suite E: Service Delivery Failure

*Objective: a delivery the service fails leaves the bundle in custody (`bpa/tests/pipeline.rs`).*
*Objective: a delivery the service fails leaves the bundle in custody, and a payload this node cannot decrypt as its security acceptor discards the bundle (`bpa/tests/pipeline.rs`).*

| Test ID | Scenario | Procedure | Expected Result |
| :--- | :--- | :--- | :--- |
| **INT-BPA-05** | **`on_deliver` returns `Err`** | 1. Register an application whose `on_deliver` fails.<br>2. Originate a bundle to it from a second application.<br>3. Unregister the failing application and register a working one on the same service id. | The failed delivery parks the bundle `WaitingForService` rather than reporting it delivered and deleting it; the working receiver gets it re-delivered, payload intact. |
| **INT-BPA-16** | **Payload fails to decrypt** | 1. Register a key provider holding the payload's key.<br>2. Deliver, via a CLA, a deletion-report-requesting bundle whose encrypted payload was tampered with, addressed to a local application. | The bundle is dropped with one deletion report carrying `FailedSecurityOperation` (RFC 9172 §5.1.1); nothing is delivered. |
| **INT-BPA-17** | **Payload in an unknown security context** | 1. Deliver, via a CLA, a deletion-report-requesting bundle whose payload a BCB covers in a security context this node does not recognise, addressed to a local application. | The bundle is dropped with one deletion report carrying `UnknownSecurityOperation` (RFC 9172 §7.1); nothing is delivered. |

### Suite F: Deferred CLA Transfer Outcomes

Expand All @@ -111,7 +113,7 @@ The unit testing strategy focuses on isolating complex logic from the async runt

| Test ID | Scenario | Procedure | Expected Result |
| :--- | :--- | :--- | :--- |
| **INT-BPA-12** | **Streamed origination and ingress** | 1. Originate through `ServiceSink::send` in several segments; drop the producer before `Final`; unregister the service while a send is parked; send a bundle whose source is not the service's endpoint; send a bundle whose Hop Count this node cannot decrypt.<br>2. Dispatch through `cla::Sink::dispatch` in several segments; unregister the CLA while a stream is parked; drop the producer before `Final`, including after a whole bundle in `Next` and after every byte of an oversized one. | Multi-segment origination and ingress match their whole-buffer forms (origination returns the built bundle's id and forwards it; ingress delivers locally). A producer dropped before `Final` cancels an origination (`StreamCancelled`, nothing enters custody) and is refused at ingress (`Acceptance::Refused`, nothing delivered), even once every byte of the bundle has arrived. Unregistration wakes a parked consumer with `StreamCancelled` at once. The spoofed source is rejected. An undecryptable Hop Count is accepted and forwarded unchanged. |
| **INT-BPA-12** | **Streamed origination and ingress** | 1. Originate through `ServiceSink::send` in several segments; drop the producer before `Final`; unregister the service while a send is parked; send a bundle whose source is not the service's endpoint; send a bundle carrying a Hop Count block, and one whose Hop Count this node cannot decrypt.<br>2. Dispatch through `cla::Sink::dispatch` in several segments; unregister the CLA while a stream is parked; drop the producer before `Final`, including after a whole bundle in `Next` and after every byte of an oversized one. | Multi-segment origination and ingress match their whole-buffer forms (origination returns the built bundle's id and forwards it; ingress delivers locally). A producer dropped before `Final` cancels an origination (`StreamCancelled`, nothing enters custody) and is refused at ingress (`Acceptance::Refused`, nothing delivered), even once every byte of the bundle has arrived. Unregistration wakes a parked consumer with `StreamCancelled` at once. The spoofed source is rejected. An originated bundle leaves with the Hop Count it was built with (origination is not a hop), and an undecryptable Hop Count is accepted and forwarded unchanged. |

## 5. Performance Benchmarks (REQ-13)

Expand Down
2 changes: 2 additions & 0 deletions bpa/docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ Every `Accepted` resolves in exactly one of three ways:

The deferred `Failed` retry loop is deliberately un-damped. An unreachable-but-routed peer cycles accept → probe → `Failed` → re-dispatch at the convergence layer's failure-discovery latency (for `tcpclv4`, a full dial cycle), bounded only by bundle lifetime. This is store-and-forward liveness for deployments without contact knowledge: with no route change and no inbound contact to wake `Waiting`, the loop is the only unilateral contact probe a plan-less static route has, and receiver-side deduplication absorbs any re-forwards it produces. Deployments with contact knowledge (contact plans, TVR) never run it — withdrawing the route parks the queue quietly. The churn is isolated per peer by the CLA's admission bound, and its pacing is a policy concern: retry damping is deferred to the policy subsystem with the rest of retry policy, and peer-health evidence feeding the RIB is routing-redesign work. The wrong-queue signal is unaffected: an address the CLA does not serve is still refused synchronously with `NoNeighbour` (whole-queue reset), and a removed peer's queues are swept event-driven at removal — dial exhaustion is the one case that moved, from link-scoped `NoNeighbour` to bundle-scoped `Failed`, because it is a probe result, not a queue-assignment error.

A synchronous failure — `forward` returning an error — is resolved by its kind. `StreamCancelled` reports a transfer cancelled before completion: a failed acceptance the bundle is valid to retry, so it re-enters Dispatch at once, exactly as a deferred `Failed` does, re-claimed out of `ForwardAckPending` by the same conditional swap. A CLA that cancels every attempt loops until the bundle expires. Any other error parks the bundle alone in `Waiting`, leaving the peer queue untouched: a synchronous failure can be deterministic and instantaneous, so it waits for the next routing or link event rather than re-running dispatch inline, and re-dispatches once if such an event landed while the transfer was in flight.

An outcome is honoured only if the named bundle is currently `ForwardAckPending` via a peer of the reporting CLA; anything else — already resolved, expired, another CLA's transfer — is logged and dropped. The check is enforced with a status-conditioned compare-and-swap on the persisted status (`MetadataStorage::swap_status`; the terminal `Completed` arm uses the conditional-tombstone form, `tombstone_if`, so a resolving bundle never transits a status another queue's poller could recover), so an outcome racing the peer sweep, the expiry reaper, or a duplicate of itself loses the swap and is dropped. Bundle lifetime is meant to bound retention even for a CLA that never resolves a transfer: a BPA-side guard timer times such a transfer out as outcome-unknown once its lifetime plus a grace period has passed, returning it to `Waiting`, where the dispatch expiry checkpoint drops it as a visible, attributable expiry. The guard timer is not yet built ([`TODO.md`](TODO.md)); until it is, a CLA that sits on a transfer holds it until its peer is removed or it unregisters, when the sweep resets it to `Waiting`.

`ForwardAckPending { peer }` is persisted metadata status like any other, and is a holding state, not a queue (see [queue_architecture.md](queue_architecture.md)): bundles leave it only via the keyed outcome or a sweep (peer loss, or restart replay resetting it to `Waiting` exactly as `ForwardPending`, since registrations do not survive a restart) — the reaper defers it, because an accepted transfer cannot be recalled from the wire. The persisted status is the only state — outcome resolution is a metadata lookup by bundle ID. The retention cost is explicit: a bundle stays in the store from acceptance to outcome, bounded by the transfer duration and, for a CLA that never resolves it, by the life of the peer's registration — the price of honest reliability accounting, and how BP/LTP stacks already behave. So is a per-forward write cost: the claim persists `ForwardAckPending` before every offer, for every CLA — one extra metadata write per bundle even on the non-deferring path (a terminal `Sent` deletes it moments later) — unavoidable because an outcome can arrive before `forward` returns.
Expand Down
Loading
Loading