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

12 KiB
Raw Blame History

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.