Files
CurrenciCombo/docs/Architecture_Note_Amendments.md
nsatoshi 6166c48426
Some checks failed
CI / Frontend Lint (push) Failing after 8s
CI / Frontend Type Check (push) Failing after 7s
CI / Frontend Build (push) Failing after 6s
CI / Frontend E2E Tests (push) Failing after 7s
CI / Orchestrator Build (push) Failing after 6s
CI / Contracts Compile (push) Failing after 7s
CI / Contracts Test (push) Failing after 5s
Security Scan / Dependency Vulnerability Scan (push) Failing after 4s
Security Scan / OWASP ZAP Scan (push) Failing after 4s
PR H: architecture note amendments (§5.1 trust / §9.2 settlement / §4.1 unwind) (#12)
2026-04-22 17:12:59 +00:00

237 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.