webledgers-teller
Part of sidestr-rs. Rust port of Melvin Carvalho's teller for Web Ledgers, AGPL-3.0-or-later: did:nostr accounts with balances on a public ledger, paid in and out on txbt4.
A teller for Web Ledgers, in Rust: accounts are did:nostr keys, balances a Web Ledger, deposits one taproot address per account, withdrawals and transfers signed requests. One operator holds the deposits and writes the ledger; everyone else reads it and signs requests. The proposal is solidpayorg/webledgers#7.
[]
= "0.1"
- The ledger is a Web Ledger JSON document with a genesis (operator,
name, currency, confirmations) fixed at creation; its hash, the SHA-256 of
the genesis's JCS, is its identity. The operator publishes it as a Nostr
event of kind 30333 (addressable by
d= the hash) and replaces it as balances move. A reader accepts only the operator's own signed copy. Deposits are credited once by outpoint; transfers and withdrawals once by request id; amounts are whole satoshis, written as strings. - Deposits. Each account's deposit 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/speckeys.mjs). The operator is read as its did (the02point of its x) whatever parity it is given as, and its secret is normalised to that same point. Anyone with the operator's did and the ledger's hash recomputes the address; only the operator can spend it. - Requests are Nostr events of kind 3700 signed by the account's key:
join(be watched),withdraw(an amount to an address),transfer(an amount to an account). - Payouts spend the deposits: coins largest first, change of at least 330
sat back to the operator's own deposit address, less folded into the fee;
each input signed with its own derived secret under the chain's sighash
rules (Knots' unified sighash,
0x21, on txbt4), every input checked, and the fee checked against the signed size. The size estimate is 58 vB per key-path input, the teller's fix for a payout once refused as "min relay fee not met, 154 < 155".
use SecretKey;
use *;
let operator = from_slice?;
let op_did = format!;
let mut ledger = new_ledger?;
let alice = "did:nostr:…";
let deposit = deposit_address?; // tb1p…
credit?;
let req = parse_request?; // kind 3700, verified
let plan = plan_payout?;
let paid = sign_payout?; // broadcast paid.hex
debit?;
let ev = ledger_event?; // publish (kind 30333)
The crate is pure: no I/O, no clock, no randomness. The caller passes the
time, request ids, the coins the chain holds and the sighash rules, and
publishes and broadcasts what comes back. Signatures use zero BIP 340
auxiliary randomness. It needs std, as sidestr-core and sidestr-nostr
do.
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. txbt4 coins are test coins.
Attribution
This crate is a port of teller by Melvin Carvalho
(github.com/solidpayorg/teller,
AGPL-3.0-or-later), from commit 7c00ceac4dc37e0526eccd5ae62e1680ac88ec2a
(lib/teller.mjs, test/teller-test.mjs, and the ledger event in
teller.js). Two parts come from the libraries the teller loads, by the same
author and under the AGPL:
- the key rule (
normalize,basePoint,tweakPoint,tweakSecret,taggedScalar, …) fromsiding/lib/keys.mjsin sidestr/spec atbd1d692(throughe8deb63161c7459ed39c01d2ca9fda3d860b65b6), with its known answerssiding/test/keys-vectors.jsonvendored astests/vectors/keys-vectors.json; - the key-path sighash and the script check (
siding/lib/txsign.mjs, the kernel of bitcoin-desktop/schema), throughsidestr-core.
Every function names its original.
What changed in the port
- The key arithmetic is libsecp256k1's through
bitcoin::secp256k1; the sighash and script check aresidestr-core's; the Nostr event and its verification aresidestr-nostr's. Nothing cryptographic is hand-rolled. - Time, request ids and the chain's sighash 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 negative and fractional numbers cannot be passed at all. Amounts read from text are refused in the teller's words. - The ledger event (kind 30333), which the teller's page writes and reads
inline, is here as
ledger_template,ledger_event,read_ledger_eventandlatest_ledger. - JCS covers the plain JSON these documents are. Integers are exact; a number with a fraction is outside what the teller writes.
Tests
cargo test runs the teller's 24 checks, named as the teller names them
(tests/teller.rs), the keys-vectors.json cases, and the edges of each
refusal. With TELLER (a teller checkout), SCHEMA, BLAKETESTNODE and
SIDESTR_LIB (or SIDESTR_SIDING) set, tests/oracle.rs also runs the
teller's own suite against those checkouts and cross-checks this crate
against lib/teller.mjs through tests/xcheck-teller.mjs. The cross-check
covers JCS, accounts, amounts, ledgers and their hashes, a script of credits,
transfers and debits, deposit addresses for every form of operator key,
secrets, watch lists, payout plans with their unsigned transactions, payouts
signed on each side and checked by the other, and requests signed on each
side and read by the other.
Licence
AGPL-3.0-or-later, as the teller is. See LICENSE.