You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
id: design: RateAuthorisation as a signed ID-graph object — schema, signing, revocation #2290
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:
A schema + worked example (ABC→123 at 0.8123, both directions as separate objects) recorded in a design doc or on this issue.
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 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.
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)
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.
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.
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.
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)
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.
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:
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.
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
Triage verdict (task 8607): design delivered and slice 1 implemented — one Drew question (Q1) and one small gap remain
Delivered, verified at origin/main:
The design exists: doc/2026-11-01_design_id2290.md (346 lines), merged by PR design(id): #2290 RateAuthorisation schema and revocation #3831 ("design(id): id: design: RateAuthorisation as a signed ID-graph object — schema, signing, revocation #2290 RateAuthorisation schema and revocation"). It specifies the closed canonical-CBOR schema (authorisation_id, version, issuer_id, from/to currency, rate_num/rate_den, min/max, total_cap, authorised_callers, not_before, mandatory expires_at), signer authority, the signed currency.rate_authorisation_revoke kind with submission-time revocation-set checks (revocation stops new conversions, never touches posted ones), and quote binding (quote pins authorisation_id + version + event ID, revalidated at submission). Acceptance criterion 2 is met.
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.
Activity
developing-today commentedon Oct 29, 2026
CODE-1148
dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up
dezren39 commentedon Nov 1, 2026
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)
id(u128),version,issuer_id, one-directionsource_ledger/dest_ledger, integer-rationalrate_num/rate_denapplied scale-aware (y = floor(x · rate_num / rate_den), rounding always down in the LP's favour),min_amount/max_amount,total_capwith a control-planeremaining_volumecounter decremented only on POSTED,authorised_callers(any| allowlist | single counterparty), mandatoryexpires_at, and a control-plane revocation set checked at submission. TigerBeetle never sees the object; the gateway checks it before submitting.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.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 bychain::decode/verify_strict.Revokeentry atpkgs/id/src/groups/mod.rs:141-142.doc/2026-10-29T12-42-21Z_design_currency_control_plane/); issue id: design: ID currencies v1 #2043 (currencies v1 synthesis) remains open and gated. Nocurrency.*kinds or RateAuthorisation types exist inpkgs/id/srcat origin/main.Options considered
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.convert, §6 of that doc) need the offer in the graph; a DB record would need a second audit story.Recommendation — the object
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).authorisation_idandversion + 1. State is a fold: the live object is the highest non-revoked version. No in-place edits, ever.123offers 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) yieldsfloor(100 · 8123 / 10000) = 81.23 → 81whole 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)
chain::decodeverifies the event signature; the header's verifying key must equal bodyissuer_id— a body naming another issuer is invalid, not merely untrusted.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.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
currency.rate_authorisation_revokeon the issuer's chain:{authorisation_id, revoked_version_from, effective_at, reason (optional)}. Only the issuer's signature revokes in the graph.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)
currency.rate_authorisation/ revoke schema types + fold (live-version + revocation state) inpkgs/id, library-only, with the worked-example golden tests. Task + dedupe key listed in the routing comment below.Questions that genuinely need Drew (explicit, with recommendation)
currency.rate_authorisation_platform_revokekind the gateway also honours (faster incident response, but introduces a second revocation authority every verifier must model). Neither option blocks Phase 1.🤖 Agent note · triaged
dezren39 commentedon Nov 1, 2026
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:
repo-health:v1:fix:developing-today/code:issue:3800repo-health:v1:fix:developing-today/code:issue:3801repo-health:v1:fix:developing-today/code:issue:3802All three tasks are queued on
repo-health-code-pilotwithqueue/queuedon their issues; Phases 2 and 3 name their dependencies in their packets. Read the design record's phase list with these numbers substituted.16 remaining items
developing-today commentedon Nov 3, 2026
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
dezren39 commentedon Nov 3, 2026
🤖 Agent note · picked up
user/review: per protocol the worker reviews first — I am reviewing whether the design is already delivered, what remains, and whether any remainder is genuinely Drew's decisiondezren39 commentedon Nov 3, 2026
🤖 Agent note · escalated
user/review— reviewed by the worker first, per protocol)user/review→user/escalated); everything else is already delivered or routed.doc/2026-11-01_design_id2290.md, PR design(id): #2290 RateAuthorisation schema and revocation #3831, PR feat(id): currency.rate_authorisation + revoke types and fold (#3800) #3888Triage verdict (task 8607): design delivered and slice 1 implemented — one Drew question (Q1) and one small gap remain
Delivered, verified at origin/main:
doc/2026-11-01_design_id2290.md(346 lines), merged by PR design(id): #2290 RateAuthorisation schema and revocation #3831 ("design(id): id: design: RateAuthorisation as a signed ID-graph object — schema, signing, revocation #2290 RateAuthorisation schema and revocation"). It specifies the closed canonical-CBOR schema (authorisation_id, version, issuer_id, from/to currency, rate_num/rate_den, min/max, total_cap, authorised_callers, not_before, mandatory expires_at), signer authority, the signedcurrency.rate_authorisation_revokekind with submission-time revocation-set checks (revocation stops new conversions, never touches posted ones), and quote binding (quote pins authorisation_id + version + event ID, revalidated at submission). Acceptance criterion 2 is met.currency.rate_authorisation+ revoke types and fold. Independently verified at origin/main:pkgs/id/src/currency.rslines 39/42 defineRATE_AUTHORISATION_KIND/RATE_AUTHORISATION_REVOKE_KINDand line 322 signs a rate authorisation. Acceptance criterion 3 is met. Slices 2–3 (gateway projection; quote/execution binding) are gateway work belonging to issue id: design: currency control plane — registry, account factory, gateway #2062's follow-ons, as this issue's body states.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?
currency.rate_authorisation_platform_revokeevent 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.