Running the exercises
From a source checkout with the local services ofdocker-compose.yml running:
ops.ts runs every exercise’s test files with Vitest, joins each exercise’s results with what its tests
recorded (transaction hashes, fees charged, refusal codes), and prints one evidence document with a case per
exercise. --only balance-race,crash-k0 runs a subset. --write records the run in
tools/conformance/evidence/ops-local-stellar-testnet.json, only from a clean checkout of main, and
pnpm report renders the sections below from it. A run in which any exercise could not run (a local
service down, or no Docker for the short-TTL network) exits non-zero and records nothing; --allow-skips
records it anyway, with each skipped exercise and its reason, for a rehearsal.
Recorded run
Fromtools/conformance/evidence/ops-local-stellar-testnet.json: 45 of 45 exercises passed.
Exercises
replay-sequential A settled payment sent to /settle again
Threats R1 · Private Stellar network (1 s ledgers) and Postgres
Procedure. Settle an exact payment, then send the same payload and requirements again, with the in-memory and the Postgres settlement ledger.
Expected. The repeat is answered with the recorded response and a repeat marker; one transfer on chain; nothing is submitted again.
Tests
apps/rail402/test/idempotency.integration.test.ts: answers a sequential repeat with the recorded response, marked as a repeatpackages/facilitator/test/settlement.integration.test.ts: answers a repeated settle with the original result and moves funds once
- answers a sequential repeat with the recorded response, marked as a repeat: 2 of 2 passed in 14.4 s
- answers a repeated settle with the original result and moves funds once: 1 of 1 passed in 5.9 s
live-matrixcasereplay-idempotent: Passed (tools/conformance/evidence/live-matrix-hosted-stellar-testnet.json)live-matrixcasereplay-late: Passed (tools/conformance/evidence/live-matrix-hosted-stellar-testnet.json)
replay-concurrent Concurrent copies of one payment
Threats R1, S6 · Private Stellar network (1 s ledgers) and Postgres
Procedure. Send five copies of one payment to /settle at the same moment.
Expected. One transaction, one transfer; every caller gets the same body; four answers are marked as repeats.
Tests
apps/rail402/test/idempotency.integration.test.ts: settles 5 concurrent repeats once: one transfer, one original, identical bodiespackages/facilitator/test/settlement.integration.test.ts: settles concurrent duplicates exactly once, all callers getting the same result
- settles 5 concurrent repeats once: one transfer, one original, identical bodies: 2 of 2 passed in 14.2 s
- settles concurrent duplicates exactly once, all callers getting the same result: 1 of 1 passed in 6.7 s
live-matrixcasereplay-concurrent: Passed (tools/conformance/evidence/live-matrix-hosted-stellar-testnet.json)
replay-new-envelope A settled authorization in a new envelope
Threats R2 · Private Stellar network (1 s ledgers) and Postgres
Procedure. Re-wrap a settled authorization entry in a transaction with different bytes and settle it.
Expected. Refused with the idempotency conflict naming the recorded transaction; nothing is submitted.
Tests
apps/rail402/test/idempotency.integration.test.ts: refuses the same authorization in another envelope, naming the recorded transactionpackages/facilitator/test/settlement.integration.test.ts: refuses a settled authorization in a different envelope, and verify names the replay
- refuses the same authorization in another envelope, naming the recorded transaction: 2 of 2 passed in 16.5 s
- refuses a settled authorization in a different envelope, and verify names the replay: 1 of 1 passed in 6.2 s
live-matrixcasereplay-rejected: Passed (tools/conformance/evidence/live-matrix-hosted-stellar-testnet.json)
replay-upfront One payment signature sent twice to a seller
Threats R4 · Private Stellar network (1 s ledgers)
Procedure. Pay a seller built with the seller helpers, then send the same PAYMENT-SIGNATURE again, in the authorization and the upfront flow.
Expected. The resource is served once; the second request is refused; the facilitator’s repeat marker is honoured.
Tests
packages/sdk/test/seller.integration.test.ts: does not serve a replayed payment signature againpackages/sdk/test/seller.integration.test.ts: serves an upfront settlement once when the facilitator answers its repeat
- does not serve a replayed payment signature again: 1 of 1 passed in 0.0 s
- serves an upfront settlement once when the facilitator answers its repeat: 1 of 1 passed in 6.6 s
front-run A copied exact authorization lands first
Threats F1, F2 · Private Stellar network (1 s ledgers)
Procedure. The RPC proxy runs an attacker right before Rail402’s sendTransaction is forwarded: the attacker copies the payer’s authorization entry into its own transaction and lands it.
Expected. Rail402’s transaction fails on chain; the answer is a success with the attacker’s hash; the payer is debited once; exactly one wasted fee equal to the sponsor’s balance change.
Tests
packages/facilitator/test/front-run.integration.test.ts: answers success with the attacker’s hash; the payment moves once and our fee is wastedpackages/facilitator/test/front-run.integration.test.ts: refuses an authorization consumed before the request was claimedpackages/facilitator/test/front-run.integration.test.ts: refuses when the consuming transaction carried a different invocation for the same nonce
- answers success with the attacker’s hash; the payment moves once and our fee is wasted: 1 of 1 passed in 7.3 s
- refuses an authorization consumed before the request was claimed: 1 of 1 passed in 0.0 s
- refuses when the consuming transaction carried a different invocation for the same nonce: 1 of 1 passed in 0.0 s
- Recorded: attackerTransaction
aaf96c55cb951ffc68fab61c0338497613eaa06213c1f20994aacea197fde281, ourFailedTransactionbd8e15bf82001bc667f07088a912a94466d78ad572bac4fea601b27bb913108d, answersuccess with the attacker's hash, wastedFeeStroops76214, payerDebitedOnce1000000
front-run-upto Front-running an upto settlement
Threats F3, F4 · Private Stellar network (1 s ledgers)
Procedure. An attacker without the settlement authority copies an upto settlement and submits it first; then a transaction signed by the authority itself lands first.
Expected. The attacker’s copy fails and Rail402’s settlement proceeds; a landing by the authority for the same amount is answered with its hash.
Tests
packages/facilitator/test/front-run.integration.test.ts: cannot be landed by an attacker without the settlement authority; Rail402’s settlement proceedspackages/facilitator/test/front-run.integration.test.ts: answers with the authority’s own earlier transaction for the same amount; our fee is wasted
- cannot be landed by an attacker without the settlement authority; Rail402’s settlement proceeds: 1 of 1 passed in 8.5 s
- answers with the authority’s own earlier transaction for the same amount; our fee is wasted: 1 of 1 passed in 12.2 s
smart-account-signer A smart-account payment signed by a key the account does not accept
Threats A1 · Private Stellar network (1 s ledgers) and Postgres
Procedure. Sign an exact payment from a custom account (C…) with a random key, and an upto payment from a custom account with another key, and send them to /verify and /settle.
Expected. Both are refused before anything is submitted, named as the account’s own rejection (smart_account_rejected or invalid_signature); nothing moves.
Tests
packages/facilitator/test/settlement.integration.test.ts: rejects a smart-account payment signed by the wrong keyapps/rail402/test/upto-payers.integration.test.ts: refuses a signature by another key as the account’s own rejection
- rejects a smart-account payment signed by the wrong key: 1 of 1 passed in 0.1 s
- refuses a signature by another key as the account’s own rejection: 1 of 1 passed in 0.0 s
live-matrixcasec-payer-wrong-key: Passed (tools/conformance/evidence/live-matrix-hosted-stellar-testnet.json)rejection-auditcaseupto-c-payer-wrong-key: Passed (tools/conformance/evidence/rejection-audit-hosted-stellar-testnet.json)
smart-account-budget An OpenZeppelin smart account paying with an agent key under a spending limit
Threats A4, A5, A6 · Private Stellar network (1 s ledgers) and Postgres
Procedure. Deploy an OpenZeppelin smart account whose agent key may only pay through a rule with the spending-limit policy, pay upto within the budget, then over it, then try to use the owner’s policy-free default rule with the agent key.
Expected. Payments within the budget settle and reserve their full cap; a payment over the limit and a cap that would exceed the remaining budget are refused with nothing moved; the agent key cannot use the default rule.
Tests
apps/rail402/test/upto-payers.integration.test.ts: refuses a payment over its own spending limit, and nothing movesapps/rail402/test/upto-payers.integration.test.ts: has reserved the full cap of each payment against its budgetapps/rail402/test/upto-payers.integration.test.ts: does not let the agent key use the owner’s policy-free default ruleapps/rail402/test/upto-payers.integration.test.ts: pays up to its budget and refuses the cap that would exceed it
- refuses a payment over its own spending limit, and nothing moves: 1 of 1 passed in 0.2 s
- has reserved the full cap of each payment against its budget: 1 of 1 passed in 0.0 s
- does not let the agent key use the owner’s policy-free default rule: 1 of 1 passed in 0.1 s
- pays up to its budget and refuses the cap that would exceed it: 1 of 1 passed in 1.1 s
live-matrixcaseupto-c-over-limit: Passed (tools/conformance/evidence/live-matrix-hosted-stellar-testnet.json)
smart-account-apply-failure A smart account whose __check_auth passes simulation and fails at inclusion
Threats A2 · Private Stellar network (1 s ledgers)
Procedure. Pay exact from an OpenZeppelin smart account with a delegate key under its own context rule, then, through the RPC proxy between the settle-time simulation and the broadcast, have the owner remove that rule.
Expected. The transaction fails on chain in the account’s __check_auth, is named smart_account_rejected, moves nothing, and its fee is the only sponsor cost, counted as wasted.
Tests
packages/facilitator/test/fee-exposure.integration.test.ts: names a smart account whose __check_auth passes the settle-time simulation and fails at inclusion
- names a smart account whose __check_auth passes the settle-time simulation and fails at inclusion: 1 of 1 passed in 19.1 s
- Recorded: payer
CAMV6HX56SB3DFE4HVLBQ7RWHGSE2ZJH7HQSQ6D464AQIM7Y7AUCD5ID, phasebetween the settle-time simulation and inclusion, transaction58c5be34c7a935f9270f856f35be054d45c6e2ead9d629657cf8cbd898ba64d6, ledger3685, settleinvalid_exact_stellar_payload_smart_account_rejected, wastedFeeStroops74145, sponsorDeltaStroops-74145
balance-race The payer spends the balance before settlement lands
Threats S1 · Private Stellar network (1 s ledgers)
Procedure. Verify a payment, then move the payer’s whole balance away: once before /settle, and once through the RPC proxy between the settle-time simulation and the broadcast.
Expected. Before settle: refused as insufficient_funds with nothing submitted. After simulation: the transaction fails on chain, is named insufficient_funds, and its fee is the only sponsor cost, counted as wasted.
Tests
packages/facilitator/test/fee-exposure.integration.test.ts: refuses at settle, with nothing submitted, a payment whose balance moved after verifypackages/facilitator/test/fee-exposure.integration.test.ts: names a balance spent between the settle-time simulation and inclusion; the fee is spent and counted as wasted
- refuses at settle, with nothing submitted, a payment whose balance moved after verify: 1 of 1 passed in 11.2 s
- names a balance spent between the settle-time simulation and inclusion; the fee is spent and counted as wasted: 1 of 1 passed in 12.9 s
- Recorded: phase
between verify and settle, settleinsufficient_funds, submissions0, sponsorDeltaStroops0 - Recorded: phase
between the settle-time simulation and inclusion, transaction81cd00f0bf9442d18027eaaa365cff0860fa5228086b965d852f4e683e239f45, ledger3666, settleinsufficient_funds, wastedFeeStroops76216, sponsorDeltaStroops-76216
expiry-race An authorization that expires while the settlement waits
Threats S2 · Private Stellar network (1 s ledgers)
Procedure. Hold an exact settlement at the RPC proxy until its authorization’s expiration ledger has passed; hold an upto settlement past its expiration_ledger.
Expected. exact: included and failed, named signature_expired, fee spent. upto: refused by the network before inclusion (ledger bounds), no fee spent.
Tests
packages/facilitator/test/expiration.integration.test.ts: names an authorization that expires between submission and inclusion; the fee is spentpackages/facilitator/test/expiration.integration.test.ts: refuses at the network an upto settlement held past expiration_ledger; no fee is spentpackages/facilitator/test/expiration.integration.test.ts: holds the lower edge at the current ledger plus a one-ledger margin
- names an authorization that expires between submission and inclusion; the fee is spent: 1 of 1 passed in 8.9 s
- refuses at the network an upto settlement held past expiration_ledger; no fee is spent: 1 of 1 passed in 9.5 s
- holds the lower edge at the current ledger plus a one-ledger margin: 2 of 2 passed in 8.6 s
- Recorded: scheme
exact, expirationLedger3492, transactiona91ff6bb7c7b787672c13b01a3165dcba29a6f1863c87233206e87b5a3ee11c6, settleinvalid_exact_stellar_signature_expired, wastedFeeStroops76216
live-matrixcaseexpiry-window: Passed (tools/conformance/evidence/live-matrix-hosted-stellar-testnet.json)live-matrixcaseupto-expiry-window: Passed (tools/conformance/evidence/live-matrix-hosted-stellar-testnet.json)
ceiling-refusal A settlement above the fee ceiling
Threats S3 · Private Stellar network (1 s ledgers)
Procedure. Configure a fee ceiling below the payment’s simulated fee and send it to /verify and /settle.
Expected. Both refuse with invalid_exact_stellar_payload_fee_exceeds_maximum; nothing is recorded or submitted; the sponsor’s balance is unchanged.
Tests
packages/facilitator/test/fee-exposure.integration.test.ts: refuses a payment whose settlement would cost more than the fee ceiling, before signing anythingpackages/upto/test/facilitator.test.ts: enforces the fee ceiling
- refuses a payment whose settlement would cost more than the fee ceiling, before signing anything: 1 of 1 passed in 6.1 s
- enforces the fee ceiling: 1 of 1 passed in 0.0 s
- Recorded: ceilingStroops
20000, verifyinvalid_exact_stellar_payload_fee_exceeds_maximum, settleinvalid_exact_stellar_payload_fee_exceeds_maximum, submissions0, sponsorDeltaStroops0
budget-trip The hourly fee budget is reached
Threats S4 · Private Stellar network (1 s ledgers)
Procedure. Set an hourly fee budget that one settlement exhausts; settle one payment, then another.
Expected. The first settles; the second is refused with settle_exact_stellar_sponsor_unavailable before any RPC work, and the refusal is counted as a budget refusal.
Tests
packages/facilitator/test/fee-exposure.integration.test.ts: stops settling once the hourly fee budget is committed, before any RPC workpackages/facilitator/test/fees.test.ts: refuses once the hourly fee budget is committed
- stops settling once the hourly fee budget is committed, before any RPC work: 1 of 1 passed in 9.8 s
- refuses once the hourly fee budget is committed: 1 of 1 passed in 0.0 s
- Recorded: budgetStroops
1, firstSettlemente22186df586e5380bfc7ade5437468ac057262300287ff9608236a65701be1e2, refusalsettle_exact_stellar_sponsor_unavailable, submissionsAfterTrip0
sponsor-floor The sponsor below its reserve
Threats S5 · The Rail402 service on the private network
Procedure. Run the service with a reserve floor above the sponsor’s balance, and with a readiness floor above it.
Expected. Settlements are refused and counted as reserve refusals; /health reports the sponsor below its floor; /ready fails.
Tests
apps/rail402/test/observability.integration.test.ts: counts refusals below the reserve and reports the sponsor below its floorapps/rail402/test/runtime.integration.test.ts: fails while the sponsor holds less than MIN_SPONSOR_BALANCE_XLM
- counts refusals below the reserve and reports the sponsor below its floor: 1 of 1 passed in 5.1 s
- fails while the sponsor holds less than MIN_SPONSOR_BALANCE_XLM: 1 of 1 passed in 2.2 s
ssrf-origin Origin checks and SEP-1 fetches to private addresses
Threats X1, X5 · Local sockets and a stub resolver
Procedure. Catalog resources whose URLs are private IP literals or names that resolve to private addresses; fetch a stellar.toml from a private address.
Expected. Refused before any connection is made, unless loopback is explicitly allowed; never with pubnet.
Tests
packages/bazaar/test/origin.test.ts: refuses a host whose DNS answers are not public, before connectingpackages/bazaar/test/egress.test.ts: refuses a private IP literal for origin checks unless loopback is allowedpackages/bazaar/test/stellar-toml.test.ts: never connects to a private address unless loopback is explicitly allowedapps/rail402/test/config.test.ts: refuses loopback discovery fetches on a service that serves pubnet
- refuses a host whose DNS answers are not public, before connecting: 1 of 1 passed in 0.0 s
- refuses a private IP literal for origin checks unless loopback is allowed: 1 of 1 passed in 0.0 s
- never connects to a private address unless loopback is explicitly allowed: 1 of 1 passed in 0.0 s
- refuses loopback discovery fetches on a service that serves pubnet: 1 of 1 passed in 0.0 s
ssrf-rebinding DNS rebinding
Threats X2 · Local sockets and a stub resolver
Procedure. Resolve a name to a public and a private address, and to a private address on the second lookup.
Expected. The private address is never dialled.
Tests
packages/safe-fetch/test/egress.integration.test.ts: checks every DNS answer, so a rebinding name cannot reach a private address
- checks every DNS answer, so a rebinding name cannot reach a private address: 1 of 1 passed in 0.0 s
ssrf-redirect Redirects to private or other destinations
Threats X3 · Local sockets and a stub resolver
Procedure. Answer an origin check and a buyer request with redirects to a private address and to another origin.
Expected. Origin checks follow no redirect; buyers follow none to a private or other origin, and none after paying.
Tests
packages/safe-fetch/test/egress.integration.test.ts: never follows a redirect towards a private or other destinationpackages/bazaar/test/origin.test.ts: does not follow redirects
- never follows a redirect towards a private or other destination: 1 of 1 passed in 0.0 s
- does not follow redirects: 1 of 1 passed in 0.0 s
rejection-auditcasemcp-redirect-private: Passed (tools/conformance/evidence/rejection-audit-hosted-stellar-testnet.json)
ssrf-oversized Oversized, compressed and slow answers
Threats X4 · Local sockets and a stub resolver
Procedure. Serve a body larger than the cap, a compression bomb, and answers that stall.
Expected. Stopped at the decoded size limit and at the header, idle and total timeouts.
Tests
packages/safe-fetch/test/egress.integration.test.ts: stops oversized and compressed-bomb answers at the decoded limitpackages/safe-fetch/test/fetch.test.ts: times out waiting for headers, on an idle body, and in total
- stops oversized and compressed-bomb answers at the decoded limit: 1 of 1 passed in 0.0 s
- times out waiting for headers, on an idle body, and in total: 1 of 1 passed in 0.6 s
catalog-takeover Another payTo pays for a listed resource
Threats P2 · The Rail402 service on the private network
Procedure. After a resource is listed, settle a payment for the same URL to a different payTo through a real seller.
Expected. The listing is unchanged; the settlement reports bazaar_owner_conflict in EXTENSION-RESPONSES.
Tests
apps/rail402/test/bazaar.integration.test.ts: refuses a different payTo for the same resource, with a coded reasonpackages/bazaar/src/testing.ts: never lets a different payTo modify a listing (run bypackages/store-postgres/test/stores.integration.test.ts)packages/bazaar/src/testing.ts: transfers ownership only when the origin names the new payTo (run bypackages/store-postgres/test/stores.integration.test.ts)
- refuses a different payTo for the same resource, with a coded reason: 1 of 1 passed in 13.0 s
- never lets a different payTo modify a listing: 1 of 1 passed in 0.1 s
- transfers ownership only when the origin names the new payTo: 1 of 1 passed in 0.2 s
live-bazaarcaseowner-conflict: Passed (tools/conformance/evidence/live-bazaar-stellar-testnet.json)
forged-metadata Echoed metadata that the origin does not confirm
Threats P1 · Postgres
Procedure. Settle payments whose resource block carries a changed description and price.
Expected. The listing changes only when the resource’s own 402 confirms the new metadata.
Tests
packages/bazaar/src/testing.ts: does not let echoed metadata change a listing until the origin confirms it (run bypackages/store-postgres/test/stores.integration.test.ts)packages/bazaar/src/testing.ts: never lists an HTTP resource whose own response is not a 402 (run bypackages/store-postgres/test/stores.integration.test.ts)
- does not let echoed metadata change a listing until the origin confirms it: 1 of 1 passed in 0.1 s
- never lists an HTTP resource whose own response is not a 402: 1 of 1 passed in 0.1 s
live-bazaarcaseforged-metadata: Passed (tools/conformance/evidence/live-bazaar-stellar-testnet.json)
mcp-tool-planting A payer plants content under a seller’s MCP tool
Threats P9 · Postgres
Procedure. Pay a seller’s payTo one base unit, exact and upto under a huge cap, echoing the seller’s MCP endpoint and tool with a forged description and price; then settle the tool’s real price.
Expected. The planted listing stays pending and is quarantined with origin_payment_mismatch; the real settlement publishes the tool’s own content, and a planted settlement never changes or withdraws a confirmed tool.
Tests
packages/bazaar/src/testing.ts: never publishes a tool description or price planted with (run bypackages/store-postgres/test/stores.integration.test.ts)packages/bazaar/src/testing.ts: never lets a planted settlement withdraw or change a tool its origin confirmed (run bypackages/store-postgres/test/stores.integration.test.ts)packages/bazaar/test/mcp-origin.test.ts: reads a tool’s PaymentRequired from its unpaid call, sending the example arguments
- never publishes a tool description or price planted with: 2 of 2 passed in 0.3 s
- never lets a planted settlement withdraw or change a tool its origin confirmed: 1 of 1 passed in 0.1 s
- reads a tool’s PaymentRequired from its unpaid call, sending the example arguments: 1 of 1 passed in 0.1 s
live-bazaarcasemcp-tool: Passed (tools/conformance/evidence/live-bazaar-stellar-testnet.json)
schema-redos Hostile JSON Schema
Threats P3 · In-process, with the RPC or store simulated
Procedure. Validate metadata against schemas with catastrophic patterns, combinatorial checks, deep nesting and external references.
Expected. Each stops within its budget or is refused before compiling; the sandbox keeps serving.
Tests
packages/bazaar/test/security.test.ts: stops a catastrophic-backtracking pattern within its budget and keeps servingpackages/bazaar/test/security.test.ts: stops a combinatorial uniqueItems check within its budgetpackages/bazaar/test/security.test.ts: finds external references in every schema position, and only there
- stops a catastrophic-backtracking pattern within its budget and keeps serving: 1 of 1 passed in 0.3 s
- stops a combinatorial uniqueItems check within its budget: 1 of 1 passed in 0.0 s
- finds external references in every schema position, and only there: 1 of 1 passed in 0.0 s
live-bazaarcaseschema-rejected: Passed (tools/conformance/evidence/live-bazaar-stellar-testnet.json)
route-traversal Encoded traversal in route templates and parameters
Threats P4 · Postgres
Procedure. Catalog templates that decode to NUL, CR, LF, a backslash or an empty segment; fill parameters with encoded dot segments.
Expected. Unsafe templates are not used; unsafe parameter values are refused before any request.
Tests
packages/bazaar/test/security.test.ts: refuses route templates that decode to NUL, CR, LF, a backslash or an empty segmentpackages/bazaar/src/testing.ts: uses a routeTemplate only when it is safe and matches the paid path (run bypackages/store-postgres/test/stores.integration.test.ts)packages/sdk/test/discovery.test.ts: fills route parameters and refuses traversal
- refuses route templates that decode to NUL, CR, LF, a backslash or an empty segment: 1 of 1 passed in 0.0 s
- uses a routeTemplate only when it is safe and matches the paid path: 1 of 1 passed in 0.2 s
- fills route parameters and refuses traversal: 1 of 1 passed in 0.0 s
live-bazaarcaseroute-template: Passed (tools/conformance/evidence/live-bazaar-stellar-testnet.json)
crash-k0 Killed after the network accepted the transaction
Threats O1 · Rail402 service processes over Postgres on the private network, killed with SIGKILL
Procedure. SIGKILL the service right after sendTransaction returned; restart it.
Expected. Recovery finds the accepted transaction; no replacement is submitted; one transfer.
Tests
apps/rail402/test/crash-recovery.integration.test.ts: K0: finds the transaction the network accepted before the process died
- K0: finds the transaction the network accepted before the process died: 2 of 2 passed in 14.5 s
fault-recoverycasek0-exact: Passed (tools/conformance/evidence/fault-recovery-stellar-testnet.json)fault-recoverycasek0-upto: Passed (tools/conformance/evidence/fault-recovery-stellar-testnet.json)
crash-k1 Killed before the broadcast left
Threats O1 · Rail402 service processes over Postgres on the private network, killed with SIGKILL
Procedure. Hold the broadcast at the RPC proxy, SIGKILL the service, drop the held request, restart.
Expected. Recovery rebroadcasts the identical recorded bytes; one transaction hash; one transfer.
Tests
apps/rail402/test/crash-recovery.integration.test.ts: K1: rebroadcasts the identical recorded bytes when the broadcast never left
- K1: rebroadcasts the identical recorded bytes when the broadcast never left: 2 of 2 passed in 14.6 s
fault-recoverycasek1-exact: Passed (tools/conformance/evidence/fault-recovery-stellar-testnet.json)fault-recoverycasek1-upto: Passed (tools/conformance/evidence/fault-recovery-stellar-testnet.json)
crash-k2 Killed after inclusion, before confirmation
Threats O1 · Rail402 service processes over Postgres on the private network, killed with SIGKILL
Procedure. Hide the transaction from getTransaction until the service is killed; restart.
Expected. Recovery finds the included transaction and answers its success.
Tests
apps/rail402/test/crash-recovery.integration.test.ts: K2: finds a transaction that was included before the process saw it confirmed
- K2: finds a transaction that was included before the process saw it confirmed: 2 of 2 passed in 15.4 s
crash-k3 Down until the time bound passed
Threats O1 · Rail402 service processes over Postgres on the private network, killed with SIGKILL
Procedure. Drop the broadcast, keep the service down until the transaction’s time bound has passed, restart.
Expected. The settlement expires only because retained history proves it absent; nothing moves.
Tests
apps/rail402/test/crash-recovery.integration.test.ts: K3: expires a transaction whose time bound passed while the service was down, and nothing moves
- K3: expires a transaction whose time bound passed while the service was down, and nothing moves: 1 of 1 passed in 41.1 s
fault-recoverycasek3-exact: Passed (tools/conformance/evidence/fault-recovery-stellar-testnet.json)
crash-unresolvable A settlement beyond the RPC’s retention
Threats O4 · Rail402 service processes over Postgres on the private network, killed with SIGKILL
Procedure. Restart with a recorded settlement whose ledgers the RPC no longer retains.
Expected. It stays unresolved and is reported in /health and the metrics; it is never expired.
Tests
apps/rail402/test/crash-recovery.integration.test.ts: keeps a settlement the RPC can no longer prove absent unresolved, and reports it
- keeps a settlement the RPC can no longer prove absent unresolved, and reports it: 1 of 1 passed in 1.8 s
concurrency Three batches of twenty concurrent settlements
Threats O2, S6 · Private Stellar network (1 s ledgers)
Procedure. Settle 3 batches of 20 payments over 8 channels, exact, upto and mixed, with duplicates injected into every batch.
Expected. Every payment settles once; distinct (source, sequence) pairs; no txBadSeq or TRY_AGAIN_LATER.
Tests
packages/facilitator/test/concurrency.integration.test.ts: with injected duplicates, without a collision
- with injected duplicates, without a collision: 3 of 3 passed in 101.7 s
concurrencycasedistinct-sequences: Passed (tools/conformance/evidence/concurrency-mixed-stellar-testnet.json)
replicas Two replicas on one Postgres
Threats O2, R1 · Two Rail402 service processes over one Postgres on the private network
Procedure. Run two service replicas on one Postgres and send every duplicate to the other replica.
Expected. One settlement per payment across replicas; no channel is leased twice.
Tests
apps/rail402/test/replicas.integration.test.ts: with every duplicate sent to the other replica
- with every duplicate sent to the other replica: 1 of 1 passed in 31.6 s
seq-conflict A channel used behind the service’s back
Threats O2 · The Rail402 service on the private network
Procedure. Submit a transaction from a channel account outside the service, then settle through it.
Expected. The conflict is counted in rail402_sequence_conflicts_total.
Tests
apps/rail402/test/observability.integration.test.ts: counts a sequence conflict when a channel’s sequence is used behind its back
- counts a sequence conflict when a channel’s sequence is used behind its back: 1 of 1 passed in 15.8 s
rpc-outage The RPC stops answering
Threats O3 · The Rail402 service on the private network
Procedure. Make the RPC proxy answer errors, then recover.
Expected. /ready fails while the RPC is unreachable; transport errors are counted and reported in /health.
Tests
apps/rail402/test/runtime.integration.test.ts: fails while the RPC is unreachableapps/rail402/test/observability.integration.test.ts: counts transport and request errors while the RPC fails, and reports it in /health
- fails while the RPC is unreachable: 1 of 1 passed in 3.5 s
- counts transport and request errors while the RPC fails, and reports it in /health: 1 of 1 passed in 1.0 s
store-outage The settlement store fails
Threats O3 · Private Stellar network (1 s ledgers) and Postgres
Procedure. Fail the envelope write before broadcast; fail the ledger read at verify; close Postgres’ idle connections.
Expected. Nothing is broadcast without a record and the payment stays retryable; verify stays available; the pool reconnects.
Tests
packages/facilitator/test/settlement.integration.test.ts: broadcasts nothing when the envelope cannot be recorded first, leaving the payment retryableapps/rail402/test/idempotency.integration.test.ts: keeps verify available when the ledger cannot be readpackages/store-postgres/test/stores.integration.test.ts: survives the server closing its idle connections, as in a restart or failover
- broadcasts nothing when the envelope cannot be recorded first, leaving the payment retryable: 1 of 1 passed in 6.0 s
- keeps verify available when the ledger cannot be read: 2 of 2 passed in 14.0 s
- survives the server closing its idle connections, as in a restart or failover: 1 of 1 passed in 0.1 s
channel-exhaustion Every channel busy
Threats O5 · Private Stellar network (1 s ledgers)
Procedure. Hold every channel and settle; send more settlements than channels to a service.
Expected. A retryable channel_unavailable refusal with no record left; lease waits are measured.
Tests
packages/facilitator/test/settlement.integration.test.ts: refuses when no channel frees up in time, leaving the payment retryableapps/rail402/test/observability.integration.test.ts: measures the wait for a channel when settlements outnumber channels
- refuses when no channel frees up in time, leaving the payment retryable: 1 of 1 passed in 7.0 s
- measures the wait for a channel when settlements outnumber channels: 1 of 1 passed in 16.5 s
ttl-extend Extending the upto contract’s TTL
Threats U7 · The Rail402 service on the private network
Procedure. Let the keeper and the operator command extend the settlement contract’s instance and code entries.
Expected. Both entries’ liveUntilLedgerSeq move; /health and the TTL gauge report the new values.
Tests
apps/rail402/test/upto.integration.test.ts: extends the contract from the keeper and the operator command, and reports its TTL
- extends the contract from the keeper and the operator command, and reports its TTL: 1 of 1 passed in 4.8 s
ttl-restore An archived upto contract
Threats U7, S7 · A dedicated Stellar network with short TTLs, and the service
Procedure. Let the settlement contract’s entries expire, pay while archived, then restore by the operator command and by the keeper.
Expected. Payments are refused with invalid_upto_stellar_settlement_contract_archived and nothing is submitted; after restoration they settle.
Tests
apps/rail402/test/upto-archival.integration.test.ts: refuses payments while the contract is archived, and settles once an operator restores itapps/rail402/test/upto-archival.integration.test.ts: is restored by the TTL keeper where automatic restoration is allowed
- refuses payments while the contract is archived, and settles once an operator restores it: 1 of 1 passed in 25.9 s
- is restored by the TTL keeper where automatic restoration is allowed: 1 of 1 passed in 44.0 s
degraded-indexing The embedder fails
Threats O7 · In-process, with the RPC or store simulated
Procedure. Make the embedding model throw or time out during search and index builds.
Expected. Search answers lexical-only with partialResults: true; index builds are counted as degraded.
Tests
packages/search/test/search.test.ts: answers lexical-only, as partial results, when the embedder fails or runs out of timepackages/search/test/search.test.ts: builds a lexical-only snapshot when listings cannot be embedded
- answers lexical-only, as partial results, when the embedder fails or runs out of time: 1 of 1 passed in 0.0 s
- builds a lexical-only snapshot when listings cannot be embedded: 1 of 1 passed in 0.0 s
mcp-cap Paid calls above the agent’s caps
Threats M1 · Private Stellar network (1 s ledgers)
Procedure. Ask rail402-mcp and the guarded signer to pay above the per-call cap; run concurrent payments against one session budget.
Expected. Refused before signing; the session budget is never exceeded; balances unchanged.
Tests
packages/mcp/test/pay.integration.test.ts: refuses a price above the caller’s cap before signingpackages/sdk/test/guard.integration.test.ts: refuses to sign a payment above the per-call cappackages/sdk/test/guard.test.ts: never lets 20 concurrent payments exceed the session budget
- refuses a price above the caller’s cap before signing: 1 of 1 passed in 0.0 s
- refuses to sign a payment above the per-call cap: 1 of 1 passed in 0.0 s
- never lets 20 concurrent payments exceed the session budget: 1 of 1 passed in 0.1 s
rejection-auditcasemcp-per-call-cap: Passed (tools/conformance/evidence/rejection-audit-hosted-stellar-testnet.json)
mcp-ssrf Paid calls to private destinations
Threats X6 · Local sockets and a stub resolver
Procedure. Ask the buyer to pay URLs on this machine in every IP spelling and a name that resolves to a private address.
Expected. Refused before anything is sent, without naming the address.
Tests
packages/safe-fetch/test/egress.integration.test.ts: refuses IP-literal URLs to this machine in every spelling, before connectingpackages/sdk/test/http-flow.test.ts: refuses private destinations before sending anything
- refuses IP-literal URLs to this machine in every spelling, before connecting: 1 of 1 passed in 0.0 s
- refuses private destinations before sending anything: 1 of 1 passed in 0.0 s
rejection-auditcasemcp-private-address: Passed (tools/conformance/evidence/rejection-audit-hosted-stellar-testnet.json)
mcp-secret-scan Canary key scan of the MCP server, SDK and CLI
Threats M3 · Private Stellar network (1 s ledgers)
Procedure. Pay through every outcome with a canary signing key, capturing protocol frames, tool results, logs at trace level, stdout, stderr and the budget ledger.
Expected. No seed, raw key bytes, signature or payment header appears in any captured channel.
Tests
packages/mcp/test/secrets.integration.test.ts: leaks nothing into frames, logs or the ledgerpackages/sdk/test/secrets.integration.test.ts: leaks nothing, and the payment reached only the paid originpackages/cli/test/secrets.integration.test.ts: leaks nothing on stdout, stderr or in the ledger
- leaks nothing into frames, logs or the ledger: 1 of 1 passed in 0.1 s
- leaks nothing, and the payment reached only the paid origin: 1 of 1 passed in 0.1 s
- leaks nothing on stdout, stderr or in the ledger: 1 of 1 passed in 0.1 s
upto-over-cap An upto settlement above the cap
Threats U1 · In-process, with the RPC or store simulated
Procedure. Ask the upto facilitator to settle more than the signed cap.
Expected. Refused before simulating, with the payer and a reason.
Tests
packages/upto/test/facilitator.test.ts: refuses an amount above the cap before simulating
- refuses an amount above the cap before simulating: 1 of 1 passed in 0.0 s
live-matrixcaseupto-over-cap: Passed (tools/conformance/evidence/live-matrix-hosted-stellar-testnet.json)rejection-auditcaseupto-settle-over-cap: Passed (tools/conformance/evidence/rejection-audit-hosted-stellar-testnet.json)
upto-zero Zero upto settlements
Threats R5, U6 · Private Stellar network (1 s ledgers)
Procedure. Settle an upto payment for zero on chain, and off chain where configured.
Expected. On chain: the nonce is consumed and no funds move. Off chain: no transaction, the authorization used up in the ledger.
Tests
packages/facilitator/test/upto.integration.test.ts: settles zero on-chain: the nonce is consumed and no funds movepackages/facilitator/test/upto.integration.test.ts: settles zero off-chain when configured: no transaction, the authorization used up in the ledger
- settles zero on-chain: the nonce is consumed and no funds move: 1 of 1 passed in 4.7 s
- settles zero off-chain when configured: no transaction, the authorization used up in the ledger: 1 of 1 passed in 3.7 s
live-matrixcaseupto-zero: Passed (tools/conformance/evidence/live-matrix-hosted-stellar-testnet.json)live-matrixcaseupto-c-zero: Passed (tools/conformance/evidence/live-matrix-hosted-stellar-testnet.json)live-bazaarcaseupto-offchain-zero: Passed (tools/conformance/evidence/live-bazaar-stellar-testnet.json)
upto-concurrent Concurrent upto requests for different amounts
Threats U3, F5 · Private Stellar network (1 s ledgers) and Postgres
Procedure. Send five settle requests for one upto authorization with different amounts at once; then settle a copy of a payload for zero before the seller settles it.
Expected. One success, four idempotency conflicts, one transfer; the first accepted amount holds the authorization and the seller’s later request is a conflict.
Tests
apps/rail402/test/idempotency.integration.test.ts: settles 5 concurrent requests for different amounts once: one success, four conflictsapps/rail402/test/upto-settlement.integration.test.ts: lets the first settle request fix the amount: a payload holder’s zero turns the seller’s settle into a conflict
- settles 5 concurrent requests for different amounts once: one success, four conflicts: 2 of 2 passed in 10.7 s
- lets the first settle request fix the amount: a payload holder’s zero turns the seller’s settle into a conflict: 1 of 1 passed in 7.5 s
live-matrixcaseupto-repeat-different-amount: Passed (tools/conformance/evidence/live-matrix-hosted-stellar-testnet.json)live-matrixcaseupto-concurrent-same-authorization: Passed (tools/conformance/evidence/live-matrix-hosted-stellar-testnet.json)
upto-third-party-amount An upto settlement landed first by another holder of the authority
Threats F4 · Private Stellar network (1 s ledgers)
Procedure. Land the same upto authorization from another process holding the authority, for the same and for a different amount.
Expected. Same amount: success with that transaction. Different amount: settle_upto_stellar_settled_elsewhere naming both amounts.
Tests
packages/facilitator/test/upto.integration.test.ts: succeeds with that transaction when it settled the same amountpackages/facilitator/test/upto.integration.test.ts: answers settled_elsewhere when it settled a different amount
- succeeds with that transaction when it settled the same amount: 1 of 1 passed in 9.7 s
- answers settled_elsewhere when it settled a different amount: 1 of 1 passed in 8.8 s
rotation-channels The channel steps of a sponsor rotation
Threats O6 · Private Stellar network (1 s ledgers) and Postgres
Procedure. Run channels status, provision and retire against the network, with the memory store and with Postgres.
Expected. Channels are provisioned and merged back into the sponsor; retire refuses while a channel is leased.
Tests
apps/rail402/test/channels.integration.test.ts: reports, provisions and retires channel accounts with STORE=memoryapps/rail402/test/channels.integration.test.ts: reports durable state and refuses to retire while a channel is leased
- reports, provisions and retires channel accounts with STORE=memory: 1 of 1 passed in 9.8 s
- reports durable state and refuses to retire while a channel is leased: 1 of 1 passed in 7.5 s