Skip to content

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 worgl
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(&note)?, 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.

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.

crates/worgl/examples/lifecycle.rs
//! 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", &note);
// 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"),
}
}

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 whenever log_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.

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.

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.

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