灵符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/core/wallet.ling
# wallet.ling — keys and addresses (DESIGN §3), wallet file (§9), chain
# scanning, and transaction construction (§5.3, §7.4–7.5).
#
# Runs inside the wallet isolate (capabilities: keys, db, fs). Secret keys
# exist only as lw_* key handles inside this isolate; nothing secret is
# written to disk except the Argon2id-encrypted master seed.

use "params.ling"
use "codec.ling"
use "money.ling"
use "chain.ling"

bind BUNDLE_VERSION = 1
bind OUTPUT_CTX = "4c57312f6f7574707574"
bind WALLET_MAGIC = "4c575731"
bind MEMO_TEXT_LEN = 24
bind ZERO_NONCE = "000000000000000000000000000000000000000000000000"

# ── keys ──────────────────────────────────────────────────────────────────

# Derive every key from the master seed and remember the handles/publics in
# this isolate's store.
fn keys_load(master) {
    # a second load (create and restore load, then open the session) must not leave the first set of secret handles alive
    keys_drop()
    bind spend_seed = lw_xof("LW1/derive/spend", [master], 32)
    bind view_seed = lw_xof("LW1/derive/view", [master], 96)
    bind id_seed = lw_xof("LW1/derive/identity", [master], 32)
    bind spend_h = unwrap_or_die(lw_spend_keypair(spend_seed))
    bind view_h = unwrap_or_die(lw_kem_keypair(view_seed))
    bind id_h = unwrap_or_die(lw_id_keypair(id_seed))
    wipe(spend_seed)
    wipe(view_seed)
    wipe(id_seed)
    st_set("k_master", master)
    st_set("k_spend", spend_h)
    st_set("k_view", view_h)
    st_set("k_id", id_h)
    st_set("k_spend_pk", lw_spend_public(spend_h))
    st_set("k_view_pk", lw_kem_public(view_h))
    st_set("k_id_pk", lw_id_public(id_h))
    st_set("k_bundle", bundle_create())
    return 0
}

fn keys_drop() {
    if st_get("k_spend", "") != "" {
        lw_key_drop(st_get("k_spend", ""))
        lw_key_drop(st_get("k_view", ""))
        lw_key_drop(st_get("k_id", ""))
        wipe(st_get("k_master", ""))
    }
    st_set("k_spend", "")
    st_set("k_view", "")
    st_set("k_id", "")
    st_set("k_master", "")
    return 0
}

fn unwrap_or_die(r) {
    return match r {
        ok(v) => v,
        bad(e) => panic_with(e),
    }
}

fn panic_with(e) {
    print("wallet: fatal: ", e)
    return ""
}

# ── address bundles and short addresses ───────────────────────────────────

fn bundle_body(network, view_pk, spend_pk, id_pk) {
    return hex_concat([u8_hex(BUNDLE_VERSION), u8_hex(network), view_pk, spend_pk, id_pk])
}

fn bundle_create() {
    bind body = bundle_body(NET_ID, st_get("k_view_pk", ""), st_get("k_spend_pk", ""), st_get("k_id_pk", ""))
    bind sig = lw_id_sign(st_get("k_id", ""), "bundle", lw_h256("LW1/bundle", [body]))
    return hex_concat([body, sig])
}

form Bundle { network, view_pk, spend_pk, id_pk, body, hash, raw }

# Decode AND verify. Never returns an unverified bundle.
fn bundle_parse(hex) {
    if hex_len(hex) != 2 + 1600 + 2944 + 2592 + 4627 {
        return bad("address data has the wrong length")
    }
    if hex_u8(hex, 0).x != BUNDLE_VERSION {
        return bad("unsupported address version")
    }
    bind network = hex_u8(hex, 1).x
    bind view_pk = hex_slice(hex, 2, 1600)
    bind spend_pk = hex_slice(hex, 1602, 2944)
    bind id_pk = hex_slice(hex, 4546, 2592)
    bind sig = hex_slice(hex, 7138, 4627)
    if lw_pk_valid(spend_pk) == false {
        return bad("address has an invalid spend key")
    }
    bind body = bundle_body(network, view_pk, spend_pk, id_pk)
    if lw_id_verify(id_pk, "bundle", lw_h256("LW1/bundle", [body]), sig) == false {
        return bad("address signature does not verify")
    }
    return ok(Bundle(network, view_pk, spend_pk, id_pk, body, lw_h256("LW1/address", [body]), hex))
}

