Commit 1be2f41
committed
feat(mint): add a replayable append-only journal for mint state
The mint kept current state only in mutable tables, so every update erased the
previous value and every delete dropped the row. Auditing, replay, and
reconciliation of financially meaningful transitions were impossible after the
fact. Add a single insert-only `journal` table that is replayable on its own:
from an empty database a consumer loads each entity's creation snapshot and
applies its field-level deltas in id order to reconstruct current state. The
same table doubles as an ordered event stream keyed by a time-sortable id.
Event model (cdk-common `database::event_log`):
- `Event` is either a `Snapshot`, the full base object captured at creation
(mint quote, melt quote, proof, blind signature, keyset), or a `Delta`, one
writable field's new value. `From` conversions let call sites write
`value.into()` instead of spelling out the wrapping.
- Events serialize as JSON, the encoding the mint already uses for these types.
A binary format like CBOR is more compact but several cashu types do not
round-trip through a non-human-readable serializer.
- `id` is an in-house lock-free Snowflake i64 (millisecond timestamp, node id,
per-millisecond sequence) that is monotonic, time-sortable, and unique across
concurrent writers and, given a node id per instance, across mint instances.
Compound (entity, record) key:
- A row is identified by a typed `Entity` discriminant plus the row's bare
primary key, not a concatenated `"table_name:pk"` string. `Entity` has
explicit `#[repr(u8)]` discriminants, `as_u8`, and a `TryFrom<u8>` that
rejects unknown values; the writer derives it from the event via
`Event::entity()`, so an event and its entity can never disagree.
Schema (sqlite + postgres migrations):
- Add the insert-only `journal` table with columns `id`, `entity`, `record`,
`event`, `created_at`, indexed by `(entity, record, id)` so both a single
row's history and a whole entity type are indexed lookups.
Orchestration (mint layer + signatory):
- Journaling is a `JournalTransaction::add_journal` supertrait method shared by
the main write transaction and the keyset transaction. The SQL layer provides
only the durable append, running in the caller's transaction so it commits or
rolls back with the mutation it records. The mint decides which events to
emit and wires the issue, melt, swap, rollback, and compensation flows; the
signatory emits keyset creation and activation on rotation.
Serde support:
- Derive serde on the snapshotted domain types (`MintQuote`, `IncomingPayment`,
`Issuance`), reusing the existing amount-currency helper and adding
`amount_currency_serde_opt` for the optional amount field.
Tests:
- Flow-driven tests drive a real issue, swap, and melt and assert the emitted
journal rows replay to the expected state. A cdk-sqlite test replays the
keyset and melt-quote lifecycles and cross-checks every stored entity against
its event. Unit tests cover the id generator, the event-to-entity mapping,
and the discriminant round-trip; a `read_journal` helper reads rows back
through rusqlite.1 parent 76e7f15 commit 1be2f41
43 files changed
Lines changed: 1397 additions & 95 deletions
File tree
- crates
- cashu/src/nuts
- cdk-common/src
- database
- mint
- mint
- cdk-lnd/src/proto
- cdk-signatory/src
- cdk-sql-common/src
- mint
- auth
- migrations
- postgres
- sqlite
- wallet
- cdk-sqlite/src/mint
- cdk-supabase/src
- wallet
- cdk
- src
- mint
- issue
- melt
- melt_saga
- tests
- swap
- swap_saga
- tests
- test_helpers
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| 25 | + | |
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
| |||
0 commit comments