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/isolates/node.ling
# node.ling — the node isolate (capabilities: db, fs, p2p).
#
# Owns the chain database, the mempool and the P2P network. Talks to:
# inbox ← UI, wallet and miners
# ui → status snapshots for the UI
# wallet → tip notifications, submit results, bundle arrivals
# miners → one control channel per miner thread (templates, config)
use "../core/params.ling"
use "../core/codec.ling"
use "../core/money.ling"
use "../core/chain.ling"
use "../core/mempool.ling"
use "../core/wallet.ling"
# Message kinds on the wire (≥ 0x10; lower kinds are the transport's).
bind K_STATUS = 16
bind K_GET_IDS = 17
bind K_IDS = 18
bind K_GET_BLOCKS = 19
bind K_BLOCK = 20
bind K_NEW_BLOCK = 21
bind K_RELAY = 22
bind K_GET_BUNDLE = 23
bind K_BUNDLE = 24
bind TAG_TX = "01"
bind TAG_BUNDLE = "02"
fn now() {
return floor(epoch_now())
}
fn send_ui(msg) {
chan_try_send(st_get("ch_ui", ""), msg)
return 0
}
fn send_wallet(msg) {
chan_try_send(st_get("ch_wallet", ""), msg)
return 0
}
fn net() {
return st_get("net", "")
}
fn node_main(data_dir, inbox, ui, wallet, miners, listen, peers, proxy) {
st_set("lw_core_dir", "../core")
st_set("ch_in", inbox)
st_set("ch_ui", ui)
st_set("ch_wallet", wallet)
st_set("miners", miners)
make_dir(data_dir)
chain_open(data_dir + "/chain.db")
# The wallet isolate opens the database only after this.
send_wallet(["tip", chain_tip().height])
bind start = start_network(data_dir, listen, peers, proxy)
if start != "" {
send_ui(["fatal", "network: " + start])
return bad(start)
}
st_set("template_dirty", 1)
st_set("mining_enabled", 0)
st_set("mining_threads", 1)
st_set("mining_intensity", 35)
st_set("last_status", 0)
st_set("last_template", 0)
st_set("running", 1)
while st_get("running", 0) == 1 {
bind busy = 0
# Our own inbox first (UI, wallet, miners).
bind k = 0
while k < 32 {
bind m = chan_try_recv(inbox)
bind msg = match m {
ok(v) => v,
bad(e) => (),
}
if msg == () {
bind k = 32
} else {
on_local(msg)
bind busy = 1
bind k = k + 1
}
}
# Network events.
bind events = p2p_poll(net(), 64)
bind i = 0
while i < len(events) {
# a handler that raises on malformed peer data must not stop the node: catch it and penalize the sender
match try(on_net_event, list_get(events, i)) {
ok(v) => 0,
bad(e) => on_event_error(list_get(events, i), e),
}
bind i = i + 1
}
if len(events) > 0 {
bind busy = 1
}
periodic()
if busy == 0 {
sleep(20)
}
}
p2p_shutdown(net())
return ok(())
}
fn start_network(data_dir, listen, peers, proxy) {
bind seeds = ""
bind seeds_ok = 0
bind authority = seed_authority_hex()
if file_exists(data_dir + "/seeds.lwseeds") {
bind raw = match file_read_hex(data_dir + "/seeds.lwseeds") {
ok(v) => v,
bad(e) => "",
}
match p2p_seeds_verify(raw, authority) {
ok(info) => st_set("seed_info", info),
bad(e) => st_set("seed_info", ()),
}
bind info = st_get("seed_info", ())
if info != () {
# Same network, not dated in the future.
if list_get(info, 0) == NET_ID && list_get(info, 1) <= now() + 86400 {
bind seeds = raw
bind seeds_ok = 1
}
}
}
st_set("seeds_ok", seeds_ok)
bind idfile = data_dir + "/node_identity.hex"
if file_exists(idfile) == false {
file_write_hex(idfile, lw_random(32))
}
bind identity = match file_read_hex(idfile) {
ok(v) => v,
bad(e) => lw_random(32),
}
bind cfg = [["magic_hex", NET_MAGIC], ["network", NET_ID], ["data_dir", data_dir + "/p2p"], ["authority_pk_hex", authority], ["max_outbound", 8], ["max_inbound", 32], ["identity_seed_hex", identity], ["ephemeral_outbound", true], ["peers", peers]]
if proxy != "" {
bind cfg = list_push(cfg, ["proxy", proxy])
}
if seeds != "" {
bind cfg = list_push(cfg, ["seeds_hex", seeds])
}
st_set("proxy", proxy)
if listen == "" { return try_start(cfg, "") }
# The port can be taken (a seed node or a second wallet on this computer): try the next few, then go outbound-only,
# which still reaches the seeds but takes no inbound connections.
bind colon = last_colon(listen)
bind host = substr(listen, 0, colon)
bind port = to_number(substr(listen, colon + 1, str_len(listen) - colon - 1))
bind k = 0
while k < 10 {
bind addr = host + ":" + num_str(port + k)
bind err = try_start(cfg, addr)
if err == "" {
if k > 0 { println("ling-wallet: port ", port, " is in use; listening on ", addr) }
return ""
}
if !port_taken(err) { return err }
bind k = k + 1
}
println("ling-wallet: ports ", port, "-", port + 9, " are in use; running outbound-only (no inbound connections)")
return try_start(cfg, "")
}
# "" when the network started on `listen` ("" = no listening socket), else the error
fn try_start(cfg, listen) {
bind c = cfg
if listen != "" { bind c = list_push(cfg, ["listen", listen]) }
bind r = match p2p_start(c, "") {
ok(h) => started(h),
bad(e) => e,
}
if r == "" { st_set("listen_addr", listen) }
return r
}
fn last_colon(s) {
bind at = 0 - 1
bind i = 0
while i < str_len(s) {
if substr(s, i, 1) == ":" { bind at = i }
bind i = i + 1
}
return at
}
fn port_taken(err) {
bind e = str_lower(err)
return str_find(e, "10048") >= 0 || str_find(e, "in use") >= 0 || str_find(e, "only one usage") >= 0
}
fn started(h) {
st_set("net", h)
return ""
}
# ── wire helpers ──────────────────────────────────────────────────────────
fn status_payload() {
bind tip = chain_tip()
return hex_concat([u64le_hex(tip.height), tip.id, text_hex(tip.cumdiff)])
}
fn send_get_ids(peer) {
bind loc = chain_locator()
bind parts = [varint_hex(len(loc))]
bind i = 0
while i < len(loc) && i < 64 {
bind parts = list_push(parts, list_get(loc, i))
bind i = i + 1
}
p2p_send(net(), peer, K_GET_IDS, hex_concat(parts))
return 0
}
fn read_hashes(hex, max) {
rd_begin(hex)
bind n = rd_count(max)
bind out = []
bind i = 0
while i < n {
bind out = list_push(out, rd_fixed(32))
bind i = i + 1
}
rd_end()
if rd_failed() {
return ()
}
return out
}
fn hashes_hex(ids) {
bind parts = [varint_hex(len(ids))]
bind i = 0
while i < len(ids) {
bind parts = list_push(parts, list_get(ids, i))
bind i = i + 1
}
return hex_concat(parts)
}
fn peer_key(peer, field) {
return format("peer/{}/{}", peer, field)
}
# ── network events ────────────────────────────────────────────────────────
# A caught handler error: drop a point on the peer that sent the message (bad data from a peer is misbehaviour),
# and carry on. The node stays up.
fn on_event_error(ev, err) {
if list_get(ev, 0) == "message" {
p2p_misbehaving(net(), list_get(ev, 1), 20, "handler error")
}
return 0
}
# A decimal cumulative-difficulty string from a peer: 1..78 ASCII digits, nothing else.
fn is_decimal(s) {
bind n = str_len(s)
if n < 1 || n > 78 { return false }
bind digits = "0123456789"
bind i = 0
while i < n {
if str_find(digits, substr(s, i, 1)) < 0 { return false }
bind i = i + 1
}
return true
}
fn on_net_event(ev) {
bind kind = list_get(ev, 0)
if kind == "connected" {
bind peer = list_get(ev, 1)
st_set(peer_key(peer, "height"), 0)
st_set(peer_key(peer, "cumdiff"), "0")
st_set(peer_key(peer, "inflight"), 0)
p2p_send(net(), peer, K_STATUS, status_payload())
}
if kind == "message" {
on_message(list_get(ev, 1), list_get(ev, 2), list_get(ev, 3))
}
if kind == "disconnected" {
bind peer = list_get(ev, 1)
st_del(peer_key(peer, "height"))
st_del(peer_key(peer, "cumdiff"))
st_del(peer_key(peer, "inflight"))
st_del(peer_key(peer, "relay_w"))
st_del(peer_key(peer, "relay_c"))
st_del(peer_key(peer, "get_w"))
st_del(peer_key(peer, "get_c"))
}
if kind == "stem" {
on_relay(list_get(ev, 1), list_get(ev, 2), true)
}
return 0
}
# A fixed-window per-peer counter. Returns true while the peer is under `limit`
# messages of this `kind` in the current window; false (drop it) once over.
fn rate_ok(peer, kind, limit) {
bind wk = peer_key(peer, kind + "_w")
bind ck = peer_key(peer, kind + "_c")
bind t = now()
if t - st_get(wk, 0) >= RATE_WINDOW_SECS {
st_set(wk, t)
st_set(ck, 0)
}
bind c = st_get(ck, 0) + 1
st_set(ck, c)
return c <= limit
}
# Reasons a relayed transaction was rejected that no honest peer would ever
# send us (forged or structurally invalid) — worth a misbehaviour penalty.
# State- or timing-dependent reasons (already spent, conflicts, young/missing
# ring member, small ring) are NOT penalized: an honest peer can hit those.
fn tx_reject_is_forgery(reason) {
bind bad = ["ring signature invalid", "signature count mismatch", "bad tx version", "is not a denomination", "inputs != outputs", "ring too large", "duplicate key image", "empty inputs", "fee below minimum", "reference height out of range", "unexpected coinbase"]
bind i = 0
while i < len(bad) {
if str_find(reason, list_get(bad, i)) >= 0 { return true }
bind i = i + 1
}
return false
}
fn on_message(peer, kind, payload) {
if kind == K_STATUS {
# a status is tiny (8 + 32 + at most 78 bytes); anything larger is junk
if hex_len(payload) > 118 {
p2p_misbehaving(net(), peer, 20, "oversize status")
return 0
}
rd_begin(payload)
bind height = rd_u64()
bind tip_id = rd_fixed(32)
bind cdr = hex_text(rd_fixed(rd_remaining()))
bind cd = match cdr {
ok(v) => v,
bad(e) => "",
}
# cumdiff must be a plain decimal number before big_cmp (which raises on anything else)
if rd_failed() || !is_decimal(cd) {
p2p_misbehaving(net(), peer, 20, "bad status")
return 0
}
st_set(peer_key(peer, "height"), height)
st_set(peer_key(peer, "cumdiff"), cd)
if big_cmp(cd, chain_tip().cumdiff) > 0 && st_get(peer_key(peer, "inflight"), 0) == 0 {
send_get_ids(peer)
}
return 0
}
if kind == K_GET_IDS {
if !rate_ok(peer, "get", MAX_GETS_PER_WINDOW) {
p2p_misbehaving(net(), peer, 5, "too many requests")
return 0
}
bind loc = read_hashes(payload, 64)
if loc == () {
p2p_misbehaving(net(), peer, 20, "bad locator")
return 0
}
p2p_send(net(), peer, K_IDS, hashes_hex(ids_after_locator(loc, 500)))
return 0
}
if kind == K_IDS {
bind ids = read_hashes(payload, 500)
if ids == () {
p2p_misbehaving(net(), peer, 20, "bad ids")
return 0
}
bind missing = []
bind i = 0
while i < len(ids) {
if has_block(list_get(ids, i)) == false {
bind missing = list_push(missing, list_get(ids, i))
}
bind i = i + 1
}
bind i = 0
while i < len(missing) {
bind chunk = []
bind k = i
while k < len(missing) && k < i + 16 {
bind chunk = list_push(chunk, list_get(missing, k))
bind k = k + 1
}
p2p_send(net(), peer, K_GET_BLOCKS, hashes_hex(chunk))
bind i = i + 16
}
st_set(peer_key(peer, "inflight"), len(missing))
return 0
}
if kind == K_GET_BLOCKS {
if !rate_ok(peer, "get", MAX_GETS_PER_WINDOW) {
p2p_misbehaving(net(), peer, 5, "too many requests")
return 0
}
bind ids = read_hashes(payload, 16)
if ids == () {
p2p_misbehaving(net(), peer, 20, "bad getblocks")
return 0
}
bind i = 0
while i < len(ids) {
bind data = block_hex(list_get(ids, i))
if data != "" {
p2p_send(net(), peer, K_BLOCK, data)
}
bind i = i + 1
}
return 0
}
if kind == K_BLOCK || kind == K_NEW_BLOCK {
bind b = match dec_block(payload) {
ok(v) => v,
bad(e) => (),
}
if b == () {
p2p_misbehaving(net(), peer, 50, "undecodable block")
return 0
}
process_block(b, peer)
if kind == K_BLOCK {
bind left = st_get(peer_key(peer, "inflight"), 0) - 1
if left < 0 {
bind left = 0
}
st_set(peer_key(peer, "inflight"), left)
if left == 0 && big_cmp(st_get(peer_key(peer, "cumdiff"), "0"), chain_tip().cumdiff) > 0 {
send_get_ids(peer)
}
}
return 0
}
if kind == K_RELAY {
if !rate_ok(peer, "relay", MAX_RELAYS_PER_WINDOW) {
p2p_misbehaving(net(), peer, 5, "relay flood")
return 0
}
on_relay(peer, payload, false)
return 0
}
if kind == K_GET_BUNDLE {
if hex_len(payload) == 32 {
bind data = bundle_get(payload)
if data != "" {
p2p_send(net(), peer, K_BUNDLE, data)
}
}
return 0
}
if kind == K_BUNDLE {
# Only bundles we asked for, and only if they verify.
bind bd = match bundle_parse(payload) {
ok(v) => v,
bad(e) => (),
}
if bd == () {
p2p_misbehaving(net(), peer, 30, "forged address bundle")
return 0
}
if bd.network != NET_ID {
p2p_misbehaving(net(), peer, 30, "bundle for another network")
return 0
}
if st_has("lookup/" + bd.hash) == 1 {
bundle_put(bd.hash, payload)
send_wallet(["bundle_arrived", bd.hash])
}
return 0
}
p2p_misbehaving(net(), peer, 5, "unknown message kind")
return 0
}
# Dandelion++ relay of a tagged payload (stem or fluff).
fn on_relay(peer, payload, stem) {
if hex_len(payload) < 2 {
return 0
}
bind tag = hex_slice(payload, 0, 1)
bind body = hex_slice(payload, 1, hex_len(payload) - 1)
if tag == TAG_TX {
bind tx = match dec_tx(body) {
ok(v) => v,
bad(e) => (),
}
if tx == () {
p2p_misbehaving(net(), peer, 30, "undecodable tx")
return 0
}
bind id = tx_id(tx)
bind r = pool_add(tx, now())
if r == "added" {
st_set("template_dirty", 1)
if stem {
p2p_relay(net(), id, payload, K_RELAY, peer)
} else {
p2p_mark_fluffed(net(), id)
p2p_broadcast(net(), K_RELAY, payload, peer)
}
}
if r == "have" && stem == false {
p2p_mark_fluffed(net(), id)
}
# a forged / structurally invalid tx cost us a full ring verification: penalize
if str_starts_with(r, "rejected: ") && tx_reject_is_forgery(r) && peer >= 0 {
p2p_misbehaving(net(), peer, 20, "invalid transaction")
}
return 0
}
if tag == TAG_BUNDLE {
bind bd = match bundle_parse(body) {
ok(v) => v,
bad(e) => (),
}
if bd == () {
p2p_misbehaving(net(), peer, 30, "forged address bundle")
return 0
}
if bd.network != NET_ID {
return 0
}
if bundle_get(bd.hash) != "" {
if stem == false {
p2p_mark_fluffed(net(), bd.hash)
}
return 0
}
bundle_put(bd.hash, body)
if stem {
p2p_relay(net(), bd.hash, payload, K_RELAY, peer)
} else {
p2p_mark_fluffed(net(), bd.hash)
p2p_broadcast(net(), K_RELAY, payload, peer)
}
return 0
}
p2p_misbehaving(net(), peer, 10, "unknown relay tag")
return 0
}
# from_peer: peer id, or -1 for our own miner.
fn process_block(b, from_peer) {
bind r = accept_block(b, now())
bind res = match r {
ok(v) => v,
bad(e) => ["invalid", e],
}
bind what = list_get(res, 0)
if what == "extended" {
pool_on_new_tip([], now())
after_new_tip(b, from_peer)
return true
}
if what == "reorg" {
bind removed = list_get(res, 2)
bind returned = []
bind i = 0
while i < len(removed) {
bind txs = list_get(removed, i).txs
bind k = 0
while k < len(txs) {
bind returned = list_push(returned, list_get(txs, k))
bind k = k + 1
}
bind i = i + 1
}
pool_on_new_tip(returned, now())
after_new_tip((), from_peer)
return true
}
if what == "orphan" && from_peer >= 0 {
send_get_ids(from_peer)
}
if what == "invalid" && from_peer < 0 {
print("node: our miner's block was rejected: ", list_get(res, 1))
}
if what == "invalid" && from_peer >= 0 {
# a block that only fails because it would reorg deeper than allowed may still have valid PoW (an honest peer
# far behind on another branch) — don't ban for that; penalize genuinely invalid blocks
if str_find(list_get(res, 1), "reorg deeper") < 0 {
p2p_misbehaving(net(), from_peer, 50, "invalid block: " + list_get(res, 1))
}
}
return false
}
fn after_new_tip(b, from_peer) {
st_set("template_dirty", 1)
# the per-denomination audit only changes when the output/key-image set does,
# i.e. on a new tip (extend or reorg) — mark it stale so the next status push
# recomputes it once, instead of re-scanning the whole outputs table every second
st_set("audit_dirty", 1)
if b != () {
p2p_broadcast(net(), K_NEW_BLOCK, enc_block(b), from_peer)
}
p2p_broadcast(net(), K_STATUS, status_payload(), -1)
send_wallet(["tip", chain_tip().height])
return 0
}
# ── local messages ────────────────────────────────────────────────────────
fn on_local(msg) {
bind kind = list_get(msg, 0)
if kind == "found" {
match dec_block(list_get(msg, 1)) {
ok(b) => process_block(b, -1),
bad(e) => 0,
}
st_add("blocks_found", 1)
return 0
}
if kind == "hashstat" {
st_add("hashes", list_get(msg, 2))
return 0
}
if kind == "mining_cfg" {
st_set("mining_enabled", list_get(msg, 1))
st_set("mining_threads", list_get(msg, 2))
st_set("mining_intensity", list_get(msg, 3))
kv_put("mining_prefs", format("{},{},{},{}", list_get(msg, 1), list_get(msg, 2), list_get(msg, 3), list_get(msg, 4)))
push_miner_cfg()
st_set("template_dirty", 1)
return 0
}
if kind == "publish_bundle" {
bind data = list_get(msg, 1)
bind bd = match bundle_parse(data) {
ok(v) => v,
bad(e) => (),
}
if bd != () {
bundle_put(bd.hash, data)
kv_put("mining_bundle", data)
p2p_relay(net(), bd.hash, TAG_BUNDLE + data, K_RELAY, -1)
st_set("template_dirty", 1)
}
return 0
}
if kind == "lookup" {
bind h = list_get(msg, 1)
if st_has("lookup/" + h) == 0 {
st_set("lookup/" + h, now())
bind peers = p2p_peers(net())
bind i = 0
while i < len(peers) {
p2p_send(net(), list_get(list_get(peers, i), 0), K_GET_BUNDLE, h)
bind i = i + 1
}
}
return 0
}
if kind == "submit_tx" {
bind req = list_get(msg, 1)
bind data = list_get(msg, 2)
bind tx = match dec_tx(data) {
ok(v) => v,
bad(e) => (),
}
if tx == () {
send_wallet(["submitted", req, "", "malformed transaction"])
return 0
}
bind r = pool_add(tx, now())
if r == "added" || r == "have" {
bind id = tx_id(tx)
p2p_relay(net(), id, TAG_TX + data, K_RELAY, -1)
st_set("template_dirty", 1)
send_wallet(["submitted", req, id, ""])
} else {
send_wallet(["submitted", req, "", r])
}
return 0
}
if kind == "in_pool" {
send_wallet(["in_pool", list_get(msg, 1), pool_has(list_get(msg, 1))])
return 0
}
if kind == "shutdown" {
st_set("running", 0)
return 0
}
return 0
}
# ── mining templates ──────────────────────────────────────────────────────
fn push_miner_cfg() {
bind miners = st_get("miners", [])
bind i = 0
while i < len(miners) {
bind active = st_get("mining_enabled", 0) == 1 && i < st_get("mining_threads", 1)
chan_try_send(list_get(miners, i), ["cfg", active, st_get("mining_intensity", 35)])
bind i = i + 1
}
return 0
}
fn refresh_template() {
bind data = kv_get("mining_bundle", "")
if data == "" {
return 0
}
bind bd = match bundle_parse(data) {
ok(v) => v,
bad(e) => (),
}
if bd == () {
return 0
}
bind tip = chain_tip()
bind meta = block_meta(tip.id).x
bind difficulty = difficulty_after(tip.id)
bind median = median_timestamp_after(tip.id)
bind height = tip.height + 1
# Size the coinbase BEFORE choosing transactions. Its byte size depends only on how many denomination outputs the
# reward splits into; building it with the whole pool's fees gives an upper bound on that size (selected fees are a
# subset), so reserving that many bytes guarantees the real coinbase plus the chosen txs stay under MAX_BLOCK_BYTES.
bind cb_max = coinbase_tx(height, coinbase_split(block_reward(meta.generated) + pool_total_fees()), bd.view_pk, bd.spend_pk)
bind txs = pool_select(hex_len(enc_tx(cb_max)))
bind fees = 0
bind i = 0
while i < len(txs) {
bind fees = fees + list_get(txs, i).fee
bind i = i + 1
}
bind cb = coinbase_tx(height, coinbase_split(block_reward(meta.generated) + fees), bd.view_pk, bd.spend_pk)
bind all = [cb]
bind i = 0
while i < len(txs) {
bind all = list_push(all, list_get(txs, i))
bind i = i + 1
}
bind ts = now()
if ts <= median {
bind ts = median + 1
}
bind blk = Block(Header(BLOCK_VERSION, NET_ID, height, tip.id, ts, "", 0), all)
bind h = with_merkle(blk.header, block_merkle(blk))
bind header_hex = enc_header(h)
bind full = enc_block(Block(h, all))
# [difficulty, header without nonce (82 bytes), everything after the header]
bind tpl = ["template", difficulty, hex_slice(header_hex, 0, 82), hex_slice(full, HEADER_LEN, hex_len(full) - HEADER_LEN)]
st_set("difficulty", difficulty)
bind miners = st_get("miners", [])
bind i = 0
while i < len(miners) {
chan_try_send(list_get(miners, i), tpl)
bind i = i + 1
}
return 0
}
fn periodic() {
bind t = now()
if st_get("template_dirty", 0) == 1 || t - st_get("last_template", 0) > 30 {
st_set("template_dirty", 0)
st_set("last_template", t)
refresh_template()
}
if t - st_get("last_status", 0) >= 1 {
st_set("last_status", t)
push_status()
if t % 30 == 0 {
p2p_broadcast(net(), K_STATUS, status_payload(), -1)
pool_expire(t)
}
}
return 0
}
fn push_status() {
bind tip = chain_tip()
bind peers = p2p_peers(net())
bind best = tip.height
bind i = 0
while i < len(peers) {
bind ph = st_get(peer_key(list_get(list_get(peers, i), 0), "height"), 0)
if ph > best {
bind best = ph
}
bind i = i + 1
}
bind route = if st_get("proxy", "") == "" { "Direct" } else { "SOCKS5 / Tor (" + st_get("proxy", "") + ")" }
send_ui(["node_status", tip.height, best, difficulty_after(tip.id), pool_len(), peers, route, st_get("seeds_ok", 0), audit_cached(), st_get("hashes", 0), st_get("blocks_found", 0)])
st_set("hashes", 0)
return 0
}
# Per-denomination audit rows [denom, outputs, key_images] for the UI. audit_rows()
# scans the whole outputs table, so recompute only when a new tip marked it dirty
# (see after_new_tip); between blocks the once-a-second status push reuses the cache.
fn audit_cached() {
if st_get("audit_dirty", 1) == 0 {
return st_get("audit_cache", [])
}
bind audit = audit_rows()
bind arows = []
bind i = 0
while i < len(audit) {
bind a = list_get(audit, i)
bind arows = list_push(arows, [a.denom, a.outputs, a.key_images])
bind i = i + 1
}
st_set("audit_cache", arows)
st_set("audit_dirty", 0)
return arows
}