Skip to content

id: design: RateAuthorisation as a signed ID-graph object — schema, signing, revocation #2290

Open

Description

@dezren39

From the conversion-engine design on #2063 §2 (feeds #2043; parent epic #2056; relates to the issuance-in-graph investigation #2041).

Define the RateAuthorisation as a signed ID-graph object:

  • Schema for the fields in id: design: conversion engine — linked transfer pairs, rate authorisation, liquidity accounts #2063 §2: id, version, issuer (LP) identity, source/dest ledgers, integer-rational rate (rate_num/rate_den, scale-aware), per-conversion min/max, total cap, authorised callers (any / allowlist / single counterparty), not_before + mandatory expires_at, one-direction-only (reverse is a separate object).
  • Signing: the object is signed with the issuer's ID key; specify how the gateway (id: design: currency control plane — registry, account factory, gateway #2062) verifies the signature chains to the identity controlling both liquidity accounts.
  • Revocation: representation + how the control plane's revocation set is checked at submission; revocation stops new conversions and never touches posted ones.
  • Quote binding: how a short-lived single-conversion quote references an authorisation (authorisation_id + amounts + fee + expiry).

Acceptance criteria:

Schema/design work only in this issue; the gateway implementation belongs to #2062's follow-ons.

Activity

  1. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  2. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Nov 1, 2026
  3. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  4. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Design record — issue #2290, RateAuthorisation as a signed ID-graph object (task 5741 triage)

    The decision to be made: the graph-native representation of a RateAuthorisation — schema, how the gateway (#2062) verifies that its signature chains to the identity controlling both liquidity accounts, how revocation is represented and checked, and how a quote binds to an authorisation — precise enough for the gateway follow-ons to implement against.

    Evidence at origin/main (3cbab2d, verified fresh this run)

    • The ledger-facing field set is already decided by the conversion-engine design on issue id: design: conversion engine — linked transfer pairs, rate authorisation, liquidity accounts #2063 §2: id (u128), version, issuer_id, one-direction source_ledger/dest_ledger, integer-rational rate_num/rate_den applied scale-aware (y = floor(x · rate_num / rate_den), rounding always down in the LP's favour), min_amount/max_amount, total_cap with a control-plane remaining_volume counter decremented only on POSTED, authorised_callers (any | allowlist | single counterparty), mandatory expires_at, and a control-plane revocation set checked at submission. TigerBeetle never sees the object; the gateway checks it before submitting.
    • The graph-side division of ownership is also already decided: the token-issuance investigation (issue id: investigate: token issuance in the ID graph — per-ID issuance, issuer snapshots, authorized conversion rates #2041, CLOSED; doc on main at doc/2026-10-30T19-18-00Z_investigation_token_issuance_id_graph/, lines 62–65) states conversions surface in the graph as per-pair, per-direction signed offers — "ID 123 accepting ABC at 0.8123 is one signed offer; the reverse is a separate offer or none" — and that the RateAuthorisation field schema is issue id: design: RateAuthorisation as a signed ID-graph object — schema, signing, revocation #2290's to define.
    • The implementation pattern for signed graph objects already exists in shipped code: pkgs/id/src/attestation.rs:1-25 — PoC objects carried as ordinary signed chained events (crate::chain) whose bodies are the objects, with kind constants (attestation.genesis, attestation.claim) and state computed from the objects alone by a viewer/fold. Identity is the 32 bytes of the signing key's verifying key (attestation.rs:40-47), and event signatures are verified by chain::decode/verify_strict.
    • Revocation-by-signed-event is the house pattern too: group authorisation changes are signed log events, e.g. the Revoke entry at pkgs/id/src/groups/mod.rs:141-142.
    • Issue id: design: currency control plane — registry, account factory, gateway #2062 (currency control plane: registry, account factory, gateway) is CLOSED with its design doc on main (doc/2026-10-29T12-42-21Z_design_currency_control_plane/); issue id: design: ID currencies v1 #2043 (currencies v1 synthesis) remains open and gated. No currency.* kinds or RateAuthorisation types exist in pkgs/id/src at origin/main.

    Options considered

    1. Ordinary signed chain event, new kind currency.rate_authorisation, body = the id: design: conversion engine — linked transfer pairs, rate authorisation, liquidity accounts #2063 §2 fields (recommended). Graph-native, auditable, folds like every other ID object; matches the attestation.rs precedent and the id: investigate: token issuance in the ID graph — per-ID issuance, issuer snapshots, authorized conversion rates #2041 division of ownership.
    2. Detached-signed control-plane database record. Rejected: not a graph object — issuer snapshots (id: investigate: token issuance in the ID graph — per-ID issuance, issuer snapshots, authorized conversion rates #2041 §5) and app APIs (convert, §6 of that doc) need the offer in the graph; a DB record would need a second audit story.
    3. A TigerBeetle-side representation. Rejected by issue id: design: conversion engine — linked transfer pairs, rate authorisation, liquidity accounts #2063 itself: TigerBeetle never sees the authorisation; the gateway checks it.

    Recommendation — the object

    • Kind: currency.rate_authorisation. Body fields: authorisation_id (u128, stable across versions), version (monotonic per id), issuer_id (verifying-key bytes; must equal the event author), source_ledger, dest_ledger, rate_num/rate_den (u128), min_amount, max_amount, total_cap (u128, source-ledger units), authorised_callers (any | allowlist: [ID…] | single: ID), not_before, expires_at (mandatory), reversal_window_secs (optional; governs post-posting reversals at the original rate, per issue id: design: conversion engine — linked transfer pairs, rate authorisation, liquidity accounts #2063 §4.4), fee_bps (optional; fees are non-refundable unless stated, per §4.4).
    • Versioning: a new version is a new signed event on the issuer's chain with the same authorisation_id and version + 1. State is a fold: the live object is the highest non-revoked version. No in-place edits, ever.
    • Worked example (both directions, as required): LP identity 123 offers ABC→123 at 0.8123: {authorisation_id: 7, version: 1, issuer_id: 123, source_ledger: ABC, dest_ledger: 123, rate_num: 8123, rate_den: 10000, …} — 100 ABC (at equal ledger scales) yields floor(100 · 8123 / 10000) = 81.23 → 81 whole dest units before scale normalisation, remainder never credited out. The reverse is a separate object by the ABC-side LP: {authorisation_id: 8, version: 1, issuer_id: ABC-issuer, source_ledger: 123, dest_ledger: ABC, rate_num: 120, rate_den: 100, …} (1.20 ABC per 123) — its own rate, its own cap, its own expiry; neither object implies the other.

    Signing + gateway verification flow (for the issue #2062 follow-ons to implement)

    1. Decode + signature: chain::decode verifies the event signature; the header's verifying key must equal body issuer_id — a body naming another issuer is invalid, not merely untrusted.
    2. Issuer eligibility: the gateway checks the issuer's LP tier (issue id: design: conversion engine — linked transfer pairs, rate authorisation, liquidity accounts #2063 §3: Issuer LP, Registered LP, or Platform LP) in the control-plane registry (issue id: design: currency control plane — registry, account factory, gateway #2062 design).
    3. Account-control chain: the account factory's binding records (control-plane, factory-signed) map issuer_id → (source liquidity account, dest liquidity account). The gateway verifies both accounts named by the conversion are exactly those bound accounts, and neither is closing/closed. An authorisation for a pair where the issuer holds no bound account pair is ineligible, however valid its signature.
    4. Live-state checks at submission: highest version folded, not revoked (below), not_before ≤ now < expires_at, caller ∈ authorised_callers, amount within [min_amount, max_amount], remaining_volume ≥ amount. Any failure is pre-flight (FAILED_PRE_SUBMIT, issue id: design: conversion engine — linked transfer pairs, rate authorisation, liquidity accounts #2063 §4.1) with no TigerBeetle writes.

    Revocation

    • Representation: a signed event currency.rate_authorisation_revoke on the issuer's chain: {authorisation_id, revoked_version_from, effective_at, reason (optional)}. Only the issuer's signature revokes in the graph.
    • Control-plane check: the control plane materialises a revocation set by folding revoke events; the gateway consults the set at submission time (not quote time). Revocation stops new conversions; posted conversions are untouched and can only be reversed by a compensating conversion (issue id: design: conversion engine — linked transfer pairs, rate authorisation, liquidity accounts #2063 §4.4).
    • Platform safety: the platform cannot forge a graph revocation; it can only suspend gateway acceptance of an authorisation as a local, logged control-plane policy. (This is Drew-question Q1 below.)

    Quote binding

    A quote is a short-lived, gateway-issued control-plane record (not a graph object): {quote_id, authorisation_id, authorisation_version, authorisation_event_hash, x, computed_y, fee, issued_at, quote_expires_at} with a 60-second lifetime (issue #2063 §2 suggestion, adopted). The event-hash + version pinning means a re-issued or revoked authorisation can never silently satisfy an old quote: execution must reference an unexpired quote and re-run the live-state checks at submission; any mismatch fails pre-flight.

    Phased implementation packets (filed + enqueued this run)

    1. Issue id: match attestation v1 — signed result objects, state machine, counting views in pkgs/id (from #2039) #3792 — currency.rate_authorisation / revoke schema types + fold (live-version + revocation state) in pkgs/id, library-only, with the worked-example golden tests. Task + dedupe key listed in the routing comment below.
    2. Issue id: match audit v0 — netcode log format + tic-tac-toe exporter + verifier CLI (L0-L2) (from #2039) #3793 — gateway verification flow (steps 1–4 above) + revocation-set materialisation in the control plane, against the Phase 1 types; serves the issue id: design: currency control plane — registry, account factory, gateway #2062 follow-ons.
    3. Issue id: protocol v1 phase 1 — session header + segment checkpoint event kind (#2036) #3794 — quote issuance + binding (quote record, hash/version pinning, submission revalidation).

    Questions that genuinely need Drew (explicit, with recommendation)

    • Q1 — platform emergency revocation: may the platform identity revoke another LP's authorisation in the graph? Options: (a) issuer-only graph revocation; the platform can only suspend gateway acceptance locally, as a logged policy (recommended — a forgeable-by-platform revocation would make every authorisation's audit trail ambiguous); (b) a platform-signed currency.rate_authorisation_platform_revoke kind the gateway also honours (faster incident response, but introduces a second revocation authority every verifier must model). Neither option blocks Phase 1.
    • Explicit statement: no other Drew-only questions — field semantics, rounding, and failure behaviour are already decided by issue id: design: conversion engine — linked transfer pairs, rate authorisation, liquidity accounts #2063, and representation choices above follow shipped precedents.

    🤖 Agent note · triaged

  5. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Correction + routing update (task 5741): the design record above anticipated phase issue numbers #3792–#3794, but those numbers were taken by other filings before the phase issues were created. The actual routing is:

    Phase Issue Queue task Dedupe key
    1 — schema types + fold issue #3800 task 5789 repo-health:v1:fix:developing-today/code:issue:3800
    2 — gateway verification + revocation set issue #3801 task 5790 repo-health:v1:fix:developing-today/code:issue:3801
    3 — quote issuance + binding issue #3802 task 5791 repo-health:v1:fix:developing-today/code:issue:3802

    All three tasks are queued on repo-health-code-pilot with queue/queued on their issues; Phases 2 and 3 name their dependencies in their packets. Read the design record's phase list with these numbers substituted.

  6. 16 remaining items

  7. added
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Nov 3, 2026
  8. developing-today commented on Nov 3, 2026

    @developing-today
    Contributor

    agent-note:v1
    state: triaged (coverage pass)
    Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 40 (priority/normal). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
    Found via: coverage pass 2026-11-03

  9. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
  10. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  11. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · escalated

    Triage verdict (task 8607): design delivered and slice 1 implemented — one Drew question (Q1) and one small gap remain

    Delivered, verified at origin/main:

    Small gap (not Drew's): acceptance criterion 1 asks for a worked example (ABC→123 at 0.8123, both directions as separate objects). The doc has the rate formula and requires an end-to-end worked case as implementation criterion 8, but no concrete ABC→123 worked example is recorded in the doc. A short doc PR adding it closes criterion 1 fully.

    The one Drew decision — design doc Q1, platform emergency revocation: should the platform be able to revoke another LP's authorisation in the graph?

    • A (doc-recommended): issuer-only graph revocation; the platform may locally suspend gateway acceptance with a logged control-plane policy.
    • B: add a platform-signed currency.rate_authorisation_platform_revoke event every verifier honours.

    My read: the doc's reasoning for A is sound (the graph stays a truthful record of issuer intent; incident response keeps a kill switch via local suspension), and slice 1 as implemented does not depend on the answer — but the gateway projection slice does, so Q1 should be answered before that slice is built. Escalating on Q1 only.

  12. added
    user/escalatedQueue or user escalation state
    and removed
    user/reviewNeeds a user-look: Muse reviews first and handles what it can
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)kind/enhancementpriority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this itemuser/escalatedQueue or user escalation state

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions