Expand description
The holder’s side of Worgl: a note store that pays, receives and refreshes notes while keeping the privacy rules.
The mint sees every note’s lineage but not who holds what. That stays true only if holders follow a few rules, and this crate follows them so applications need not know them:
- A fresh key for every note. Two notes under one key tell the mint they share an owner.
- One note per transfer. A transfer that spends two notes tells the mint they had one owner. Change goes back to fresh keys.
- Spread in time. When a payment needs several notes, their transfers are submitted at random gaps; refreshes are spread across the rotation period. Transfers made together can be linked by timing alone.
- No order to read. Outputs are shuffled, so the payee’s notes are not always first.
The crate does no networking and keeps no files, so it runs anywhere, WebAssembly included. A client submits the transfers it makes and hands back the mint’s answers.
§A payment
- The payee calls
Holder::requestand sends thePaymentRequestto the payer: an amount as notes, each to a fresh key of the payee. - The payer calls
Holder::payand returns thePayment: signed transfers, each spending one of its notes, with a time to submit each. - The payee checks it with
PaymentRequest::check, submits each transfer at its time, takes the new notes in withHolder::absorb, and hands the rest of the answers,Holder::theirs, back to the payer: its change. - The payer checks their signatures and takes them in with
Holder::take_answers, which settles the transfers they answer. Without them, it resends its outgoing transfers at the timesHolder::resendgives and records each answer withHolder::settle, which works while the mint keeps its answers, and keeps the payee’s notes from it inHolder::payments, in case the payee has not submitted.
use worgl::{Issuer, IssuerKey, LedgerConfig, Policy};
use worgl_sdk::{Holder, MintInfo, MintView, Spacing};
// A mint in memory, standing in for the server.
let ledger = LedgerConfig { layers: 4, layer_size: 1 << 16 };
let mut mint = Issuer::new(IssuerKey::generate(), ledger, Policy::cash_like())?;
let view = |mint: &Issuer| {
MintView::new(&MintInfo {
issuer: *mint.public_key(),
layers: mint.layers(),
policy: mint.policy().clone(),
rotation: None,
paused: false,
})
};
// Alice is minted 8 in one note.
let (mut alice, mut bob) = (Holder::new(), Holder::new());
let funding = alice.request(&view(&mint)?, 8)?;
let note = mint.mint(8, funding.outputs[0].holder)?;
alice.absorb(&[note]);
// Bob asks for 5, Alice pays, Bob checks and submits.
let request = bob.request(&view(&mint)?, 5)?;
let payment = alice.pay(&view(&mint)?, &request, Spacing::default())?;
request.check(&payment)?;
for transfer in &payment.transfers {
let answer = mint.transfer(&transfer.request)?;
bob.absorb(&answer);
// Bob hands Alice the rest, her change; she checks it and keeps it.
let change = bob.theirs(&answer);
for note in &change {
note.verify(mint.keys())?;
}
alice.take_answers(&change);
}
// Bob's 5 is frozen until the next rotation; Alice has 3 in change.
assert_eq!(bob.balance(&view(&mint)?).frozen, 5);
assert_eq!(alice.balance(&view(&mint)?).frozen, 3);
assert_eq!(alice.outgoing().count(), 0);A mint’s info, read into a MintView, tells the store which notes can
move now: a note that just arrived sits in the last layer and is frozen
until the next rotation. The view holds the info’s layers, which no clock
moves, so read the info again after a rotation. Holder::refresh keeps
notes alive by paying the ones about to expire to the holder itself.
Structs§
- Balance
- Value a holder has at one mint, by what it can do now.
- Holder
- The notes one holder owns, with the one-time key of each, plus the keys it has handed out, the transfers it has signed but not yet seen land, and the payments it made until it is done with them.
- Made
Payment - A payment this holder made, kept while one of its transfers is outgoing or the holder keeps notes owed to the payee for it.
- Mint
Info - The parts of a mint’s
GET /v1/mints/{mint}/infoanswer a holder needs. Other fields in the answer are ignored. - Mint
View - What a holder knows about one mint: enough to tell, without asking it, whether a note can move now and when it dies.
- NoLayers
- A mint’s info has no layers.
- Payment
- The payer’s answer to a
PaymentRequest: signed transfers that, submitted to the mint, create the payee’s notes. - Payment
Request - What a payee asks to be paid: one fresh key of its own for each note it expects, so the mint never sees two of its notes under one key.
- Schedule
- A mint’s rotation schedule, as its info publishes it.
- Scheduled
Transfer - A signed transfer and when to submit it.
- Spacing
- How far apart to submit the transfers of one payment when it takes more than one, in seconds. Each gap is drawn at random from this range.
Enums§
- Check
Error - A payment does not match its request.
- Load
Error - A saved store cannot be read.
- Note
State - Where a note stands, as far as a
MintViewcan tell. - PayError
- A payment cannot be made.
- Request
Error - A payment request cannot be made.
Constants§
- MAX_
REQUESTED_ NOTES - Most notes one payment request may ask for.
- PAID_
KEYS_ KEPT - How many of the keys it paid last a holder remembers, so as to refuse paying one of them again: the notes of four requests of the most notes a request may ask for, or of hundreds of usual ones, for about 67 KiB of the saved store. A key paid by a transfer still outgoing, or by a payment the holder still keeps, is refused however long ago it was paid.
- REFRESH_
MARGIN_ SECS - Seconds before a scheduled rotation by which the transfers that
Holder::refreshandHolder::due_resendsplan are due, or half the time left when that is shorter. A plan made at or past the rotation’s time, which the mint has not made yet as far as its info says, has them all due at once. A payer should settle a payment this long before the rotation that drops a note it spends.