Staking & the ledger
Two things live in the ledger layer: how balances are stored, and how proof-of-stake works (validators, staking, rewards, cooldowns) — plus multisig wallets on the side.
Balances
A balance is just a row: (address, asset) → amount. Two assets exist — XCH (native coin) and xUSD (the quote currency for trading). All amounts are integer minor units — no floating point, so no rounding bugs.
1pub async fn get_balance(pool: &PgPool, address: &str, asset: &str) -> Result<i64, String> {2// SELECT balance FROM balances WHERE address=$1 AND asset=$2 (0 if absent)3}4pub async fn upsert_balance(pool, address, asset, new_balance) -> Result<(), String> {5// INSERT ... ON CONFLICT (address, asset) DO UPDATE SET balance = EXCLUDED.balance6}
The block producer works on a full in-memory snapshot — load_view pulls every balance and nonce into a HashMap, and persist_view writes it all back after a block. That's the choke point where "chain state" meets Postgres.
i64 math, thousands of rows). A real chain would use an in-memory state trie instead.Proof of stake, concretely
To take part in consensus you become a validator: lock up at least MIN_STAKE (10 XCH), and your weight in the proposer lottery grows with your stake. Others can delegate by staking to you.
1 · Register
1INSERT INTO validators (address, stake, status, rewards)2VALUES ($1, $2, 'active', 0)3ON CONFLICT (address) DO UPDATE SET status='active'
2 · Stake (one DB transaction)
Note: staking escrow is an on-chain transaction? No — it's a direct balance write inside a DB transaction, like the exchange. Only wallet-to-wallet transfers use the blockchain.
3 · Unstake = schedule a release
You can't pull staked coins out instantly (that would break consensus incentives). Unstaking marks positions with a future unlock_epoch = current + cooldown (default 2 epochs). The coins stay in escrow until that epoch's boundary, when process_epoch releases them.
1let eligible: i64 = sqlx::query_scalar(2"SELECT COALESCE(SUM(amount),0)::bigint FROM staking_positions3WHERE address=$1 AND validator=$2 AND unlock_epoch <= $3",4) ...;5if amount > eligible { return Err(format!("only {eligible} eligible to unstake")); }
4 · Epoch rewards
At each epoch boundary (height % 100 == 0), process_epoch mints the epoch reward and splits it among active validators proportionally to stake — plus releases matured cooldowns:
1for v in &validators {2let reward = (total_reward as i128 * v.stake as i128 / total_stake as i128) as i64;3if reward <= 0 { continue; }4// credit v.address with [reward], accumulate validators.rewards5}6// then: release all positions with unlock_epoch <= current_epoch,7// move escrow back to staker, drop validator stake, delete rows
Multisig wallets
A multisig wallet requires N of M signers to approve a transfer — e.g. "2 of 3" people must sign before funds move. Useful for shared or corporate accounts.
- Address is derived deterministically:
sha3-256(sorted(signers) + "|" + threshold)[..20]. Same signers + threshold → same address, so creation is idempotent. - Create — propose a tx (only a signer).
- Sign — each signer endorses (JWT-authenticated; deduped).
- Execute — once unique valid signatures ≥ threshold: move balances, bump the wallet's nonce, write a confirmed tx row.
1pub fn derive_multisig_address(signers: &[String], threshold: i32) -> String {2let mut sorted = signers.to_vec();3sorted.sort();4let mut hasher = Sha3_256::new();5hasher.update(sorted.join(",").as_bytes());6hasher.update(format!("|{threshold}"));7format!("0x{}", hex::encode(&hasher.finalize()[..20]))8}
Where this lives
- Database → validators, staking_positions, balances, multisig_* tables
- Frontend → staking & multisig pages
- Backend → epoch boundary logic in the block producer