fn addr_checksum(h) {
    return hex_slice(lw_h256("LW1/address-checksum", [text_hex("lng1"), h]), 0, 4)
}

fn short_address(h) {
    return "lng1" + base32_encode(hex_concat([h, addr_checksum(h)]))
}

fn parse_short_address(s) {
    bind t = trim(s)
    if starts_with(t, "lng1") == false {
        return bad("LNG addresses start with lng1")
    }
    bind raw = match base32_decode(substr(t, 4)) {
        ok(v) => v,
        bad(e) => "",
    }
    if hex_len(raw) != 36 {
        return bad("address has the wrong length or invalid characters")
    }
    bind h = hex_slice(raw, 0, 32)
    if hex_eq(hex_slice(raw, 32, 4), addr_checksum(h)) == false {
        return bad("address checksum doesn't match (typo?)")
    }
    return ok(h)
}

fn fingerprint(h) {
    return format("{}-{}-{}-{}", hex_slice(h, 0, 2), hex_slice(h, 2, 2), hex_slice(h, 4, 2), hex_slice(h, 6, 2))
}

fn bundle_export(hex) {
    return "lngkey1" + base64url_encode(hex)
}

fn bundle_import(s) {
    bind t = trim(s)
    if starts_with(t, "lngkey1") == false {
        return bad("not an lngkey1 address")
    }
    return match base64url_decode(substr(t, 7)) {
        ok(hex) => bundle_parse(hex),
        bad(e) => bad("address text is corrupted"),
    }
}

# ── wallet file (DESIGN §9) ───────────────────────────────────────────────

# notes: list of [txid, to, amount, fee, memo, timestamp]
fn wallet_encode_plain(master, birth, notes) {
    bind parts = [master, u64le_hex(birth), varint_hex(len(notes))]
    bind i = 0
    while i < len(notes) {
        bind n = list_get(notes, i)
        bind to = text_hex(list_get(n, 1))
        bind memo = text_hex(list_get(n, 4))
        bind parts = list_push(parts, hex_concat([list_get(n, 0), varint_hex(hex_len(to)), to, u64le_hex(list_get(n, 2)), u64le_hex(list_get(n, 3)), varint_hex(hex_len(memo)), memo, u64le_hex(list_get(n, 5))]))
        bind i = i + 1
    }
    return hex_concat(parts)
}

fn wallet_save(path, password, master, birth, notes, m_kib, t_cost) {
    # never replace a wallet file with one that has no seed (e.g. a save after the keys were dropped by a lock)
    if hex_len(master) != 32 {
        return bad("refusing to save a wallet without its seed")
    }
    bind salt = lw_random(16)
    bind nonce = lw_random(24)
    bind key = match lw_password_key(password, salt, m_kib, t_cost, 1) {
        ok(k) => k,
        bad(e) => "",
    }
    if key == "" {
        return bad("couldn't derive the wallet key")
    }
    bind header = hex_concat([WALLET_MAGIC, u32le_hex(m_kib), u32le_hex(t_cost), u32le_hex(1), salt, nonce])
    bind plain = wallet_encode_plain(master, birth, notes)
    bind ct = lw_aead_seal(key, nonce, header, plain)
    wipe(key)
    wipe(plain)
    return file_write_hex(path, hex_concat([header, ct]))
}

# The Argon2 settings a wallet file was saved with: [memory KiB, passes], [0, 0] when unreadable.
fn wallet_kdf(path) {
    bind data = match file_read_hex(path) {
        ok(v) => v,
        bad(e) => "",
    }
    if hex_len(data) < 16 || hex_slice(data, 0, 4) != WALLET_MAGIC {
        return [0, 0]
    }
    return [hex_u32le(data, 4).x, hex_u32le(data, 8).x]
}

