灵符lingfu
L

ling-wallet

↓ 0 downloads

Ling Wallet: a post-quantum privacy-coin wallet, full node and HyperTorus miner for LNG

Windows · x86-64 ✕Linux · x86-64 ✕LingOS · x86-64LingOS · Pi 5 ARM64 ✕
Keywords wallet, cryptocurrency, lng, node, miner, ling
lingfu install ling-wallet
LicenseMIT
Authorsling-lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / 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
}

← Back to index