webledgers_teller/lib.rs
1//! A teller for [Web Ledgers](https://webledgers.org/) on txbt4: accounts are
2//! did:nostr keys, balances a Web Ledger, deposits one taproot address per
3//! account, withdrawals and transfers signed requests. A Rust port of
4//! [solidpayorg/teller](https://github.com/solidpayorg/teller) `lib/teller.mjs`
5//! at `7c00cea` by Melvin Carvalho (AGPL-3.0-or-later); the proposal is
6//! [solidpayorg/webledgers#7](https://github.com/solidpayorg/webledgers/issues/7).
7//!
8//! One operator holds the deposits and writes the ledger; everyone else reads
9//! it and signs requests.
10//!
11//! - **The ledger** ([`ledger`]) is a Web Ledger JSON document with a genesis
12//! (operator, name, currency, confirmations) fixed at creation; its hash is
13//! the SHA-256 of the genesis's JCS and is its identity. The operator
14//! publishes it as a Nostr event of kind [`LEDGER_KIND`], addressable by
15//! `d` = the hash, and replaces it as balances move. Deposits are credited
16//! once by outpoint; transfers and withdrawals once by request id.
17//! - **Deposits** ([`deposit`]): each account's address is the operator's
18//! point plus `tagged("webledgers/deposit", ledgerHash ‖ account ‖ nonce)·G`,
19//! added to the full point and never its even-y lift (`sidestr/spec`
20//! `keys.mjs`). Anyone with the operator's did and the ledger's hash
21//! recomputes it; only the operator can spend it.
22//! - **Requests** ([`request`]) are Nostr events of kind [`REQUEST_KIND`]
23//! signed by the account's key: `join`, `withdraw`, `transfer`.
24//! - **Payouts** ([`payout`]) spend the deposits, each input signed with its
25//! own derived secret, checked under the chain's sighash rules and against
26//! the signed size before anything is returned.
27//!
28//! What it is not: not private (anyone who knows the operator's point can
29//! link a ledger's deposit addresses), not hardware-wallet signable (a plain
30//! additive tweak), and custodial (the operator can refuse a withdrawal). It
31//! is checkable: every deposit traces to an account from public data, every
32//! change is a signed request or a deposit seen on-chain.
33//!
34//! # Pure
35//!
36//! No I/O, no clock, no randomness: the caller passes the time (`now`,
37//! `created_at`), request ids, the chain's coins and the sighash rules, and
38//! publishes and broadcasts what comes back. Signatures use zero BIP 340
39//! auxiliary randomness, so every event and payout is a function of its
40//! inputs and the key. The crate needs `std` because `sidestr-core` and
41//! `sidestr-nostr`, whose sighash, verifier, addresses and Nostr event it
42//! uses, are `std` crates.
43//!
44//! # Example
45//!
46//! ```
47//! use bitcoin::secp256k1::SecretKey;
48//! use webledgers_teller::*;
49//!
50//! let operator = SecretKey::from_slice(&[0x11; 32]).unwrap();
51//! let alice = SecretKey::from_slice(&[0x22; 32]).unwrap();
52//! let op_did = format!("did:nostr:{}", pubkey_hex(&operator));
53//! let a = format!("did:nostr:{}", pubkey_hex(&alice));
54//!
55//! // a new ledger: its hash is the hash of its genesis
56//! let mut l = new_ledger(&LedgerParams::new(&op_did, "Table 7", 1_759_300_000)).unwrap();
57//! check_ledger(&l).unwrap();
58//!
59//! // Alice's deposit address, which anyone can recompute from the operator's did
60//! let dep = deposit_address(&op_did, &l.hash, &a, 0, DEFAULT_HRP).unwrap();
61//! assert!(dep.address.starts_with("tb1p"));
62//!
63//! // a deposit seen on-chain is credited once
64//! let funding = "ab".repeat(32);
65//! let c = Credit { account: a.clone(), txid: funding.clone(), vout: 0, value: 50_000, height: Some(152_100) };
66//! assert!(credit(&mut l, &c, 1_759_300_100).unwrap().applied());
67//!
68//! // Alice signs a withdrawal request; the operator reads it back verified
69//! let to = "tb1pfu64hh9hes90w2808n8tjc2ajp5yhddjef0ctx4s7zmsgp6cwx4quvla6g";
70//! let id = request_id([1; 16]);
71//! let ev = request_event(&alice, &RequestParams::withdraw(&l.hash, 20_000, to, &id), 1_759_300_200).unwrap();
72//! let req = parse_request(&ev, &l.hash).unwrap();
73//! assert_eq!((req.op, req.account.as_str(), req.amount), (Op::Withdraw, a.as_str(), Some(20_000)));
74//!
75//! // the payout: the deposit is the coin, change to the operator's own deposit address
76//! let change = deposit_address(&op_did, &l.hash, &op_did, 0, DEFAULT_HRP).unwrap();
77//! let to_script = sidestr_core::address::address_to_script(to).unwrap().to_hex_string();
78//! let coins = [Coin::from_deposit(&dep, &funding, 0, 50_000)];
79//! let plan = plan_payout(&PayoutParams { coins: &coins, amount: 20_000, rate: 1, to_script: &to_script, change_script: &change.script }).unwrap();
80//! let paid = sign_payout(&plan, &operator, TXBT4_RULES).unwrap();
81//! assert!(plan.fee >= paid.vsize);
82//!
83//! // and the ledger debited, the payout's txid the receipt
84//! let d = Debit { id: req.id, account: a.clone(), amount: 20_000, to: to.into(), txid: paid.txid };
85//! assert!(debit(&mut l, &d, 1_759_300_300).unwrap().applied());
86//! assert_eq!(balance(&l, &a).unwrap(), 30_000);
87//! ```
88//!
89//! # What changed in the port
90//!
91//! - The key rule (`keys.mjs`) is libsecp256k1's arithmetic through
92//! `bitcoin::secp256k1`; the sighash and the script check are
93//! `sidestr-core`'s (Knots' unified sighash for txbt4), and the Nostr event
94//! and its verification `sidestr-nostr`'s. Nothing cryptographic is
95//! hand-rolled.
96//! - Time, request ids and the chain's rules are parameters; the teller reads
97//! the clock, draws ids from `crypto.getRandomValues` and loads the chain's
98//! engine.
99//! - Fee rates are whole sat/vB (the teller pays at 1 and accepts fractions).
100//! - Amounts given as numbers are `u64`, so the refusals of negative and
101//! fractional numbers become the type; amounts read from text ([`sats`])
102//! are refused in the teller's words.
103//! - The ledger event (kind 30333) the teller's page writes and reads inline
104//! is here as [`ledger_template`], [`ledger_event`], [`read_ledger_event`]
105//! and [`latest_ledger`].
106#![forbid(unsafe_code)]
107#![deny(
108 missing_docs,
109 missing_debug_implementations,
110 rustdoc::broken_intra_doc_links
111)]
112
113pub mod account;
114pub mod deposit;
115pub mod error;
116mod keys;
117pub mod ledger;
118pub mod payout;
119pub mod request;
120mod sign;
121
122pub use account::{account_of, check_sats, sats, x_of, MAX_SATS};
123pub use deposit::{deposit_address, deposit_secret, watch_list, DepositAddress, DEFAULT_HRP};
124pub use error::{Error, Result};
125pub use ledger::{
126 balance, check_ledger, credit, debit, jcs, latest_ledger, ledger_event, ledger_hash,
127 ledger_template, new_ledger, parse_ledger, read_ledger_event, total, transfer, Credit, Debit,
128 Deposit, Entry, Genesis, Ledger, LedgerParams, Outcome, Payout, Transfer,
129};
130pub use payout::{
131 plan_payout, prevouts, sign_payout, sign_payout_txbt4, unsigned_tx, vsize_estimate, vsize_of,
132 Coin, PayoutParams, Plan, PlanOutput, SignedPayout,
133};
134pub use request::{
135 parse_request, request_event, request_id, request_tags, request_template, Op, Request,
136 RequestParams,
137};
138pub use sidestr_core::sighash::SighashRules;
139pub use sidestr_nostr::event::{Event, UnsignedEvent};
140pub use sign::pubkey_hex;
141
142/// The tag a deposit address's tweak is hashed under.
143pub const DEPOSIT_TAG: &str = "webledgers/deposit";
144/// The ledger's Nostr kind: addressable by `d` = its hash, replaced by the
145/// operator as balances move.
146pub const LEDGER_KIND: u32 = 30333;
147/// A request's Nostr kind (join, withdraw, transfer): regular, signed by the
148/// account's key.
149pub const REQUEST_KIND: u32 = 3700;
150/// The smallest change output a payout makes; less is left to the fee.
151pub const DUST: u64 = 330;
152/// The smallest withdrawal or payout, in satoshis.
153pub const MIN_PAY: u64 = 546;
154/// The Web Ledgers JSON-LD context.
155pub const CONTEXT: &str = "https://w3id.org/webledgers";
156/// The currency a ledger is in unless it says otherwise.
157pub const DEFAULT_CURRENCY: &str = "txbt4";
158/// The sighash rules of txbt4, the BLAKE2b testnet4 the teller runs on:
159/// Knots' unified sighash, hash type `0x21`
160/// (`sidestr_core::sighash::rules_for(Family::Blake2b)`).
161pub const TXBT4_RULES: SighashRules = SighashRules::KnotsUnified;