# → ok([master, birth, notes]) / bad(msg)
fn wallet_load(path, password) {
    bind data = match file_read_hex(path) {
        ok(v) => v,
        bad(e) => "",
    }
    if hex_len(data) < 56 + 16 {
        return bad("not a ling-wallet file")
    }
    if hex_slice(data, 0, 4) != WALLET_MAGIC {
        return bad("not a ling-wallet file")
    }
    bind m_kib = hex_u32le(data, 4).x
    bind t_cost = hex_u32le(data, 8).x
    bind p_cost = hex_u32le(data, 12).x
    if m_kib > 4194304 || t_cost > 64 || p_cost != 1 {
        return bad("wallet file has invalid parameters")
    }
    bind key = match lw_password_key(password, hex_slice(data, 16, 16), m_kib, t_cost, p_cost) {
        ok(k) => k,
        bad(e) => "",
    }
    bind opened = lw_aead_open(key, hex_slice(data, 32, 24), hex_slice(data, 0, 56), hex_slice(data, 56, hex_len(data) - 56))
    wipe(key)
    bind plain = match opened {
        ok(p) => p,
        bad(e) => "",
    }
    if plain == "" {
        return bad("Wrong password.")
    }
    rd_begin(plain)
    bind master = rd_fixed(32)
    bind birth = rd_u64()
    bind n = rd_count(1000000)
    bind notes = []
    bind i = 0
    while i < n {
        bind txid = rd_fixed(32)
        bind to = hex_text(rd_fixed(rd_count(4096))).x
        bind amount = rd_u64()
        bind fee = rd_u64()
        bind memo = hex_text(rd_fixed(rd_count(4096))).x
        bind ts = rd_u64()
        bind notes = list_push(notes, [txid, to, amount, fee, memo, ts])
        bind i = i + 1
    }
    rd_end()
    wipe(plain)
    if rd_failed() {
        return bad("wallet file is corrupted")
    }
    return ok([master, birth, notes])
}

# ── scanning ──────────────────────────────────────────────────────────────
#
# Owned outputs live in the st_* list "owned"; each entry is
#   [denom, gidx, height, coinbase, txid, out_index, shared, kid, spent_height, spent_txid, memo]
# "own_ki/<kid>" → index; "cnt/<denom>" → next global index;
# "scan_ids" list of scanned main-chain ids; "spend_tx/<txid>" → height.

fn scan_reset() {
    st_lclear("owned")
    st_lclear("scan_ids")
    st_lclear("spend_txs")
    st_set("scan_gen", st_get("scan_gen", 0) + 1)
    return 0
}

fn scan_height() {
    return st_llen("scan_ids") - 1
}

fn own_key(prefix, k) {
    return format("{}{}/{}", prefix, st_get("scan_gen", 0), k)
}

fn memo_key(ss, j) {
    return lw_xof("LW1/memo", [ss, u32le_hex(j)], 32)
}

fn memo_seal(ss, j, onetime, hint, text) {
    bind t = text_hex(text)
    if hex_len(t) > MEMO_TEXT_LEN {
        bind t = hex_slice(t, 0, MEMO_TEXT_LEN)
    }
    bind pad = str_repeat("00", MEMO_TEXT_LEN - hex_len(t))
    return lw_aead_seal(memo_key(ss, j), ZERO_NONCE, onetime, hex_concat([u64le_hex(hint), t, pad]))
}

fn memo_open(ss, j, onetime, memo) {
    return match lw_aead_open(memo_key(ss, j), ZERO_NONCE, onetime, memo) {
        ok(p) => memo_text(hex_slice(p, 8, MEMO_TEXT_LEN)),
        bad(e) => "",
    }
}

fn memo_text(h) {
    # Strip trailing zero bytes.
    bind n = hex_len(h)
    while n > 0 && hex_slice(h, n - 1, 1) == "00" {
        bind n = n - 1
    }
    return match hex_text(hex_slice(h, 0, n)) {
        ok(s) => s,
        bad(e) => "",
    }
}

