Embedding the library
The worgl crate is the issuer itself: ledger, note format, transfer
protocol, policy, issuer state machine and crash-safe storage. The server is
built on it. Embed it when your program should be the mint, rather than talk
to one over HTTP.
cargo add worglA mint and a transfer
Section titled “A mint and a transfer”use worgl::{HolderKey, Issuer, IssuerKey, LedgerConfig, NoteStatus, Output, Policy, TransferRequest};
let ledger = LedgerConfig { layers: 4, layer_size: 1 << 16 };let mut issuer = Issuer::new(IssuerKey::generate(), ledger, Policy::cash_like())?;
// Alice makes a one-time key and is minted a note bound to it.let alice = HolderKey::generate();let note = issuer.mint(8, alice.public_key())?;
// Bob makes a one-time key; Alice signs the note over to it.let bob = HolderKey::generate();let request = TransferRequest::single(note, &alice, Output { amount: 8, holder: bob.public_key() })?;let bobs_note = issuer.transfer(&request)?.remove(0);
assert_eq!(issuer.status(¬e)?, NoteStatus::Spent);assert!(matches!(issuer.status(&bobs_note)?, NoteStatus::Live { position: 3, .. }));This is the example in the crate’s documentation, which CI runs. Issuer
keeps no accounts and stores no notes. HolderKey::generate makes the
one-time key a note is bound to, and TransferRequest is what its holder
signs to move it.
A note’s whole life
Section titled “A note’s whole life”cargo run --example lifecycle walks through a note’s whole life: mint,
split, pay, freeze in the last layer, rotate, replay refused, idle notes
expiring and refreshed notes living on. CI runs it on every push and pull
request.
//! A note's whole life, in memory: mint, split, pay, freeze, rotate, expire.//!//! Run with `cargo run --example lifecycle`.
use worgl::{ Error, HolderKey, Issuer, IssuerKey, LedgerConfig, Note, NoteStatus, Output, Policy, TransferRequest,};
/// A holder's notes, each with the one-time key it is bound to. This lives/// with the holder; the issuer never sees it.type Pocket = Vec<(Note, HolderKey)>;
fn main() -> Result<(), Box<dyn std::error::Error>> { let ledger = LedgerConfig { layers: 4, layer_size: 1024, }; let mut issuer = Issuer::new(IssuerKey::generate(), ledger, Policy::cash_like())?; println!("issuer {}\n", issuer.public_key());
// Alice makes a one-time key and is minted 16 into layer 2 of 0..=3. let mut alice: Pocket = Vec::new(); let key = HolderKey::generate(); let note = issuer.mint(16, key.public_key())?; show(&issuer, "minted to Alice", ¬e);
// Alice breaks the 16 into 8 + 4 + 2 + 1 + 1, each to a fresh key of her // own. The issuer sees one note become five, which it could already tell // were related. The split is a hop, so the new notes land in layer 3. let parts = [8, 4, 2, 1, 1].map(|amount| (amount, HolderKey::generate())); let outputs = parts .iter() .map(|(amount, k)| Output { amount: *amount, holder: k.public_key(), }) .collect(); let request = TransferRequest::sign(&[(note, &key)], outputs)?; for (note, (_, k)) in issuer.transfer(&request)?.into_iter().zip(parts) { alice.push((note, k)); } show(&issuer, "Alice's 4 after the split", &alice[1].0);
// Layer 3 is the last layer: those notes cannot move until a rotation. let refused = pay(&mut issuer, &mut alice, 4).unwrap_err(); println!("Alice pays 4 right away: refused, {refused}\n");
let report = issuer.rotate().unwrap(); println!( "rotation: generation {} dropped, {} notes destroyed\n", report.dropped, report.destroyed_notes );
// Alice pays Bob 5 as two one-note transfers, 4 and 1, to two fresh keys // Bob made. Neither transfer reveals that the other exists. let mut bob: Pocket = Vec::new(); for amount in [4, 1] { bob.push(pay(&mut issuer, &mut alice, amount)?); } show(&issuer, "Bob's 4", &bob[0].0);
// Alice kept a copy of the 16 she split. Spending it again is refused. let again = TransferRequest::single( note, &key, Output { amount: 16, holder: HolderKey::generate().public_key(), }, )?; let refused = issuer.transfer(&again).unwrap_err(); println!("Alice replays her old 16: refused, {refused}\n");
issuer.rotate().unwrap(); let mut carol: Pocket = Vec::new(); carol.push(pay(&mut issuer, &mut bob, 4)?); show(&issuer, "Carol's 4", &carol[0].0);
// Four rotations: Alice leaves her change alone, Carol refreshes hers by // paying it to a fresh key of her own once per period. for round in 1..=4 { let report = issuer.rotate().unwrap(); println!( "rotation {round}: {} notes worth {} destroyed", report.destroyed_notes, report.destroyed_value ); let (note, key) = carol.remove(0); let fresh = HolderKey::generate(); let output = Output { amount: note.amount(), holder: fresh.public_key(), }; let request = TransferRequest::single(note, &key, output)?; carol.push((issuer.transfer(&request)?.remove(0), fresh)); } println!(); show(&issuer, "Alice's idle 8", &alice[0].0); show(&issuer, "Carol's refreshed 4", &carol[0].0); println!("supply {:?}", issuer.supply()); Ok(())}
/// Pays the first note of exactly `amount` from `from` to a fresh key the payee makes.fn pay(issuer: &mut Issuer, from: &mut Pocket, amount: u64) -> Result<(Note, HolderKey), Error> { let i = from .iter() .position(|(note, _)| note.amount() == amount) .expect("the pocket holds a note of that amount"); let (note, key) = &from[i]; let payee = HolderKey::generate(); let output = Output { amount, holder: payee.public_key(), }; let request = TransferRequest::single(*note, key, output)?; let new = issuer.transfer(&request)?.remove(0); from.remove(i); Ok((new, payee))}
fn show(issuer: &Issuer, label: &str, note: &Note) { match issuer.status(note).expect("genuine note") { NoteStatus::Live { position, hops_left, expires_after_rotations, } => println!( "{label}: serial {}, layer {position}, {hops_left} hops left, \ dies after {expires_after_rotations} idle rotations\n", note.serial() ), other => println!("{label}: {other:?}\n"), }}Keeping it on disk
Section titled “Keeping it on disk”Issuer lives in memory. For durability, wrap it in PersistentIssuer,
which logs every event before applying it. Each change is an Event that
the issuer plans without side effects, the store writes and flushes to a
log, and only then the issuer applies. Applying is deterministic and
checked, so a snapshot plus its log rebuilds the exact same state, and a note
is never handed out unless the event that created it is on disk.
A store is a directory holding snapshot.bin, log.bin, keys.bin (the
generation keys’ secrets, readable by its owner only) and lock, which
stops two processes from writing the same store. See Backups.
PersistentIssuer::create(dir, issuer)makes a store for a new issuer;PersistentIssuer::open(dir, key, policy)opens one.batch()starts a group commit: each change takes effect in memory as it is made, and all of them reach the disk in one log entry and one flush. Nothing a batch returns may be handed out before it commits. The server puts every request waiting at the moment of a flush into one batch.compact()writes a fresh snapshot and purges the log up to it. Call it after rotations or wheneverlog_events()grows large.
If a batch’s flush fails, the issuer is ahead of its log: issuer() still
shows the batch’s changes, which never reached the disk, so do not answer
reads from it, and every further change is refused, until recover()
rebuilds it from the snapshot and log on disk or the store is reopened;
either restores exactly what reached the disk. A single change whose flush
fails is not applied at all. If the log could not even be cut back after a
failed write, the store is stopped instead, and only reopening it reads the
log again. state() tells which it is: InStep, Ahead or Stopped.
Opening compacts the log, and if that fails, as on a full disk, opens from
the snapshot and log as they are; take_compaction_error() then tells why.
Checking signatures outside a lock
Section titled “Checking signatures outside a lock”To keep signature checks out of a lock, verify with issuer.verifier()
first and pass the VerifiedTransfer in. A verifier is bound to the
issuer’s keys and policy as they are when you get it; a rotation adds a key,
so get a new one after each.
Minting
Section titled “Minting”Issuer::mint mints directly, for code that embeds the issuer. A server
should take signed mint requests instead: Issuer::authorized_mint accepts
a MintRequest signed by a key the policy lists in mint_authorities, and
logs the signature with the note. See
Mint authorities and keys.
Reference
Section titled “Reference”The Rust API reference documents every module:
ledger, note, keyset, transfer, policy, issuer, log, kek and
store.
v0.1Not yet independently security reviewed. Don't use it to hold anything you can't afford to lose.MIT or Apache-2.0