Skip to content

Commit 1be2f41

Browse files
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

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cashu/src/nuts/nut09.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub struct RestoreResponse {
2222
pub signatures: Vec<BlindSignature>,
2323
}
2424

25+
#[cfg(test)]
2526
mod test {
2627

2728
#[test]

0 commit comments

Comments
 (0)