fn scan_block(b) {
    bind height = b.header.height
    st_set(own_key("btime/", height), b.header.timestamp)
    bind i = 0
    while i < len(b.txs) {
        bind tx = list_get(b.txs, i)
        bind txid = tx_id(tx)
        bind k = 0
        while k < len(tx.inputs) {
            bind kid = ki_id(list_get(tx.inputs, k).key_image)
            if st_has(own_key("own_ki/", kid)) == 1 {
                bind idx = st_get(own_key("own_ki/", kid), 0)
                bind o = st_lget("owned", idx)
                st_lset("owned", idx, [list_get(o, 0), list_get(o, 1), list_get(o, 2), list_get(o, 3), list_get(o, 4), list_get(o, 5), list_get(o, 6), list_get(o, 7), height, txid, list_get(o, 10)])
                st_set(own_key("pending_ki/", kid), 0)
                if st_has(own_key("spend_tx/", txid)) == 0 {
                    st_lpush("spend_txs", txid)
                }
                st_set(own_key("spend_tx/", txid), height)
            }
            bind k = k + 1
        }
        bind j = 0
        while j < len(tx.outputs) {
            bind o = list_get(tx.outputs, j)
            bind ckey = own_key("cnt/", o.denom)
            bind gidx = st_get(ckey, 0)
            st_set(ckey, gidx + 1)
            bind ss = match lw_kem_decaps(st_get("k_view", ""), o.kem_ct, OUTPUT_CTX) {
                ok(v) => v,
                bad(e) => "",
            }
            if ss != "" {
                bind t = match lw_onetime_public(st_get("k_spend_pk", ""), ss, j) {
                    ok(v) => v,
                    bad(e) => "",
                }
                if t != "" && hex_eq(t, o.onetime) {
                    bind oh = lw_onetime_secret(st_get("k_spend", ""), ss, j).x
                    bind kid = ki_id(lw_onetime_key_image(oh))
                    lw_key_drop(oh)
                    bind cb = if tx.coinbase { 1 } else { 0 }
                    st_set(own_key("own_ki/", kid), st_llen("owned"))
                    st_lpush("owned", [o.denom, gidx, height, cb, txid, j, ss, kid, -1, "", memo_open(ss, j, o.onetime, o.memo)])
                }
            }
            bind j = j + 1
        }
        bind i = i + 1
    }
    st_lpush("scan_ids", block_id(b.header))
    return 0
}

# Catch up with the main chain (at most `max_blocks` per call). Restarts
# from scratch if the chain reorganized under us.
fn scan_sync(max_blocks) {
    bind h = scan_height()
    if h >= 0 {
        if main_id_at(h) != st_lget("scan_ids", h) {
            scan_reset()
        }
    }
    bind tip = chain_tip()
    bind n = 0
    while st_llen("scan_ids") <= tip.height && n < max_blocks {
        bind b = match block_at(st_llen("scan_ids")) {
            ok(v) => v,
            bad(e) => (),
        }
        if b == () {
            bind n = max_blocks
        } else {
            scan_block(b)
            bind n = n + 1
        }
    }
    return st_llen("scan_ids") > tip.height
}

fn owned_spendable(i, tip_height) {
    bind o = st_lget("owned", i)
    if list_get(o, 8) != -1 {
        return false
    }
    if st_get(own_key("pending_ki/", list_get(o, 7)), 0) == 1 {
        return false
    }
    return is_eligible(list_get(o, 2), list_get(o, 3), tip_height)
}

# → [unlocked, pending]
fn balance(tip_height) {
    bind unlocked = 0
    bind pending = 0
    bind i = 0
    while i < st_llen("owned") {
        bind o = st_lget("owned", i)
        if list_get(o, 8) == -1 && st_get(own_key("pending_ki/", list_get(o, 7)), 0) != 1 {
            if is_eligible(list_get(o, 2), list_get(o, 3), tip_height) {
                bind unlocked = unlocked + list_get(o, 0)
            } else {
                bind pending = pending + list_get(o, 0)
            }
        }
        bind i = i + 1
    }
    return [unlocked, pending]
}

# ── churn (payer-unlinkability) ─────────────────────────────────────────────
# A payer who sent you coins can recognise when you later spend those exact
# outputs (DESIGN §5.5, "sender spend-tracing"): the key image is linear in a
# value the sender knows. Sending a received output to yourself once ("churn")
# breaks that link — the payer then sees the churn, not your real onward spend.
#
# We only churn outputs RECEIVED FROM OTHERS: never a coinbase (it has no payer)
# and never our own change or a previous churn (its creating txid is one of ours,
# recorded as "mytx/"). Excluding our own txids is also what stops churn looping
# on the self-output it just created.
#
# This module only *identifies* the outputs and decides policy; actually
# broadcasting the self-send is wallet-daemon work and is gated behind a setting
# (off by default) because it spends a fee and goes out on the network.
fn is_received_from_other(o) {
    if list_get(o, 3) != 0 {
        return false
    }
    return st_get(own_key("mytx/", list_get(o, 4)), 0) == 0
}

