灵符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)

docs/LING_API.md
# Ling runtime additions for ling-wallet (contract)

The wallet is written entirely in Ling. The interpreter (`../ling`) gains
three builtin families. Bytes always cross the Ling boundary as **lowercase
hex strings**; numbers are Ling numbers (f64, exact below 2^53 — every
amount, height, index and timestamp the wallet uses fits). Fallible calls
return `ok(v)` / `bad("reason")` unless noted.

Consensus rules, byte layouts and constants are those of `docs/DESIGN.md`;
the primitives below implement §2–§6 and §8 exactly.

---

## 1. Threads: isolates + channels (`src/runtime/threads.rs`)

Each thread is an **isolate**: its own interpreter on its own OS thread.
Nothing is shared except channels and the write-once shared store, so Ling
code can't race. Values crossing a channel are deep-copied into a frozen
thread-safe form (numbers, strings, bools, unit, lists, structs, variants,
ok/bad, channel handles). Functions can't be sent.

### Spawning and supervision
| builtin | returns | notes |
|---|---|---|
| `thread_spawn(file, fn_name, args_list, caps_list)` | `ok(thread_handle)` | runs `fn_name(args…)` from `file` (resolved relative to the caller's file) in a new isolate. Parsed modules are cached process-wide, so repeated spawns are cheap. |
| `thread_join(h)` | `ok(return_value)` / `bad(error)` | blocks; a runtime error or panic in the isolate becomes `bad`, never a process crash |
| `thread_join_timeout(h, ms)` | `ok(v)` / `bad("timeout")` / `bad(err)` | |
| `thread_alive(h)` | bool | |
| `thread_cancel(h)` | bool | cooperative: the isolate aborts at its next loop iteration / call with `bad("cancelled")` |
| `thread_self()` | handle string | |
| `thread_cpus()` | number | available parallelism |
| `thread_caps()` | list of strings | this isolate's capabilities |

**Capabilities** (`caps_list`, default `[]`): `"fs"` (files), `"db"`
(SQLite), `"net"` (TCP/HTTP/lingtp), `"p2p"` (LWP-1 builtins), `"proc"`
(run_process), `"keys"` (wallet key handles), `"clipboard"`. Pure compute,
hashing, PoW, channels, `sleep`, time and `st_*` (isolate-local) are always
allowed. The GUI (`open_window`, `px_*`, input) is **main-thread only**. A
builtin needing a missing capability fails with a runtime error
`capability "<cap>" not granted`. A child can never receive a capability
its parent lacks.

### Channels
| builtin | returns |
|---|---|
| `chan_new(capacity)` | `[send_handle, recv_handle]` (capacity ≥ 1; bounded) |
| `chan_new_secure(capacity)` | same, but string payloads are zeroized after delivery and on drop |
| `chan_send(h, v)` | `ok(())` / `bad("closed")`; blocks while full |
| `chan_try_send(h, v)` | `ok(())` / `bad("full")` / `bad("closed")` |
| `chan_recv(h)` | `ok(v)` / `bad("closed")`; blocks |
| `chan_recv_timeout(h, ms)` | `ok(v)` / `bad("timeout")` / `bad("closed")` |
| `chan_try_recv(h)` | `ok(v)` / `bad("empty")` / `bad("closed")` |
| `chan_select(recv_handles_list, timeout_ms)` | `ok([index, value])` / `bad("timeout")` / `bad("closed")` (all closed) |
| `chan_close(h)` | bool (closing the send side ends the channel once drained) |
| `chan_len(h)` | number queued |

**Handles are capabilities**: 128-bit random hex strings, valid only in an
isolate's handle table. An isolate gets a handle by creating the channel,
receiving it as a spawn argument, or receiving it in a message. Using any
other string is a runtime error. Limits: 64 MiB per message, 1024 live
threads, 65 536 queued messages per channel.

### Shared, read-only data
| builtin | returns |
|---|---|
| `shared_put(key, v)` | `ok(())`, or `bad("exists")` — each key is write-once |
| `shared_get(key, default)` | the frozen value (cheap: Arc-shared) |

---

## 2. Byte and number helpers (`src/runtime/bytes.rs`)

| builtin | returns |
|---|---|
| `hex_concat(list)` | concatenation (validates hex) |
| `hex_len(h)` | byte length |
| `hex_slice(h, start, len)` | bytes `start..start+len` (byte offsets) |
| `hex_eq(a, b)` | constant-time equality |
| `u8_hex(n)`, `u32le_hex(n)`, `u64le_hex(n)` | fixed-width little-endian |
| `varint_hex(n)` | minimal LEB128 |
| `hex_u8(h, pos)`, `hex_u32le(h, pos)`, `hex_u64le(h, pos)` | number (u64 must be < 2^53 or `bad`) — `ok(n)` |
| `hex_varint(h, pos)` | `ok([value, new_pos])`; rejects non-minimal / > 2^53 |
| `text_hex(s)`, `hex_text(h)` | UTF-8 ⇄ hex (`hex_text` → `bad` on invalid UTF-8) |
| `base32_encode(hex)`, `base32_decode(s)` | RFC 4648 lowercase, no padding |
| `base64url_encode(hex)`, `base64url_decode(s)` | |
| `big_add(a,b)`, `big_sub(a,b)`, `big_mul(a,b)`, `big_div(a,b)`, `big_cmp(a,b)` | unsigned 256-bit integers as **decimal strings** (cmp → -1/0/1); `big_sub` underflow / div-by-zero → runtime error |
| `file_read_hex(path)` / `file_write_hex(path, hex)` | needs `"fs"`; atomic write (temp + rename) |
| `wipe(s)` | best-effort zeroize of a string value in place; returns `()` (currently a no-op: builtins receive copies of their arguments, so the caller's buffer is unreachable without an interpreter hook) |
| `sys_idle_secs()` | seconds since the last system-wide keyboard/mouse input (Windows: `GetTickCount − GetLastInputInfo`; other OSes: `1e9`) — the miner runs only while idle |
| `sys_on_battery()` | `true` iff on battery (Windows: `GetSystemPowerStatus().ACLineStatus == 0`; unknown / other OSes: `false`) |

Notes on §2 as implemented:

- Hex input is strict (even length, hex digits only; either case accepted,
  output is always lowercase). Builtins documented with `ok`/`bad` return
  `bad(reason)` on malformed input; the rest (`hex_concat`, `hex_len`,
  `hex_slice`, `hex_eq`, `*_hex` encoders, `text_hex`, `*_encode`, `big_*`)
  raise a runtime error. Integer arguments must be non-negative integral
  Ling numbers `< 2^53` (so `hex_varint` rejects values `≥ 2^53`).
- `base32_decode` is case-insensitive but rejects padding, foreign
  characters, impossible lengths and non-zero trailing bits (canonical);
  `base64url_decode` accepts optional padding and rejects non-zero trailing
  bits. Both return `ok(hex)` / `bad`.
- `big_*` also accept a non-negative integer Ling number for either operand;
  overflow past 2^256 − 1 is a runtime error; `big_div` floors.
- `file_read_hex` → `ok(hex)` / `bad`; `file_write_hex` → `ok(())` / `bad`
  (bad hex included). A missing `"fs"` capability is a runtime error.

---

## 3. Wallet cryptography (`src/runtime/lwcrypto.rs`, backed by `ling-crypto`)

| builtin | returns |
|---|---|
| `lw_h256(domain, parts_hex_list)` | hex32 — DESIGN §2 `H` |
| `lw_xof(domain, parts_hex_list, len)` | hex(len) — `XOF` |
| `lw_random(n)` | hex(n) from the OS CSPRNG |
| `lw_kem_keypair(seed_hex96)` | `ok(handle)` — hybrid X25519+ML-KEM-1024 secret (needs `"keys"`) |
| `lw_kem_public(h)` | hex(1600) |
| `lw_kem_encaps(pk_hex, context_hex)` | `ok([ct_hex(1600), ss_hex32])` / `bad` on invalid pk |
| `lw_kem_decaps(h, ct_hex, context_hex)` | `ok(ss_hex32)` (needs `"keys"`) |
| `lw_id_keypair(seed_hex32)` | `ok(handle)` — ML-DSA-87 (needs `"keys"`) |
| `lw_id_public(h)` | hex(2592) |
| `lw_id_sign(h, context, msg_hex)` | hex(4627) |
| `lw_id_verify(pk_hex, context, msg_hex, sig_hex)` | bool |
| `lw_id_fingerprint(pk_hex)` | "xxxx-xxxx-xxxx-xxxx" |
| `lw_spend_keypair(seed_hex32)` | `ok(handle)` — LRS-87 spend secret (needs `"keys"`) |
| `lw_spend_public(h)` | hex(2944) |
| `lw_onetime_public(spend_pk_hex, ss_hex, index)` | `ok(hex2944)` |
| `lw_onetime_secret(spend_h, ss_hex, index)` | `ok(handle)` (needs `"keys"`) |
| `lw_onetime_key_image(h)` | hex(2944) |
| `lw_onetime_public_of(h)` | hex(2944) |
| `lw_ring_sign(msg_hex32, ring_pk_hex_list, signer_index, onetime_h)` | `ok(sig_hex)` |
| `lw_ring_verify(msg_hex32, ring_pk_hex_list, key_image_hex, sig_hex)` | bool |
| `lw_pk_valid(pk_hex)` | bool (2944 B, coefficients < q) |
| `lw_key_drop(h)` | zeroizes and frees any key handle |
| `lw_aead_seal(key_hex32, nonce_hex24, aad_hex, pt_hex)` / `lw_aead_open(...)` | hex / `ok(hex)`/`bad` |
| `lw_password_key(password, salt_hex16, m_kib, t, p)` | `ok(key_hex32)` (Argon2id) |
| `lw_mnemonic_from(hex32)` | list of 24 words (BIP-39 English) |
| `lw_mnemonic_to(words_string)` | `ok(hex32)` / `bad(reason)` |
| `lw_pow_hash(header_hex)` | hex32 — HyperTorus (each isolate keeps its own 16 MiB scratchpad) |
| `lw_pow_trace(header_hex, every)` | `[hash_hex, [[x,y,z,w,twist]…]]` |
| `lw_meets_difficulty(hash_hex, difficulty_decimal)` | bool |
| `lw_qr(text)` | list of rows, each a string of `0`/`1` modules |

Key handles are per-isolate and never cross channels; a thread that holds
keys (`"keys"` capability) signs on behalf of others via messages, so
secrets live in exactly one isolate.

Notes on §3 as implemented: handles are random 128-bit lowercase hex
strings. Every builtin that creates **or uses** a secret handle requires
`"keys"` (`lw_key_drop` excepted; it returns `true` if a handle was freed).
Verifiers (`lw_id_verify`, `lw_ring_verify`, `lw_pk_valid`) return `false`
for any malformed input. `lw_id_sign`'s `context` is a text string (signs
`H("LW1/sig/" + context, msg)`). `lw_random` / `lw_xof` cap output at 1 MiB;
`lw_password_key` caps `m_kib ≤ 4 GiB`, `t ≤ 1024`, `p ≤ 64`.
`lw_meets_difficulty` also accepts a Ling number. `lw_qr` uses ECC level M,
no quiet zone. `lw_mnemonic_to` lowercases and collapses whitespace and
requires exactly 24 words.

---

## 4. P2P transport (`src/runtime/p2p.rs`, backed by `ling-net::lwp`)

The LWP-1 stack (DESIGN §8: hybrid PQ handshake, padded DICE-42 +
XChaCha20 frames, cover traffic, peer DB, signed seeds, Dandelion++,
SOCKS5). All calls need `"p2p"`. Events are delivered on a Ling channel
(`events_send_handle`) or, with `events_send_handle = ""`, queued for
`p2p_poll`.

| builtin | returns |
|---|---|
| `p2p_start(config_list, events_send_handle)` | `ok(net_handle)` / `bad(reason)`; config is a list of `[key, value]` pairs (below). `events_send_handle` is a channel send handle, or `""` (poll mode). |
| events (sent to the channel / returned by `p2p_poll`) | `["connected", peer_id, addr, inbound, fingerprint, is_seed]`, `["disconnected", peer_id]`, `["message", peer_id, kind, payload_hex]`, `["stem", peer_id, payload_hex]` |
| `p2p_poll(net, max, timeout_ms = 0)` | list of up to `max` queued events (oldest first; `[]` if none). With `timeout_ms > 0` it waits up to that long for the first event. Poll mode only (always `[]` when a channel was given). The queue is bounded (256): a program that stops polling back-pressures every peer. |
| `p2p_send(net, peer_id, kind, payload_hex)` | bool — `false` if the peer is not connected, `kind` ∉ 16..255, or the payload is not hex / exceeds 4 MiB + 64 KiB − 5 bytes |
| `p2p_broadcast(net, kind, payload_hex, except_peer_or_minus1)` | bool |
| `p2p_relay(net, id_hex32, payload_hex, fluff_kind, from_peer_or_minus1)` | bool — Dandelion++ (a validated tx; `-1` = created locally) |
| `p2p_mark_fluffed(net, id_hex32)` | bool |
| `p2p_peers(net)` | list of `[peer_id, addr, inbound, fingerprint, is_seed, connected_secs]`, sorted by `peer_id` |
| `p2p_misbehaving(net, peer_id, score, reason)` | bool (at a total of 100: disconnect + 24 h ban) |
| `p2p_local_addr(net)` | `"ip:port"` actually bound (useful with port 0), or `""` when not listening |
| `p2p_netgroup(net, peer_id)` | the peer's network group (e.g. `"v4:1.2"`), `""` if unknown — for the "tips across ≥3 groups" rule (DESIGN §8.3) |
| `p2p_seeds_sign(network, issued_at, entries_list, authority_seed_hex)` | hex — entries are `[addr, identity_pk_hex]`; invalid input is a runtime error (operator tooling) |
| `p2p_seeds_verify(list_hex, authority_pk_hex)` | `ok([network, issued_at, entries])` / `bad` (entries as for `sign`) |
| `p2p_shutdown(net)` | bool — `true` the first time; stops all threads and persists the peer DB |

Peer ids are positive integers (`-1` means "no peer" where allowed).
Network handles are 128-bit random hex strings valid only in the isolate
that started the network; any other string is a runtime error. After
`p2p_shutdown` the handle stays valid as a tombstone: `p2p_poll` /
`p2p_peers` return `[]` and the other calls `false`. Invalid arguments
(wrong types, bad hex, reserved kinds) make the bool calls return `false`.

`p2p_start` config keys — each at most once; unknown keys, wrong types or
out-of-range values return `bad`:

| key | value | default |
|---|---|---|
| `magic_hex` | 8 hex chars (4-byte network magic) | **required** |
| `network` | integer 0..255 | **required** |
| `data_dir` | non-empty path (created if missing): peer DB, anchors, bans | **required** |
| `listen` | `"ip:port"` (port 0 = any) or `""` | `""` (no listener) |
| `seeds_hex` | signed seed list (§8.3) or `""`; verified against `authority_pk_hex` and must be for `network`, else `bad` | `""` |
| `authority_pk_hex` | ML-DSA-87 public key hex (2592 bytes) or `""` | `""` |
| `proxy` | SOCKS5 `"ip:port"` (e.g. Tor `127.0.0.1:9050`) or `""` | `""` |
| `max_outbound` | integer 0..64 | 8 |
| `max_inbound` | integer 0..1024 | 32 |
| `identity_seed_hex` | 64 hex chars (long-term ML-DSA-87 identity) or `""` | fresh random identity |
| `ephemeral_outbound` | bool (fresh identity per outbound connection) | `true` |
| `peers` | list of manual peer addresses (DESIGN §8.3 forms; onion ones need `proxy`) | `[]` |
| `fast_timers` | bool — short backoff/embargo timers and local-address gossip, for single-machine tests only | `false` |

← Back to index