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/real_backend.ling
# real_backend.ling — the live backend behind the be_* contract
# (docs/LING_BACKEND.md). Runs on the UI (main) thread: it spawns the
# isolates, owns the channels, caches their status pushes in the st_* store,
# and answers every be_* call without blocking (except the few calls that
# must wait for a password check: create / restore / unlock / reveal /
# change password / confirm).
#
# miners ×N (no capabilities) ── found / hashstat ──▶ node
# node (db, fs, p2p) ── templates/cfg ─────▶ miners
# wallet (keys, db, fs) ◀─ tip / submitted ──── node
# all of them ── status, replies ────▶ ui (this thread)
use "core/params.ling"
bind ABOUT_ROWS = [
["Identity signatures", "standard", "ML-DSA-87 (FIPS 204) signs address bundles and peer handshakes."],
["Key exchange", "standard", "X25519 combined with ML-KEM-1024 (FIPS 203); secure if either holds."],
["Encryption", "standard", "XChaCha20-Poly1305 for the wallet file, memos and network frames."],
["Password hashing", "standard", "Argon2id, 256 MiB, 3 passes, for the wallet file key."],
["Hashing", "standard", "SHAKE256 (SHA-3) with domain separation for every hash."],
["Network wire layer", "novel", "DICE-42 cascade around the encrypted frames; defense in depth only."],
["Ring signatures", "experimental", "LRS-87 lattice linkable ring signatures, 11 members; unaudited."],
["Proof of work", "novel", "HyperTorus memory-hard walk (16 MiB); ends in SHAKE256."],
["Network privacy", "standard", "Dandelion++ relay, padded frames, cover traffic, optional Tor (SOCKS5)."],
["Limitation", "known", "Whoever paid you can tell when you spend those exact coins. Send them to yourself once to break the link."],
["Limitation", "known", "Amounts use fixed denominations; per-output amounts are visible."],
["Limitation", "known", "Small network: rings draw from few outputs until more people use LNG."]
]
fn real_now() {
return floor(epoch_now())
}
fn arg_list(name) {
bind args = get_args()
bind out = []
bind i = 0
while i + 1 < len(args) {
if list_get(args, i) == name {
bind out = list_push(out, list_get(args, i + 1))
}
bind i = i + 1
}
return out
}
fn real_init() {
bind data_dir = cli_arg("data-dir", default_data_dir())
# every interface by default, so other machines can reach this node; `--listen none` takes no inbound connections
bind proxy = cli_arg("proxy", "")
# Default: listen on every interface. With a proxy (Tor), default to NO inbound listener, so a clearnet socket can't
# be linked to the Tor-routed node. --listen none turns inbound off; an explicit --listen overrides either default.
bind listen = cli_arg("listen", if proxy == "" { "0.0.0.0:" + num_str(NET_PORT) } else { "none" })
if listen == "none" { bind listen = "" }
bind peers = arg_list("--peer")
# --fast-kdf: cheap Argon2 for testing only, and only on a folder named with --data-dir: it can never reach the
# real wallet in the default folder (and a real wallet unlocked once with strong settings is re-encrypted with them)
bind fast = cli_arg("fast-kdf", "") != "" && cli_arg("data-dir", "") != ""
if cli_arg("fast-kdf", "") != "" && !fast { println("ling-wallet: --fast-kdf ignored: it needs an explicit --data-dir") }
bind m_kib = if fast { 64 } else { 262144 }
bind t_cost = if fast { 1 } else { 3 }
make_dir(data_dir)
install_seeds(data_dir)
bind ui = chan_new(1024)
bind node_in = chan_new(1024)
# the node's notices to the wallet (tip, submitted, ...) and the UI's requests (passwords, phrases) on separate
# channels: the wallet takes requests only from the UI, and the UI's channel wipes what passes through it
bind wallet_in = chan_new(256)
bind wallet_ui = chan_new_secure(64)
st_set("ui_send", list_get(ui, 0))
st_set("ui_recv", list_get(ui, 1))
st_set("node_send", list_get(node_in, 0))
st_set("wallet_send", list_get(wallet_ui, 0))
# Mining is off by default, so don't spawn a miner isolate per core up front —
# that was one idle interpreter thread per CPU (32 on a 32-core box), each
# parked on a channel wait, holding memory for nothing. Create every control
# channel now so the node can address all potential miners, but keep the
# receiver ends and spawn the isolates lazily in ensure_miners(), the first
# time mining is turned on and only as many as the chosen thread count.
bind cpus = thread_cpus()
bind miners = []
bind recvs = []
bind i = 0
while i < cpus {
bind mc = chan_new(16)
bind miners = list_push(miners, list_get(mc, 0))
bind recvs = list_push(recvs, list_get(mc, 1))
bind i = i + 1
}
st_set("miner_recv", recvs)
st_set("miners_spawned", 0)
st_set("max_threads", cpus)
thread_spawn("isolates/node.ling", "node_main", [data_dir, list_get(node_in, 1), list_get(ui, 0), list_get(wallet_in, 0), miners, listen, peers, proxy], ["db", "fs", "p2p"])
thread_spawn("isolates/walletd.ling", "wallet_main", [data_dir, list_get(wallet_in, 1), list_get(ui, 0), list_get(node_in, 0), m_kib, t_cost, list_get(wallet_ui, 1)], ["keys", "db", "fs"])
st_set("w_state", "nowallet")
st_set("next_req", 1)
st_set("hashrate", 0)
st_set("trace", [])
# Mining preferences survive restarts (node keeps them in its kv table
# too; this is the UI's copy).
st_set("m_enabled", false)
st_set("m_threads", 1)
st_set("m_intensity", 35)
st_set("m_pause", true)
st_set("m_paused_reason", "")
return 0
}
# %APPDATA%/LingWallet/mainnet. A wallet from before the rename (…/testnet) is moved there once; if that folder is in use
# (another wallet running on it), this start keeps using it and the move happens next time.
fn default_data_dir() {
bind root = env_get("APPDATA", ".") + "/LingWallet"
bind dir = root + "/mainnet"
bind old = root + "/testnet"
if !file_exists(dir) && file_exists(old) {
rename_file(old, dir)
if !file_exists(dir) { return old }
}
return dir
}
# The seed list shipped with the wallet (ling/assets/seeds.lwseeds) goes into the data folder when that has none, or an
# older one, so a fresh install can find the network. A list put there by hand that is newer stays.
fn install_seeds(data_dir) {
bind shipped = ""
bind cands = ["assets/seeds.lwseeds", "ling/assets/seeds.lwseeds"]
bind i = 0
while i < len(cands) {
if shipped == "" && file_exists(list_get(cands, i)) { bind shipped = list_get(cands, i) }
bind i = i + 1
}
if shipped == "" { return () }
bind fresh = match file_read_hex(shipped) {
ok(v) => v,
bad(e) => "",
}
bind fresh_at = seeds_issued(fresh)
if fresh_at < 0 { return () }
bind dest = data_dir + "/seeds.lwseeds"
if file_exists(dest) {
bind cur = match file_read_hex(dest) {
ok(v) => v,
bad(e) => "",
}
if seeds_issued(cur) >= fresh_at { return () }
}
file_write_hex(dest, fresh)
}
# When a seed list was signed (unix seconds), or -1 when it is not a valid list for this network's authority.
fn seeds_issued(hex) {
if hex == "" { return 0 - 1 }
return match p2p_seeds_verify(hex, seed_authority_hex()) {
ok(info) => sel_issued(info),
bad(e) => 0 - 1,
}
}
fn sel_issued(info) {
if list_get(info, 0) != NET_ID { return 0 - 1 }
return list_get(info, 1)
}
# Drain everything the isolates pushed since the last frame.
fn real_tick() {
bind n = 0
while n < 128 {
bind m = match chan_try_recv(st_get("ui_recv", "")) {
ok(v) => v,
bad(e) => (),
}
if m == () {
bind n = 128
} else {
on_push(m)
bind n = n + 1
}
}
apply_pause_policy()
return 0
}
fn on_push(m) {
bind kind = list_get(m, 0)
if kind == "wallet_status" {
# After a lock, statuses the wallet sent before it processed the
# lock may still be queued; never let them flip us back to unlocked.
if st_get("locking", 0) == 1 {
if list_get(m, 1) == "unlocked" {
return 0
}
st_set("locking", 0)
}
st_set("w_state", list_get(m, 1))
if list_get(m, 1) == "unlocked" {
st_set("w_unlocked", list_get(m, 2))
st_set("w_pending", list_get(m, 3))
st_set("w_short", list_get(m, 4))
st_set("w_hash", list_get(m, 5))
st_set("w_scanned", list_get(m, 6))
st_set("w_tip", list_get(m, 7))
st_set("w_history", list_get(m, 8))
st_set("w_export", list_get(m, 9))
}
return 0
}
if kind == "node_status" {
st_set("n_height", list_get(m, 1))
st_set("n_best", list_get(m, 2))
st_set("n_difficulty", list_get(m, 3))
st_set("n_mempool", list_get(m, 4))
st_set("n_peers", list_get(m, 5))
st_set("n_route", list_get(m, 6))
st_set("n_seeds", list_get(m, 7))
st_set("n_audit", list_get(m, 8))
# hashes since the previous push (~1 s); smooth it.
bind inst = list_get(m, 9)
st_set("hashrate", st_get("hashrate", 0) * 0.7 + inst * 0.3)
st_set("blocks_found", list_get(m, 10))
return 0
}
if kind == "trace" {
st_set("trace", list_get(m, 1))
return 0
}
if kind == "reply" {
st_set(format("rep/{}", list_get(m, 1)), [list_get(m, 2)])
return 0
}
if kind == "progress" {
st_set(format("prog/{}", list_get(m, 1)), list_get(m, 2))
return 0
}
if kind == "fatal" {
st_set("fatal", list_get(m, 1))
print("ling-wallet: ", list_get(m, 1))
return 0
}
return 0
}
# Send a request to the wallet isolate and wait (pumping other pushes) for
# its reply. Used only for the password-gated calls.
fn wallet_call(msg_tail, timeout_s) {
bind id = st_get("next_req", 1)
st_set("next_req", id + 1)
bind msg = [list_get(msg_tail, 0), id]
bind i = 1
while i < len(msg_tail) {
bind msg = list_push(msg, list_get(msg_tail, i))
bind i = i + 1
}
chan_send(st_get("wallet_send", ""), msg)
bind key = format("rep/{}", id)
bind deadline = epoch_now() + timeout_s
while st_has(key) == 0 && epoch_now() < deadline {
match chan_recv_timeout(st_get("ui_recv", ""), 50) {
ok(m) => on_push(m),
bad(e) => 0,
}
}
if st_has(key) == 0 {
return bad("The wallet didn't answer in time.")
}
bind r = list_get(st_get(key, []), 0)
# replies can carry the 24 words: not kept in the store once read
st_set(key, [()])
return r
}
# Fire-and-poll request (lookups): returns the last answer for `tag`.
fn wallet_ask(tag, msg_tail) {
bind id = st_get("next_req", 1)
st_set("next_req", id + 1)
bind msg = [list_get(msg_tail, 0), id]
bind i = 1
while i < len(msg_tail) {
bind msg = list_push(msg, list_get(msg_tail, i))
bind i = i + 1
}
chan_try_send(st_get("wallet_send", ""), msg)
st_set("ask/" + tag, id)
return 0
}
# ── the be_* contract ─────────────────────────────────────────────────────
fn real_state() {
return st_get("w_state", "nowallet")
}
fn real_create(password) {
st_set("locking", 0)
bind r = wallet_call(["create", password], 30)
return r
}
fn real_restore(phrase, password) {
st_set("locking", 0)
return wallet_call(["restore", phrase, password], 30)
}
fn real_unlock(password) {
st_set("locking", 0)
bind r = wallet_call(["unlock", password], 30)
match r {
ok(v) => st_set("w_state", "unlocked"),
bad(e) => 0,
}
return r
}
fn real_lock() {
st_set("locking", 1)
chan_try_send(st_get("wallet_send", ""), ["lock"])
st_set("w_state", "locked")
st_set("w_history", [])
return 0
}
fn real_summary() {
if st_get("w_state", "") != "unlocked" {
return bad("locked")
}
bind peers = len(st_get("n_peers", []))
bind tip = st_get("n_height", 0)
bind best = st_get("n_best", 0)
bind scanned = st_get("w_scanned", 0)
bind kind = "synced"
bind h = tip
bind target = tip
if peers == 0 && tip == 0 {
bind kind = "connecting"
} else {
if best > tip {
bind kind = "syncing"
bind target = best
} else {
if scanned < tip {
bind kind = "syncing"
bind h = scanned
}
}
}
return ok(Summary(st_get("w_unlocked", 0), st_get("w_pending", 0), st_get("w_short", ""), st_get("w_hash", ""), kind, h, target, NET_NAME))
}
fn real_history(limit) {
bind rows = sort_history(st_get("w_history", []))
bind out = []
bind i = 0
while i < len(rows) && i < limit {
bind r = list_get(rows, i)
bind out = list_push(out, TxRecord(list_get(r, 0), list_get(r, 1), list_get(r, 2), list_get(r, 3), list_get(r, 4), list_get(r, 5), list_get(r, 6), list_get(r, 7), list_get(r, 8), list_get(r, 9)))
bind i = i + 1
}
return out
}
# Newest first; mempool entries (height -1) on top.
fn hist_key(r) {
bind h = list_get(r, 4)
if h == -1 {
return 9000000000000
}
return h
}
fn sort_history(rows) {
bind out = []
bind i = 0
while i < len(rows) {
bind v = list_get(rows, i)
bind tmp = []
bind placed = false
bind k = 0
while k < len(out) {
bind w = list_get(out, k)
if placed == false && hist_key(v) > hist_key(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
}
fn real_lookup(address) {
bind tag = "lookup:" + address
if st_has("ask/" + tag) == 0 || real_now() - st_get("asked/" + tag, 0) >= 1 {
st_set("asked/" + tag, real_now())
wallet_ask(tag, ["lookup", address])
}
bind id = st_get("ask/" + tag, 0)
bind r = st_get(format("rep/{}", id), ())
if r == () {
bind prev = st_get("last/" + tag, ())
if prev == () {
return ["pending"]
}
return prev
}
bind ans = list_get(r, 0)
st_set("last/" + tag, ans)
return ans
}
fn real_prepare(to, amount, memo) {
bind r = wallet_call(["prepare", to, amount, memo], 30)
return match r {
ok(p) => ok(Prepared(list_get(p, 0), list_get(p, 1), list_get(p, 2), list_get(p, 3), list_get(p, 4), list_get(p, 5), list_get(p, 6), list_get(p, 7), list_get(p, 8), list_get(p, 9))),
bad(e) => r,
}
}
fn real_confirm(id, password) {
return wallet_call(["confirm", id, password], 60)
}
fn real_progress(id) {
return st_get(format("prog/{}", id), ["none"])
}
fn real_export_bundle() {
if st_get("w_state", "") != "unlocked" {
return ""
}
return st_get("w_export", "")
}
fn real_reveal(password) {
return wallet_call(["reveal", password], 30)
}
fn real_change_password(old, new) {
return wallet_call(["change_pw", old, new], 60)
}
fn real_mining() {
bind paused = st_get("m_paused_reason", "")
return Mining(st_get("m_enabled", false), st_get("m_threads", 1), st_get("max_threads", 1), st_get("m_intensity", 35), st_get("hashrate", 0), st_get("blocks_found", 0), st_get("n_difficulty", "0"), st_get("trace", []), st_get("m_pause", true), paused)
}
# Spawn miner isolates on demand, up to `k` threads. Called the first time mining
# turns on (and again if the thread count grows). Spawned miners persist — when
# inactive they park on a 500 ms channel wait, costing almost nothing — so we never
# spawn more than the high-water-mark thread count, and a wallet that never mines
# spawns none at all. The node already holds every control channel's sender end.
fn ensure_miners(k) {
bind recvs = st_get("miner_recv", [])
bind want = k
if want > len(recvs) { bind want = len(recvs) }
bind spawned = st_get("miners_spawned", 0)
while spawned < want {
thread_spawn("isolates/miner.ling", "miner_main", [spawned, list_get(recvs, spawned), st_get("node_send", ""), st_get("ui_send", "")], [])
bind spawned = spawned + 1
}
st_set("miners_spawned", spawned)
return 0
}
fn send_mining_cfg() {
bind effective = st_get("m_enabled", false) && st_get("m_paused_reason", "") == ""
bind e = if effective { 1 } else { 0 }
bind p = if st_get("m_pause", true) { 1 } else { 0 }
chan_try_send(st_get("node_send", ""), ["mining_cfg", e, st_get("m_threads", 1), st_get("m_intensity", 35), p])
return 0
}
fn real_set_mining(enabled, threads, intensity, pause) {
st_set("m_enabled", enabled)
st_set("m_threads", threads)
st_set("m_intensity", intensity)
st_set("m_pause", pause)
# Bring the miner isolates into existence the first time they're needed (and
# when the thread count grows); send_mining_cfg then activates the first
# `threads` of them via the node.
if enabled { ensure_miners(threads) }
send_mining_cfg()
return 0
}
# Pause while the computer is in use (no input for < 60 s) or on battery.
fn apply_pause_policy() {
if real_now() - st_get("pause_checked", 0) < 2 {
return 0
}
st_set("pause_checked", real_now())
bind reason = ""
if st_get("m_pause", true) {
if sys_on_battery() {
bind reason = "On battery"
} else {
if sys_idle_secs() < 60 {
bind reason = "You're using the computer"
}
}
}
if reason != st_get("m_paused_reason", "") {
st_set("m_paused_reason", reason)
send_mining_cfg()
}
return 0
}
fn real_network() {
bind peers = st_get("n_peers", [])
bind rows = []
bind i = 0
while i < len(peers) {
bind p = list_get(peers, i)
# p2p_peers rows: [peer_id, addr, inbound, fingerprint, is_seed, connected_secs]
bind rows = list_push(rows, [list_get(p, 1), list_get(p, 2), list_get(p, 3), list_get(p, 4), list_get(p, 5)])
bind i = i + 1
}
bind seeds = st_get("n_seeds", 0) == 1
return NetStatus(rows, st_get("n_route", "Direct"), seeds, st_get("n_height", 0), st_get("n_difficulty", "0"), st_get("n_mempool", 0), "")
}
fn real_audit() {
return st_get("n_audit", [])
}
fn real_about() {
return ABOUT_ROWS
}