L
ling-wallet
↓ 0 downloadsLing Wallet: a post-quantum privacy-coin wallet, full node and HyperTorus miner for LNG
Keywords wallet, cryptocurrency, lng, node, miner, ling
lingfu install ling-walletCode / Files (116)
ling/core/chain.ling
# chain.ling — chain state on SQLite and consensus validation (DESIGN §7).
#
# Only the node isolate writes. Main-chain state changes only inside
# chain_apply / chain_rollback, each one SQLite transaction, so a crash can
# never leave half a block applied. Side blocks are stored with their
# metadata and only touch state when a reorg makes them main-chain.
use "params.ling"
use "codec.ling"
use "money.ling"
fn chain_open(path) {
db_open(path)
# Performance pragmas (set once; journal_mode persists in the file). The node
# and the wallet isolate both open this same chain.db — under the default
# rollback journal a write takes a whole-file lock, so the wallet's reads and
# the node's block writes serialise against each other. WAL lets them run
# concurrently. synchronous=NORMAL drops the per-block fsync to one at WAL
# checkpoints (still crash-safe; only a power cut can lose the last block or
# two, which re-sync from peers) — the big win during initial sync. The cache
# is bounded at 8 MiB and mmap is left off, so this is faster without growing
# resident memory.
db_query("PRAGMA journal_mode=WAL")
db_exec("PRAGMA synchronous=NORMAL")
db_exec("PRAGMA cache_size=-8000")
db_exec("CREATE TABLE IF NOT EXISTS blocks (id TEXT PRIMARY KEY, height INTEGER NOT NULL, prev TEXT NOT NULL, timestamp INTEGER NOT NULL, difficulty TEXT NOT NULL, cumdiff TEXT NOT NULL, generated INTEGER NOT NULL, data TEXT NOT NULL)")
db_exec("CREATE TABLE IF NOT EXISTS main_chain (height INTEGER PRIMARY KEY, id TEXT NOT NULL)")
db_exec("CREATE TABLE IF NOT EXISTS outputs (denom INTEGER NOT NULL, gidx INTEGER NOT NULL, onetime TEXT NOT NULL, height INTEGER NOT NULL, coinbase INTEGER NOT NULL, PRIMARY KEY (denom, gidx))")
db_exec("CREATE TABLE IF NOT EXISTS key_images (ki TEXT PRIMARY KEY, denom INTEGER NOT NULL, height INTEGER NOT NULL)")
db_exec("CREATE INDEX IF NOT EXISTS ki_denom ON key_images (denom)")
db_exec("CREATE TABLE IF NOT EXISTS mempool (txid TEXT PRIMARY KEY, data TEXT NOT NULL, fee_rate REAL NOT NULL, size INTEGER NOT NULL, added INTEGER NOT NULL)")
db_exec("CREATE TABLE IF NOT EXISTS mempool_ki (ki TEXT PRIMARY KEY, txid TEXT NOT NULL)")
db_exec("CREATE TABLE IF NOT EXISTS bundles (hash TEXT PRIMARY KEY, data TEXT NOT NULL)")
db_exec("CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL)")
if len(db_query("SELECT height FROM main_chain LIMIT 1")) == 0 {
bind g = genesis_block()
chain_apply(g, block_id(g.header), 0, "", genesis_difficulty(), genesis_difficulty(), 0)
}
return 0
}
fn genesis_block() {
bind cb = Tx(TX_VERSION, true, 0, 0, [], [], 0, text_hex(GENESIS_EXTRA), [])
bind h = Header(BLOCK_VERSION, NET_ID, 0, hex_concat([u64le_hex(0), u64le_hex(0), u64le_hex(0), u64le_hex(0)]), GENESIS_TIMESTAMP, merkle_root([tx_id(cb)]), 0)
return Block(h, [cb])
}
# ── reads ─────────────────────────────────────────────────────────────────
form Tip { height, id, cumdiff }
form Meta { height, prev, timestamp, difficulty, cumdiff, generated }
fn chain_tip() {
bind r = list_get(db_query("SELECT m.height, m.id, b.cumdiff FROM main_chain m JOIN blocks b ON b.id = m.id ORDER BY m.height DESC LIMIT 1"), 0)
return Tip(r.height, r.id, r.cumdiff)
}
fn block_meta(id) {
bind rows = db_query("SELECT height, prev, timestamp, difficulty, cumdiff, generated FROM blocks WHERE id = ?1", id)
if len(rows) == 0 {
return bad("unknown block")
}
bind r = list_get(rows, 0)
return ok(Meta(r.height, r.prev, r.timestamp, r.difficulty, r.cumdiff, r.generated))
}
fn has_block(id) {
return len(db_query("SELECT 1 AS x FROM blocks WHERE id = ?1", id)) > 0
}
fn main_id_at(height) {
bind rows = db_query("SELECT id FROM main_chain WHERE height = ?1", height)
if len(rows) == 0 {
return ""
}
return list_get(rows, 0).id
}
fn block_hex(id) {
bind rows = db_query("SELECT data FROM blocks WHERE id = ?1", id)
if len(rows) == 0 {
return ""
}
return list_get(rows, 0).data
}
fn output_count(denom) {
return list_get(db_query("SELECT COUNT(*) AS n FROM outputs WHERE denom = ?1", denom), 0).n
}
fn key_image_count(denom) {
return list_get(db_query("SELECT COUNT(*) AS n FROM key_images WHERE denom = ?1", denom), 0).n
}
fn key_image_spent(ki_id) {
return len(db_query("SELECT 1 AS x FROM key_images WHERE ki = ?1", ki_id)) > 0
}
form OutRec { onetime, height, coinbase }
fn output_rec(denom, gidx) {
bind rows = db_query("SELECT onetime, height, coinbase FROM outputs WHERE denom = ?1 AND gidx = ?2", denom, gidx)
if len(rows) == 0 {
return bad("no such output")
}
bind r = list_get(rows, 0)
return ok(OutRec(r.onetime, r.height, r.coinbase))
}
fn required_age(coinbase) {
if coinbase == 1 {
return COINBASE_MATURITY
}
return MIN_OUTPUT_AGE
}
# May an output created at `out_height` be used in a tx referencing `ref`?
fn is_eligible(out_height, coinbase, ref) {
return ref >= out_height + required_age(coinbase)
}
# Outputs of `denom` eligible at `ref`, counting at most `stop_after + 1`.
fn eligible_count(denom, ref, stop_after) {
bind rows = db_query("SELECT COUNT(*) AS n FROM (SELECT 1 FROM outputs WHERE denom = ?1 AND ((coinbase = 1 AND height + ?3 <= ?2) OR (coinbase = 0 AND height + ?4 <= ?2)) LIMIT ?5)", denom, ref, COINBASE_MATURITY, MIN_OUTPUT_AGE, stop_after + 1)
return list_get(rows, 0).n
}
# Up to `n` metas ending at `id` (inclusive), oldest first.
fn ancestry(id, n) {
bind out = []
bind cur = id
bind k = 0
bind done = false
while k < n && done == false {
bind m = match block_meta(cur) {
ok(v) => v,
bad(e) => (),
}
if m == () {
bind done = true
} else {
bind out = list_push(out, m)
bind cur = m.prev
if m.height == 0 {
bind done = true
}
}
bind k = k + 1
}
# reverse to oldest-first
bind rev = []
bind i = len(out) - 1
while i >= 0 {
bind rev = list_push(rev, list_get(out, i))
bind i = i - 1
}
return rev
}
fn difficulty_after(parent) {
bind hist = ancestry(parent, LWMA_WINDOW + 1)
bind ts = []
bind cd = []
bind i = 0
while i < len(hist) {
bind ts = list_push(ts, list_get(hist, i).timestamp)
bind cd = list_push(cd, list_get(hist, i).cumdiff)
bind i = i + 1
}
return next_difficulty(ts, cd, genesis_difficulty())
}
fn median_timestamp_after(parent) {
bind hist = ancestry(parent, MEDIAN_WINDOW)
bind ts = []
bind i = 0
while i < len(hist) {
bind ts = list_push(ts, list_get(hist, i).timestamp)
bind i = i + 1
}
bind sorted = sort_numbers(ts)
return list_get(sorted, floor(len(sorted) / 2))
}
# Insertion sort (lists here are ≤ 11 long).
fn sort_numbers(l) {
bind out = []
bind i = 0
while i < len(l) {
bind v = list_get(l, i)
bind tmp = []
bind placed = false
bind k = 0
while k < len(out) {
bind w = list_get(out, k)
if placed == false && v < w {
bind tmp = list_push(tmp, v)
bind placed = true
}
bind tmp = list_push(tmp, w)
bind k = k + 1
}
if placed == false {
bind tmp = list_push(tmp, v)
}
bind out = tmp
bind i = i + 1
}
return out
}
# ── validation ────────────────────────────────────────────────────────────
# Contextual header checks incl. PoW (cheap checks first: PoW costs ~50 ms).
# Returns ok([difficulty, cumdiff, generated]).
fn check_header(h, now) {
if h.version != BLOCK_VERSION {
return bad("bad block version")
}
if h.network != NET_ID {
return bad("wrong network")
}
bind parent = match block_meta(h.prev_id) {
ok(m) => m,
bad(e) => (),
}
if parent == () {
return bad("unknown parent")
}
if h.height != parent.height + 1 {
return bad("bad height")
}
if h.timestamp <= median_timestamp_after(h.prev_id) {
return bad("timestamp not after median of last 11")
}
if h.timestamp > now + MAX_FUTURE_SECS {
return bad("timestamp too far in the future")
}
bind difficulty = difficulty_after(h.prev_id)
if lw_meets_difficulty(lw_pow_hash(enc_header(h)), difficulty) == false {
return bad("insufficient proof of work")
}
return ok([difficulty, big_add(parent.cumdiff, difficulty), parent.generated + block_reward(parent.generated)])
}
# Validate a regular tx against main-chain state for inclusion at `height`.
# `seen` is an st_* key prefix collecting key images already used in the
# same block/batch (pass a fresh prefix per batch).
fn validate_tx(tx, height, seen) {
if tx.coinbase {
return bad("unexpected coinbase")
}
if tx.version != TX_VERSION {
return bad("bad tx version")
}
if tx.ref_height >= height || height - tx.ref_height > TX_REF_WINDOW {
return bad("reference height out of range")
}
if len(tx.inputs) == 0 || len(tx.outputs) == 0 {
return bad("empty inputs or outputs")
}
if len(tx.sigs) != len(tx.inputs) {
return bad("signature count mismatch")
}
if tx.fee < tx_size(tx) * FEE_PER_BYTE {
return bad("fee below minimum")
}
bind sum_out = tx.fee
bind j = 0
while j < len(tx.outputs) {
bind o = list_get(tx.outputs, j)
if is_denomination(o.denom) == false {
return bad("output is not a denomination")
}
bind sum_out = sum_out + o.denom
bind j = j + 1
}
bind sum_in = 0
bind i = 0
while i < len(tx.inputs) {
bind inp = list_get(tx.inputs, i)
if is_denomination(inp.denom) == false {
return bad("input is not a denomination")
}
bind sum_in = sum_in + inp.denom
bind i = i + 1
}
if sum_in != sum_out || sum_in > MONEY_SUPPLY {
return bad("inputs != outputs + fee")
}
bind prefix = tx_prefix_hash(tx)
bind tx_seen = format("{}/tx{}", seen, prefix)
bind i = 0
while i < len(tx.inputs) {
bind inp = list_get(tx.inputs, i)
bind r = validate_input(inp, list_get(tx.sigs, i), prefix, tx.ref_height, seen, tx_seen)
if r != "" {
return bad(r)
}
bind i = i + 1
}
# Commit this tx's key images to the batch.
bind i = 0
while i < len(tx.inputs) {
st_set(format("{}/{}", seen, ki_id(list_get(tx.inputs, i).key_image)), 1)
bind i = i + 1
}
return ok(())
}
fn ki_id(ki) {
return lw_h256("LW1/lrs/ki-id", [ki])
}
# Returns "" if valid, else the reason.
fn validate_input(inp, sig, prefix, ref, seen, tx_seen) {
bind kid = ki_id(inp.key_image)
if st_has(format("{}/{}", tx_seen, kid)) == 1 || st_has(format("{}/{}", seen, kid)) == 1 {
return "duplicate key image"
}
st_set(format("{}/{}", tx_seen, kid), 1)
if key_image_spent(kid) {
return "key image already spent"
}
if len(inp.ring) > RING_SIZE {
return "ring too large"
}
bind ring = []
bind k = 0
while k < len(inp.ring) {
bind rec = match output_rec(inp.denom, list_get(inp.ring, k)) {
ok(v) => v,
bad(e) => (),
}
if rec == () {
return "ring member does not exist"
}
if is_eligible(rec.height, rec.coinbase, ref) == false {
return "ring member too young"
}
bind ring = list_push(ring, rec.onetime)
bind k = k + 1
}
if len(inp.ring) < RING_SIZE {
if eligible_count(inp.denom, ref, len(inp.ring)) != len(inp.ring) {
return "small ring while more outputs are eligible"
}
}
if lw_ring_verify(prefix, ring, inp.key_image, sig) == false {
return "ring signature invalid"
}
return ""
}
# Full body validation of a block whose parent is the main tip.
fn validate_body(b, height, prev_id) {
if hex_len(enc_block(b)) > MAX_BLOCK_BYTES {
return bad("block too large")
}
if b.header.merkle_root != block_merkle(b) {
return bad("merkle root mismatch")
}
if len(b.txs) == 0 {
return bad("no coinbase")
}
bind cb = list_get(b.txs, 0)
if cb.coinbase == false || cb.height != height {
return bad("first tx must be the coinbase for this height")
}
bind seen = format("blk{}", lw_random(8))
bind fees = 0
bind ki_denoms = []
bind i = 1
while i < len(b.txs) {
bind tx = list_get(b.txs, i)
if tx.coinbase {
return bad("extra coinbase")
}
bind tid = tx_id(tx)
if st_has(format("{}/id{}", seen, tid)) == 1 {
return bad("duplicate transaction")
}
st_set(format("{}/id{}", seen, tid), 1)
bind r = validate_tx(tx, height, seen)
bind err = match r {
ok(v) => "",
bad(e) => e,
}
if err != "" {
return bad(err)
}
bind fees = fees + tx.fee
bind k = 0
while k < len(tx.inputs) {
bind ki_denoms = list_push(ki_denoms, list_get(tx.inputs, k).denom)
bind k = k + 1
}
bind i = i + 1
}
if cb.version != TX_VERSION || cb.fee != 0 || hex_len(cb.extra) > MAX_EXTRA_BYTES {
return bad("malformed coinbase")
}
bind parent = match block_meta(prev_id) {
ok(m) => m,
bad(e) => (),
}
bind expected = coinbase_split(block_reward(parent.generated) + fees)
if len(expected) != len(cb.outputs) {
return bad("coinbase does not pay the canonical reward split")
}
bind i = 0
while i < len(expected) {
if list_get(expected, i) != list_get(cb.outputs, i).denom {
return bad("coinbase does not pay the canonical reward split")
}
bind i = i + 1
}
# Supply audit rule (DESIGN §7.7).
bind i = 0
while i < len(ki_denoms) {
bind d = list_get(ki_denoms, i)
bind used = count_in(ki_denoms, d)
bind outs = output_count(d) + count_denom_outputs(b, d)
if key_image_count(d) + used > outs {
return bad("more key images than outputs for a denomination")
}
bind i = i + 1
}
return ok(())
}
fn count_in(l, v) {
bind n = 0
bind i = 0
while i < len(l) {
if list_get(l, i) == v {
bind n = n + 1
}
bind i = i + 1
}
return n
}
fn count_denom_outputs(b, d) {
bind n = 0
bind i = 0
while i < len(b.txs) {
bind outs = list_get(b.txs, i).outputs
bind k = 0
while k < len(outs) {
if list_get(outs, k).denom == d {
bind n = n + 1
}
bind k = k + 1
}
bind i = i + 1
}
return n
}
# ── state changes ─────────────────────────────────────────────────────────
fn store_block(b, id, height, prev, difficulty, cumdiff, generated) {
db_exec("INSERT OR IGNORE INTO blocks (id, height, prev, timestamp, difficulty, cumdiff, generated, data) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
id, height, prev, b.header.timestamp, difficulty, cumdiff, generated, enc_block(b))
return 0
}
# Append a fully validated block to the main chain (one transaction).
fn chain_apply(b, id, height, prev_unused, difficulty, cumdiff, generated) {
db_exec("BEGIN IMMEDIATE")
store_block(b, id, height, b.header.prev_id, difficulty, cumdiff, generated)
db_exec("INSERT INTO main_chain (height, id) VALUES (?1, ?2)", height, id)
bind i = 0
while i < len(b.txs) {
bind tx = list_get(b.txs, i)
bind k = 0
while k < len(tx.inputs) {
bind inp = list_get(tx.inputs, k)
db_exec("INSERT INTO key_images (ki, denom, height) VALUES (?1, ?2, ?3)", ki_id(inp.key_image), inp.denom, height)
bind k = k + 1
}
bind j = 0
while j < len(tx.outputs) {
bind o = list_get(tx.outputs, j)
bind cb = if tx.coinbase { 1 } else { 0 }
db_exec("INSERT INTO outputs (denom, gidx, onetime, height, coinbase) VALUES (?1, ?2, ?3, ?4, ?5)", o.denom, output_count(o.denom), o.onetime, height, cb)
bind j = j + 1
}
bind i = i + 1
}
db_exec("COMMIT")
return 0
}
# Undo the main-chain tip; the block stays stored as a side block.
fn chain_rollback() {
bind tip = chain_tip()
if tip.height == 0 {
return bad("cannot roll back genesis")
}
bind b = match dec_block(block_hex(tip.id)) {
ok(v) => v,
bad(e) => (),
}
db_exec("BEGIN IMMEDIATE")
# Outputs are appended per denomination, so the tip's outputs are exactly
# those created at this height; key images likewise.
db_exec("DELETE FROM outputs WHERE height = ?1", tip.height)
db_exec("DELETE FROM key_images WHERE height = ?1", tip.height)
db_exec("DELETE FROM main_chain WHERE height = ?1", tip.height)
db_exec("COMMIT")
return ok(b)
}
# Accept a block from anywhere. Returns one of:
# ok(["have"]) ok(["orphan", missing_parent]) ok(["side"])
# ok(["extended"]) ok(["reorg", depth, removed_blocks_list])
# bad(reason) — consensus violation (penalize the sender)
fn accept_block(b, now) {
bind id = block_id(b.header)
if has_block(id) {
return ok(["have"])
}
if has_block(b.header.prev_id) == false {
return ok(["orphan", b.header.prev_id])
}
# A block whose parent is more than MAX_REORG_DEPTH below our tip can never win a reorg, so there is no reason to
# verify its (possibly cheap, low-difficulty) proof of work or to store it — the main defence against side-chain /
# low-work fork spam. Rejected cheaply and without penalty (an honest peer may simply be far behind).
bind pmeta = match block_meta(b.header.prev_id) {
ok(v) => v,
bad(e) => (),
}
if pmeta != () && pmeta.height + 1 + MAX_REORG_DEPTH <= chain_tip().height {
return ok(["too-deep"])
}
bind hc = check_header(b.header, now)
bind meta = match hc {
ok(v) => v,
bad(e) => (),
}
if meta == () {
return hc
}
bind difficulty = list_get(meta, 0)
bind cumdiff = list_get(meta, 1)
bind generated = list_get(meta, 2)
bind tip = chain_tip()
if b.header.prev_id == tip.id {
bind vb = validate_body(b, b.header.height, b.header.prev_id)
bind err = match vb {
ok(v) => "",
bad(e) => e,
}
if err != "" {
return bad(err)
}
chain_apply(b, id, b.header.height, b.header.prev_id, difficulty, cumdiff, generated)
return ok(["extended"])
}
if b.header.merkle_root != block_merkle(b) {
return bad("merkle root mismatch")
}
store_block(b, id, b.header.height, b.header.prev_id, difficulty, cumdiff, generated)
if big_cmp(cumdiff, tip.cumdiff) <= 0 {
return ok(["side"])
}
return reorg_to(id, tip)
}
fn reorg_to(new_tip, old_tip) {
# Walk the side chain back to the fork point.
bind branch = []
bind cur = new_tip
bind found = false
while found == false {
bind m = match block_meta(cur) {
ok(v) => v,
bad(e) => (),
}
if main_id_at(m.height) == cur {
bind found = true
} else {
bind branch = list_push(branch, cur)
bind cur = m.prev
}
}
bind fork_height = match block_meta(cur) {
ok(v) => v.height,
bad(e) => 0,
}
bind depth = old_tip.height - fork_height
if depth > MAX_REORG_DEPTH {
return bad("reorg deeper than the limit")
}
bind removed = []
while chain_tip().height > fork_height {
bind rb = match chain_rollback() {
ok(v) => v,
bad(e) => (),
}
bind removed = list_push(removed, rb)
}
bind applied = 0
bind failure = ""
bind i = len(branch) - 1
while i >= 0 && failure == "" {
bind bid = list_get(branch, i)
bind blk = match dec_block(block_hex(bid)) {
ok(v) => v,
bad(e) => (),
}
bind m = match block_meta(bid) {
ok(v) => v,
bad(e) => (),
}
bind vb = validate_body(blk, m.height, m.prev)
bind failure = match vb {
ok(v) => "",
bad(e) => e,
}
if failure == "" {
chain_apply(blk, bid, m.height, m.prev, m.difficulty, m.cumdiff, m.generated)
bind applied = applied + 1
}
bind i = i - 1
}
if failure != "" {
bind k = 0
while k < applied {
chain_rollback()
bind k = k + 1
}
bind k = len(removed) - 1
while k >= 0 {
bind rb = list_get(removed, k)
bind rid = block_id(rb.header)
bind m = match block_meta(rid) {
ok(v) => v,
bad(e) => (),
}
chain_apply(rb, rid, m.height, m.prev, m.difficulty, m.cumdiff, m.generated)
bind k = k - 1
}
return bad(failure)
}
return ok(["reorg", depth, removed])
}
# ── sync helpers ──────────────────────────────────────────────────────────
fn chain_locator() {
bind tip = chain_tip()
bind out = []
bind h = tip.height
bind step = 1
while h > 0 {
bind out = list_push(out, main_id_at(h))
if len(out) >= 10 {
bind step = step * 2
}
bind h = h - step
}
return list_push(out, main_id_at(0))
}
fn ids_after_locator(locator, max) {
bind start = 0
bind found = false
bind i = 0
while i < len(locator) && found == false {
bind id = list_get(locator, i)
bind m = match block_meta(id) {
ok(v) => v,
bad(e) => (),
}
if m != () {
if main_id_at(m.height) == id {
bind start = m.height + 1
bind found = true
}
}
bind i = i + 1
}
bind tip = chain_tip()
bind out = []
bind h = start
while h <= tip.height && len(out) < max {
bind out = list_push(out, main_id_at(h))
bind h = h + 1
}
return out
}
fn block_at(height) {
bind id = main_id_at(height)
if id == "" {
return bad("no block at that height")
}
return dec_block(block_hex(id))
}
fn kv_get(k, dflt) {
bind rows = db_query("SELECT v FROM kv WHERE k = ?1", k)
if len(rows) == 0 {
return dflt
}
return list_get(rows, 0).v
}
fn kv_put(k, v) {
db_exec("INSERT INTO kv (k, v) VALUES (?1, ?2) ON CONFLICT(k) DO UPDATE SET v = excluded.v", k, v)
return 0
}
fn bundle_get(hash) {
bind rows = db_query("SELECT data FROM bundles WHERE hash = ?1", hash)
if len(rows) == 0 {
return ""
}
return list_get(rows, 0).data
}
fn bundle_put(hash, data) {
db_exec("INSERT OR IGNORE INTO bundles (hash, data) VALUES (?1, ?2)", hash, data)
# cap the table: evict the oldest rows (lowest rowid) once over MAX_BUNDLES, so bundle
# flooding can't grow the database without bound
bind n = db_query("SELECT COUNT(*) AS c FROM bundles")
if len(n) > 0 && list_get(n, 0).c > MAX_BUNDLES {
db_exec("DELETE FROM bundles WHERE rowid IN (SELECT rowid FROM bundles ORDER BY rowid LIMIT ?1)", list_get(n, 0).c - MAX_BUNDLES)
}
return 0
}
fn audit_rows() {
return db_query("SELECT o.denom AS denom, COUNT(*) AS outputs, (SELECT COUNT(*) FROM key_images k WHERE k.denom = o.denom) AS key_images FROM outputs o GROUP BY o.denom ORDER BY o.denom DESC")
}