PR H: architecture note amendments (§5.1 trust / §9.2 settlement / §4.1 unwind) #12

Merged
nsatoshi merged 1 commits from devin/1776876637-arch-note-amendments into main 2026-04-22 17:13:01 +00:00

View File

@@ -0,0 +1,236 @@
# Architecture Note — Amendments
**Reference:** *Multi-Layer Atomic Settlement Architecture for SBLC Issuance and
Payment Coordination* (Draft 1.0).
**Purpose:** Three amendments identified during the CurrenciCombo gap-analysis
(§2 of `docs/ADRs` / gap-analysis note) that tighten the contract between the
note and the orchestrator implementation landing in PRs AG.
These amendments are **normative**: where the text here conflicts with the
original draft, this document takes precedence.
---
## Amendment 1 — §5.1 Transaction Coordinator (trust model)
### Problem
The original §5.1 names the Transaction Coordinator but does not specify **who
runs it** or **what trust assumptions the other participants must accept** to
use it. In a multi-bank SBLC + payment flow this is not a detail — the
Coordinator holds the state registry, issues `transaction.prepared`
instructions, and decides `COMMITTED` vs `ABORTED`. Whoever runs it is, in
effect, the workflow authority.
Three candidate topologies exist:
1. **Single-party hosted** — one participant (e.g. the issuing bank, the
beneficiary's bank, or a shared utility) runs a single Coordinator instance
and the rest consume its API.
2. **Federated** — each participant runs their own Coordinator; they reach
consensus over the state via signed events exchanged peer-to-peer (the
architecture note §7 normalised events).
3. **Neutral third-party utility** — a non-participant (e.g. a FinTech utility,
a central bankadjacent entity, or an SRO) runs the Coordinator under a
published operating model.
### Amendment text (replaces §5.1)
> **5.1 Transaction Coordinator.** Central orchestration service that manages
> the lifecycle of a transaction instance. The operator of the Coordinator
> SHALL be named in the governing documents (§4.1) as the *Workflow Authority*.
> The Workflow Authority:
>
> - is a single named legal entity for any given transaction;
> - MUST be a participant in, or a party contractually bound to, that
> transaction's governing documents;
> - MUST NOT be the same entity that provides the Identity and Authorization
> Service (§5.8) or the Ledger Anchor (§5.7) — separation of the control
> plane, the trust anchor, and the audit anchor is a requirement, not an
> option;
> - MUST publish its operating model, availability commitments, and exception
> escalation paths to all participants;
> - MUST sign every state transition it records to the State Registry (§5.6)
> with a key bound to its identity in the Identity and Authorization
> Service; participants verify those signatures before accepting a state as
> canonical.
>
> CurrenciCombo's reference topology is (1) **single-party hosted**, where
> the issuing bank of the SBLC operates the Coordinator and the payment-side
> bank, beneficiary, and applicant consume its API. Federated (2) and
> neutral-utility (3) topologies are out of scope for v1 but are not
> prohibited — they can be layered on top by replacing the Coordinator
> implementation while preserving the API surface.
### Implementation impact
- **`orchestrator` (CurrenciCombo)**: the orchestrator IS the Coordinator.
`NOTARY_REGISTRY_ADDRESS` (Ledger Anchor) and the signing key used for the
event bus (§5.8 / PR D `EVENT_BUS_SECRET`) must be held in separate key
stores. PR A's SoD matrix (`stateMachine.ts`) already prevents a single
actor from driving the 4 SoD-gated transitions.
- **Operational**: the config (`orchestrator/src/config/env.ts`) SHOULD grow
a `WORKFLOW_AUTHORITY_NAME` + `WORKFLOW_AUTHORITY_JWK_URL` pair so
consumers can resolve and verify the Coordinator's identity without
out-of-band trust. Tracked as a follow-up ticket; not blocking.
---
## Amendment 2 — §9.2 Commit Rule ("accepted ≠ settled")
### Problem
§9.2 currently reads:
> A transaction may enter **COMMITTED** only when:
> - the instrument leg has produced valid dispatch evidence
> - the payment leg has produced valid settlement or **accepted completion
> evidence**
> - all key transaction attributes reconcile against expected values
> - no outstanding exception blocks remain
The phrase "accepted completion evidence" is too loose. In SWIFT and ISO 20022
terms, **acceptance is not settlement**:
| Message | Meaning | Is settlement? |
|------------------|-------------------------------------------------|----------------|
| `pacs.002 ACCP` | Instruction technically accepted by receiver | **No** |
| `pacs.002 ACSP` | Accepted, settlement in process | **No** |
| `pacs.002 ACSC` | Accepted, settlement completed | Yes |
| `camt.025 ACCP` | Receipt: accepted | **No** |
| `camt.025 ACSC` | Receipt: settlement completed | Yes |
| `camt.054 CRDT` | Account credit notification | Yes (on receiver) |
| MT910 | Confirmation of credit | Yes |
| MT900 | Confirmation of debit | Yes (on sender) |
Treating `ACCP` as sufficient for `COMMITTED` introduces a window where the
Coordinator has locked-in the issuance but the payment has not cleared — the
exact failure mode the two-phase commit was meant to prevent.
### Amendment text (replaces §9.2)
> **9.2 Commit Rule.** A transaction may enter **COMMITTED** only when **all**
> of the following are true:
>
> 1. The instrument leg has produced valid dispatch evidence: an authenticated
> `MT760` issuance acknowledgment or an ISO 20022 instrument-specific
> equivalent, signed and time-stamped.
> 2. The payment leg has produced valid **settlement** evidence — not merely
> acceptance. Valid settlement evidence is one of:
> - `pacs.002` with status `ACSC` on the pacs.009 / pacs.008 interbank
> leg;
> - `camt.025` with status `ACSC`;
> - `camt.054` credit notification referencing the expected
> `EndToEndIdentification`, `InstructedAmount`, and `Currency`;
> - An `MT910` (credit confirmation) on the beneficiary side or an
> `MT900` (debit confirmation) on the originator side with matching
> transaction reference and amount.
> 3. The Coordinator has run the `VALIDATING` phase (§4.3 / PR B) and all
> reconciliation checks have passed — in particular amount, currency,
> credit/debit direction, and end-to-end identifier.
> 4. No outstanding exception blocks remain in the Exception Manager (§5.9 /
> PR B).
>
> `ACCP` / `ACSP` / `PDNG` statuses SHALL NOT satisfy (2) on their own. If
> only acceptance-level evidence has arrived and the settlement-deadline
> timer has not expired, the transaction remains in `VALIDATING`. On timer
> expiry, the Coordinator transitions to `ABORTED` under §9.3 timing-exception
> rules.
### Implementation impact
- **`orchestrator/src/services/swift/camt.ts` (PR E)**: `parseCamt025` already
distinguishes `ACCP | ACSC | ACSP | RJCT | PDNG`. The
`ExecutionCoordinator` must only accept `ACSC` (camt.025) or `CRDT`
(camt.054 matching reconciliation) as the settlement trigger. **Tracked
follow-up**: wire this into `executionCoordinator.validatePlan()` so that
`VALIDATING → COMMITTED` is blocked when the latest camt message is
`ACCP` / `ACSP`. Current code does not reference these statuses yet;
correctness is preserved today only because the mocked dispatch always
synthesises `ACSC`.
- **`orchestrator/src/services/exceptionManager.ts` (PR B)**: add a
`Timing.settlementDeadlineExpired` class routed to `ABORTED`. Current
taxonomy has generic `Timing.dispatchTimeout` / `acknowledgmentDelay`
which is too coarse for this distinction.
---
## Amendment 3 — §4.1 UNWIND_PENDING matrix (MT760 irrevocability)
### Problem
§8.1 defines a single `UNWIND_PENDING` state after `ABORTED`, and §11 Phase 6
says "if needed, initiate unwind process". This glosses over a hard banking
fact: **an issued MT760 guarantee/SBLC is irrevocable under UCP 600 / URDG
758** once it has been dispatched to the beneficiary's bank. The set of
"unwind" actions is therefore not uniform — it depends on *which leg* has
progressed how far.
The original note's state diagram implies `UNWIND_PENDING` is reachable from
any `ABORTED`, regardless of what was already dispatched. That is
operationally wrong: if the MT760 has left the issuing bank and been
authenticated by the beneficiary's bank, the instrument itself **cannot be
withdrawn unilaterally** — it can only be discharged (on expiry or on
beneficiary release) or replaced by a counter-instrument.
### Amendment text (adds §4.1.1 and refines §8.1 / §11 Phase 6)
> **4.1.1 Instrument irrevocability matrix.** The `UNWIND_PENDING` state
> SHALL NOT imply that the instrument leg is reversible. The set of unwind
> actions available depends on the observable state of each leg at the
> moment of `ABORTED`:
>
> | Instrument-leg observable state | Instrument unwind action |
> |--------------------------------------------|--------------------------|
> | `instrument.dispatched` not yet emitted | **Withdraw** — cancel before dispatch. No counter-instrument needed. |
> | `instrument.dispatched` emitted, `instrument.acknowledged` not yet emitted | **Recall request** — non-binding; beneficiary's bank MAY reject. If rejected, fall through to "acknowledged" row. |
> | `instrument.acknowledged` emitted | **Irrevocable — no unwind available.** The instrument stands until expiry (§11 tenor) or beneficiary-side release. The only control-plane actions are (a) accelerated expiry on mutual written consent, (b) issuance of a **counter-guarantee** from the beneficiary of the original instrument back to the applicant, (c) legal discharge via governing-law procedure. |
>
> | Payment-leg observable state | Payment unwind action |
> |--------------------------------------------|--------------------------|
> | `payment.dispatched` not yet emitted | **Withhold** — do not dispatch. |
> | `payment.dispatched` emitted, `payment.accepted` not yet emitted | **Recall** (`pacs.028` request-to-modify / `camt.056` request-for-cancellation). Best-effort. |
> | `payment.accepted` but not `payment.settled` | **Recall before settlement** — MAY succeed depending on receiver bank's processing window. |
> | `payment.settled` | **Return payment** — requires a fresh, separately-instructed return payment (`pacs.009` in reverse direction). Not an unwind of the original; a compensating transfer. |
>
> `UNWIND_PENDING` is a **state of the orchestrator**, not a guarantee that
> the underlying banking artefacts can be reversed. On entry to
> `UNWIND_PENDING`, the Coordinator SHALL record in the State Registry the
> observable state of each leg at the moment of `ABORTED` and the unwind
> action selected from the matrix above.
### Implementation impact
- **`orchestrator/src/services/stateMachine.ts` (PR A)**: the transition
table is unchanged (`ABORTED → UNWIND_PENDING → CLOSED` remains valid).
What changes is the *payload* recorded on the `ABORTED → UNWIND_PENDING`
transition — the `reason` field must include the instrument-leg and
payment-leg observable states.
- **`orchestrator/src/services/execution.ts`**: on entry to
`UNWIND_PENDING`, the Coordinator must select and persist the unwind
actions per the matrix. **Tracked follow-up**: this requires the
ExecutionCoordinator to consume real SWIFT events (PR E outbound +
inbound parsers are in place; wiring them to drive `instrument.dispatched`
/ `instrument.acknowledged` / `payment.*` events is a separate
coordinator-focused PR).
- **Portal `/transactions` page (PR G)**: the audit-trail card already
renders `reason` inline; no UI change required. The unwind-action
tracking will naturally surface as additional event rows.
---
## Summary of downstream tickets
Tracked as separate work items, not blockers for AG:
1. `WORKFLOW_AUTHORITY_NAME` + JWK URL in orchestrator env (Amendment 1).
2. Wire `executionCoordinator.validatePlan()` to discriminate `ACCP`/`ACSP`
from `ACSC`/`CRDT` using the PR E parsers (Amendment 2).
3. Add `Timing.settlementDeadlineExpired` to the Exception taxonomy
(Amendment 2).
4. Capture instrument-leg and payment-leg observable state in the
`ABORTED → UNWIND_PENDING` transition `reason` field (Amendment 3).
5. Persist the selected unwind action per the matrix in Amendment 3.
None of the five items regress AG — they extend behaviour on top of
already-landed structures.