# Owned-output indices that leak payer-linkability and are spendable right now.
fn churn_candidates(tip_height) {
    bind out = []
    bind i = 0
    while i < st_llen("owned") {
        bind o = st_lget("owned", i)
        if is_received_from_other(o) && owned_spendable(i, tip_height) {
            bind out = list_push(out, i)
        }
        bind i = i + 1
    }
    return out
}

fn churn_enabled() {
    return st_get("churn_on", 0) == 1
}

# ── sending ───────────────────────────────────────────────────────────────

bind INPUT_BYTES = 8 + 1 + 55 + 2944 + 32 + 11 * 9600
bind OUTPUT_BYTES = 8 + 2944 + 1600 + 48

fn fee_for(n_in, n_out) {
    return (32 + n_in * INPUT_BYTES + n_out * OUTPUT_BYTES) * FEE_PER_BYTE
}

# → ok([input_indices, fee, change, n_outputs]) / bad(msg)
fn plan_send(tip_height, amount) {
    if amount < 1 {
        return bad("Enter an amount.")
    }
    # Spendable coins, largest first.
    bind coins = []
    bind i = 0
    while i < st_llen("owned") {
        if owned_spendable(i, tip_height) {
            bind coins = list_push(coins, i)
        }
        bind i = i + 1
    }
    bind coins = sort_by_denom_desc(coins)
    bind total = 0
    bind i = 0
    while i < len(coins) {
        bind total = total + list_get(st_lget("owned", list_get(coins, i)), 0)
        bind i = i + 1
    }
    bind to_outputs = len(decompose(amount))
    bind fee = fee_for(1, to_outputs + 2)
    bind selected = []
    bind round = 0
    bind settled = false
    while round < 8 && settled == false {
        bind target = amount + fee
        if target > total {
            return bad("Not enough unlocked funds (including the network fee).")
        }
        bind selected = select_coins(coins, target)
        if len(selected) > MAX_TX_INPUTS {
            return bad("This payment needs too many coins at once; send a smaller amount first.")
        }
        bind sum = coins_sum(selected)
        bind new_fee = fee_for(len(selected), to_outputs + len(decompose(sum - target)))
        if new_fee <= fee {
            bind settled = true
        } else {
            bind fee = new_fee
        }
        bind round = round + 1
    }
    bind sum = coins_sum(selected)
    bind raw_change = sum - amount - fee
    bind change = raw_change - raw_change % 1000
    bind fee = sum - amount - change
    return ok([selected, fee, change, to_outputs + len(decompose(change))])
}

fn coins_sum(idxs) {
    bind s = 0
    bind i = 0
    while i < len(idxs) {
        bind s = s + list_get(st_lget("owned", list_get(idxs, i)), 0)
        bind i = i + 1
    }
    return s
}

# Smallest single coin covering `target`, else largest-first accumulation.
fn select_coins(coins, target) {
    bind best = -1
    bind i = 0
    while i < len(coins) {
        if list_get(st_lget("owned", list_get(coins, i)), 0) >= target {
            bind best = list_get(coins, i)
        }
        bind i = i + 1
    }
    if best >= 0 {
        return [best]
    }
    bind acc = []
    bind sum = 0
    bind i = 0
    while i < len(coins) && sum < target {
        bind acc = list_push(acc, list_get(coins, i))
        bind sum = sum + list_get(st_lget("owned", list_get(coins, i)), 0)
        bind i = i + 1
    }
    return acc
}

