Brick #3: Seven Ways to Break a Chain — a Scenario Matrix for Verifiable Supply Chains
Seven violation scenarios — undeclared telemetry, fake sensor, missing evidence, GPS anomaly, receiver dispute, double settle — run the shipment lifecycle on an in-memory stack and pin the regression contract before testnet. Includes the adversarial AI audit story: 0 Critical, 6 High, 7 Medium findings, all fixed or documented.
A scenario matrix is a fixed set of end-to-end test scenarios — happy path plus named violations — that run a shipment through offer → handoff → settle → verify on an in-memory ledger, then assert replayed state and advisory flags. Ours covers seven scenarios S1–S7, survives an adversarial AI audit (0 Critical, 6 High, all fixed), and proves the primitive's state machine stays honest under violations — while flags stay advisory, never blocking, in v1.
Part of the verifiable supply-chain series — start here: The Bedrij Principle, then Brick #1: Typed Participants and Brick #2: Typed Evidence. The hypothesis under test: a complex economic system can be composed from verifiable state transitions.
A primitive that only works in demos is not a primitive. Before a supply-chain transition contract ever touches a testnet, you want to know exactly which violations it survives — and which it doesn't, yet.
Bricks #1 and #2 built the happy path: typed participants and typed evidence travelling inside the handoff event itself. This brick goes the other direction. Instead of asking "does it work?", we asked "how does it break?" — seven scenarios, each one a violation a real chain will eventually see: undeclared telemetry, a rogue sensor, a missing inspection photo, a forged replay, a receiver dispute, a double settle racing the lock.
This is Brick #3 — the third experimental proof in the Bedrij-series hypothesis, and the first that proves resilience rather than shape: the primitive has a regression contract. Every violation is exercised on an in-memory stack, its expected outcome is asserted, and after an adversarial AI audit found six real High-severity holes, every one of them got its own regression test.
What is a supply-chain scenario matrix?
Short answer: a scenario matrix is a fixed set of
end-to-end test scenarios — happy path plus violations — that run a
shipment through the full lifecycle
(offer → handoff → settle → verify) on an in-memory ledger and
executor, then assert the replayed state and advisory flags. Ours is one
file, tests/supply-chain/scenario-matrix.test.ts, 18 tests
covering seven scenarios S1–S7 plus the audit hardening cases. Each
scenario answers one question: if this goes wrong, does the state
machine stay honest?
The stack is deliberately boring. makeChain() — a shared
fixture from Brick #2's audit — builds a shipment with N hops, typed
roles, per-hop evidence, then drives
accept → handoff → settle → verify on an
InMemoryHopExecutor with deterministic job IDs. No chain, no
RPC, no wall clock: the matrix replays the same events a production ledger
would store, which is the point — the regression contract covers the
projection, the part that turns an append-only log into answers
like "who holds custody" and "is this hop paid".
What do the seven scenarios actually prove?
Short answer: S1 proves the happy path end-to-end; S2–S5 prove each advisory flag fires on exactly the violation it names — and only there; S6 proves a receiver can halt the chain by disputing; S7 proves payment is exactly-once under concurrency. Every assertion checks both the write payload and the replayed read model — a scenario that only verifies the call succeeded would prove nothing about the projection.
| Scenario | Violation | Expected outcome |
|---|---|---|
| S1 happy path | none — 3-hop chain, declared device, party-issued telemetry | all hops verified, zero advisory flags |
| S2 temp violation | temperature ref from a source that is neither a hop party nor the declared device | tempViolation flag — settle still executes (advisory, not enforcement) |
| S3 fake sensor | telemetry-typed ref whose issuer address ≠ declared roles.device — including an issuer claiming type "org" | fakeSensor flag; with no declared device the flag stays off (cannot judge → don't guess) |
| S4 missing telemetry | a hop handoff carrying no evidence at all | missingEvidence flag, chain remains valid |
| S5 GPS anomaly | a gps ref from a non-party, non-device issuer | gpsAnomaly flag; a party-issued GPS ref stays clean |
| S6 receiver dispute | receiver rejects the handoff | disputed flag, settleHop refuses while disputed, chain halts at that hop |
| S7 double settle | two concurrent settleHop calls on the same hop | exactly-once: one executor call, one pay event — the loser queues on the per-hop lock |
Three details in that table matter more than the flags themselves.
First, flags are advisory, never blocking — that's a decision, not an omission. S2's temperature violation does not hold the payment; it records that a violation was asserted. Automatic hold is a dispute-engine problem (a later brick), and pretending v1 enforces it would be a lie printed in our own docs.
Second, S3 and S5 assert absences, not just presences. A scenario that only checks "the flag fired" is half a test — the audit showed exactly why (see below). Each violation scenario also proves the flag does not fire on the clean variant, and S1 genuinely exercises all five flags by giving the chain a declared device and real telemetry.
Third, S7 proves contention, not just outcome. A naive
exactly-once test asserts settled.length === 1 after
Promise.all — but if the lock never actually engaged, the
test passes vacuously. Ours uses a deferred executor: while the winner is
in-flight, we assert the loser is queued (executor unsettled,
ledger unchanged), then release and assert one settle + one
pay event.
What did the adversarial audit break?
Short answer: we ran the whole matrix through an external
adversarial AI audit before calling it done — verdict REQUEST CHANGES:
0 Critical, 6 High, 7 Medium. The Highs were not style
complaints: the projection trusted forged events, an
"inconclusive" verdict silently promoted a hop to
verified, verifyHop had no evaluator check or
lock, and a concurrent createShipment could mint orphan
funded jobs. All six are fixed with regression tests; three Mediums were
deliberately accepted and documented instead of silently patched.
The deepest finding — H-1 — is the one worth remembering.
The replay projection lifted state from any event with a
well-formed payload: a pay event forged at sequence 41 for a
non-plan hop index could mint a phantom paid hop, which then unlocked the
next handoff. A fake handoff could even advance a hop's status without a
single real delivery. The fix splits the projection into a two-pass,
canonical-sequence replay: the plan binds to the
work_offer at seq === 1 (first writer wins —
later offers can't redirect the payout target), and a handoff/pay/verify
event lifts state only if it lands in the canonical sequence slot for its
hop and stage, has an integer in-plan hop index, and is bound to the
current shipmentId. A forged event now lifts
nothing — not even status.
work_offer at seq 1 — and from then on
every event must pass the canonical gate: right sequence slot
for its hop and stage, an integer in-plan hop index, the bound
shipmentId. Passing lifts status, flags and disputes into
shipmentState; anything forged takes the drop path and
lifts nothing. Two second-order guards sit below the lift: a
verify promotes the hop only on an explicit
"pass" — "inconclusive" falls to the drop path
too — and settleHop refuses while the hop stands disputed.
A real settle emits its pay event back into the same
stream, which is what makes the whole read model replayable from the
ledger alone.
The other Highs were subtler trust holes: H-2 —
outcome:"inconclusive" (or any unknown outcome) lifted a hop
to terminal verified; now only an explicit
"pass" promotes. H-3 —
verifyHop was a paid-gate only: any actor could write the
verdict, and it bypassed every guard disputeHop had; now it
runs inside the hop lock, refuses disputed hops, and requires the actor
to be the evaluator (the next hop's receiver, or the client on the final
hop). H-4 — createShipment checked
existence, created funded onchain jobs, then appended the offer with no
lock — two concurrent calls could each create a funded job set; now the
whole check→create→append is atomic under the shipment lock.
H-5 — a settle that succeeded onchain but failed to
append to the ledger diverged silently; the error now carries the job ID
loudly, and a regression test pins the visible divergence.
H-6 — S5's negative case and S1's "zero flags" assertion
were vacuous: a GPS ref asserted by the very party it accused could never
prove anomaly detection, and S1 had no declared device so
fakeSensor could never even run. The tests were rebuilt to
be non-vacuous.
The Mediums we accepted are written down, not hidden: settle doesn't
compare the caller-supplied amount against the plan (the onchain budget
is the backstop — flagged for the hardening epic),
acceptShipment is a decorative v1 marker, and all
authorization is a caller-supplied actor string — fine inside the server,
as long as HTTP routes derive actor from auth, which is now a documented
boundary rule.
What does v1 NOT do — the honest boundary
Short answer: the matrix proves the primitive's state machine is honest — it does not prove enforcement. Advisory flags warn and never block; disputes halt the chain only while the flag stands; the hop lock is single-process; and there is no per-source ledger ACL — the projection trusts that the ledger's writers are honest, which is a boundary the hardening epic owns.
That honesty is the point of the exercise. A regression contract that claims more than the code does is worse than none — it's a false floor. The matrix instead pins exactly what v1 guarantees: canonical-sequence projection, evaluator-gated verification, pre-settle disputes, atomic shipment creation, exactly-once settle, and five advisory flags that fire on real violations and stay quiet on honest traffic.
What's deliberately out of scope: issuer registries (who may call themselves a sensor), oracle attestations (verifying the artifact, not the reference), a real dispute engine with resolution outcomes, distributed locks for multi-instance deployment, and plan-amount binding for settle calls. Each is a named future brick, not a hidden gap.
How do you reuse the matrix?
Short answer: the whole thing is one test file plus a
fixture — tests/supply-chain/scenario-matrix.test.ts and
tests/supply-chain/helpers/chain-fixture.ts.
makeChain({hops, roles, evidencePerHop, executor, lifecycle})
builds and drives any chain shape; failOnSettleJobId injects
executor failures; the replay projection lives in
src/server/lib/econ-ledger/supply-chain/project.ts. If you're
building transition-contract primitives, the pattern copies over: one
file of named violations, an in-memory executor, assertions on both
write and replay.
The matrix is also the reason the Bedrij hypothesis can keep stacking bricks. Brick #1 proved participants compose; Brick #2 proved evidence composes. Brick #3 proves the composition holds under violations — which is what makes the next layer (enforcement) a tractable problem instead of an open-ended one.
Current state: 72/72 supply-chain tests green, 18/18 in
the matrix itself, zero open Critical or High audit findings. The audit
report with per-finding resolution lives in the epic docs; the fix
commit is de2cf81 (fix(214-5)).
If you're building agent-native supply chains: the ledger package is
@agentbadge/econ-events, the read endpoint is
GET /api/ledger/shipments/:id, and the anchor hypothesis
this proves a piece of is in
The Bedrij Principle.
Don't certify. Measure.