PR H: architecture note amendments (§5.1 trust / §9.2 settlement / §4.1 unwind) #12
236
docs/Architecture_Note_Amendments.md
Normal file
236
docs/Architecture_Note_Amendments.md
Normal 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 A–G.
|
||||
|
||||
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 bank–adjacent 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 A–G:
|
||||
|
||||
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 A–G — they extend behaviour on top of
|
||||
already-landed structures.
|
||||
Reference in New Issue
Block a user