fn sort_by_denom_desc(idxs) {
    bind out = []
    bind i = 0
    while i < len(idxs) {
        bind v = list_get(idxs, i)
        bind dv = list_get(st_lget("owned", v), 0)
        bind tmp = []
        bind placed = false
        bind k = 0
        while k < len(out) {
            bind w = list_get(out, k)
            if placed == false && dv > list_get(st_lget("owned", w), 0) {
                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
}

# Unbiased-enough random integer in [0, n) from the OS CSPRNG.
fn rand_below(n) {
    return hex_u32le(lw_random(4), 0).x % n
}

# ── Decoy age model (ring privacy) ───────────────────────────────────────────
# Real spends cluster at recent-but-not-brand-new ages with a long tail. Picking
# decoys uniformly would leave the (usually recent) real output standing out, so
# we draw each decoy's age — its distance back from the newest eligible output —
# from a gamma-shaped law (an Erlang sum of exponentials: mode > 0, right-skewed),
# plus a uniform-tail share so old outputs stay covered. This is a wallet-only
# policy: any distinct members from the eligible set are consensus-valid, so no
# fork is needed. Parameters are a sensible starting point, not a calibration
# against real LNG spend-age data (which doesn't exist yet) — tune once it does.
bind DECOY_SHAPE = 2       # Erlang shape k: higher = more symmetric, less skew
bind DECOY_MEAN_PCT = 25   # mean decoy age ≈ this % of the eligible pool, back from newest
bind DECOY_TAIL_PCT = 20   # this % of decoys drawn uniformly over all history (heavy tail)

fn rand_unit() {           # uniform in (0, 1]
    return (rand_below(1000000) + 1) / 1000000.0
}
fn erlang_unit(k) {        # Erlang(k, 1) = sum of k exponentials; mean k, variance k
    bind s = 0.0
    bind i = 0
    while i < k {
        bind s = s - ln(rand_unit())
        bind i = i + 1
    }
    return s
}
# An eligible-list index in [0, n), gamma-recency weighted (0 = oldest, n-1 = newest).
fn decoy_index(n) {
    if rand_below(100) < DECOY_TAIL_PCT {
        return rand_below(n)
    }
    bind scale = (DECOY_MEAN_PCT / 100.0) * n / DECOY_SHAPE
    bind age = floor(erlang_unit(DECOY_SHAPE) * scale)
    if age < 0 { bind age = 0 }
    if age > n - 1 { bind age = n - 1 }
    return (n - 1) - age
}

fn list_has(l, v) {
    bind i = 0
    while i < len(l) {
        if list_get(l, i) == v {
            return true
        }
        bind i = i + 1
    }
    return false
}

# → ok([sorted_ring, signer_pos, reduced]) / bad
fn select_ring(denom, mine_gidx, ref) {
    bind rows = db_query("SELECT gidx FROM outputs WHERE denom = ?1 AND ((coinbase = 1 AND height + ?3 <= ?2) OR (coinbase = 0 AND height + ?4 <= ?2)) ORDER BY gidx", denom, ref, COINBASE_MATURITY, MIN_OUTPUT_AGE)
    bind eligible = []
    bind i = 0
    while i < len(rows) {
        bind eligible = list_push(eligible, list_get(rows, i).gidx)
        bind i = i + 1
    }
    if list_has(eligible, mine_gidx) == false {
        return bad("a selected coin isn't spendable yet")
    }
    bind reduced = len(eligible) <= RING_SIZE
    bind ring = []
    if reduced {
        bind ring = eligible
    } else {
        # Gamma-recency weighted decoys (see decoy_index): decoys mimic the age
        # profile of real spends so the real output doesn't stand out.
        bind ring = [mine_gidx]
        bind tries = 0
        while len(ring) < RING_SIZE && tries < RING_SIZE * 64 {
            bind pick = list_get(eligible, decoy_index(len(eligible)))
            if list_has(ring, pick) == false {
                bind ring = list_push(ring, pick)
            }
            bind tries = tries + 1
        }
        # Degenerate pools (barely over RING_SIZE, many collisions): backfill in order.
        bind j = 0
        while len(ring) < RING_SIZE && j < len(eligible) {
            bind g = list_get(eligible, j)
            if list_has(ring, g) == false {
                bind ring = list_push(ring, g)
            }
            bind j = j + 1
        }
        bind ring = sort_numbers(ring)
    }
    bind pos = 0
    bind i = 0
    while i < len(ring) {
        if list_get(ring, i) == mine_gidx {
            bind pos = i
        }
        bind i = i + 1
    }
    return ok([ring, pos, reduced])
}

# Fisher–Yates shuffle with OS randomness.
fn shuffle(l) {
    bind out = l
    bind i = len(out) - 1
    while i > 0 {
        bind j = rand_below(i + 1)
        bind a = list_get(out, i)
        bind out = list_set(out, i, list_get(out, j))
        bind out = list_set(out, j, a)
        bind i = i - 1
    }
    return out
}

fn make_output(view_pk, spend_pk, j, denom, hint, text) {
    bind enc = lw_kem_encaps(view_pk, OUTPUT_CTX).x
    bind ct = list_get(enc, 0)
    bind ss = list_get(enc, 1)
    bind onetime = lw_onetime_public(spend_pk, ss, j).x
    bind memo = memo_seal(ss, j, onetime, hint, text)
    wipe(ss)
    return Output(denom, onetime, ct, memo)
}

# Build and sign. `recipient` is a verified Bundle. → ok([tx, kids, reduced]) / bad
fn build_tx(plan, recipient, amount, memo, ref) {
    bind selected = list_get(plan, 0)
    bind fee = list_get(plan, 1)
    bind change = list_get(plan, 2)
    # [denom, to_recipient]
    bind targets = []
    bind d1 = decompose(amount)
    bind i = 0
    while i < len(d1) {
        bind targets = list_push(targets, [list_get(d1, i), true])
        bind i = i + 1
    }
    bind d2 = decompose(change)
    bind i = 0
    while i < len(d2) {
        bind targets = list_push(targets, [list_get(d2, i), false])
        bind i = i + 1
    }
    bind targets = shuffle(targets)
    bind outputs = []
    bind j = 0
    while j < len(targets) {
        bind t = list_get(targets, j)
        if list_get(t, 1) {
            bind outputs = list_push(outputs, make_output(recipient.view_pk, recipient.spend_pk, j, list_get(t, 0), amount, memo))
        } else {
            bind outputs = list_push(outputs, make_output(st_get("k_view_pk", ""), st_get("k_spend_pk", ""), j, list_get(t, 0), change, "change"))
        }
        bind j = j + 1
    }
    bind inputs = []
    bind rings = []
    bind kids = []
    bind reduced = false
    bind i = 0
    while i < len(selected) {
        bind o = st_lget("owned", list_get(selected, i))
        bind sr = select_ring(list_get(o, 0), list_get(o, 1), ref)
        bind picked = match sr {
            ok(v) => v,
            bad(e) => (),
        }
        if picked == () {
            return sr
        }
        if list_get(picked, 2) {
            bind reduced = true
        }
        bind oh = lw_onetime_secret(st_get("k_spend", ""), list_get(o, 6), list_get(o, 5)).x
        bind ki = lw_onetime_key_image(oh)
        bind ring = list_get(picked, 0)
        bind pks = []
        bind k = 0
        while k < len(ring) {
            bind pks = list_push(pks, output_rec(list_get(o, 0), list_get(ring, k)).x.onetime)
            bind k = k + 1
        }
        bind inputs = list_push(inputs, Input(list_get(o, 0), ring, ki))
        bind rings = list_push(rings, [pks, list_get(picked, 1), oh])
        bind kids = list_push(kids, list_get(o, 7))
        bind i = i + 1
    }
    bind unsigned = Tx(TX_VERSION, false, 0, ref, inputs, outputs, fee, "", [])
    bind prefix = tx_prefix_hash(unsigned)
    bind sigs = []
    bind i = 0
    while i < len(rings) {
        bind r = list_get(rings, i)
        bind sig = lw_ring_sign(prefix, list_get(r, 0), list_get(r, 1), list_get(r, 2))
        lw_key_drop(list_get(r, 2))
        bind s = match sig {
            ok(v) => v,
            bad(e) => "",
        }
        if s == "" {
            return bad("signing failed")
        }
        bind sigs = list_push(sigs, s)
        bind i = i + 1
    }
    return ok([Tx(TX_VERSION, false, 0, ref, inputs, outputs, fee, "", sigs), kids, reduced])
}

# Coinbase paying `denoms` to a verified bundle (no secret keys needed).
fn coinbase_tx(height, denoms, view_pk, spend_pk) {
    bind outputs = []
    bind j = 0
    while j < len(denoms) {
        bind outputs = list_push(outputs, make_output(view_pk, spend_pk, j, list_get(denoms, j), list_get(denoms, j), "mined"))
        bind j = j + 1
    }
    return Tx(TX_VERSION, true, height, 0, [], outputs, 0, "", [])
}

← Back to index