code wiki / (root) / nx_ledger_lib.nx

nx_ledger_lib.nx source

↩ module page · 273 lines · 12137 B

1// nx_ledger_lib.nx -- F981: THE SOVEREIGN DOUBLE-ENTRY LEDGER (the first byte of Nishi finance). 2// Two entities (accounts, transfers) and ONE invariant: every transfer DEBITS one account and 3// CREDITS another by the same amount, so sum(debits) == sum(credits) ALWAYS. Money cannot be 4// created or destroyed -- not by promise, BY CONSTRUCTION, and the gate proves it. 5// 6// Money = integer MINOR UNITS (cents). No float anywhere -- a float ledger drifts cent-by-cent and 7// an auditor cannot reproduce it. Append-only on the immutable CID plane (rule 13, history sacred): 8// nothing is ever mutated or deleted; a reversal is a NEW opposing transfer, a two-phase resolution 9// is a NEW record. That is exactly what regulators and bar audits want. 10// 11// TWO-PHASE (pending -> post|void): a pending transfer reserves funds without moving them. It counts 12// toward an account's RESERVED total and reduces AVAILABLE, but never touches the posted balance 13// until a resolution record posts it. Resolutions are keyed by the transfer id = O(1) lookup, not a 14// second scan. 15// 16// LINKED CHAINS: led_chain validates EVERY leg BEFORE committing ANY (validate-then-commit, rule 12) 17// so a refused chain leaves ZERO partial state -- no half-moved money. 18// 19// BALANCE CONVENTION: led_balance = credits - debits (custodial/liability sense: a deposit CREDITS 20// the holder, matching nx_trust). An ASSET account reads the negation; led_debits/led_credits are 21// exposed raw so a caller can apply either convention explicitly. 22// 23// SCALE ENVELOPE (declared, never silent): led_sums scans the whole transfer index each call -- 24// O(transfers) per query, index buffer LED_IDXCAP. A per-account index is the next rung. 25// DRY: reuses nx_matter_lib helpers (mt_catcopy/mt_catn/mt_field/mt_streq, canon_encode, reg_*). 26// license_tier: ORIGINAL No hw writes (Rule 26). LIB (no main). 27 28import "nx_matter_lib.nx" 29 30const LED_IDXCAP: i64 = 1048576 // transfer-index read buffer (declared envelope) 31const LED_RECCAP: i64 = 1024 // one canonical transfer record 32const LED_IDCAP: i64 = 128 // max id length 33const LED_COLON: i64 = 58 34 35// decimal string -> i64 minor units 36func led_atoi(s: *u8) -> i64 { 37 var v: i64 = 0 38 var i: i64 = 0 39 while s[i] != (0 as u8) { 40 if s[i] >= 48 as u8 { if s[i] <= 57 as u8 { v = v * 10 + (s[i] - 48) } } 41 i = i + 1 42 } 43 return v 44} 45 46// format minor units as "$X.XX" 47func led_dollars(cents: i64, out: *u8) -> i64 { 48 var o: i64 = 0 49 var m: i64 = cents 50 if m < 0 { out[o] = 45 as u8; o = o + 1; m = 0 - m } 51 out[o] = 36 as u8 52 o = o + 1 53 o = mt_catn(out, o, m / 100) 54 out[o] = 46 as u8 55 o = o + 1 56 let c: i64 = m % 100 57 out[o] = (48 + (c / 10)) as u8 58 o = o + 1 59 out[o] = (48 + (c % 10)) as u8 60 o = o + 1 61 out[o] = 0 as u8 62 return o 63} 64 65// PER-ACCOUNT INDEX KEY (the scale rung). reg_put takes the index key as a parameter, so an account 66// gets its OWN index: "ledacct:<acct>". led_sums_fast then walks only THAT account's transfers instead 67// of the whole book -- O(transfers-for-this-account) not O(all-transfers). 68// COST DECLARED HONESTLY: the record is written under the same id for each index it joins (global + 69// debit side + credit side), so writes amplify ~3x on storage. Newest-wins on an identical key makes 70// that safe, and it buys the read path, which is what every balance/statement/reconciliation call hits. 71func led_acct_idx(acct: *u8, out: *u8) -> i64 { 72 var o: i64 = mt_catcopy(out, 0, "led:acct:" as *u8) 73 o = mt_catcopy(out, o, acct) 74 out[o] = 0 as u8 75 return o 76} 77 78// Append a transfer. ttype = "posted" (moves money now) or "pending" (reserves it). 79// Returns reg_put's result. Amount must be > 0 -- a negative amount would be a disguised 80// reverse transfer and is REFUSED (-1) so direction is always explicit. 81func led_xfer(prefix: *u8, id: *u8, dr: *u8, cr: *u8, cents: i64, ttype: *u8) -> i64 { 82 if cents <= 0 { return 0 - 1 } 83 if mt_streq(dr, cr) == 1 { return 0 - 2 } // self-transfer is a no-op that fakes activity 84 let amt: *u8 = sys_mmap(32) 85 var ao: i64 = mt_catn(amt, 0, cents) 86 amt[ao] = 0 as u8 87 let k: *i64 = sys_mmap(8 * 4) as *i64 88 let v: *i64 = sys_mmap(8 * 4) as *i64 89 k[0] = ("amount" as *u8) as i64 90 v[0] = amt as i64 91 k[1] = ("dr" as *u8) as i64 92 v[1] = dr as i64 93 k[2] = ("cr" as *u8) as i64 94 v[2] = cr as i64 95 k[3] = ("type" as *u8) as i64 96 v[3] = ttype as i64 97 let rec: *u8 = sys_mmap(LED_RECCAP) 98 let rl: i64 = canon_encode(k, v, 4, rec) 99 // ⚠SCALE RUNG REVERTED 2026-07-23: joining per-account indexes here (an extra reg_put per side) 100 // made the organ die with no output (tools/call status=0) and TWO attempted fixes did not resolve 101 // it, so per rule 3 it was reverted rather than patched further. The write path is back to its 102 // gate-proven single-put shape. See debt seq707 for the diagnosis path; led_sums_fast is retained 103 // below but currently falls through to the full scan, so it is CORRECT, just not yet faster. 104 return reg_put(prefix, "led:" as *u8, "led:__idx__" as *u8, id, rec, rl) 105} 106 107// Append a resolution for a pending transfer. res = "post" or "void". Keyed BY the transfer id. 108func led_resolve(prefix: *u8, id: *u8, res: *u8) -> i64 { 109 let k: *i64 = sys_mmap(8 * 1) as *i64 110 let v: *i64 = sys_mmap(8 * 1) as *i64 111 k[0] = ("res" as *u8) as i64 112 v[0] = res as i64 113 let rec: *u8 = sys_mmap(LED_RECCAP) 114 let rl: i64 = canon_encode(k, v, 1, rec) 115 return reg_put(prefix, "ledres:" as *u8, "ledres:__idx__" as *u8, id, rec, rl) 116} 117 118// resolution state of a pending transfer: 1 = posted, 2 = voided, 0 = unresolved 119func led_resolution(prefix: *u8, id: *u8) -> i64 { 120 let po: *i64 = sys_mmap(16) as *i64 121 let lo: *i64 = sys_mmap(16) as *i64 122 if reg_get(prefix, "ledres:" as *u8, id, po, lo) != 1 { return 0 } 123 let rf: *u8 = sys_mmap(32) 124 mt_field(po[0] as *u8, lo[0], "res" as *u8, 3, rf) 125 if mt_streq(rf, "post" as *u8) == 1 { return 1 } 126 if mt_streq(rf, "void" as *u8) == 1 { return 2 } 127 return 0 128} 129 130// THE CORE SCAN. Accumulates into out[]: 131// out[0] = posted debits out[1] = posted credits 132// out[2] = reserved debits out[3] = reserved credits (pending + unresolved) 133// acct = "" (empty) means ALL ACCOUNTS -- that mode is how the global invariant is measured. 134func led_sums_idx(prefix: *u8, acct: *u8, idxkey: *u8, out: *i64) -> i64 { 135 out[0] = 0 136 out[1] = 0 137 out[2] = 0 138 out[3] = 0 139 let idx: *u8 = sys_mmap(LED_IDXCAP) 140 let ilen: i64 = reg_index(prefix, idxkey, idx, LED_IDXCAP) 141 let idb: *u8 = sys_mmap(LED_IDCAP * 2) 142 let po: *i64 = sys_mmap(16) as *i64 143 let lo: *i64 = sys_mmap(16) as *i64 144 let af: *u8 = sys_mmap(64) 145 let df: *u8 = sys_mmap(LED_IDCAP) 146 let cf: *u8 = sys_mmap(LED_IDCAP) 147 let tf: *u8 = sys_mmap(32) 148 var all: i64 = 0 149 if acct[0] == (0 as u8) { all = 1 } 150 var seen: i64 = 0 151 var ls: i64 = 0 152 var i: i64 = 0 153 while i <= ilen { 154 var eol: i64 = 0 155 if i == ilen { eol = 1 } 156 if i < ilen { if idx[i] == 10 as u8 { eol = 1 } } 157 if eol == 1 { 158 if i > ls { 159 var c: i64 = 0 160 while ls + c < i { idb[c] = idx[ls + c]; c = c + 1 } 161 idb[c] = 0 as u8 162 if reg_get(prefix, "led:" as *u8, idb, po, lo) == 1 { 163 mt_field(po[0] as *u8, lo[0], "amount" as *u8, 6, af) 164 mt_field(po[0] as *u8, lo[0], "dr" as *u8, 2, df) 165 mt_field(po[0] as *u8, lo[0], "cr" as *u8, 2, cf) 166 mt_field(po[0] as *u8, lo[0], "type" as *u8, 4, tf) 167 let amt: i64 = led_atoi(af) 168 var isdr: i64 = 0 169 var iscr: i64 = 0 170 if all == 1 { isdr = 1; iscr = 1 } 171 if all == 0 { 172 if mt_streq(df, acct) == 1 { isdr = 1 } 173 if mt_streq(cf, acct) == 1 { iscr = 1 } 174 } 175 var posted: i64 = 0 176 var reserved: i64 = 0 177 if mt_streq(tf, "posted" as *u8) == 1 { posted = 1 } 178 if mt_streq(tf, "pending" as *u8) == 1 { 179 let r: i64 = led_resolution(prefix, idb) 180 if r == 1 { posted = 1 } 181 if r == 0 { reserved = 1 } 182 } 183 if posted == 1 { 184 if isdr == 1 { out[0] = out[0] + amt } 185 if iscr == 1 { out[1] = out[1] + amt } 186 } 187 if reserved == 1 { 188 if isdr == 1 { out[2] = out[2] + amt } 189 if iscr == 1 { out[3] = out[3] + amt } 190 } 191 seen = seen + 1 192 } 193 } 194 ls = i + 1 195 } 196 i = i + 1 197 } 198 return seen 199} 200 201// FULL-BOOK scan (walks every transfer). Kept as the ORACLE the fast path is proven against, 202// and it is the only correct mode for acct=="" (all accounts). 203func led_sums(prefix: *u8, acct: *u8, out: *i64) -> i64 { 204 return led_sums_idx(prefix, acct, "led:__idx__" as *u8, out) 205} 206 207// ★SCALE PATH: walk ONLY this account's index. Same answer as led_sums for a named account 208// (proven by nx_ledger_scale_gate), but O(transfers-for-this-account) instead of O(whole book). 209// Falls back to the full scan for the all-accounts mode, which has no per-account index by definition. 210func led_sums_fast(prefix: *u8, acct: *u8, out: *i64) -> i64 { 211 // ⚠Currently DELEGATES to the full scan: correct, but NOT yet faster. The per-account index that 212 // would make it faster is reverted (see led_xfer). Callers may use this name today and get right 213 // answers; when the index lands, they get the speedup with no call-site change. Named honestly 214 // rather than left looking optimised. 215 return led_sums(prefix, acct, out) 216} 217 218// posted balance, custodial sense (credits - debits) 219func led_balance(prefix: *u8, acct: *u8) -> i64 { 220 let s: *i64 = sys_mmap(8 * 4) as *i64 221 led_sums(prefix, acct, s) 222 return s[1] - s[0] 223} 224 225// AVAILABLE = posted balance minus funds reserved by unresolved outgoing (debit) pendings 226func led_available(prefix: *u8, acct: *u8) -> i64 { 227 let s: *i64 = sys_mmap(8 * 4) as *i64 228 led_sums(prefix, acct, s) 229 return (s[1] - s[0]) - s[2] 230} 231 232// ★THE INVARIANT: over ALL accounts, sum(credits) - sum(debits) MUST be 0. 233// Non-zero means money was created or destroyed. Returns the drift (0 = sound). 234func led_drift(prefix: *u8) -> i64 { 235 let s: *i64 = sys_mmap(8 * 4) as *i64 236 led_sums(prefix, "" as *u8, s) 237 return s[1] - s[0] 238} 239func led_sound(prefix: *u8) -> i64 { 240 if led_drift(prefix) == 0 { return 1 } 241 return 0 242} 243 244// 1 if `acct` can fund a `cents` outflow from AVAILABLE funds 245func led_can_fund(prefix: *u8, acct: *u8, cents: i64) -> i64 { 246 if led_available(prefix, acct) >= cents { return 1 } 247 return 0 248} 249 250// LINKED ATOMIC CHAIN: validate EVERY leg first, commit only if all pass. 251// A refused chain writes NOTHING (validate-then-commit, rule 12) -- no half-moved money. 252// `funder` = the account whose AVAILABLE funds must cover the total debited from it ( "" = skip). 253// Returns legs committed, or a negative refusal code with zero writes: 254// -1 bad amount -2 self-transfer -3 funder cannot cover the chain total 255func led_chain(prefix: *u8, ids: *i64, drs: *i64, crs: *i64, amts: *i64, n: i64, funder: *u8) -> i64 { 256 var i: i64 = 0 257 var need: i64 = 0 258 var checkf: i64 = 1 259 if funder[0] == (0 as u8) { checkf = 0 } 260 while i < n { 261 if amts[i] <= 0 { return 0 - 1 } 262 if mt_streq(drs[i] as *u8, crs[i] as *u8) == 1 { return 0 - 2 } 263 if checkf == 1 { if mt_streq(drs[i] as *u8, funder) == 1 { need = need + amts[i] } } 264 i = i + 1 265 } 266 if checkf == 1 { if led_can_fund(prefix, funder, need) == 0 { return 0 - 3 } } 267 i = 0 268 while i < n { 269 led_xfer(prefix, ids[i] as *u8, drs[i] as *u8, crs[i] as *u8, amts[i], "posted" as *u8) 270 i = i + 1 271 } 272 return n 273}