Skip to main content

Crate webledgers_teller

Crate webledgers_teller 

Source
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 kind LEDGER_KIND, addressable by d = 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 plus tagged("webledgers/deposit", ledgerHash ‖ account ‖ nonce)·G, added to the full point and never its even-y lift (sidestr/spec keys.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 kind REQUEST_KIND signed 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 through bitcoin::secp256k1; the sighash and the script check are sidestr-core‘s (Knots’ unified sighash for txbt4), and the Nostr event and its verification sidestr-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.getRandomValues and 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_event and latest_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_tags;
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, at 7c00cea).
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, at 7c00cea).
request
Requests: Nostr events of kind REQUEST_KIND signed 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.
UnsignedEvent
An event template before it has been signed: everything the id is computed over and nothing else.

Enums§

SighashRules
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.