Expand description
A teller for Web Ledgers on txbt4: accounts are
did:nostr keys, balances a Web Ledger, deposits one taproot address per
account, withdrawals and transfers signed requests. A Rust port of
solidpayorg/teller lib/teller.mjs
at 7c00cea by Melvin Carvalho (AGPL-3.0-or-later); the proposal is
solidpayorg/webledgers#7.
One operator holds the deposits and writes the ledger; everyone else reads it and signs requests.
- The ledger (
ledger) is a Web Ledger JSON document with a genesis (operator, name, currency, confirmations) fixed at creation; its hash is the SHA-256 of the genesis’s JCS and is its identity. The operator publishes it as a Nostr event of kindLEDGER_KIND, addressable byd= the hash, and replaces it as balances move. Deposits are credited once by outpoint; transfers and withdrawals once by request id. - Deposits (
deposit): each account’s address is the operator’s point plustagged("webledgers/deposit", ledgerHash ‖ account ‖ nonce)·G, added to the full point and never its even-y lift (sidestr/speckeys.mjs). Anyone with the operator’s did and the ledger’s hash recomputes it; only the operator can spend it. - Requests (
request) are Nostr events of kindREQUEST_KINDsigned by the account’s key:join,withdraw,transfer. - Payouts (
payout) spend the deposits, each input signed with its own derived secret, checked under the chain’s sighash rules and against the signed size before anything is returned.
What it is not: not private (anyone who knows the operator’s point can link a ledger’s deposit addresses), not hardware-wallet signable (a plain additive tweak), and custodial (the operator can refuse a withdrawal). It is checkable: every deposit traces to an account from public data, every change is a signed request or a deposit seen on-chain.
§Pure
No I/O, no clock, no randomness: the caller passes the time (now,
created_at), request ids, the chain’s coins and the sighash rules, and
publishes and broadcasts what comes back. Signatures use zero BIP 340
auxiliary randomness, so every event and payout is a function of its
inputs and the key. The crate needs std because sidestr-core and
sidestr-nostr, whose sighash, verifier, addresses and Nostr event it
uses, are std crates.
§Example
use bitcoin::secp256k1::SecretKey;
use webledgers_teller::*;
let operator = SecretKey::from_slice(&[0x11; 32]).unwrap();
let alice = SecretKey::from_slice(&[0x22; 32]).unwrap();
let op_did = format!("did:nostr:{}", pubkey_hex(&operator));
let a = format!("did:nostr:{}", pubkey_hex(&alice));
// a new ledger: its hash is the hash of its genesis
let mut l = new_ledger(&LedgerParams::new(&op_did, "Table 7", 1_759_300_000)).unwrap();
check_ledger(&l).unwrap();
// Alice's deposit address, which anyone can recompute from the operator's did
let dep = deposit_address(&op_did, &l.hash, &a, 0, DEFAULT_HRP).unwrap();
assert!(dep.address.starts_with("tb1p"));
// a deposit seen on-chain is credited once
let funding = "ab".repeat(32);
let c = Credit { account: a.clone(), txid: funding.clone(), vout: 0, value: 50_000, height: Some(152_100) };
assert!(credit(&mut l, &c, 1_759_300_100).unwrap().applied());
// Alice signs a withdrawal request; the operator reads it back verified
let to = "tb1pfu64hh9hes90w2808n8tjc2ajp5yhddjef0ctx4s7zmsgp6cwx4quvla6g";
let id = request_id([1; 16]);
let ev = request_event(&alice, &RequestParams::withdraw(&l.hash, 20_000, to, &id), 1_759_300_200).unwrap();
let req = parse_request(&ev, &l.hash).unwrap();
assert_eq!((req.op, req.account.as_str(), req.amount), (Op::Withdraw, a.as_str(), Some(20_000)));
// the payout: the deposit is the coin, change to the operator's own deposit address
let change = deposit_address(&op_did, &l.hash, &op_did, 0, DEFAULT_HRP).unwrap();
let to_script = sidestr_core::address::address_to_script(to).unwrap().to_hex_string();
let coins = [Coin::from_deposit(&dep, &funding, 0, 50_000)];
let plan = plan_payout(&PayoutParams { coins: &coins, amount: 20_000, rate: 1, to_script: &to_script, change_script: &change.script }).unwrap();
let paid = sign_payout(&plan, &operator, TXBT4_RULES).unwrap();
assert!(plan.fee >= paid.vsize);
// and the ledger debited, the payout's txid the receipt
let d = Debit { id: req.id, account: a.clone(), amount: 20_000, to: to.into(), txid: paid.txid };
assert!(debit(&mut l, &d, 1_759_300_300).unwrap().applied());
assert_eq!(balance(&l, &a).unwrap(), 30_000);§What changed in the port
- The key rule (
keys.mjs) is libsecp256k1’s arithmetic throughbitcoin::secp256k1; the sighash and the script check aresidestr-core‘s (Knots’ unified sighash for txbt4), and the Nostr event and its verificationsidestr-nostr’s. Nothing cryptographic is hand-rolled. - Time, request ids and the chain’s rules are parameters; the teller reads
the clock, draws ids from
crypto.getRandomValuesand loads the chain’s engine. - Fee rates are whole sat/vB (the teller pays at 1 and accepts fractions).
- Amounts given as numbers are
u64, so the refusals of negative and fractional numbers become the type; amounts read from text (sats) are refused in the teller’s words. - The ledger event (kind 30333) the teller’s page writes and reads inline
is here as
ledger_template,ledger_event,read_ledger_eventandlatest_ledger.
Re-exports§
pub use account::account_of;pub use account::check_sats;pub use account::sats;pub use account::x_of;pub use account::MAX_SATS;pub use deposit::deposit_address;pub use deposit::deposit_secret;pub use deposit::watch_list;pub use deposit::DepositAddress;pub use deposit::DEFAULT_HRP;pub use error::Error;pub use error::Result;pub use ledger::balance;pub use ledger::check_ledger;pub use ledger::credit;pub use ledger::debit;pub use ledger::jcs;pub use ledger::latest_ledger;pub use ledger::ledger_event;pub use ledger::ledger_hash;pub use ledger::ledger_template;pub use ledger::new_ledger;pub use ledger::parse_ledger;pub use ledger::read_ledger_event;pub use ledger::total;pub use ledger::transfer;pub use ledger::Credit;pub use ledger::Debit;pub use ledger::Deposit;pub use ledger::Entry;pub use ledger::Genesis;pub use ledger::Ledger;pub use ledger::LedgerParams;pub use ledger::Outcome;pub use ledger::Payout;pub use ledger::Transfer;pub use payout::plan_payout;pub use payout::prevouts;pub use payout::sign_payout;pub use payout::sign_payout_txbt4;pub use payout::unsigned_tx;pub use payout::vsize_estimate;pub use payout::vsize_of;pub use payout::Coin;pub use payout::PayoutParams;pub use payout::Plan;pub use payout::PlanOutput;pub use payout::SignedPayout;pub use request::parse_request;pub use request::request_event;pub use request::request_id;pub use request::request_template;pub use request::Op;pub use request::Request;pub use request::RequestParams;
Modules§
- account
- Accounts and amounts: the two values every other part of the teller reads
first (
teller.mjs accountOf,xOf,sats). - deposit
- Deposit addresses: one taproot output per account (and nonce), derived
from the operator’s point, which anyone can recompute from public data and
only the operator can spend (
teller.mjs depositAddress,depositSecret,watchList, at7c00cea). - error
- The one error type every fallible function in this crate returns.
- ledger
- The ledger: a Web Ledger document whose identity is the hash of its
genesis, and the three moves that change its balances, each applied once
(
teller.mjs newLedger,ledgerHash,checkLedger,balance,credit,transfer,debit,total,jcs). - payout
- Payouts: the deposits are the coins, and each input is signed with its
own derived secret (
teller.mjs planPayout,unsignedTx,signPayout,vsizeOf, at7c00cea). - request
- Requests: Nostr events of kind
REQUEST_KINDsigned by an account’s key, each applied at most once by its id (teller.mjs requestTags,requestEvent,parseRequest).
Structs§
- Event
- A signed event, as it travels on the wire and arrives from a relay.
- Unsigned
Event - An event template before it has been signed: everything the id is computed over and nothing else.
Enums§
- Sighash
Rules - Which signature-hash rules a spend is judged by.
Constants§
- CONTEXT
- The Web Ledgers JSON-LD context.
- DEFAULT_
CURRENCY - The currency a ledger is in unless it says otherwise.
- DEPOSIT_
TAG - The tag a deposit address’s tweak is hashed under.
- DUST
- The smallest change output a payout makes; less is left to the fee.
- LEDGER_
KIND - The ledger’s Nostr kind: addressable by
d= its hash, replaced by the operator as balances move. - MIN_PAY
- The smallest withdrawal or payout, in satoshis.
- REQUEST_
KIND - A request’s Nostr kind (join, withdraw, transfer): regular, signed by the account’s key.
- TXBT4_
RULES - The sighash rules of txbt4, the BLAKE2b testnet4 the teller runs on:
Knots’ unified sighash, hash type
0x21(sidestr_core::sighash::rules_for(Family::Blake2b)).
Functions§
- pubkey_
hex - The x-only public key of a secret, as 64 lowercase hex digits: a Nostr pubkey and the x of a did:nostr account.