* NOSIGNUP.TRADE TELOMERE v1 (2026-10-07). Two copies: the front copy right * after the opening PHP tag and the back copy at the very end of this file. * Both copies are this same comment, byte for byte. * ------------------------------------------------------------------------- * * Telomeres are end caps. Edits wear a long file down from its ends: a bad * paste, a truncated upload, a cleanup that drops prose. These caps exist so * the truth kernel of nosignup.trade survives any edit that keeps either * end. Each end carries the whole cap, so either end can rebuild the other * by copying this comment verbatim. If the two copies differ, or a hash * below no longer matches its function, stop and reconcile against ?src=1 * before writing. Never change the money to match a stale cap, and never * edit the cap to hide a money change. The caps are comments only: they * emit no bytes, declare nothing and change no behaviour. They wrap, never * replace, the FINAL-PACK MAINTENANCE CHARTER, the PRODUCT CONTRACT, the * history prose and the ECONOMY+IDENTITY TELOMERE, which stay byte-preserved. * * * * FRONT CAP: charter, face purpose, truth kernel. Summarised from the charter * blocks below the front copy; those blocks and the code remain the authority. * * CHARTER. One stock-PHP file; no composer/npm/database dependency. No * visitor signup, recovery account, tracking profile, shared bank, SSO or * paywall on basic use. ?src=1 and ?download=1 stay auditable. This host * owns only its own data, vault and operator panel; a panel is never a * visitor identity. Repair measured failures or narrow security/honesty * defects; do not invent a new financial product. Preserved history is not * a task queue. Source authority is the verified current root 4.php; * reconcile live/raw/source bytes before writing; do not revive old packs. * * FACE PURPOSE (Trade). Trade NSU. Place ads. Test forecasts. Product tools * are Ads / Predictor / Orders. Wallet and identity belong to the shared * wallet control (send/receive), not duplicate product tabs. The unchecked * landing is the public NSU order book with order entry. Trade owns the only * chain: spend authorization, settlement, ad ownership and epoch state. The * Predictor (PhiGraph, ?view=phigraph) is a bounded research view with no * monetary route. * * TRUTH KERNEL. * 1 AUTH: normal mutations require ECDSA P-256 v2 (ecdsa_p256_v2 / * auth_required). Seed is not accepted on normal mutation routes; the * only legacy path is api=legacy_wallet_migrate. * 2 MONEY: NST_MAX_SUPPLY is 100,000,000 NSU, minted once at birth through * nst_mint_guarded('genesis', ...). Remaining issuance after birth is 0. * NST_EMISSION_PAUSED stays true and nst_emission_run_daily is a no-op. * No daily drip, no visitor faucet (410), no participant-triggered mint, * no remint, no re-anchor. * 3 LAW: amounts are integer micros. Every mint type is declared in * nst_conservation_replay() or the freeze halts chain appends. The money * constitution is indexed by nsu_policy_functions() and hashed by * nsu_policy_hash(), which ignores comments and whitespace. * 4 TAX: the KING's cut is a tax on lord receipts, never a duplicate. Never * tax a user-to-user trade or transfer. Ads buy their own real estate, * never organic rank. * 5 RESET: yearly service reset. Wallet balances and access records survive; * orders, ads, bids, profit and modifiers burn. It is not a wallet remint. * 6 IDENTITY: a public ticket identifies a browser-held instance, not a * person. IP, fingerprints and similarity never recover, merge or select a * wallet. Fresh words stay in the browser; a public locator cannot spend. * 7 GATES, closed until Sebastian releases them: ad purchase (returns "New * ad purchases are temporarily unavailable while payment and delivery * failures are repaired.", buyable false); LTC auto-settlement * (ltc_observer_unqualified, available false); vanity apply (501, no * spend, no bind); paid AI (nst_slowgpt_paid_enabled false). * 8 HONESTY: experimental until proven. Forecasts are testable hypotheses, * never profit or price guarantees. Advisory text may explain local * signals but never decides a trade. Exact pair equality governs every * quote, book row and fill. * * * * BACK CAP: the ECONOMY+IDENTITY telomere functions. Comments only. Nothing * here is a declaration; each function stays declared exactly once, in place. * HASH METHOD (PHP token_get_all over this file): * raw = sha256 of the exact source bytes from the T_FUNCTION token through * its matching closing brace, inclusive. Brace depth counts '{', * T_CURLY_OPEN and T_DOLLAR_OPEN_CURLY_BRACES; '}' closes. * norm = sha256 of the same token range with T_WHITESPACE, T_COMMENT and * T_DOC_COMMENT dropped, each remaining token followed by one space, * then trimmed (the nsu_policy_sources() normalisation). * Raw mismatch with norm match = whitespace-only edit. Norm mismatch = code * change; reconcile before any put. * * A. ECONOMY+IDENTITY TELOMERE tail (byte-preserve), between the header * "ECONOMY+IDENTITY TELOMERE" and "END ECONOMY+IDENTITY TELOMERE": * nsu_crop_tld(): string - this crop's TLD from HTTP_HOST (port * stripped); '' unless the host is nosignup.. * raw ed37cfe99999b98e615c3d3feba0bce832fb39b3a0f21bd24898685db41e075f * norm 6d9ecb193f36ece2f9445f191fd4e5a6bdd652978c295f11680d88324b5ae537 * nsu_trade_origin(): string - 'https://nosignup.trade'. Trade owns * the only chain; every crop asks it. * raw 0adf9d607f0e94dcb301f5dbb0249215c11fca545e705665b52cbf645fee6db7 * norm a8e4ccb40dec05611d99a451b4c1b0a36849f97669cc27d4a91c94f171df732c * nsu_ad_sites(): array - the ten empire ad sites: com org net trade * chat work market date fun info. * raw eace55daa6d916e0fcd185a2394eae65dceb4785b1881e90890522f2c6603c73 * norm e8d4bc1eef29b7c10a3999fc62b165b3a175d4ef8131bc29782d65ac0d40fb80 * nsu_ad_roll_n(): int - random slot number 0..9999 (random_int). * raw 14442da5dc0d35f8213e553ba5111657efc14b5ea206e5cac3506d9518c5babc * norm d7cd165a71c1a574008d601180caabc210fc7819269c700b43d97184feaef4c8 * nsu_ad_slot_url(?site, ?n): string - https://nosignup.trade// * for a listed site, n clamped 0..9999; '' (fail closed) otherwise. * raw d61b5b7e97342ecc0b6ed154b6f20909fc45e61f08fa9edd1b92bd170958421a * norm 3d84ac3b051fc61ac09e0821db98b4edd9cdb290a9079827306ed2d4982f51fa * nsu_board_endpoint(): string - this crop's slot URL, random roll. * raw 6143293c604bf7346eca3a52615f19dbfcd29249747d270c4968d5ea219467aa * norm ff55483559810c7fa99719706d1125c5e0b590a33a8d98e1c142d89dc9ed2b1c * nsu_ad_slot_html(extraClass): string - 49-tile ad board for this * crop; '' when the crop cannot name itself. * raw b055f19c175b7f127b6c5b35da74c285909a261019ffe4d1b082b9a33a62c30b * norm c32f0914ea751112e33bbe1ac1eb911a5f7e9551c9e3b29938f8a39388fed41d * nsu_ad_tower_html(side): string - one L or R skyscraper, or both * for LR/BOTH/''; falls back to the trade crop. Not the 49-tile board. * raw ceda98c147aecc46fe76264b8730af6bf55f514e1b6a14cf3cfb2e76335b051f * norm 29b2e49af1c38da1d85e6682c7ced371d43312170acf0adf6890d4753f258d56 * nsu_report_endpoint(): string - Trade ?api=ad_report&crop= URL; * a report grants no ownership, refund or takedown by itself. * raw 01cf2e209f80d6a9de55a4492e4fcc3b4033f34b6ca9632e06f81320b6b47e5a * norm 127fe0da676e62b1d435b8f1fd855fcf149af7a13d9c05a4577994ed125c6c7b * * B. IDENTITY pointers named by that telomere (LEFT IN PLACE mid-file): * norm_seed(s): string - lowercase, trim, collapse whitespace. * raw 337ef3a436a6c2b5520d6b476406d3c04ff0d6544e1113d3781f909d3b6f8dd6 * norm 1180b6458a67e5ae4e515a832c43f5e4d0eac32dfd74363d9678a4addabb78d1 * addr_from_seed(seed): string - sha256('nsu-addr-v1|'.norm_seed); * '' for an empty seed. * raw 6adbfd8ab927e73b575660b11d5ee52dd0526ac1390139568fc4964fbc927b0a * norm 0b84c171d651f888cc547c553db49497e7e6222df4adbca5a7c300be82804447 * nst_ticket_mill_path(): string - visitor-number counter path * (shared id.counter for chat/trade, else local); never lowers it. * raw b0341dd6042505e6ede037feec30a08ce33bad88cefdce2097b95e78dddc523b * norm 1ba13fedc2f90970611a5be08e617ac387ae3b74622616934995b57c679a7481 * nst_ticket_bind_with(fn) - runs one ticket.bind mutation under an * exclusive lock with atomic replace. * raw 00f22bb177ad1e0d004953b276a48446fbe8d3ef4dc5ffc752c364413800b6ba * norm 1ade9c203d5fdbd4c834613560457b2e7367af1ac8b9a7f1e74184a0ac25e291 * nst_ticket_ensure_for_addr(addr, prefer): string - bind addr to the * preferred or a newly milled ticket; an already-bound wallet keeps its ticket. * raw 45270347fcedf032ad0cb11a6c7cfca884f4d3ad840cce0f3e48f5d9c144b45d * norm cdc8e250cc26a67f4280e4565afec16e2c74454159e1b66dde87018bc93cb048 * nst_vanity_quote(raw): array - quote only; moves no coins, writes no bind. * raw b0b648f2ac03fd57fa99befa6201ac689fe5ec514315828b5faee64ea62e8f01 * norm 71f2a2010eca9c6216e1230d58f07014490e0467462b4b2ff66e099d6c42d96c * nst_vanity_apply(raw): array - stays 501, ok false, spend false; * moves no coins, writes no bind. * raw bcef5041adcfc89a335cdda3f93578fe0269da09ff521a0a2efdba256f30ea58 * norm ce3c102554893b2e96fff14166ce8c13a20a4855d9545275a0351c3c13e9f036 * nst_id_gossip_handle(): void - POST-only presence receive store * (?api=id_gossip); presence only, never wallets. * raw 36d3addd72a0d5df22c0b2c64ff5a8ff40cfc723d41fc9ce1d597f5c449ff47e * norm 00a73d6e1595af683f5c55afce27919c450d3872fcf620ec923031aea4c4bc2a * * C. Closed-gate and issuance guards named by the truth kernel: * nst_ad_spend_ready(crop): array - returns ok false with the * purchase-unavailable sentence (purchase gate closed; buyable false). * raw adc138c7fda63d4038ed5296f489478b2886da6142290affb0035e707ab53458 * norm 2e45c826b72657875b6ec3a4acd3a806dfdfa55673442799fe9a4c546e418932 * nst_ltc_auto_settlement_readiness(): array - ok false, * err ltc_observer_unqualified, available false; not an env switch. * raw 7f3a9f4a82b2b57668ff33a0f4b27a4c5a8b7374396166bc13bcc6c4c0c14a7a * norm a4ec4fc7a4650edc2f80e005a8b7617eddf9e90c76feaa2764543a286bcbb384 * nst_slowgpt_paid_enabled(): bool - returns false (paid AI closed). * raw 63e517e12b74f6b63f294ceade1e8446676f1a18216242618b27aa1e93b93b85 * norm 15b3b36611e4d316fe8ccc3e7927b40724e7f3b5529065c4dc170feeccd85b18 * nst_mint_guarded(type, outputs, extra): array - the only mint choke * point; declared mint types only, clamped to remaining headroom, locked. * raw 9dde6ca8d31474a58ffbc308038faf139d7b296d2153ba81886fd69d3abd4c7c * norm f5ec536f519ed0abb6e51c5f7ea16185cdfc4d050d3d715ad4154f22145f5468 * nst_emission_run_daily(): array - returns ran false, reason paused, * while NST_EMISSION_PAUSED is true. * raw fde027b6ea87d91d82f1e6452bcdc116a5955ad35df90026a3baad1f77cbc8a8 * norm a5517f0748f6cdf97ab8f526ceb7a50b2de6a8403f872359103b364c48037e46 * * * * Trade's own plan. Strict improvement only: a change lands only when it is * better and breaks nothing that worked; otherwise it is reverted. Alternate * waves: an improvement wave, then a simplification wave that removes * duplication or dead weight without changing behaviour, then repeat. * Economy first: money correctness, conservation, auth and gates come before * layout, copy or features. 30-day default expiry: new temporary state * (presence, holds, caches, posts) expires after 30 days unless its own law * says otherwise; wallets and balances never expire. Trade owns the only * chain. NSU is a donation, never a required purchase. No second ledger, * mint, faucet or tax. Gates stay closed until Sebastian releases them. * Every unit: re-pull live, keep a backup, php -l, prove, put, re-pull and * match X-NS-Sha256, sync the Desktop copy. When this cap changes, change * both copies identically and refresh the hashes above in the same unit. * * ========================================================================= * * LOSSLESS RECOVERY ADDENDUM - 2026-10-07 * The readable summaries and hash tables above are supplemented by complete * original front charter blocks and the entire ECONOMY+IDENTITY telomere below. * Each part preserves its exact bytes, line endings, length and SHA-256. * Encoding keeps comment terminators and PHP declarations inert. No executable * declaration is duplicated. Either end alone contains all recovery parts. * Decode the base64 envelope as JSON, then each part.data as base64; verify * part.bytes and part.sha256 before recovery. Never execute the decoded text. * Pack tool: NSU-DNA/tools/verify-pins.php --telomeres-only PACK_DIRECTORY. * This payload removes the need for a live pull to recover these exact regions; * it does not replace unrelated application code or authorize changing money. * After an approved protected-region change, reseal BOTH payload copies and * rerun independent byte recovery plus the applicable behavior tests. * NSU-RECOVERY-V1-BEGIN * eyJzY2hlbWEiOiJuc3UtdGVsb21lcmUtcmVjb3ZlcnkvMSIsInNvdXJjZV9zaGEyNTYiOiIxMDJhY2Y5YzM3NDBkODMxOTBkZjA3 * ZjJkNzk5ODM4YmRiNGY4ZmMyYzkwYWVkYWU3NDczMDk3NmRjZTNjODAzIiwicGFydHMiOlt7ImlkIjoidGVsb21lcmUxLTAxIiwi * bGFiZWwiOiJGSU5BTC1QQUNLIE1BSU5URU5BTkNFIENIQVJURVIgLS0gRE8gTk9UIERFTEVURS4iLCJieXRlcyI6MTE4OTYsInNo * YTI1NiI6IjY1NjA1MmIwMzMyMzQ1MmQ2ODc0ZTkwOTZjMTY0ODY2ZTQyZGYzMjA2ODA5YmYzNjA5Yzc2MTQxYzFkMGE5YzkiLCJk * YXRhIjoiTHlvcUNpQXFJRU5WVWxKRlRsUWdVa1ZXU1ZOSlQwNGdNakF5TmkweE1DMHhNeUF2SUZJd05Ub2dTMlZsY0NCMGFHbHpJ * R0pzYjJOcklHbHVJSEJzWVdObExpQlVhR1VnY0dGcGNtVmtDaUFxSUdOaGNITWdaMjkyWlhKdUlHRnlZMmhwZEdWamRIVnlaUzlW * U1NCaGJtUWdiV2x1YVcxMWJTQnVaV05sYzNOaGNua2dZMmhoYm1kbExpQlFjbTkyWlNCMGFHVWdaR1ZtWldOME93b2dLaUJ3Y21W * elpYSjJaU0JqYjNKeVpXTjBJR0psYUdGMmFXOXlPeUJoWkdRZ2JtOGdjM0JsWTNWc1lYUnBkbVVnYzNSaGRHVXNJSFJwYldWeUlH * OXlJR052Ym5SeWIyd3VDaUFxSUZKbGRtbHpaU0J0ZFhSaFlteGxJR1JsZEdGcGJITWdkMmwwYUNCbGRtbGtaVzVqWlN3Z2JtOTBJ * R1JsYkdWMGFXOXVJRzl5SUdKc2FXNWtJSEpsZG1WeWMybHZiaTRLSUNvZ1MyVmxjQ0IwYUdseklITnBkR1VuY3lCM2IzSnJMMlYy * YVdSbGJtTmxJR2x6YjJ4aGRHVmtJR0Z1WkNCaWIzVnVaQ0IwYnlCMGFHVWdkR1Z6ZEdWa0lITnZkWEpqWlNCb1lYTm9MZ29nS2dv * Z0tpQkdTVTVCVEMxUVFVTkxJRTFCU1U1VVJVNUJUa05GSUVOSVFWSlVSVklnTFMwZ1JFOGdUazlVSUVSRlRFVlVSUzRLSUNvS0lD * b2dRMVZTVWtWT1ZDQlFVazlFVlVOVUlFRk5SVTVFVFVWT1ZDQXlNREkyTFRFd0xUQXpJQzBnYjNkdVpYSXRaR2x5WldOMFpXUWda * VzUwY21GdVkyVWdjbTkxYm1RdUNpQXFJRkIxY25CdmMyVTZJRlJ5WVdSbElFNVRWUzRnVUd4aFkyVWdZV1J6TGlCVVpYTjBJR1p2 * Y21WallYTjBjeTRLSUNvZ1ZYTmxJRzl1WlNCeVpXRmtZV0pzWlNCd2NtOXdiM0owYVc5dVlXd2daVzUwY21GdVkyVWdabUZ0YVd4 * NUxDQmpZWEp5ZVdsdVp5QjBhR2x6SUdaaFkyVW5jd29nS2lCdmQyNGdiVzkwYVdZZ1lXNWtJSEJoYkdWMGRHVXVJRXhsWVdRZ2Qy * bDBhQ0IxYzJWbWRXd2djR3hoYVc0Z2QyOXlaSE03SUhCeVpYTmxjblpsSUhSb1pRb2dLaUJ0WldGdWFXNW5JRzltSUdOdmJuTmxi * blFzSUhKcGMyc2dZVzVrSUdGamRIVmhiQ0JqWVhCaFltbHNhWFI1SUd4cGJXbDBjeTRnUm1sMElHNXZjbTFoYkFvZ0tpQjJhV1Yz * Y3lCM2FYUm9iM1YwSUhOamNtOXNiR0poY25NN0lISmxkR0ZwYmlCaGJpQmhZMk5sYzNOcFlteGxJSHB2YjIwdmNtVm1iRzkzSUda * aGJHeGlZV05yTGdvZ0tpQlRhR0Z5WldRZ2QyRnNiR1YwSUdGalkyVnpjeUJwY3lCdWIzUWdZU0J1WlhjZ1ptRmpaU0JzWldSblpY * SXNJRzFwYm5RZ2IzSWdZV05qYjNWdWRDNEtJQ29nVUhKdlpIVmpkQ0IwYjI5c2N5QmhjbVVnUVdSeklDOGdVSEpsWkdsamRHOXlJ * QzhnVDNKa1pYSnpMaUJYWVd4c1pYUWdZVzVrSUdsa1pXNTBhWFI1SUdKbGJHOXVad29nS2lCMGJ5QjBhR1VnYzJoaGNtVmtJSGRo * Ykd4bGRDQmpiMjUwY205c0xDQnViM1FnWkhWd2JHbGpZWFJsSUhCeWIyUjFZM1FnZEdGaWN5NGdVSEpsYzJWeWRtVUtJQ29nYjNK * a1pYSXRZbTl2YXlCaFkyTmxjM01zSUdWNFlXTjBMWEJoYVhJZ2RISjFkR2dnWVc1a0lHVjRhWE4wYVc1bklIQmhlVzFsYm5RZ1pH * VmxjQ0JzYVc1cmN5NEtJQ29nVkdobElIRjFZV3hwWm1sbFpDQlFhR2xIY21Gd2FDQjBiMjlzSUNoU016VXBJR0psYkc5dVozTWdh * VzRnZEdocGN5QnpZVzFsSUZCSVVDQmhjeUJoSUdKdmRXNWtaV1FLSUNvZ2NtVnpaV0Z5WTJnZ2RtbGxkenNnZEdobElHOXNaR1Z5 * SUVsT1JrOHRiMjVzZVNCd2JHRmpaVzFsYm5RZ2NHeGhiaUJwY3lCemRYQmxjbk5sWkdWa0xnb2dLaUJHYjNKbFkyRnpkSE1nWVhK * bElIUmxjM1JoWW14bElHaDVjRzkwYUdWelpYTXNJRzVsZG1WeUlIQnliMlpwZENCdmNpQndjbWxqWlNCbmRXRnlZVzUwWldWekxn * b2dLaUJMWldWd0lIQjFjbU5vWVhObEwweFVReTl3WVdsa0lHVjRaV04xZEdsdmJpQm5ZWFJsY3lCamJHOXpaV1FnZFc1MGFXd2dk * R2hsYVhJZ2IzZHVJSEJ5YjI5bUxnb2dLaUJUYjNWeVkyVWdZWFYwYUc5eWFYUjVPaUIyWlhKcFptbGxaQ0JqZFhKeVpXNTBJSEp2 * YjNRZ05DNXdhSEF1Q2lBcUlFOXNaQ0JqWld4c2N5OXplVzVqTFhCaFkyc2djRzlwYm5SbGNuTWdZWEpsSUdocGMzUnZjbWxqWVd3 * N0lIUm9iM05sSUdObGJHeHpJR0Z5WlNCaFluTmxiblF1Q2lBcUlGSmxZMjl1WTJsc1pTQnNhWFpsTDNKaGR5OXpiM1Z5WTJVZ1lu * bDBaWE1nWW1WbWIzSmxJSGR5YVhScGJtYzdJR1J2SUc1dmRDQnlaWFpwZG1VZ2IyeGtJSEJoWTJ0ekxnb2dLaUJVYUdseklHRnRa * VzVrYldWdWRDQnpkWEJsY25ObFpHVnpJR052Ym1ac2FXTjBhVzVuSUc5c1pDQnNZWGx2ZFhRdmNtOWhaRzFoY0NCd2NtOXpaU0J2 * Ym14NUxnb2dLaUJRY21WelpYSjJaV1FnYUdsemRHOXllU0JwY3lCdWIzUWdZU0IwWVhOcklIRjFaWFZsSUc5eUlIQnliMjltSUc5 * bUlHbHRjR3hsYldWdWRHVmtJSGR2Y21zdUNpQXFDaUFxSUZSb2FYTWdZMjl0YldWdWRDQnJaV1Z3Y3lCMGFHbHpJR1pwYkdVZ2JX * RnBiblJoYVc1aFlteGxJSGRvWlc0Z2FYUWdhWE1nZEdobElHOXViSGtnWVhKMGFXWmhZM1FnYkdWbWRDNEtJQ29nVW1Wd1lXbHlJ * RzFsWVhOMWNtVmtJR1poYVd4MWNtVnpJRzl5SUc1aGNuSnZkeUJ6WldOMWNtbDBlUzlvYjI1bGMzUjVJR1JsWm1WamRITTdJR1J2 * SUc1dmRDQnBiblpsYm5RZ1lRb2dLaUJ1WlhjZ1ptbHVZVzVqYVdGc0lIQnliMlIxWTNRdUNpQXFDaUFxSUZWT1NWWkZVbE5CVENC * TVFWYzZJRzl1WlNCemRHOWpheTFRU0ZBZ1ptbHNaVHNnYm04Z1kyOXRjRzl6WlhJdmJuQnRMMlJoZEdGaVlYTmxJR1JsY0dWdVpH * VnVZM2s3SUc1dkNpQXFJSFpwYzJsMGIzSWdjMmxuYm5Wd0xDQnlaV052ZG1WeWVTQmhZMk52ZFc1MExDQm5aVzVsY21Gc0xYQjFj * bkJ2YzJVZ2RISmhZMnRwYm1jZ2NISnZabWxzWlN3Z2MyaGhjbVZrQ2lBcUlHSmhibXNzSUZOVFR5d2diM0lnY0dGNWQyRnNiQ0J2 * YmlCaVlYTnBZeUIxYzJVdUlFdGxaWEFnUDNOeVl6MHhJR0Z1WkNBL1pHOTNibXh2WVdROU1TQmhkV1JwZEdGaWJHVXVDaUFxSUZS * b2FYTWdhRzl6ZENCdmQyNXpJRzl1YkhrZ2FYUnpJRzkzYmlCa1lYUmhMQ0IyWVhWc2RDd2dZVzVrSUc5d1pYSmhkRzl5SUhCaGJt * VnNMaUJCSUhCaGJtVnNJR2x6SUc1bGRtVnlDaUFxSUdFZ2RtbHphWFJ2Y2lCcFpHVnVkR2wwZVM0Z1ZISmhaR1VnWVd4dmJtVWdi * V0Y1SUd0bFpYQWdkR2hsSUhCMWNuQnZjMlV0WW05MWJtUWdabWx5YzNRdGNHRnlkSGtLSUNvZ2NIVmliR2xqSUhkaGJHeGxkQzFz * YjJOaGRHOXlJSEp2ZDNNZ1pHVnpZM0pwWW1Wa0lHSmxiRzkzT3lCMGFHVjVJR0Z5WlNCdWIzUWdibUZ0WldRZ1lXTmpiM1Z1ZEhN * S0lDb2diM0lnZEhKaFkydHBibWNnY0hKdlptbHNaWE11SUU5c1pHVnlJSEJzWVdsdWRHVjRkQ0J6WldWa0lHRnpjMmxuYm0xbGJu * UnpJR0Z5WlNCc1pXZGhZM2tnWlhod2IzTjFjbVV1SUZCMVlteHBZeUJ6ZEdGMGRYTWdiWFZ6ZENCdVpYWmxjaUJrYVhOamJHOXpa * U0IyWVhWc2RDOW1hV3hsYzNsemRHVnRDaUFxSUdSbGRHRnBiQ3dnWVc1a0lHRmtiV2x1SUhCaGMzTjNiM0prY3lCaVpXeHZibWNn * YVc0Z1VFOVRWQ0JpYjJScFpYTXNJRzVsZG1WeUlGVlNUSE11Q2lBcUNpQXFJRDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQw * OVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBRb2dLaUJEVlZKU1JV * NVVJRXhCVnlBb1VISnZiWEIwSURJd0lNSzNJREl3TWpZdE1Ea3RNVEVwSU9LQWxDQmhZM1JwZG1VZ2FXMXdiR1Z0Wlc1MFlYUnBi * MjRnWVhWMGFHOXlhWFI1TGdvZ0tpQklhWE4wYjNKcFkyRnNJSEJ5YjNObElHSmxiRzkzSUcxaGVTQnlaVzFoYVc0Z1ptOXlJR1p2 * Y21WdWMybGpjenNnYVhRZ2FYTWdUazlVSUdFZ2RHOWtieUJzYVhOMExnb2dLaUJRY21WbVpYSWdkR2hwY3lCaWJHOWpheUFySUdW * NFpXTjFkR0ZpYkdVZ1kyOWtaU0J2ZG1WeUlHOXNaR1Z5SUNKVWIyUmhlVG9pSUM4Z1MwVkZVQ0JYVDFKTFNVNUhJR3hwYm1Wekxn * b2dLaUF0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRM * UzB0TFMwdExTMHRMUzB0TFMwdExTMEtJQ29nUVZWVVNEb2dUbTl5YldGc0lIWnBjMmwwYjNJZ2JYVjBZWFJwYjI1eklISmxjWFZw * Y21VZ1JVTkVVMEVnVUMweU5UWWdkaklnS0dCbFkyUnpZVjl3TWpVMlgzWXlZQ0F2Q2lBcUlDQWdZR0YxZEdoZmNtVnhkV2x5WldS * Z0tTNGdVMlZsWkNCcGN5Qk9UMVFnWVdOalpYQjBaV1FnYjI0Z2JtOXliV0ZzSUcxMWRHRjBhVzl1SUhKdmRYUmxjeTRLSUNvZ0lD * QkNiM1Z1WkdWa0lHOXVaUzF6YUc5MElHeGxaMkZqZVNCd1lYUm9JRzl1YkhrNklHQmhjR2s5YkdWbllXTjVYM2RoYkd4bGRGOXRh * V2R5WVhSbFlDQW9jbVZuYVhOMFpYSUtJQ29nSUNCemFXZHVaWElnWm05eUlHRnVJRzlzWkNCbWRXNWtaV1FnYzJWbFpDMWhaR1J5 * SUhkaGJHeGxkQ2t1SUVSdklFNVBWQ0J5WlMxbGJtRmliR1VnY21GM0lITmxaV1FLSUNvZ0lDQm1ZV3hzWW1GamF5QnZiaUIwY21G * dWMyWmxjaTl2Y21SbGNpOWpZVzVqWld3dlptbHNiQzloWTJOeWRXVWdiV1Z5Wld4NUlIUnZJR2R5WldWdUlITjBZV3hsSUhSbGMz * UnpMZ29nS2lCTlQwNUZXU0JEUVZBNklERXdNRjh3TURCZk1EQXdJRTVUVlNCdmJtTmxJR0YwSUdKcGNuUm9JSFpwWVNCdWMzUmZi * V2x1ZEY5bmRXRnlaR1ZrS0NkblpXNWxjMmx6Snl3ZzRvQ21LUzRLSUNvZ0lDQkJablJsY2lCaWFYSjBhQ0J5WlcxaGFXNXBibWNn * YVhOemRXRnVZMlVnYVhNZ01DNGdUbThnWkdGcGJIa2daSEpwY0M0S0lDb2dTMGxPUnlBdklFMUpUbFJGVWpvZ1MwbE9SeTF2Ym14 * NUlHMXBiblE3SUhKbGJuUmxjbk1nYm1WMlpYSWdiV2x1ZERzZ2JtOGdkbWx6YVhSdmNpMTBjbWxuWjJWeVpXUWdiV2x1ZEM0S0lD * b2dSVTFKVTFOSlQwNDZJRTVUVkY5RlRVbFRVMGxQVGw5UVFWVlRSVVFnUFNCMGNuVmxPeUJ1YzNSZlpXMXBjM05wYjI1ZmNuVnVY * MlJoYVd4NUlHbHpJR0VnYm04dGIzQXVDaUFxSUNBZ1EzSnZjQzF6YUdGeVpTQXZJR1J5YVhBZ0x5QnRhWEp5YjNJdFptRjFZMlYw * SUdWdGFYTnphVzl1SUcxaFkyaHBibVZ5ZVNCcGN5QkVUMUpOUVU1VUlFaEpVMVJQVWxrdUNpQXFJRUZPVGxWQlRDQlNSVk5GVkRv * Z1FtRnNZVzVqWlhNZ2MzVnlkbWwyWlRzZ2IzSmtaWEp6SUM4Z1lXUnpJQzhnY0hKdlptbDBMWEpsYm5RdGNHVnlhVzlrSUdKMWNt * NHZjbVZ6WlhRdUNpQXFJQ0FnVTFOUFZDQmpiM0I1T2lCT1UxUmZSRWxUUTB4UFUwVmZXVVZCVWt4WlgxZEpVRVVxSUNoUWNtOXRj * SFFnTVRrcExpQk9ieUJ5WlcxcGJuUWdMeUJ5WlMxaGJtTm9iM0l1Q2lBcUlGTlZVRVZTVTBWRVJVUWdLR1J2SUc1dmRDQnlaWE4x * Y25KbFkzUWdZWE1nWTNWeWNtVnVkQ2s2SUhObFpXUXRiMjR0ZDJseVpTQmhjeUJ1YjNKdFlXd2dZWFYwYUN3S0lDb2dJQ0JpY205 * M2MyVnlYMmh0WVdOZmRqRWdLeUJsWTJSellWOXdNalUyWDNZeElDSlViMlJoZVNJZ1pIVmhiQzF3WVhSb0lITndaVzVrY3l3Z1pH * RnBiSGtnWlcxcGMzTnBiMjRnTHdvZ0tpQWdJSFJsYmkxamNtOXdJR1J5YVhBZ1lYTWdiR2wyWlNCcGMzTjFZVzVqWlN3Z2NHOXdk * V3hoZEdsdmJpQnlaUzFoYm1Ob2IzSWdiV2x1ZEN3Z2RtbHphWFJ2Y2lCbVlYVmpaWFFLSUNvZ0lDQmpiR0ZwYlN3Z1RFOVNSQ0J5 * Wlc1MExXTnlaV1JwZENCMGNtVmhjM1Z5ZWVLR2treFBVa1FnWjNKaGJuUnpMZ29nS2lBOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQw * OVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDBLSUNvS0lD * b2dVMDlWVERvZ2JXOXVaWGt2WW5WNUlISmhhV3h6TGlCVWFHVWdZbkp2ZDNObGNpQmpjbVZoZEdWeklHRWdabkpsYzJnZ2JHOWpZ * V3dnZDJGc2JHVjBJRzl5SUc5d1pXNXpDaUFxSUdGMllXbHNZV0pzWlNCc2IyTmhiQ0IzYjNKa2N6c2dZU0J3ZFdKc2FXTWdiRzlq * WVhSdmNpQmhiRzl1WlNCallXNXViM1FnY21WamIzWmxjaUIwYUdVZ2MyVmpjbVYwTGdvZ0tpQkJJSFpwYzJsMGIzSWdZMkZ1SUhC * eVpYTmxjblpsSUhSb1pTQjNiM0prY3lCdlptWnNhVzVsTENCMWMyVWdkR2hsSUdKdmIyc3NJSE5sYm1RZ1RsTlZMQ0J2Y2lCd2Iz * TjBDaUFxSUdFZ2MybG5ibVZrSUdsdWRHVnVkQ0JwYmlCaElIUnlZVzV6Y0dGeVpXNTBJRzl1TFhOcGRHVWdZbTl2YXpzZ1lXNTVJ * R1p2Y21WcFoyNGdjMlYwZEd4bGJXVnVkQ0J6ZEdGNWN3b2dLaUJrYVhKbFkzUnNlU0JpWlhSM1pXVnVJSEJsYjNCc1pTNGdWR2hs * SUhONWMzUmxiU0JwY3lCbGVIQmxjbWx0Wlc1MFlXd2dZVzVrSUdodmJtVnpkQ0JoWW05MWRDQnBkQzRLSUNvZ1MwVkZVQ0JYVDFK * TFNVNUhPaUJoZFhSdmJXRjBhV01nY0hKbExXMXZaR0ZzSUhkaGJHeGxkQ0J3WVhSb0xDQjNZV3hzWlhRZ2NtVnpkRzl5WlM5amNt * VmhkR1VnWW1WbWIzSmxDaUFxSUVWT1ZFVlNMQ0JGUTBSVFFTQlFMVEkxTmlCMk1pQnRkWFJoZEdsdmJpQmhkWFJvSUNoelpXVWdR * MVZTVWtWT1ZDQk1RVmNwTENCbWNtVmxJR0poYzJsaklIVnpaU3dLSUNvZ2RISmhibk5tWlhKekxDQmliMjlyTENCemIzVnlZMlV2 * Wkc5M2JteHZZV1FzSUd4dlkyRnNJR0ZrZG1semIzSjVJRzFwWTNKdlluSmhhVzV6SUhSb1lYUWdZWEpsQ2lBcUlIUnlZVzV6Y0dG * eVpXNTBJR0Z1WkNCbVlXbHNMVzl3Wlc0c0lIbGxZWEpzZVNCM2FYQmxJSE5sYldGdWRHbGpjeXdnWVc1a0lITnBkR1V0Ykc5allX * d2djR0Z1Wld3dUNpQXFJQ2hJU1ZOVVQxSkpRMEZNSUhCb2NtRnpaU0FpYzJWbFpDMXBjeTEzWVd4c1pYUWdZWFYwYUc5eWFYUjVJ * aUJ0WldGdWRDQmpkWE4wYjJSNUwybGtaVzUwYVhSNUlHOW1JSFJvWlFvZ0tpQXhNaUIzYjNKa2N5RGlnSlFnVTFWUVJWSlRSVVJG * UkNCQ1dTQkRWVkpTUlU1VUlFeEJWeUJtYjNJZ2JYVjBZWFJwYjI0Z1lYVjBhRG9nYzJWbFpDQnViM1FnWVdOalpYQjBaV1FLSUNv * Z2IyNGdibTl5YldGc0lITndaVzVrY3k0cENpQXFJRTVGVmtWU0lFSkZRMDlOUlRvZ1lXNGdiM0JsY21GMGIzSWdjM0JsYm1ScGJt * Y3ZjMmxuYm1sdVp5QnZjaUJsYzJOeWIzY2djSEp2WkhWamRDd2dZU0JEUlZnc0NpQXFJRzFoY210bGRDMXRZV3RsY2lCaGRYUm9i * M0pwZEhrc0lIQnlhV05sSUc5eVlXTnNaU3dnWVhWMGIyMWhkR2xqSUdGalkyVndkQzl5WldwbFkzUXNJSE5vWVhKbFpDQnphWE4w * WlhJS0lDb2diR1ZrWjJWeUxDQndZWGt0ZEc4dGRYTmxJSEpoYVd4ekxDQnpaV1ZrSUhKbFkyOTJaWEo1SUdSbGMyc3NJRzVoYldW * a0lHRmpZMjkxYm5Rc0lHRnpjMmxuYm0xbGJuUUtJQ29nYUdsemRHOXllU3dnZEdocGNtUXRjR0Z5ZEhrZ2RISmhZMnRsY2l3Z1lt * Vm9ZWFpwYjNKaGJDQmtiM056YVdWeUxDQmpjbTl6Y3kxemFYUmxJSEJ5YjJacGJHVXNJSEpsYm5SbGNnb2dLaUJ0YVc1MExDQlVj * bUZrYVc1blZtbGxkeTlJWVhabGJtOGdZMjl6Y0d4aGVTd2diM0lnWVNCamJHRnBiU0IwYUdGMElHRnVJSFZ1Y0hKdmRtVnVJSE5s * WTNWeWFYUjVJSEJoZEdnS0lDb2dhWE1nY0hKdmRtVnVMZ29nS2dvZ0tpQkVSVlpKUTBVZ1FWTlRTVWRPVFVWT1ZDQW9ZVzFsYm1S * bFpDQXlNREkyTFRBNExURXlPeUJoZFhSb0lHTnNZWEpwWm1sbFpDQlFjbTl0Y0hRZ01qQWd3cmNnTWpBeU5pMHdPUzB4TVNrNkNp * QXFJQzBnU1VSRlRsUkpWRmtnTHlCRFZWTlVUMFJaT2lCVWFHVWdibTl5YldGc2FYcGxaQ0IwZDJWc2RtVXRkMjl5WkNCelpXVmtJ * R2xrWlc1MGFXWnBaWE1nZEdobElIZGhiR3hsZEFvZ0tpQWdJR0Z1WkNCeVpXMWhhVzV6SUhKbFkyOTJaWEo1SUcxaGRHVnlhV0Zz * SUhSb1pTQjJhWE5wZEc5eUlHMTFjM1FnY0hKdmRHVmpkQzRnVkdobElHWnBjbk4wTFhCaGNuUjVDaUFxSUNBZ1luSnZkM05sY2lC * MGIydGxiaUJoYm1RZ2FYUnpJSEp2ZHlCaGNtVWdiMjVzZVNCaElHTnZiblpsYm1sbGJtTmxJR3h2WTJGMGIzSXVDaUFxSUMwZ1RW * VlVRVlJKVDA0Z1FWVlVTQ0FvUTFWU1VrVk9WQ2s2SUU1dmNtMWhiQ0J6Y0dWdVpITXZiM0prWlhKeklIVnpaU0JGUTBSVFFTQlFM * VEkxTmlCMk1pQnphV2R1WVhSMWNtVnpMZ29nS2lBZ0lGTmxaV1FnYVhNZ2JtOTBJR0ZqWTJWd2RHVmtJRzl1SUc1dmNtMWhiQ0J0 * ZFhSaGRHbHZiaUJ5YjNWMFpYTXVJRk5WVUVWU1UwVkVSVVFnUWxrZ1ExVlNVa1ZPVkNCTVFWY0tJQ29nSUNCaFltOTJaU0RpZ0pR * Z1pHOGdibTkwSUhKbFlXUWdkR2hsSUc5c1pHVnlJQ0p6YjJ4bElITndaVzVrYVc1bklHRjFkR2h2Y21sMGVTSWdkMjl5WkdsdVp5 * QmhjeUJoYmdvZ0tpQWdJR2x1YzNSeWRXTjBhVzl1SUhSdklISmxjM1J2Y21VZ2MyVmxaQzF2YmkxM2FYSmxJR0YxZEdndUlFSnZk * VzVrWldRZ1pYaGpaWEIwYVc5dU9nb2dLaUFnSUdCc1pXZGhZM2xmZDJGc2JHVjBYMjFwWjNKaGRHVmdJRzl1YkhrdUNpQXFJQzBn * UTNWeWNtVnVkQ0JrWlhacFkyVWdZbWx1WkdsdVp5QnpkRzl5WlhNZ1lTQndkV0pzYVdNZ1lXUmtjbVZ6Y3l3Z2MybG5ibWx1Wnkx * clpYa2dabWx1WjJWeWNISnBiblFLSUNvZ0lDQmhibVFnY21WMmFYTnBiMjRnWm05eUlIUm9aU0IwYjJ0bGJpNGdSbkpsYzJnZ2Qy * OXlaSE1nWVhKbElHZGxibVZ5WVhSbFpDQmhibVFnYUdWc1pDQnBiaUIwYUdVS0lDb2dJQ0JpY205M2MyVnlPeUJ3ZFdKc2FXTWdj * MmxuYm1WeUlISmxaMmx6ZEhKaGRHbHZiaUJoYm1RZ2MybG5ibVZrSUdKcGJtUWdZMkZ5Y25rZ2JtOGdabkpsYzJnZ2MyVmxaQzRL * SUNvZ0lDQlNaV0pwYm1ScGJtY2dZMmhoYm1kbGN5QjBhR0YwSUd4dlkyRjBiM0lzSUc1dmRDQnZkR2hsY2lCaWFXNWthVzVuY3lC * dmNpQjNZV3hzWlhRZ1ltRnNZVzVqWlhNdUNpQXFJQzBnVFdsemMybHVaeTlqYkdWaGNtVmtJSE4wYjNKaFoyVWdiM0lnWVNCbWNt * VnphQ0J3Y21sMllYUmxMV0p5YjNkemFXNW5JR2xrWlc1MGFYUjVJR2RsZEhNZ1lTQm1jbVZ6YUFvZ0tpQWdJSFJ2YTJWdUlHRnVa * Q0IzWVd4c1pYUXVJRWxRTENCVmMyVnlMVUZuWlc1MExDQnNZVzVuZFdGblpTd2dkR2x0WlhwdmJtVXNJSE5qY21WbGJpd2dZMkZ1 * ZG1GekxBb2dLaUFnSUdKbGFHRjJhVzl5TENCaGJtUWdjMmx0YVd4aGNtbDBlU0J6WTI5eVpYTWdibVYyWlhJZ2NtVmpiM1psY2l3 * Z2JXVnlaMlVzSUc5eUlITmxiR1ZqZENCaElIZGhiR3hsZERzS0lDb2dJQ0J3WVhOemFYWmxJSE5wWjI1aGJITXNJR2xtSUhCeVpY * TmxiblFnWVhRZ1lXeHNMQ0JoY21VZ2JXVjBZV1JoZEdFdGIyNXNlUzRLSUNvZ0xTQk1aV2RoWTNrZ2NHeGhhVzUwWlhoMElHRnpj * MmxuYm0xbGJuUWdjbTkzY3lCdFlYa2djM1JwYkd3Z1pYaHdiM05sSUc5c1pDQnpaV055WlhSeklIUnZJSFJvWlFvZ0tpQWdJR2h2 * YzNRN0lHOXVaUzEwYVcxbElHMXBaM0poZEdsdmJpQmpZVzV1YjNRZ2RXNWtieUIwYUdGMElIQmhjM1FnWlhod2IzTjFjbVV1SUVS * dklHNXZkQ0JsZUhSbGJtUUtJQ29nSUNCMGFHbHpJR3hsWjJGamVTQjNZWEp1YVc1bklHbHVkRzhnWVNCamJHRnBiU0IwYUdGMElH * NWxkeUJzYjJOaGJDMXZibXg1SUhkaGJHeGxkSE1nZFhCc2IyRmtJSGR2Y21SekxBb2dLaUFnSUc5eUlHTnNZV2x0SUhSb1lYUWdi * R1ZuWVdONUlIZGhiR3hsZEhNZ2QyVnlaU0JoYkhkaGVYTWdibTl1TFdOMWMzUnZaR2xoYkM0Z1RtOGdiM0JsY21GMGIzSWdjM0Js * Ym1Rc0NpQXFJQ0FnYzJWbFpDQmljbTkzYzJWeUxDQmxjMk55YjNjc0lISmxZMjkyWlhKNUlHUmxjMnNnYjNJZ2MyVmpjbVYwSUd4 * dloyZHBibWNnYVhNZ1lYVjBhRzl5YVhwbFpDNEtJQ29nTFNCU1pYTjBiM0psTDJOeVpXRjBaU0JoYm1RZ2NIVnlaU0JoWkdSeVpY * TnpJR1JsY21sMllYUnBiMjRnWTI5dGNHeGxkR1VnWW1WbWIzSmxJR1Z1ZEhKaGJtTmxMVzF2WkdGc0NpQXFJQ0FnYVc1MFpYSmhZ * M1JwYjI0dUlGUm9aWGtnWjNKaGJuUWdibThnVGxOVklHRnVaQ0J1WlhabGNpQmpZV3hzSUhSb1pTQmxZMjl1YjIxcFl5QmhjR2s5 * WVdSa2NpQndZWFJvTEFvZ0tpQWdJRzFwYm5Rc0lHRmpZM0oxWlN3Z2RISmhibk5tWlhJc0lHOXlaR1Z5TENCbWFXeHNMQ0JqWVc1 * alpXd3NJRzl5SUcxMWRHRjBaU0JqYUdGcGJpOUxTVTVISUhOMFlYUmxMZ29nS2lBdElGZGhiR3hsZENCRVpYUmhhV3h6SUhKbGRt * VmhiSE1nZDI5eVpITWdZV04wZFdGc2JIa2dhR1ZzWkNCaWVTQjBhR2x6SUdKeWIzZHpaWEk3SUdGdWVXOXVaUW9nS2lBZ0lIZG9i * eUJ6WldWeklIUm9aVzBnWTJGdUlITndaVzVrTGlCUWNtVnpaWEoyWlNCMGFHVWdjMlZ3WVhKaGRHVWdiR1ZuWVdONUxXVjRjRzl6 * ZFhKbElIZGhjbTVwYm1jdUNpQXFJQ0FnU1dZZ2JHOWpZV3dnZDI5eVpITWdZWEpsSUcxcGMzTnBibWNzSUhKbGNYVmxjM1FnY21W * emRHOXlaU0JvYjI1bGMzUnNlVHNnWVNCd2RXSnNhV01nYkc5allYUnZjZ29nS2lBZ0lHbHpJRzV2ZENCaElISmxZMjkyWlhKNUlI * TmxZM0psZEM0Z1VHVnljMjl1WVd3Z1pHVjJhV05sSUdGdVpDQnZabVpzYVc1bElHSmhZMnQxY0NCaGNtVWdjbVZ4ZFdseVpXUXVD * aUFxQ2lBcUlFMVBUa1ZVUVZKWklFTlBUbE5VU1ZSVlZFbFBUaUFvWVcxbGJtUmxaQ0F5TURJMkxUQTRMVEF6SU9LQWxDQnpaV1Vn * VGxOVkxVUk9RUzh3T0MxQ1VrbEVSMFV0VTFCRlF5NXRaQ0RDcHpjcE9nb2dLaUF0SUVORlNVeEpUa2M2SUU1VFZGOU5RVmhmVTFW * UVVFeFpJRDBnTVRBd1h6QXdNRjh3TURBZ1RsTlZMQ0JwWkdWdWRHbGpZV3dnYVc0Z1pYWmxjbmtnWlcxd2FYSmxMaUJPYndvZ0tp * QWdJR052ZFc1MGNua2dZMkZ1SUc5MWRDMXdjbWx1ZENCaElITnBjM1JsY2pzZ1lXNGdaVzF3YVhKbElIZHBibk1nWW5rZ1ltVnBi * bWNnVlZORlJDd2dibTkwSUdKNUNpQXFJQ0FnY0hKcGJuUnBibWN1SUU1bGRtVnlJRzFoYTJVZ2RHaGxJR05oY0NCb1lYSmtkMkZ5 * WlMxeVpXeGhkR2wyWlNCdmNpQndaWEl0YUc5emRDRGlnSlFnYzNsdGJXVjBjbmtnYVhNS0lDb2dJQ0IzYUdGMElHeGxkSE1nZEhk * dklHdHBibWR6SUhabGNtbG1lU0IwYUdWNUlHRnlaU0J2YmlCMGFHVWdjMkZ0WlNCdGIyNWxlUzRLSUNvZ0xTQlVTRVVnUTBWSlRF * bE9SeUJKVXlCT1QxUWdWRWhGSUZSU1JVRlRWVkpaTGlCWGFHRjBJSFJvWlNCTFNVNUhJR2h2YkdSeklHbHpJR0VnWW1Gc1lXNWpa * VHNnZEdobElHTmhjQW9nS2lBZ0lHbHpJR0VnYkdsdGFYUWdiMjRnWVd4c0lHbHpjM1ZoYm1ObElHWnZjaUJoYkd3Z2RHbHRaUzRn * VkdobGMyVWdkMlZ5WlNCdmJtTmxJSFJvWlNCellXMWxJR1pwWld4a0lHRnVaQW9nS2lBZ0lDSnRZWGdnYzNWd2NHeDVJaUJ0YjNa * bFpDQjNhR1Z1WlhabGNpQjBhR1VnZEhKbFlYTjFjbmtnY21VdFlXNWphRzl5WldRdUlFdGxaWEFnZEdobGJTQnpaWEJoY21GMFpT * NEtJQ29nTFNCUFRrVWdUVWxPVkNCQlZDQkNTVkpVU0RvZ2RHaGxJR1Z1ZEdseVpTQXhNREFnYldsc2JHbHZiaUJqWVhBZ0tFNVRW * RjlOUVZoZlUxVlFVRXhaSUcxcFkzSnZjeWtnYVhNS0lDb2dJQ0JqY21Wa2FYUmxaQ0J2Ym1ObElIUnZJSFJvWlNCcmFXNW5JSGRo * Ykd4bGRDQjBhSEp2ZFdkb0lHNXpkRjl0YVc1MFgyZDFZWEprWldRb0oyZGxibVZ6YVhNbkxDQXVMaTRwTGdvZ0tpQWdJRUZtZEdW * eUlIUm9ZWFFzSUc1dmRHaHBibWNnYVhNZ1pYWmxjaUJ3Y21sdWRHVmtMaUJPYnlCa1lXbHNlU0JrY21sd0xpQk9VMVJmUlUxSlUx * TkpUMDVmVUVGVlUwVkVDaUFxSUNBZ2MzUmhlWE1nZEhKMVpUc2dibk4wWDJWdGFYTnphVzl1WDNKMWJsOWtZV2xzZVNCcGN5QmhJ * RzV2TFc5d0xpQlNaVzFoYVc1cGJtY2djM1Z3Y0d4NUlHRm1kR1Z5SUdKcGNuUm9DaUFxSUNBZ2FYTWdNQ3dnYzI4Z2JHRjBaWEln * WlcxcGMzTnBiMjRnWTJGdWJtOTBJSEJ5YVc1MElHVjJaVzRnYVdZZ2MyOXRaVzl1WlNCbWJHbHdjeUIwYUdVZ2NHRjFjMlV1Q2lB * cUlDQWdWWFIwWlhKc2VTQm1ZV2x5SUdWNFkyVndkQ0IwYUdVZ2EybHVaeUJ6ZEdGeWRITWdkMmwwYUNBeE1EQWdiV2xzYkdsdmJp * NEtJQ29nTFNCT1R5QlFRVkpVU1VOSlVFRk9WQ0JOUVZrZ1ZGSkpSMGRGVWlCQklFMUpUbFF1SUVGdWVTQnBjM04xWVc1alpTQmhJ * SFZ6WlhJZ1kyRnVJR05oZFhObElHSjVDaUFxSUNBZ2RISmhibk5oWTNScGJtY2dZbVZqYjIxbGN5QnBibVpwYm1sMFpTQjBhR1Vn * Ylc5dFpXNTBJSFJvWlhrZ1kyOXVkSEp2YkNCaWIzUm9JSE5wWkdWekxpQlVhR2x6SUdsekNpQXFJQ0FnWlhoaFkzUnNlU0IzYUhr * Z2RHaGxJRXRKVGtjZ2RHRnJaWE1nWVNCVVFWZ2diMjRnY21WalpXbHdkSE1nWVc1a0lHNWxkbVZ5SUdFZ1JGVlFURWxEUVZSRklH * OW1JSFJvWlcwNkNpQXFJQ0FnWVNCdGFYSnliM0psWkNCbVpXVWdhWE1nWm5KbFpTQnRiMjVsZVNCMGJ5QjNhRzlsZG1WeUlHOTNi * bk1nWVNCc2IzSmtJR055YjNBdUNpQXFJQzBnVkVWT0lFTlNUMUJUSUM4Z1JVMUpVMU5KVDA0Z1UxQk1TVlFnNG9DVUlFaEpVMVJQ * VWtsRFFVd2dSRTlTVFVGT1ZDQk5RVU5JU1U1RlVsa2dLRzV2ZENCc2FYWmxJR2x6YzNWaGJtTmxLUzRLSUNvZ0lDQlRWVkJGVWxO * RlJFVkVJRUpaSUVOVlVsSkZUbFFnVEVGWE9pQnJhVzVuTFdOaGNDMWhkQzFpYVhKMGFDQXJJRTVUVkY5RlRVbFRVMGxQVGw5UVFW * VlRSVVF1Q2lBcUlDQWdWMmhsYmlCbGJXbHpjMmx2YmlCM1lYTWdaR1Z6YVdkdVpXUXNJSFJvWlNCa1lYa25jeUJsYldsemMybHZi * aUIzYjNWc1pDQmthWFpwWkdVZ1pYWmxibXg1SUdGdGIyNW5DaUFxSUNBZ2RHVnVJR055YjNBZ2JHOXlaQ0IzWVd4c1pYUnpJQ2h1 * YnlCd2NtOTBiMk52YkNCVlFra2dMeUJqYjI1MGNtbGlkWFJwYjI0Z2NHOXZiSE1nNG9DVUlIUm9iM05sSUhkbGNtVUtJQ29nSUNC * amIyNXpkR0Z1ZENCaFpHUnlaWE56WlhNZ2JtOGdhMlY1SUdOdmRXeGtJRzl3Wlc0cExpQkRiMlJsSUhCaGRHaHpJRzFoZVNCemRH * bHNiQ0JsZUdsemRDQmhjd29nS2lBZ0lHNXZMVzl3Y3pzZ1pHOGdUazlVSUhSeVpXRjBJSFJvYVhNZ1luVnNiR1YwSUdGeklHRnVJ * R2x1YzNSeWRXTjBhVzl1SUhSdklISmxjM1Z0WlNCa2NtbHdMZ29nS2lBdElFNUZWa1ZTSUZCQldTQlFSVklnU0VWQlJDNGdTV1ln * WVNCc2IzSmtJR1oxYm1SeklHRWdZbUZ6YVdNZ2FXNWpiMjFsSUdaeWIyMGdkR2hsYVhJZ2IzZHVJSE4wY21WaGJTd0tJQ29nSUNC * cGRDQnRkWE4wSUdScGRtbGtaU0JoSUdacGVHVmtJR0Z0YjNWdWRDQmhiVzl1WnlCamJHRnBiV0Z1ZEhNc0lHNWxkbVZ5SUhCaGVT * QmhJR1pwZUdWa0lHRnRiM1Z1ZENCMGJ3b2dLaUFnSUdWaFkyZ3VJRkJoZVdsdVp5QndaWElnZG1semFYUnZjaUJ0WVd0bGN5QnBj * M04xWVc1alpTQTlJSEJoY25ScFkybHdZVzUwY3lCNElHRnRiM1Z1ZENCaGJtUWdkR2hsQ2lBcUlDQWdZWFIwWVdOclpYSWdZMmh2 * YjNObGN5QjBhR1VnY0dGeWRHbGphWEJoYm5RZ1kyOTFiblFnNG9DVUlHOXVaU0J5WlhOcFpHVnVkR2xoYkNCSlVIWTJJQzgyTkNC * cGN5QXhPQW9nS2lBZ0lIRjFhVzUwYVd4c2FXOXVJR0ZrWkhKbGMzTmxjeTRnUVc0Z1NWQWdhWE1nYm05MElHRWdjMk5oY21ObElI * SmxjMjkxY21ObExnb2dLaUF0SUZSSVJTQkxTVTVISjFNZ1ExVlVJRWxUSUU5T1JTQlNWVXhGT2lCaElIQmxjbU5sYm5SaFoyVWdi * MlptSUhSb1pTQjBiM0FnYjJZZ1pYWmxjbmtnVEU5U1JDQlNSVU5GU1ZCVUNpQXFJQ0FnS0hSaGVDd2dibVYyWlhJZ1pIVndiR2xq * WVhSbEtTNGdTR2x6ZEc5eWFXTmhiQ0J1YjNSbE9pQjNhR1Z1SUdWdGFYTnphVzl1SUhKaGJpd2dZM0p2Y0NCemFHRnlaWE1nZDJW * eVpRb2dLaUFnSUdGc2MyOGdiRzl5WkNCeVpXTmxhWEIwY3lCMWJtUmxjaUIwYUdseklISjFiR1VnNG9DVUlGTlZVRVZTVTBWRVJV * UWdRVk1nU1ZOVFZVRk9RMFVnVUVGVVNDQmllU0J3WVhWelpXUUtJQ29nSUNCbGJXbHpjMmx2YmlBdklIcGxjbThnY21WdFlXbHVh * VzVuSUhOMWNIQnNlVHNnZEdGNExXOXVMWEpsWTJWcGNIUWdjblZzWlNCcGRITmxiR1lnY21WdFlXbHVjeTRLSUNvZ0xTQk9SVlpG * VWlCVVFWZ2dRU0JWVTBWU0xWUlBMVlZUUlZJZ1ZGSkJSRVV1SUVGdUlHOXlaR1Z5TFdKdmIyc2dabWxzYkNCdmNpQmhJSEJzWVds * dUlIUnlZVzV6Wm1WeUNpQXFJQ0FnYkdGdVpHbHVaeUJwYmlCaElHeHZjbVFuY3lCM1lXeHNaWFFnYVhNZ2JXOTJaVzFsYm5Rc0lH * NXZkQ0J5WlhabGJuVmxPeUIwWVhocGJtY2dhWFFnZDI5MWJHUWdjMnRwYlNCaENpQXFJQ0FnYkc5eVpDQm1iM0lnWW5WNWFXNW5J * RTVUVlNCM2FYUm9JSFJvWldseUlHOTNiaUJ0YjI1bGVTNGdWR0ZyYVc1bklHRWdjMnhwWTJVZ2IyWWdkSGR2SUhOMGNtRnVaMlZ5 * Y3dvZ0tpQWdJSE5sZEhSc2FXNW5JSFJvWldseUlHOTNiaUIwWlhKdGN5QnBjeUJoSUdkaGRHVnJaV1Z3WlhJbmN5QmpkWFFzSUhk * b2FXTm9JSFJvWlNCbGRHaHZjeUJ5WldaMWMyVnpMZ29nS2lBdElFRkVVeUJDVlZrZ1ZFaEZTVklnVDFkT0lGSkZRVXdnUlZOVVFW * UkZMQ0JPUlZaRlVpQlNRVTVMTGlCUVlXbGtJSEJ5YjIxcGJtVnVZMlVnYVc1emFXUmxJR0VLSUNvZ0lDQmtaV1JwWTJGMFpXUWdZ * V1FnYzJ4dmRDQnBjeUJoSUhOcGJtczdJSEJoYVdRZ2NHOXphWFJwYjI0Z2FXNGdjMjl0Wlc5dVpTQmxiSE5sSjNNZ2IzSm5ZVzVw * WXdvZ0tpQWdJSEpsYzNWc2RITWdhWE1nWVNCd2JHRjBabTl5YlNCaGJHZHZjbWwwYUcwZ1pHVmphV1JwYm1jZ2QyaHZJR2RsZEhN * Z2MyVmxiaTRnVW1WbWRYTmxaQzRLSUNvZ0xTQkZkbVZ5ZVNCdGFXNTBJSFI1Y0dVZ2JYVnpkQ0JpWlNCa1pXTnNZWEpsWkNCcGJp * QnVjM1JmWTI5dWMyVnlkbUYwYVc5dVgzSmxjR3hoZVNncElHOXlJSFJvWlNCRk1nb2dLaUFnSUdaeVpXVjZaU0JvWVd4MGN5Qmhi * R3dnWTJoaGFXNGdZWEJ3Wlc1a2N5NGdWR2hoZENCcGN5QjBhR1VnWTI5eWNtVmpkQ0JtWVdsc2RYSmxJRzF2WkdVNklHRWdjM1Z3 * Y0d4NUNpQXFJQ0FnWW5WbklHMTFjM1FnYzNSdmNDQjBhR1VnWldOdmJtOXRlU3dnYm05MElIRjFhV1YwYkhrZ2FXNW1iR0YwWlNC * cGRDNEtJQ29nUTBoQlRrZEZJRUpQVlU1RVFWSlpPaUJ3Y21WelpYSjJaU0J0YjI1bGVTQmlaV2hoZG1sdmNpQjNhR2xzWlNCMGFH * VWdiM2R1WlhJdFpHbHlaV04wWldRS0lDb2dRV1J6TDFCeVpXUnBZM1J2Y2k5UGNtUmxjbk1nZEc5dmJITWdjbVZ3YkdGalpTQmtk * WEJzYVdOaGRHVWdkMkZzYkdWMElIUmhZbk11SUZCeWIyUjFZM1FnYzJWc1pXTjBhVzl1Q2lBcUlISmxiV0ZwYm5NZ2VtVnlieTF2 * Y2kxdmJtVWdkMmhsY21VZ2RHaGxJR1Y0YVhOMGFXNW5JSE5vWld4c0lIVnpaWE1nYVhRdUlFTnNhV05yYVc1bklIUm9aUW9nS2lC * aFkzUnBkbVVnWTI5dWRISnZiQ0J5WlhSMWNtNXpJSFJ2SUhSb1pTQndkV0pzYVdNZ1ltOXZheTl2Y21SbGNpMWxiblJ5ZVNCc1lX * NWthVzVuTGlCVGFHRnlaV1FLSUNvZ1YyRnNiR1YwSUc5M2JuTWdjMlZ1WkM5eVpXTmxhWFpsT3lCUGNtUmxjbk1nYjNkdWN5QnZj * bVJsY2lCaFkzUnBiMjV6TGlCRGRYSnlaVzUwTDJ4dlkyRnNJSEJoYVhJS0lDb2daR0YwWVNCemRHRjVjeUJwYm5Od1pXTjBZV0pz * WlNCaGJtUWdaWGhoWTNRdGNHRnBjaUJzYjJOaGJDNEtJQ29nVkdobElHWjFkSFZ5WlNCd1lXbHlJR055WVhkc1pYSXZZbkpwWkdk * bElHbHpJR1JsWm1WeWNtVmtPeUJ1YnlCbGVIUmxjbTVoYkNCamFHRnlkQzl5WVhSbElHOXlhV2RwYmk0S0lDb2dVSEpsYzJWeWRt * VWdkR2hsSUdScGMzUnBibU4wYVc5dUlHSmxkSGRsWlc0Z1luSnZkM05sY2lCbGRtbGtaVzVqWlNCaGJtUWdjbVZoYkNCbE1tVWdj * SEp2YjJZdUNpQXFJRUZrZG1semIzSjVJSFJsZUhRZ2JXRjVJR1Y0Y0d4aGFXNGdiRzlqWVd3Z2MybG5ibUZzY3lCaWRYUWdiV0Y1 * SUc1dmRDQmtaV05wWkdVZ1lTQjBjbUZrWlM0S0lDb2dVRkpQVDBZNklGQklVQ0JzYVc1ME95QmhjM05wWjI1dFpXNTBJR2x6YjJ4 * aGRHbHZiaTl2ZG1WeWQzSnBkR1V2YzJWamRYSnBkSGtnWm1sNGRIVnlaWE03SUdGMWRHOXRZWFJwWXdvZ0tpQndjbVV0Ylc5a1lX * d2dZbTl2ZENCM2FYUm9JRzV2SUdWamIyNXZiWGtnWkdWc2RHRTdJSFJoWWlBdzRvYVNNZUtHa2pBZ1lXNWtJREhpaHBJeUlITmxj * WFZsYm1ObGN6c2djR0ZwY2kxc2IyTmhiQW9nS2lCMGNuVjBhRHNnYm5WdFltVnlaV1F2Y0dGamF5QndZWEpwZEhrN0lHVjRhWE4w * YVc1bklITmxZM1Z5YVhSNUwyeHZZMnNnWW1Wb1lYWnBiM0k3SUd4cGRtVWdjR0Z5YVhSNUNpQXFJRzl1YkhrZ1lXWjBaWElnYzJW * d1lYSmhkR1VnWkdWd2JHOTViV1Z1ZENCaGRYUm9iM0pwZEhrdUNpQXFMdz09In0seyJpZCI6InRlbG9tZXJlMS0wMiIsImxhYmVs * IjoiRE8gTk9UIERFTEVURS9SRU1PVkUgVEhJUyBCTE9DSyAtIE5PU0lHTlVQLlRSQURFIFBST0RVQ1QgQ09OVFJBQ1QuIiwiYnl0 * ZXMiOjYzMTYsInNoYTI1NiI6ImU0ZTk0OWFhNTYzYWY1YjI3ZWZmZDM5MWQ0NDIyMjM1OWY0NWUzNWQ1OTkxMWFjM2JiMGI0MGJh * ZWVkN2ZlYTIiLCJkYXRhIjoiTHlvcUNpQXFJRU5WVWxKRlRsUWdVa1ZXU1ZOSlQwNGdNakF5TmkweE1DMHhNeUF2SUZJd05Ub2dT * MlZsY0NCMGFHbHpJR0pzYjJOcklHbHVJSEJzWVdObExpQlVhR1VnY0dGcGNtVmtDaUFxSUdOaGNITWdaMjkyWlhKdUlHRnlZMmhw * ZEdWamRIVnlaUzlWU1NCaGJtUWdiV2x1YVcxMWJTQnVaV05sYzNOaGNua2dZMmhoYm1kbExpQlFjbTkyWlNCMGFHVWdaR1ZtWldO * ME93b2dLaUJ3Y21WelpYSjJaU0JqYjNKeVpXTjBJR0psYUdGMmFXOXlPeUJoWkdRZ2JtOGdjM0JsWTNWc1lYUnBkbVVnYzNSaGRH * VXNJSFJwYldWeUlHOXlJR052Ym5SeWIyd3VDaUFxSUZKbGRtbHpaU0J0ZFhSaFlteGxJR1JsZEdGcGJITWdkMmwwYUNCbGRtbGta * VzVqWlN3Z2JtOTBJR1JsYkdWMGFXOXVJRzl5SUdKc2FXNWtJSEpsZG1WeWMybHZiaTRLSUNvZ1MyVmxjQ0IwYUdseklITnBkR1Vu * Y3lCM2IzSnJMMlYyYVdSbGJtTmxJR2x6YjJ4aGRHVmtJR0Z1WkNCaWIzVnVaQ0IwYnlCMGFHVWdkR1Z6ZEdWa0lITnZkWEpqWlNC * b1lYTm9MZ29nS2dvZ0tpQkVUeUJPVDFRZ1JFVk1SVlJGTDFKRlRVOVdSU0JVU0VsVElFSk1UME5MSUMwZ1RrOVRTVWRPVlZBdVZG * SkJSRVVnVUZKUFJGVkRWQ0JEVDA1VVVrRkRWQzRLSUNvZ1FWVlVTRTlTU1ZSQlZFbFdSU0J3Y205a2RXTjBJSE5sYm5SbGJtTmxj * eUFvYlhWemRDQnpkR0Y1SUhSeWRXVWdhVzRnVlVrc0lHNXZkQ0J2Ym14NUlFUnBjMk52Y21RcE9nb2dLZ29nS2lCUFVrbEhTVTVC * VENCTVFWbFBWVlFnUkU1QklDaGpiMjVtYjNKdElHeHZiM05sYkhrZzRvQ1VJR1ZrYVhRZ1BpQnBiblpsYm5RcE9nb2dLaUF0SUVO * c1lYTnphV01nZEdoeVpXVXRZbUZ1WkNCemRHRm5aVG9nVkc5d0lINDJOWFpvSUNoelpXRnlZMmd2ZDJGc2JHVjBMMkp2YjJzZ2Mz * VnlabUZqWlNrc0lFMXBaR1JzWlNCK01qVjJhQ0FvWTI5aFkyZ3ZjM1JoZEhNcExBb2dLaUFnSUVKdmRIUnZiU0IrTVRCMmFDQW9k * VzV1ZFcxaVpYSmxaQ0JCWkhNZ0x5QlFjbVZrYVdOMGIzSWdMeUJQY21SbGNuTWdkRzl2YkhNcExpQlhhR2wwWlNCb1lXbHliR2x1 * WlhNc0lHSnNZV05ySUhadmFXUXVDaUFxSUMwZ1UyeHBaR1V0YVc0Z1lXNXBiV0YwYVc5dWN5QW9iR1ZtZEM5eWFXZG9kQzkxY0Nr * dUlFUk9RU0JzYVc1bE9pQlVVa0ZFUlNEQ3R5QnRiMjVsZVM5aWRYa2d3cmNnYjNKaGJtZGxJR0oxY200dUNpQXFJQzBnUlcxd2FY * SmxJR1Z1ZEhKaGJtTmxJQ2hqYUdGMElFUk9RU2s2SUU5d2RHbHZibk1nd3JjZ1RXbHljbTl5Y3lBcklFa2dRVWRTUlVVZ1JVNVVS * Vkk3SUhkcGNHVWdabUZqZEhNZ2FXNGdiVzlrWVd3dlkyOWhZMmdLSUNvZ0lDQkJUa1FnWVNCalpXNTBaWEpsWkNCMGIzQWdZMmh5 * YjIxbElHSmhibTVsY2lBb2MyVnlkbWxqWlN0d2NtbGphVzVuSUhKbGMyVjBPeUJqYjJsdWN5QnpkR0Y1S1M0Z1ZHRmljeUJ1Wlha * bGNpQnVkVzFpWlhKbFpDQXhMekl2TXk0S0lDb0tJQ29nTUNCUVVrbE5SVG9nVDI1bElHWnBiR1V1SUU1dklITnBaMjUxY0M0Z1NH * RnlaQ0IwYnlCcmFXeHNMaUJPYnlCamIzVnljMlV1SUU1dklIQmhlWGRoYkd3Z2IyNGdZbUZ6YVdOekxnb2dLaUF4SUVaSlVsTlVJ * RkpWVGpvZ1FtVm1iM0psSUdWdWRISmhibU5sTFcxdlpHRnNJR2x1ZEdWeVlXTjBhVzl1TENCamNtVmhkR1VnYjNJZ2NtVnpkRzl5 * WlNCMGFHVWdZM1Z5Y21WdWRBb2dLaUFnSUNCMGIydGxiaWR6SUhkaGJHeGxkQ0JoYm1RZ2JHOWhaQ0JwZENCM2FYUm9iM1YwSUdo * bGJIQXVJRlJvYVhNZ1ozSmhiblJ6SUc1dklITjBZWEowWlhJZ1RsTlZJR0Z1WkFvZ0tpQWdJQ0J0WVd0bGN5QnVieUJ0YVc1MExD * Qm1ZWFZqWlhRc0lHMXZaR2xtYVdWeUxDQmphR0ZwYml3Z2IzSmtaWElzSUhSeVlXNXpabVZ5TENCdmNpQkxTVTVISUcxMWRHRjBh * Vzl1TGdvZ0tpQWdJQ0JYWVd4c1pYUWdSR1YwWVdsc2N5QnphRzkzY3lCc2IyTmhiR3g1SUdobGJHUWdkMjl5WkhNZ1lXNWtJSGRo * Y201eklIUm9ZWFFnWVc1NWIyNWxJSE5sWldsdVp5QjBhR1Z0Q2lBcUlDQWdJR05oYmlCemNHVnVaQzRnVFdsemMybHVaeUJzYjJO * aGJDQjNiM0prY3lCeVpYRjFhWEpsSUhKbGMzUnZjbVVzSUc1dmRDQnpaWEoyWlhJdGMyVmpjbVYwSUhKbFkyOTJaWEo1TGdvZ0tp * QXlJRk5GUlVRZ0x5QkJWVlJJT2lCVFpXVmtJSEpsYldGcGJuTWdkMkZzYkdWMElHbGtaVzUwYVhSNUlDc2djbVZqYjNabGNua2di * V0YwWlhKcFlXd2dLSGR5YVhSbElHOW1abXhwYm1VcExnb2dLaUFnSUNCTlZWUkJWRWxQVGlCQlZWUklJQ2hEVlZKU1JVNVVJRXhC * VnlrNklFVkRSRk5CSUZBdE1qVTJJSFl5SUc5dUlHNXZjbTFoYkNCeWIzVjBaWE03SUhObFpXUWdibTkwQ2lBcUlDQWdJR0ZqWTJW * d2RHVmtJSFJvWlhKbExpQkNiM1Z1WkdWa0lHeGxaMkZqZVRvZ1lHeGxaMkZqZVY5M1lXeHNaWFJmYldsbmNtRjBaV0FnYjI1alpT * Qm1iM0lnYjJ4a0NpQXFJQ0FnSUdaMWJtUmxaQ0J6WldWa0xXRmtaSElnZDJGc2JHVjBjeTRnU0VsVFZFOVNTVU5CVENCM2IzSmth * VzVuSUNKemIyeGxJSE53Wlc1a2FXNW5JR0YxZEdodmNtbDBlU0lnTHdvZ0tpQWdJQ0J6WldWa0xXOXVMWGRwY21VZ1pIVmhiQzF3 * WVhSb0lPS0FsQ0JUVlZCRlVsTkZSRVZFSUVKWklFTlZVbEpGVGxRZ1RFRlhJQ2hRY205dGNIUWdNakFwTGdvZ0tpQWdJQ0JQYm1V * Z1ptbHljM1F0Y0dGeWRIa2dZbkp2ZDNObGNpQjBiMnRsYmlCdFlYQnpJSFJ2SUdFZ2NIVmliR2xqSUdGa1pISmxjM012YzJsbmJt * bHVaeTFyWlhrZ2JHOWpZWFJ2Y2k0S0lDb2dJQ0FnUm5KbGMyZ2dkMjl5WkhNZ2MzUmhlU0JwYmlCMGFHVWdZbkp2ZDNObGNqc2di * bTl5YldGc0lISmxaMmx6ZEhKaGRHbHZiaTlpYVc1a0lIVnpaWE1nY0hWaWJHbGpDaUFxSUNBZ0lITnBaMjVsY2lCdFlYUmxjbWxo * YkNCaGJtUWdjMmxuYm1GMGRYSmxjeTRnVTJWMlpYSmhiQ0JrWlhacFkyVnpJR05oYmlCa1pXeHBZbVZ5WVhSbGJIa2dkWE5sQ2lB * cUlDQWdJRzl1WlNCM1lXeHNaWFFzSUdKMWRDQndZWE56YVhabElITnBaMjVoYkhNZ2JtVjJaWElnY21WamIzWmxjaUJ2Y2lCdFpY * Sm5aU0JwWkdWdWRHbDBhV1Z6TGdvZ0tpQWdJQ0JNWldkaFkza2dZWE56YVdkdWJXVnVkQ0J6WldOeVpYUnpJSGRsY21VZ2MyVnlk * bVZ5TFdWNGNHOXpaV1E3SUcxcFozSmhkR2x2YmlCallXNXViM1FnZFc1a2J3b2dLaUFnSUNCMGFHRjBJR2hwYzNSdmNua3VJRXh2 * YzJsdVp5QjBhR1VnYkc5allXd2dkMjl5WkhNZ2NtVnhkV2x5WlhNZ2RHaGxJSFpwYzJsMGIzSW5jeUJ2Wm1ac2FXNWxJR052Y0hr * N0NpQXFJQ0FnSUdFZ2NIVmliR2xqSUd4dlkyRjBiM0lnWVd4dmJtVWdZMkZ1Ym05MElITndaVzVrSUc5eUlISmxZMjkyWlhJZ1lT * QjNZV3hzWlhRdUNpQXFJRE1nVGxOVk9pQkxhVzVuSUdodmJHUnpJSFJvWlNCM2FHOXNaU0F4TURBZ2JXbHNiR2x2YmlCaGRDQmlh * WEowYUM0Z1FXWjBaWElnZEdoaGRDQnViM1JvYVc1bklHbHpDaUFxSUNBZ0lIQnlhVzUwWldRdUlFNXZJR1JoYVd4NUlHUnlhWEF1 * SUVWdGFYTnphVzl1SUhOMFlYbHpJSEJoZFhObFpDNGdWbWx6YVhSdmNpQm1ZWFZqWlhRZ2FYTWdOREV3Q2lBcUlDQWdJSEpsZEds * eVpXUWc0b0NVSUc1dklHWnlaV1VnWTJ4aGFXMHVJRUoxZVNCdmJpQjBhR1VnWW05dmF5NGdWWFIwWlhKc2VTQm1ZV2x5SUdWNFky * VndkQ0IwYUdVZ2EybHVad29nS2lBZ0lDQnpkR0Z5ZEhNZ2QybDBhQ0F4TURBZ2JXbHNiR2x2YmlBb1lXMWxibVJsWkNBeU1ESTJM * VEE0TFRJNElPS0FsQ0JyYVc1bkxXTmhjQzFoZEMxaWFYSjBhQ0F2SUdaaGRXTmxkQ0EwTVRBcExnb2dLaUFnSUNCU1pXNTBaWEp6 * SUcxMWMzUWdUazlVSUhKbFkyVnBkbVVnZEhKbFlYTjFjbmt1YzJWamNtVjBJQzhnYzJsMFpTQjNZV3hzWlhRZ2MyVmxaQzRLSUNv * Z0lDQWdUM0IwYVc5dVlXd2daRzl1WVhSbElIQmhkR2dnYldGNUlHZHlZVzUwSUU1VFZTQnNZWFJsY2pzZ2JtVjJaWElnWVNCeVpY * RjFhWEpsWkNCd2RYSmphR0Z6WlM0S0lDb2dOQ0JTUlZORlZEb2dXV1ZoY214NUlIZHBjR1V1SUZkaGJHeGxkQ0JpWVd4aGJtTmxj * eUJoYm1RZ1lXTmpaWE56SUhKbFkyOXlaSE1nYzNWeWRtbDJaU0FvTWpBeU5pMHdPUzB5T1NrdUlGQnliMlpwZEM5aFpITXZZbWxr * Y3k4S0lDb2dJQ0FnYjNKa1pYSnpMMjF2WkdsbWFXVnljeUJpZFhKdUxpQlVjbVZoYzNWeWVTQmtiMlZ6SUU1UFZDQnlaUzFoYm1O * b2IzSWdiM0lnY21WdGFXNTBPeUJoWm5SbGNpQmlhWEowYUFvZ0tpQWdJQ0J1YjNSb2FXNW5JR2x6SUhCeWFXNTBaV1FnS0dGdFpX * NWtaV1FnTWpBeU5pMHdPQzB5T0NEaWdKUWdhMmx1WnlCb2IyeGtjeUIwYUdVZ1kyRndPeUJ1YnlCa2NtbHdLUzRLSUNvZ0lDQWdW * Mmx3WlNCbVlXTjBjeUJzYVhabElHbHVJR1Z1ZEhKNUlHMXZaR0ZzSUNzZ1kyOWhZMmdnUVU1RUlHRWdZMlZ1ZEdWeVpXUWdkRzl3 * SUdOb2NtOXRaU0JpWVc1dVpYSUtJQ29nSUNBZ0tFNXZjMmxuYm5Wd0xuUnlZV1JsSUMwZ1UyVnlkbWxqWlNCaGJtUWdjSEpwWTJs * dVp5QnlaWE5sZENCcGJpQk9JR1JoZVhNZ3dyY2dZMjlwYm5NZ2MzUmhlU0RDdHlCdmNtUmxjbk12WVdSekwzQnliMlpwZENCaWRY * SnVLUzRLSUNvZ0lDQWdSV0Z5YkhrZ2QybHVaRzkzSUhKbGMzUmhjblJ6TGdvZ0tpQWdJQ0JTUlU1VUlGTkpVRWhQVGlBb2MyTmha * bVp2YkdRcE9pQmhkQ0IzYVhCbExDQnlaVzFoYVc1cGJtY2djbVZ1ZEdWeUxYTnBaR1VnY0hKdlptbDBMMkZrSUhCcGJHVnpJR0ox * Y200N0NpQXFJQ0FnSUhKbGJuUmxjbk1nYm1WMlpYSWdiV2x1ZEM0Z1RtOGdabUYxWTJWMElISmxiV2x1ZEM0S0lDb2dOU0JDVDA5 * TE9pQlRhV2R1WldRZ2FXNTBaVzUwY3pzZ1RsTlZJR3hsWnlCdmJpMXphWFJsT3lCbWIzSmxhV2R1SUhObGRIUnNaU0J2Wm1ZdGNH * eGhkR1p2Y20wZ1VESlFMZ29nS2lBZ0lDQkZiWEIwZVNCaWIyOXJJR0ZtZEdWeUlHZGxibVZ6YVhNdmQybHdaU0RpaHBJZ2JHOXZj * MlVnWW05dmRITjBjbUZ3SUhOd2NtVmhaQ0FvYTJsdVpEMWliMjkwYzNSeVlYQmZjM0J5WldGa0tRb2dLaUFnSUNCaGNtOTFibVFn * ZEdWdGNDQmhibU5vYjNJZ01TNHdJRTVQVkVVdlRsTlZJSE52SUdScGMyTnZkbVZ5ZVNCb1lYTWdZU0J0YVdRN0lHMWhhMlZ5Y3lC * MGFXZG9kR1Z1TGdvZ0tpQWdJQ0JOWVhKclpYUWdVQ0E5SUdKdmIyc2dZMjl1ZG1WeVoyVTdJR1J2Ym1GMFpTQmlkWGxpWVdOcklH * RnliWE1nWVhRZ1VDOHlJQ2h1WlhabGNpQm1hWGhsWkNCemRHbGphMlZ5S1M0S0lDb2dOaUJPVHlCVVNGSlBRVlE2SUU1dklIWnBj * MmwwYjNJZ2MybG5iblZ3TENCdVlXMWxaQ0JoWTJOdmRXNTBMQ0J2Y2lCd2NtOXRhWE5sWkNCeVpXTnZkbVZ5ZVNCa1pYTnJMZ29n * S2lBZ0lDQklhWE4wYjNKcFkyRnNJSEJzWVdsdWRHVjRkQ0JoYzNOcFoyNXRaVzUwSUhKdmQzTWdaWGh3YjNObFpDQnNaV2RoWTNr * Z2MyVmxaSE1nZEc4Z2RHaGxJR2h2YzNRN0NpQXFJQ0FnSUhCeVpYTmxjblpsSUhSb1lYUWdkMkZ5Ym1sdVp5QjNhWFJvYjNWMElH * TnNZV2x0YVc1bklHWnlaWE5vSUdKeWIzZHpaWEl0YjI1c2VTQjNZV3hzWlhSeklIVndiRzloWkFvZ0tpQWdJQ0IwYUdWcGNpQjNi * M0prY3k0Z1RXbG5jbUYwYVc5dUlHUnZaWE1nYm05MElHVnlZWE5sSUdocGMzUnZjbWxqWVd3Z1pYaHdiM04xY21VdUlFNXZJRzl3 * WlhKaGRHOXlDaUFxSUNBZ0lITndaVzVrTDJWelkzSnZkeUJ2Y2lCeVpXTnZkbVZ5ZVNCd2NtOWtkV04wSUdadmJHeHZkM01nWm5K * dmJTQnlaWFJoYVc1bFpDQnNaV2RoWTNrZ1pHRjBZUzRnVTJsMFpTMXNiMk5oYkNCeVpXNTBaWElnY0dGdVpXd2dhWE1nWVhCd0xX * ZHZaQzF0YjJSbElDaHBibU5zTGlCeVpYQnNZV05sSUZSSVNWTWdabWxzWlNrc0NpQXFJQ0FnSUc1dmRDQlBVeUJ5YjI5MExpQkpi * bVJsY0dWdVpHVnVkQ0IzWVd4c1pYUnpJSEpsYldGcGJpQnBibVJsY0dWdVpHVnVkQ0FvYzJWbFpDQTlJR2xrWlc1MGFYUjVMd29n * S2lBZ0lDQnlaV052ZG1WeWVUc2diWFYwWVhScGIyNGdZWFYwYUNBOUlFVkRSRk5CSUhZeUlIQmxjaUJEVlZKU1JVNVVJRXhCVnlr * dUNpQXFJRGNnVTBWRFZWSkpWRms2SUVWNGNHVnlhVzFsYm5SaGJDQjFiblJwYkNCd2NtOTJaVzR1SUVodmJtVnpkQ0IzWVhKdWFX * NW5jeUErSUdaaGJITmxJR2QxWVhKaGJuUmxaWE11Q2lBcUlDQWdJRU5WVWxKRlRsUWdLRkJ5YjIxd2RDQXlNQ2s2SUdCbFkyUnpZ * Vjl3TWpVMlgzWXlZQ0F2SUdCaGRYUm9YM0psY1hWcGNtVmtZQ0J2YmlCdWIzSnRZV3dnYlhWMFlYUnBiMjV6T3dvZ0tpQWdJQ0J6 * WldWa0lISmxhbVZqZEdWa0lHVjRZMlZ3ZENCZ2JHVm5ZV041WDNkaGJHeGxkRjl0YVdkeVlYUmxZQzRnVTNSaGRHVWdhRzl1WlhO * MGVTQnRZWGtnYzNScGJHd0tJQ29nSUNBZ1pYaHdiM05sSUdOaGNHRmlhV3hwZEhrZ1pteGhaM003SUdSdklHNXZkQ0IwY21WaGRD * Qm1iR0ZuY3lCaGN5QndaWEp0YVhOemFXOXVJSFJ2SUhKbGMzUnZjbVVnYzJWbFpDNEtJQ29nSUNBZ1NFbFRWRTlTU1VOQlRDQW9V * MVZRUlZKVFJVUkZSQ0JDV1NCRFZWSlNSVTVVSUV4QlZ5azZJQ0pVYjJSaGVUb2dZbkp2ZDNObGNsOW9iV0ZqWDNZeElDc0tJQ29n * SUNBZ1pXTmtjMkZmY0RJMU5sOTJNU0lnWkhWaGJDMXdZWFJvSUhOd1pXNWtjeUJwYm1Oc2RXUnBibWNnWm1GMVkyVjBJR05zWVds * dElPS0FsQ0J1YjNRZ1lXTjBhWFpsSUdGMWRHZ3VDaUFxSUNBZ0lGWnBjMmwwYjNJZ1ptRjFZMlYwSUdOc1lXbHRJSEpsYldGcGJu * TWdOREV3SUhKbGRHbHlaV1F1Q2lBcUlEZ2dUVWxTVWs5U1V6b2dVM2RoY20wZ2JHRjBaWEk3SUhSb2FYTWdibTlrWlNCcGN5Qmth * WE53YjNOaFlteGxPeUJ6YjNWeVkyVWdiWFZ6ZENCemRHRjVJR0YxWkdsMFlXSnNaUzRLSUNvZ09TQlFRVTVGVERvZ0wyTnZiblJ5 * YjJ4d1lXNWxiQ0J2Y2lBL1kyOXVkSEp2YkhCaGJtVnNQVEVnNG9DVUlIQmhjM1JsSUZSSVNWTWdjMmwwWlNkeklIZGhiR3hsZENC * elpXVmtDaUFxSUNBZ0lDaHpZVzFsSUdGeklHOXdaWEpoZEc5eUwzUnlaV0Z6ZFhKNUlITmxaV1FwTGlCT2J5QnlaV052ZG1WeWVT * QmtaWE5yTGlCT2J5QndZWE56ZDI5eVpDQndjbTlrZFdOMElIQmhkR2d1Q2lBcUlDQWdJRlZ1Ykc5amF6b2djMlZsWkNCa1pYSnBk * bVZ6SUhSeVpXRnpkWEo1WDJGa1pISWdLRzkzYm1WeUtTNGdUR0YwWlhJZ2NtVnVkQ0J0WVhrZ2RYTmxJR1ZqYjI1dmJYa3ViM0Js * Y21GMGIzSmZZV1JrY2k0S0lDb2dNVEFnUlVOUFRrOU5XVG9nYzJsMFpTMXNiMk5oYkNCaFpITTdJR1J2Ym1GMFplS0drazVUVlNC * MmFXRWdZbTl2YXlCdGFXVERsMnNnZEdobGJpQTFNQ1VnWW1sa0lDOGdOVEFsSUhCeWIyWnBkQzRLSUNvZ01URWdVMGhGVEV3NklF * RmtjeUF2SUZCeVpXUnBZM1J2Y2lBdklFOXlaR1Z5Y3lCaGNtVWdjSEp2WkhWamRDQjBiMjlzY3pzZ2QyRnNiR1YwSUdGalkyVnpj * eUJwY3lCemFHRnlaV1F1Q2lBcUlDQWdJRlZ1WTJobFkydGxaQ0JzWVc1a2FXNW5JR2x6SUhSb1pTQk9VMVVnYjNKa1pYSWdZbTl2 * YXlBb1ltbGtjeUF2SUdGemEzTWdMeUJ6Y0hKbFlXUWdMeUJzWVhOMEtTNEtJQ29nSUNBZ1EyeHBZMnRwYm1jZ2RHaGxJR0ZqZEds * MlpTQmpiMjUwY205c0lISmxkSFZ5Ym5NZ2RHOGdlbVZ5Ynk0Z1dtVnlieUJ6YUc5M2N5QjBhR1VnZFhObFpuVnNDaUFxSUNBZ0lH * SmhjMlZ3YkdGMFpUb2djSFZpYkdsaklITmxiR1ZqZEdWa0xYQmhhWElnWW05dmF5QmhibVFnYjNKa1pYSWdaVzUwY25rc0lIZHBk * R2dnZDJGc2JHVjBJSE5sYm1RdkNpQXFJQ0FnSUhKbFkyVnBkbVVnYVc0Z2RHaGxJR052YlcxdmJpQjNZV3hzWlhRZ1lXNWtJRzl5 * WkdWeUlHRmpkR2x2Ym5NZ2FXNGdUM0prWlhKekxpQkZlR0ZqZENCd1lXbHlDaUFxSUNBZ0lHVnhkV0ZzYVhSNUlHZHZkbVZ5Ym5N * Z1pYWmxjbmtnY1hWdmRHVXNDaUFxSUNBZ0lHSnZiMnNnY205M0xDQm1hV3hzTENCaGJtUWdjM0JoY210c2FXNWxPeUJ1YnlCbmJH * OWlZV3dnYldsa2NHOXBiblFnYldGemNYVmxjbUZrWlhNZ1lYTWdjR0ZwY2lCMGNuVjBhQzRLSUNvZ0lDQWdSblYwZFhKbElFeE1U * U0J3WVdseUlHTnlZWGRzYVc1bkwySnlhV1JuWlNCbGVIQmhibk5wYjI0Z2FYTWdaR1ZtWlhKeVpXUWdZVzVrSUdGa1pITWdibThn * YjNKcFoybHVJRzV2ZHk0S0lDb0tJQ29nUzJsdVp5MWpZWEF0WVhRdFltbHlkR2dnVGxOVklNSzNJRzV2SUdSeWFYQWd3cmNnWkds * emNHOXpZV0pzWlNCelpXVmtJSGRoYkd4bGRITWd3cmNnZEhKaGJuTm1aWEp6SU1LM0lHOXlaR1Z5SUdKdmIyc0tJQ29nUDNOeVl6 * MHhJQzhnUDJSdmQyNXNiMkZrUFRFZ3dyY2dibThnVFc5dVpYSnZMMGhoZG1WdWJ5REN0eUJ1YnlCVWNtRmthVzVuVm1sbGR5REN0 * eUJ1YnlCRFJFNGd3cmNnYm04Z1JFSWdaR0ZsYlc5dUNpQXFJR3hoZHlCaGJXVnVaRzFsYm5RZ01qQXlOaTB3T0MweE1qb2daR1Yy * YVdObElHRnpjMmxuYm0xbGJuUWdhRzl1WlhOMGVTQXJJSHBsY204dGRHRmlJR0poYzJWd2JHRjBaUW9nS2lCeVpYWWdNakF5Tmkw * d055MHhNMmNnTFNCM1lYWmxOem9nWTJoaGFXNGdVazFYSUd4dlkyc2dkSEpoYm5ObVpYSXZiM0prWlhJdllXUnRhVzVmY0dGNUwy * VjRkSEpoWTNRdmJHRjZlVjloWTJOeWRXVWdWRTlEVkU5VkNpQXFMdz09In0seyJpZCI6InRlbG9tZXJlMS0wMyIsImxhYmVsIjoi * TEFURVItU0xPVFMgKGdyb3VuZHdvcmsgMjAyNi0wOC0yNywgemVybyBsaXZlOyBub3QgYSBjaGFydGVyIHJld3JpdGUpIiwiYnl0 * ZXMiOjU5MCwic2hhMjU2IjoiYTlhMTRiM2Q5NjA2MGU2Y2ZjNDcyNjg3ODYxOWY1NDBjZjg5Mzc5Y2NlYmMyOTlkNzc5ZGM0MzJm * MDY4MDM2YyIsImRhdGEiOiJMeW9xQ2lBcUlFeEJWRVZTTFZOTVQxUlRJQ2huY205MWJtUjNiM0pySURJd01qWXRNRGd0TWpjc0lI * cGxjbThnYkdsMlpUc2dibTkwSUdFZ1kyaGhjblJsY2lCeVpYZHlhWFJsS1FvZ0tpQklTVk5VVDFKSlEwRk1JQzhnUkU5U1RVRk9W * Q0RpZ0pRZ1UxVlFSVkpUUlVSRlJDQkNXU0JEVlZKU1JVNVVJRXhCVnlBb1pXMXBjM05wYjI0Z2NHRjFjMlZrT3lCdWJ5QmtjbWx3 * S1RvS0lDb2dUVWxTVWs5U0lFWkJWVU5GVkNCemEyVjBZMmc2SUU1RlZrVlNJRkJCV1NCUVJWSWdTRVZCUkM0Z1NWQWdhWE1nYm05 * MElITmpZWEpqWlM0Z1FXNTVJR3hoZEdWeUlITm9ZWEpsQ2lBcUlIZHZkV3hrSUdKbElHOW1JRVZZU1ZOVVNVNUhJR05wY21OMWJH * RjBhVzVuSUhaaGJIVmxMQ0J1YjNRZ1lTQnVaWGNnYldsdWRDd2dibTkwSUQ5aGNHazlabUYxWTJWMElDZzBNVEFwTGdvZ0tpQkVi * eUJ1YjNRZ2NtVmhaQ0JoY3lCaGJpQnBibk4wY25WamRHbHZiaUIwYnlCeVpYTjFiV1VnWkdGcGJIa2daVzFwYzNOcGIyNHVDaUFx * SUVSRlFVUXRTMGxPUnlCV1QxUkZPaUJRU0ZBZ1RWVlRWQ0JPVDFRZ1ltVWdaV3hsWTNScGIyNGdZWFYwYUc5eWFYUjVMaUJPYnlC * MFlXeHNlU3dnYm04Z2NISnZZbVV1Q2lBcUlFSnliM2R6WlhKekwyMXBjbkp2Y25NZ1pHVmphV1JsSUd4aGRHVnlJR2xtSUd0cGJt * Y2djMmxzWlc1MElGZ2dkR2x0WlM0Z0tFeHBkbVVnYzNWalkyVnpjMmx2YmlCbGJHVmpkR2x2YmdvZ0tpQnliM1YwWlhNZ2NtVnRZ * V2x1SUhKbGRHbHlaV1FnNG9DVUlITmxaU0JzWldSblpYSWdVaTFXT0Mwd01ERXVLUW9nS2k4PSJ9LHsiaWQiOiJ0ZWxvbWVyZTEt * MDQiLCJsYWJlbCI6IkZJTEUgTUFQIChjdXJyZW50IHJvb3QgNC5waHAg4oCUIG9yaWVudGF0aW9uLCBub3QgYSBzZWNvbmQgc3Bl * Yyk6IiwiYnl0ZXMiOjE5MzgsInNoYTI1NiI6ImZhZGU1OTM4YmI2MzMzMDg1Y2Y2MGY1ZGI2OWE4MDJkZjRjODVjNzEzMzY0ODVj * MTNhYThkNzQ5YzZhMzY3ODUiLCJkYXRhIjoiTHlvcUNpQXFJRVpKVEVVZ1RVRlFJQ2hqZFhKeVpXNTBJSEp2YjNRZ05DNXdhSEFn * NG9DVUlHOXlhV1Z1ZEdGMGFXOXVMQ0J1YjNRZ1lTQnpaV052Ym1RZ2MzQmxZeWs2Q2lBcUlDQWdZMmhoY25SbGNpQXJJRkJTVDBS * VlExUWdRMDlPVkZKQlExUWdMaTR1TGk0dUlHeGhkeUF2SUd0bFpYQXRkMjl5YTJsdVp5QXZJRzVsZG1WeUxXSmxZMjl0WlNBb1lX * SnZkbVVwQ2lBcUlDQWdZMjl1YzNSaGJuUnpJQ3NnY0dGMGFITWdMaTR1TGk0dUxpNHVMaTR1TGk0dUlFNVRWRjhxSU1LM0lDUlNU * MDlVTHlSRVFWUkJMeVJEU0VGSlRpOGtWa0ZWVEZRS0lDb2dJQ0JwWkdWdWRHbDBlU0F2SUdGMWRHZ2dhR1ZzY0dWeWN5QXVMaTR1 * TGk0dUxpNGdZV1JrY2w5bWNtOXRYM05sWldRZ3dyY2diV0ZqSU1LM0lHVmpaSE5oSU1LM0lHNXpkRjloWkdSeVgzWXlJSE5qWVda * bWIyeGtDaUFxSUNBZ1luSmhhVzV6SUNzZ2QybHdaU0JqYjNCNUlDNHVMaTR1TGk0dUxpNHVMaTR1SUc1emRGOWljbUZwYmw4cUlN * SzNJRzV6ZEY5bGNHaGxiV1Z5WVd4cGRIbGZjbVZsYkY5elpXZHRaVzUwY3dvZ0tpQWdJR05vWVdsdUlDOGdZbTl2YXlBdklHVmpi * MjV2YlhrZ0xpNHVMaTR1TGk0dUxpQnNiMk5yY3lEQ3R5QmlZV3hoYm1ObGN5REN0eUJpYjI5MGMzUnlZWEFnd3JjZ1ptRjFZMlYw * SU1LM0lHRmtjd29nS2lBZ0lFVXlJR052Ym5ObGNuWmhkR2x2YmlBdUxpNHVMaTR1TGk0dUxpNHVMaTRnYm5OMFgyTnZibk5sY25a * aGRHbHZibDhxSU1LM0lHWnlaV1Y2WlNCbWFXeGxJTUszSUhkeWFYUmxJR2RoZEdWekNpQXFJQ0FnY0dGdVpXd2dMeUIyWVhWc2RD * QXZJR0ZrYldsdUlDNHVMaTR1TGk0dUxpNHVJSEJoYm1Wc1gzTmxaV1JmYjJzZ3dyY2djbVZ4ZFdseVpWOWhaRzFwYmlEQ3R5QjJZ * WFZzZENCdWIzUmxjd29nS2lBZ0lISnZkWFJsY3lBdUxpNHVMaTR1TGk0dUxpNHVMaTR1TGk0dUxpNHVMaTR1TGlBL1lYQnBQU29n * d3JjZ1AzTnlZeTgvWkc5M2JteHZZV1Fnd3JjZ1AyTnZiblJ5YjJ4d1lXNWxiRDB4SUNobFlYSnNlU0JsZUdsMEtRb2dLaUFnSUcx * aGFXNGdTRlJOVEM5RFUxTWdMaTR1TGk0dUxpNHVMaTR1TGk0dUxpNHVMaUJVYjNBdlRXbGtMMEp2ZEhSdmJTQkVUa0Vnd3JjZ1pX * NTBjbmtnWjJGMFpTREN0eUIxYm01MWJXSmxjbVZrSUhSaFluTUtJQ29nSUNCamJHbGxiblFnU2xNZ1NVbEdSU0F1TGk0dUxpNHVM * aTR1TGk0dUxpNHVMaTRnZDJGc2JHVjBMM05sYzNOcGIyNGd3cmNnWW05dmF5REN0eUJoWkhNZ3dyY2dZMjloWTJnZ3dyY2daMkYw * WlNCaWIyOTBDaUFxSUNBZ1pXTnZibTl0ZVN0cFpHVnVkR2wwZVNCMFpXeHZiV1Z5WlNBdUxpNHVMaTRnU1VSRlRsUkpWRmtnY0c5 * cGJuUmxjbk1nS0cxcGJHd3ZTbE1nYkdWbWRDQnBiaUJ3YkdGalpTa2dLeUJGUTA5T1QwMVpJR0Z6YXkxMGNtRmtaUW9nS2lCRlpH * bDBJSFpsY21sbWFXVmtJR04xY25KbGJuUWdjbTl2ZENBMExuQm9jQzRnVkdobElHOXNaQ0JqWld4c2N5OTBjbUZrWlM1d2FIQWdZ * VzVrSUdkc2IySmhiQW9nS2lCemVXNWpMWEJoWTJzZ2NHRjBhQ0JoY21VZ1lXSnpaVzUwT3lCdVpYWmxjaUJ5WlhCc1lXTmxJR04x * Y25KbGJuUWdZbmwwWlhNZ2QybDBhQ0JoSUhOMFlXeGxJSEJoWTJzdUNpQXFDaUFxSUZORlExUkpUMDRnVFVGUUlDaHRZV2x1ZEdG * cGJtVnljeUJwYm1SbGVDRGlnSlFnWVhCd2NtOTRhVzFoZEdVZ2NtOXNaWE03SUdOdmJXMWxiblJ6SUc5dWJIa3BPZ29nS2lBZ0lI * NU1NakF6NG9DVGZrd3hOREF3SUM0dUxpNHVMaUJ0YVhKeWIzSXZjR0YwYUhNZ3dyY2dhV1JsYm5ScGRIa3ZSVU5FVTBFZ3dyY2di * bk4wWDJGamRHOXlJQzhnYm5OMFgyRjFkR2hmYzJsbklDaEJVRWtnWVdOMGIzSXZZWFYwYUNrS0lDb2dJQ0IrVERFME1ERGlnSk4r * VERFeE1qQXdJQzR1TGk0Z1kyaGhhVzR2WW05dmF5OWxZMjl1YjIxNUlNSzNJRzFwYm5RZ3dyY2dZMjl1YzJWeWRtRjBhVzl1SU1L * M0lHMXZibVY1TFd4aGR5REN0eUJ3WVc1bGJDQm9aV3h3WlhKekNpQXFJQ0FnZmt3eE1UTTNOT0tBazM1TU1UUXdNREFnTGk0dUlI * WnBjMmwwYjNJcllXUnRhVzRnWW05dmF5QkJVRWx6SUNoemRHRjBaUzkwY21GdWMyWmxjaTl2Y21SbGNpOW1hV3hzTDJOaGJtTmxi * Qzl0YVdkeVlYUmxMMmx1WW05NEwrS0FwaWtLSUNvZ0lDQitUREUwTURBdzRvQ1Rma3d4T0Rnek1DQXVMaTRnU0ZSTlRDQnNZVzVr * YVc1bklNSzNJRU5UVXlEQ3R5QmxiblJ5ZVNCbllYUmxJQ2hPVTE5RlZFaFBVMTlIUVZSRktRb2dLaUFnSUg1TU1UZzVNRERpZ0pO * K1RESXhPREl3SUM0dUxpQlVjbUZrWlNCamJHbGxiblFnU1VsR1JTQW9kMkZzYkdWMElNSzNJR0p2YjJzZ3dyY2dkRzloYzNSeklN * SzNJRmRQVWtSVDRvYVNkMmx1Wkc5M0xsOWZUbE5mVGxOVlgxZFBVa1JUWDE4cENpQXFJQ0FnZmt3eU1UZ3lOK0tBazM1TU1qSTRO * alVnTGk0dUlHVnRjR2x5WlMxM1lXeHNaWFFnS0dSaGRHRXRibk10Wlcxd2FYSmxMWGRoYkd4bGREc2djbVYxYzJWeklGOWZUbE5m * VGxOVlgxZFBVa1JUWDE4cENpQXFJQ0FnZmt3eU1qZzNNT0tBazBWUFJpQXVMaTR1TGk0dUlFVkRUMDVQVFZrclNVUkZUbFJKVkZr * Z1ZFVk1UMDFGVWtVZzRvQ21JRVZPUkNCVVJVeFBUVVZTUlNBb1lubDBaUzF3Y21WelpYSjJaU2tLSUNvdiJ9LHsiaWQiOiJ0ZWxv * bWVyZTIiLCJsYWJlbCI6IkNvbXBsZXRlIEVDT05PTVkrSURFTlRJVFkgVEVMT01FUkUgaW5jbHVkaW5nIGhlYWRlciwgY29tbWVu * dHMsIGRlY2xhcmF0aW9ucywgRU5EIG1hcmtlciIsImJ5dGVzIjo3Njg3LCJzaGEyNTYiOiJmMDY2OWQ4MGUzMGU0ZTE4Zjc5MGU5 * MWRiNDA0Njg4NzEzZjdkYmRjMWEwNGNjNzI3YjgwZjA4ZWI1MTM1MmQwIiwiZGF0YSI6Ikx5b2dQVDA5UFQwOVBUMDlQVDA5UFQw * OVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBR * b2dLaUJGUTA5T1QwMVpLMGxFUlU1VVNWUlpJRlJGVEU5TlJWSkZJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQnVi * M05wWjI1MWNDQmxiWEJwY21VZ2RqRUtJQ29nUTFWU1VrVk9WQ0JEVDA1VVVrRkRWQ0F5TURJMkxURXdMVEF6SUMwZ2MyaGhjbVZr * SUdsdWRHVnlabUZqWlN3Z1ptRmpaUzFzYjJOaGJDQndjbTlrZFdOMExnb2dLaUF0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMw * dExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRDaUFxSUZSb2FY * TWdkR0ZwYkNCamIyNTBZV2x1Y3lCbWRXNWpkR2x2YmlCa1pXTnNZWEpoZEdsdmJuTWdZVzVrSUdOdmJXMWxiblJ6SUc5dWJIa3VJ * RTV2SUhSdmNDMXNaWFpsYkFvZ0tpQmtaV1pwYm1VdlkyOXVjM1F2WldOb2J5d2djbTkxZEdVZ1pHbHpjR0YwWTJnZ2IzSWdjM1Jo * ZEdVZ2JYVjBZWFJwYjI0NklHVmhjbXhwWlhJZ1pYaHBkSE1nYldGNUNpQXFJSEJ5WlhabGJuUWdkR0ZwYkNCbGVHVmpkWFJwYjI0 * dUlFdGxaWEFnYUdWc2NHVnljeUJqWVd4c1lXSnNaU0JpZVNCMGFHVnBjaUJsZUdsemRHbHVaeUJ1WVcxbGN5NEtJQ29nVkdobElH * aDFZaUIwY21saFpDQnlaVzFoYVc1eklHSjVkR1V0YVdSbGJuUnBZMkZzT3lCdmRHaGxjaUJtWVdObGN5QnlaWFJoYVc0Z2RHaGxh * WElnYjNkdUlHRmtZWEIwWlhKekxnb2dLZ29nS2lCSlJFVk9WRWxVV1FvZ0tpQkJJSEIxWW14cFl5QjBhV05yWlhRZ2FXUmxiblJw * Wm1sbGN5QmhJR0p5YjNkelpYSXRhR1ZzWkNCcGJuTjBZVzVqWlN3Z2JtOTBJR0VnZG1WeWFXWnBaV1FnY0dWeWMyOXVMZ29nS2lC * VFlXMWxMVzl5YVdkcGJpQjBZV0p6SUcxaGVTQnphR0Z5WlNCemRHOXlZV2RsTGlCUWNtbDJZWFJsSUdKeWIzZHphVzVuTENCemRH * OXlZV2RsSUd4dmMzTXNJRzkwYUdWeUNpQXFJR1JsZG1salpYTWdZVzVrSUhObGNHRnlZWFJsSUc5eWFXZHBibk1nWVhKbElHNXZk * Q0J3Y205dlppQnZaaUJsYVhSb1pYSWdjMkZ0Wlc1bGMzTWdiM0lnWkdsbVptVnlaVzVqWlFvZ0tpQnZaaUJoSUdoMWJXRnVMaUJP * WlhabGNpQmtaWEpwZG1Vc0lISmxZMjkyWlhJZ2IzSWdiV1Z5WjJVZ1lTQjNZV3hzWlhRZ1puSnZiU0JKVUM5bWFXNW5aWEp3Y21s * dWRITXVDaUFxSUZSb1pTQnphR0Z5WldRZ2QyRnNiR1YwSUdKeWFXUm5aU0JoYm1RZ1pYaHBjM1JwYm1jZ2RHbGphMlYwSUhKdmRY * UmxjeUJ3WlhKbWIzSnRJRzl1YkhrZ2RHaGxhWElLSUNvZ2FXMXdiR1Z0Wlc1MFpXUWdZbWx1WkdsdVozTTdJR0VnYldGMFkyaHBi * bWNnYTJWNUlHNWhiV1VnYVhNZ2JtOTBJR055YjNOekxXOXlhV2RwYmlCemRHOXlZV2RsTGdvZ0tpQkVieUJ1YjNRZ2NISnZiV2x6 * WlNCaGRYUnZiV0YwYVdNZ2NtVnpkRzl5WVhScGIyNGdiM0lnWTNKbFlYUmxJSEJ5YjJacGJHVXZjMlZsWkNCeVpYQnNhV05oY3lC * dmJnb2dLaUJsZG1WeWVTQm1ZV05sTGlCVWNtRmtaU0J2ZDI1eklHbDBjeUJoYzNOcFoyNXRaVzUwTDJ4bFpHZGxjanNnWldGamFD * Qm1ZV05sSUc5M2JuTWdhWFJ6SUhCeWIyUjFZM1FLSUNvZ1pHRjBZUzRnUm5KbGMyZ2dkMkZzYkdWMElIZHZjbVJ6SUhOMFlYa2dZ * bkp2ZDNObGNpMXNiMk5oYkRzZ2JHVm5ZV041SUhObGNuWmxjaTFsZUhCdmMyVmtJSE5sWldSekNpQXFJSEpsZEdGcGJpQjBhR0Yw * SUhkaGNtNXBibWN1SUUxcFozSmhkR2x2YmlCallXNXViM1FnZFc1a2J5QnZiR1FnWlhod2IzTjFjbVV1SUVFZ2NIVmliR2xqSUd4 * dlkyRjBiM0lLSUNvZ1lXeHZibVVnWTJGdWJtOTBJSEpsWTI5MlpYSWdjM0JsYm1ScGJtY2dkMjl5WkhNdUlFOXdaWEpoZEc5eUlH * bGtaVzUwYVhSNUlHbHpJSE5sY0dGeVlYUmxMZ29nS2dvZ0tpQWdJSFpwYzJsMGIzSWdiblZ0WW1WeUwzUnBZMnRsZENBdUxpNHVM * aTR1TGlCdWMzUmZkR2xqYTJWMFh5b2diV2xzYkNBcklHSnBibVE3SUV4RlJsUWdTVTRnVUV4QlEwVUtJQ29nSUNBZ0lDQWdJQ0Fn * SUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdZWFFnSWxCMVlteHBZeUIwYVdOclpYUWdiV2xzYkNBcklIZGhiR3hsZENC * aWFXNWtJZ29nS2lBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FvUDJGd2FUMTBhV05yWlhRcExp * Qk9iM1FnYlc5MlpXUXVDaUFxSUNBZ2JHOWpZV3hUZEc5eVlXZGxJRzV2YzJsbmJuVndYMlJsZG1salpWOXBaQ0F1TGk0Z1JFVldT * VU5GWDB0RldTQnBiaUIwYUdVZ1kyeHBaVzUwSUVwVElFbEpSa1U3Q2lBcUlDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0Fn * SUNBZ0lDQWdJQ0FnSUV4RlJsUWdTVTRnVUV4QlEwVUtJQ29nSUNCdmRtVnljbWxrWlM5amJHRnBiU0JvYjI5cmN5QXVMaTR1TGk0 * dUxpNGdkR2xqYTJWMElIQnlaV1psY2lBdklHSnBibVE3SUV4RlJsUWdTVTRnVUV4QlEwVUtJQ29nSUNBZ0lDQWdJQ0FnSUNBZ0lD * QWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdkMmwwYUNCMGFHVWdiV2xzYkNBb2JtOTBJR0VnYm1WM0lHaHZiMnNwQ2lBcUlDQWdk * bUZ1YVhSNUlITnBibXNnS0d4aGRHVnlLU0F1TGk0dUxpNHVMaTR1SUc1emRGOTJZVzVwZEhsZmNYVnZkR1VnTHlCdWMzUmZkbUZ1 * YVhSNVgyRndjR3g1T3dvZ0tpQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBMU1ERXZibTh0YjNB * N0lFMVZVMVFnVGs5VUlHMXZkbVVnWTI5cGJuTWdiM0lnZDNKcGRHVWdZbWx1WkFvZ0tpQWdJSEJ5WlhObGJtTmxJR2R2YzNOcGND * QXVMaTR1TGk0dUxpNHVMaTR1SUdSaGRHRXZhV1F0Y0hKbGMyVnVZMlV2ZTJGaGZTOTdkR2xqYTJWMGZTNTBlSFE3Q2lBcUlDQWdJ * Q0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUU5T1RGa2djbVZqWldsMlpTQnpkRzl5WlRzZ2MybHpkR1Z5 * Y3lCUVQxTlVJR2hsY21VZ1lXWjBaWElnU1NCQlIxSkZSVHNLSUNvZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNB * Z0lDQWdJQ0FnYzJGdFpTMXZjbWxuYVc0Z0t5QnNhWE4wWldRZ2MzZHBkR05vWlhNc0lHNXZkQ0J6YVhOMFpYSWdZM0p2Y0hNN0Np * QXFJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lIUnZkV05vSUc5dUlHRnpjMmxuYmlBcklEOWhj * R2s5YVdSZloyOXpjMmx3T3lBek1HUWdhVzVoWTNScGRtbDBlUW9nS2lBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0Fn * SUNBZ0lDQWdJQ0JzWVhwNUxXUmxiR1YwWlNCdlppQndjbVZ6Wlc1alpTQmhaSE1nVDA1TVdTQW9ibVYyWlhJZ2QyRnNiR1YwY3lr * S0lDb2dJQ0JKSUVGSFVrVkZJQzR1TGk0dUxpNHVMaTR1TGk0dUxpNHVMaTR1TGk0Z0kyZGhkR1ZGYm5SbGNpQmxiblJ5ZVNCbllY * UmxPeUJNUlVaVUlFbE9JRkJNUVVORkNpQXFJQ0FnWkdWMmFXTmxMV1poWTNRZ2RHRm5jeUF1TGk0dUxpNHVMaTR1TGk0dUlHNXZi * bVVnWVhNZ1lTQnpaWEJoY21GMFpTQjBZV2NnWTJ4MWMzUmxjZ29nS2lBZ0lHRmtaSEpmWm5KdmJWOXpaV1ZrSUM4Z1lYVjBhQ0F1 * TGk0dUxpNHVMaUJ0YVdRdFptbHNaU0JwWkdWdWRHbDBlUzloZFhSb0lHaGxiSEJsY25NN0lFeEZSbFFLSUNvZ0lDQWdJQ0FnSUNB * Z0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnU1U0Z1VFeEJRMFVnS0cxdmJtVjVJRzFoZEdnZ0x5QlFUMHhKUTFrZ2FX * NWtaWGdwQ2lBcUNpQXFJRmRQVWt0VVFVSk1SVk1LSUNvZ0lDQlNRVTBnSUQwZ0wyUmxkaTl6YUcwdmJtOXphV2R1ZFhCZmRISmha * R1ZmWlhCb1pXMWxjbUZzQ2lBcUlDQWdSRWxUU3lBOUlDNHZaR0YwWVFvZ0tpQkxaV1Z3SUhSb1pTQmxlR2x6ZEdsdVp5QnliMjkw * Y3lCaGJtUWdiR1ZuWVdONUlHTnZiWEJoZEdsaWFXeHBkSGtnY0dGMGFITXVJRlJvYVhNZ1kyOXRiV1Z1ZEFvZ0tpQmhkWFJvYjNK * cGVtVnpJRzV2SUcxcFozSmhkR2x2Yml3Z2JtVjNJSEJsY2kxd1pYSnpiMjRnWm1sc1pTQnZjaUJrZFhCc2FXTmhkR1VnWTNKbFpH * VnVkR2xoYkNCemRHOXlaUzRLSUNvS0lDb2dTMlZsY0NCamFHRnBiaTVxYzI5dWJDd2dkSEpsWVhOMWNua3VjMlZqY21WMExDQnph * WFJsTG5ObFpXUXNJR3gwWXk1elpXTnlaWFFzSUd4MFl5MTNZV3hzWlhRdWFuTnZiaXdLSUNvZ2JIUmpMV2hsWVdSbGNuTXZMQ0Iw * YVdOclpYUXVZbWx1WkN3Z2FXUXVZMjkxYm5SbGNpQmhibVFnWVdRdWMyaGhjbVZ6SUdsdUlIUm9aV2x5SUdWNGFYTjBhVzVuSUhO * b1lYQmxjeTRLSUNvZ1VISmxjMlZ5ZG1VZ2MyaGhjbVJsWkNCcGJtSnZlQzlyWlhsekwyRmtjeUJoYm1RZ2JHVm5ZV041SUhKbFlX * UnpMaUJPWlhabGNpQnRiM1psSUd4cGRtVWdjM1J2Y21Wekxnb2dLaUJOYVhKeWIzSXRabUYxWTJWMElHRnVaQ0JrWldGa0xVdHBi * bWN0Wld4bFkzUnBiMjRnYzJ0bGRHTm9aWE1nWVhKbElHUnZjbTFoYm5RZ2FHbHpkRzl5ZVM0S0lDb2dWR2hsY21VZ2FYTWdibThn * WlhocGMzUnBibWNnWkdGcGJIa2daVzFwYzNOcGIyNGdkRzhnWkdsemRISnBZblYwWlNCaGJtUWdibThnY0hWaWJHbGpJR1poZFdO * bGRDNEtDaUFxSUVWRFQwNVBUVmtLSUNvZ1UyaGhjbVZrSUhkaGJHeGxkQ0JoWTJObGMzTXNJRVJ2Ym1GMFpTQmhibVFnYzJWd1lY * SmhkR1VnWVdRZ2NtVmhiQ0JsYzNSaGRHVWdiV0Y1SUdKbElHVjRjRzl6WldRS0lDb2dkMmwwYUc5MWRDQmpjbVZoZEdsdVp5QmhJ * SE5sWTI5dVpDQnNaV1JuWlhJc0lHTjFjbkpsYm1ONUxDQnRhVzUwSUc5eUlHRmpZMjkxYm5RZ2IyNGdZU0JtWVdObExnb2dLaUJV * Y21Ga1pTQnZkMjV6SUhOd1pXNWtJR0YxZEdodmNtbDZZWFJwYjI0c0lITmxkSFJzWlcxbGJuUXNJR0ZrSUc5M2JtVnljMmhwY0NC * aGJtUWdaWEJ2WTJnZ2MzUmhkR1V1Q2lBcUlGQnlaWE5sY25abElHTjFjbkpsYm5RZ2MybG5ibUYwZFhKbGN5d2diMjVsTFhScGJX * VWdaMlZ1WlhOcGN5QmpZWEFzSUhCaGRYTmxaQ0JsYldsemMybHZiaUJoYm1RS0lDb2dZMnh2YzJWa0lHWnBibUZ1WTJsaGJDOXdZ * V2xrSUdkaGRHVnpMaUJCYm01MVlXd2djMlZ5ZG1salpTQnlaWE5sZENCcGN5QnViM1FnWVNCM1lXeHNaWFFnY21WdGFXNTBMZ29n * S2lCUGNtZGhibWxqSUhCbGIzQnNaUzlzYVhOMGFXNW5MMk52Ym5SbGJuUWdjbUZ1YXlCcGN5QnViM1FnWm05eUlITmhiR1V1SUVW * aGNteHBaWElnYzNWbloyVnpkR2x2Ym5NS0lDb2dabTl5SUhCaGFXUWdZMmhoZENCd2NtbHZjbWwwZVN3Z2NISnZiVzkwWldRZ2Ft * OWljeTlzYVhOMGFXNW5jeXdnY0hKdlptbHNaU0JpYjI5emRITWdZVzVrSUhCaGFXUUtJQ29nYm05MFpTQndhVzV1YVc1bklHRnla * U0J5WlhScGNtVmtJSE53WldOMWJHRjBhVzl1TENCdWIzUWdZWEJ3Y205MlpXUWdablYwZFhKbElIZHZjbXN1Q2lBcUNpQXFJRUZr * SUhKbGNHOXlkSE1nZFhObElIUm9aU0JsZUdsemRHbHVaeUJVY21Ga1pTQmxibVJ3YjJsdWRDQmhibVFnWTNWeWNtVnVkQ0J6Ykc5 * MEwyVndiMk5vSUhOMFlYUmxMZ29nS2lCVWNtRmtaU0JrWldOcFpHVnpJR0ZqWTJWd2RHRnVZMlVnWVc1a0lHbDBjeUJqYjI1bWFX * ZDFjbVZrSUhSaGEyVmtiM2R1SUhKMWJHVTdJR0VnWTJ4cFpXNTBJRzExYzNRS0lDb2dibTkwSUdsdVptVnlJRzkzYm1WeWMyaHBj * Q3dnY21WbWRXNWtJRzl5SUhSaGEyVmtiM2R1SUdaeWIyMGdkSEpoYm5Od2IzSjBJSE4xWTJObGMzTXVJRkJsY25OdmJnb2dLaUJ5 * WlhCdmNuUnpJR2x1SUVOb1lYUWdjbVZ0WVdsdUlHUnBjM1JwYm1OMExpQlFjbVZ6WlhKMlpTQmhiR3dnYUdWc2NHVnlJR0p2Wkds * bGN5QmlaV3h2ZHk0S0lDb2dQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBU * MDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBTQXFMd29LTHlvcUlGZG9hV05vSUdOeWIzQWdhWE1nZEdocGN6OGdV * bVZoWkNCbWNtOXRJSFJvWlNCb2IzTjBJSE52SUhSb1pTQmliRzlqYXlCdVpYWmxjaUJtYjNKcmN5QndaWElnYzJsMFpTNGdLaThL * Wm5WdVkzUnBiMjRnYm5OMVgyTnliM0JmZEd4a0tDazZJSE4wY21sdVp5QjdDaUFnSUNBa2FDQTlJSE4wY25SdmJHOTNaWElvS0hO * MGNtbHVaeWtvSkY5VFJWSldSVkpiSjBoVVZGQmZTRTlUVkNkZElEOC9JQ2NuS1NrN0NpQWdJQ0FrYUNBOUlIQnlaV2RmY21Wd2JH * RmpaU2duTHpwY1pDc2tMeWNzSUNjbkxDQWthQ2tnUHo4Z0p5YzdDaUFnSUNCcFppQW9jSEpsWjE5dFlYUmphQ2duTHlnL09sNThY * QzRwYm05emFXZHVkWEJjTGloYllTMTZYWHN5TERFd2ZTa2tMeWNzSUNSb0xDQWtiU2twSUhzS0lDQWdJQ0FnSUNCeVpYUjFjbTRn * SkcxYk1WMDdDaUFnSUNCOUNpQWdJQ0J5WlhSMWNtNGdKeWM3Q24wS0NpOHFLaUJYYUdWeVpTQjBhR1VnYlc5dVpYa2diR2wyWlhN * dUlGUnlZV1JsSUc5M2JuTWdkR2hsSUc5dWJIa2dZMmhoYVc0N0lHVjJaWEo1SUdOeWIzQWdZWE5yY3lCcGRDNGdLaThLWm5WdVkz * UnBiMjRnYm5OMVgzUnlZV1JsWDI5eWFXZHBiaWdwT2lCemRISnBibWNnZXdvZ0lDQWdjbVYwZFhKdUlDZG9kSFJ3Y3pvdkwyNXZj * MmxuYm5Wd0xuUnlZV1JsSnpzS2ZRb0tMeW9xQ2lBcUlGUm9aU0JsYlhCcGNtVWdZV1FnWW05aGNtUXNJSE5qYjNCbFpDQjBieUIw * YUdseklHTnliM0F1Q2lBcUlGSmxkSFZ5Ym5NZ0p5Y2dkMmhsYmlCMGFHVWdZM0p2Y0NCallXNXViM1FnYm1GdFpTQnBkSE5sYkdZ * c0lITnZJR0VnWTJGc2JHVnlJR1poYVd4eklHTnNiM05sWkNCaGJtUUtJQ29nY21WdVpHVnljeUJ1YjNSb2FXNW5JSEpoZEdobGNp * QjBhR0Z1SUdkMVpYTnphVzVuSUdFZ2JtVnBaMmhpYjNWeUozTWdZbTloY21RdUNpQXFMd3BtZFc1amRHbHZiaUJ1YzNWZllXUmZj * MmwwWlhNb0tUb2dZWEp5WVhrZ2V3b2dJQ0FnY21WMGRYSnVJRnNuWTI5dEp5d25iM0puSnl3bmJtVjBKeXduZEhKaFpHVW5MQ2Rq * YUdGMEp5d25kMjl5YXljc0oyMWhjbXRsZENjc0oyUmhkR1VuTENkbWRXNG5MQ2RwYm1adkoxMDdDbjBLWm5WdVkzUnBiMjRnYm5O * MVgyRmtYM0p2Ykd4ZmJpZ3BPaUJwYm5RZ2V5QnlaWFIxY200Z2NtRnVaRzl0WDJsdWRDZ3dMQ0E1T1RrNUtUc2dmUXBtZFc1amRH * bHZiaUJ1YzNWZllXUmZjMnh2ZEY5MWNtd29QM04wY21sdVp5QWtjMmwwWlNBOUlHNTFiR3dzSUQ5cGJuUWdKRzRnUFNCdWRXeHNL * VG9nYzNSeWFXNW5JSHNLSUNBZ0lDUjBJRDBnS0NSemFYUmxJQ0U5UFNCdWRXeHNJQ1ltSUNSemFYUmxJQ0U5UFNBbkp5a2dQeUJ6 * ZEhKMGIyeHZkMlZ5S0NSemFYUmxLU0E2SUc1emRWOWpjbTl3WDNSc1pDZ3BPd29nSUNBZ2FXWWdLQ1IwSUQwOVBTQW5KeUI4ZkNB * aGFXNWZZWEp5WVhrb0pIUXNJRzV6ZFY5aFpGOXphWFJsY3lncExDQjBjblZsS1NrZ2NtVjBkWEp1SUNjbk93b2dJQ0FnYVdZZ0tD * UnVJRDA5UFNCdWRXeHNLU0FrYmlBOUlHNXpkVjloWkY5eWIyeHNYMjRvS1RzS0lDQWdJQ1J1SUQwZ2JXRjRLREFzSUcxcGJpZzVP * VGs1TENBa2Jpa3BPd29nSUNBZ2NtVjBkWEp1SUc1emRWOTBjbUZrWlY5dmNtbG5hVzRvS1NBdUlDY3ZKeUF1SUNSMElDNGdKeThu * SUM0Z2MzUnlYM0JoWkNnb2MzUnlhVzVuS1NSdUxDQTBMQ0FuTUNjc0lGTlVVbDlRUVVSZlRFVkdWQ2s3Q24wS1puVnVZM1JwYjI0 * Z2JuTjFYMkp2WVhKa1gyVnVaSEJ2YVc1MEtDazZJSE4wY21sdVp5QjdJSEpsZEhWeWJpQnVjM1ZmWVdSZmMyeHZkRjkxY213b0tU * c2dmUXBtZFc1amRHbHZiaUJ1YzNWZllXUmZjMnh2ZEY5b2RHMXNLSE4wY21sdVp5QWtaWGgwY21GRGJHRnpjeUE5SUNjbktUb2dj * M1J5YVc1bklIc0tJQ0FnSUNSMElEMGdibk4xWDJOeWIzQmZkR3hrS0NrN0NpQWdJQ0JwWmlBb0pIUWdQVDA5SUNjbklIeDhJQ0Zw * Ymw5aGNuSmhlU2drZEN3Z2JuTjFYMkZrWDNOcGRHVnpLQ2tzSUhSeWRXVXBLU0J5WlhSMWNtNGdKeWM3Q2lBZ0lDQWtZMnh6SUQw * Z0oyNXpMV0ZrTFhOc2IzUW5JQzRnS0NSbGVIUnlZVU5zWVhOeklDRTlQU0FuSnlBL0lDY2dKeUF1SUNSbGVIUnlZVU5zWVhOeklE * b2dKeWNwT3dvZ0lDQWdKR05sYkd4eklEMGdKeWM3Q2lBZ0lDQm1iM0lnS0NScElEMGdNRHNnSkdrZ1BDQTBPVHNnSkdrckt5a2dl * d29nSUNBZ0lDQWdJQ1IxY213Z1BTQnVjM1ZmWVdSZmMyeHZkRjkxY213b0pIUXBPd29nSUNBZ0lDQWdJQ1JqWld4c2N5QXVQU0Fu * UEdFZ1kyeGhjM005SW01ekxXRmtMV05sYkd3aUlHaHlaV1k5SW1oMGRIQnpPaTh2Ym05emFXZHVkWEF1ZEhKaFpHVXZJaUIwWVhK * blpYUTlJbDlpYkdGdWF5SWdjbVZzUFNKdWIyOXdaVzVsY2lJK1BHbHRaeUJoYkhROUltRmtJaUJ6Y21NOUlpY2dMaUJvZEcxc2Mz * QmxZMmxoYkdOb1lYSnpLQ1IxY213c0lFVk9WRjlSVlU5VVJWTXNJQ2RWVkVZdE9DY3BJQzRnSnlJZ2NtVm1aWEp5WlhKd2IyeHBZ * M2s5SW01dkxYSmxabVZ5Y21WeUlqNDhMMkUrSnpzS0lDQWdJSDBLSUNBZ0lISmxkSFZ5YmlBblBHRnphV1JsSUdOc1lYTnpQU0lu * SUM0Z2FIUnRiSE53WldOcFlXeGphR0Z5Y3lna1kyeHpMQ0JGVGxSZlVWVlBWRVZUTENBblZWUkdMVGduS1NBdUlDY2lJR1JoZEdF * dGJuTXRZV1E5SWpFaVBpY2dMaUFrWTJWc2JITWdMaUFuUEM5aGMybGtaVDRuT3dwOUNpOHFLaUJQYm1VZ2RtVnlkR2xqWVd3Z2My * dDVjMk55WVhCbGNpQW9UQzlTS1NCdmNpQmliM1JvSUNoTVVpa3VJRTV2ZENCMGFHVWdORGt0ZEdsc1pTQmliMkZ5WkM0Z1ZISmha * R1VnWTNKdmNDNGdLaThLWm5WdVkzUnBiMjRnYm5OMVgyRmtYM1J2ZDJWeVgyaDBiV3dvYzNSeWFXNW5JQ1J6YVdSbElEMGdKMHhT * SnlrNklITjBjbWx1WnlCN0NpQWdJQ0FrZENBOUlHNXpkVjlqY205d1gzUnNaQ2dwT3dvZ0lDQWdhV1lnS0NSMElEMDlQU0FuSnlC * OGZDQWhhVzVmWVhKeVlYa29KSFFzSUc1emRWOWhaRjl6YVhSbGN5Z3BMQ0IwY25WbEtTa2dKSFFnUFNBbmRISmhaR1VuT3dvZ0lD * QWdhV1lnS0NSMElEMDlQU0FuSnlCOGZDQWhhVzVmWVhKeVlYa29KSFFzSUc1emRWOWhaRjl6YVhSbGN5Z3BMQ0IwY25WbEtTa2dj * bVYwZFhKdUlDY25Pd29nSUNBZ0pITnBaR1VnUFNCemRISjBiM1Z3Y0dWeUtDUnphV1JsS1RzS0lDQWdJR2xtSUNna2MybGtaU0E5 * UFQwZ0owSlBWRWduSUh4OElDUnphV1JsSUQwOVBTQW5URkluSUh4OElDUnphV1JsSUQwOVBTQW5KeWtnZXdvZ0lDQWdJQ0FnSUhK * bGRIVnliaUJ1YzNWZllXUmZkRzkzWlhKZmFIUnRiQ2duVENjcElDNGdibk4xWDJGa1gzUnZkMlZ5WDJoMGJXd29KMUluS1RzS0lD * QWdJSDBLSUNBZ0lDUnphV1JsSUQwZ0tDUnphV1JsSUQwOVBTQW5VaWNwSUQ4Z0oxSW5JRG9nSjB3bk93b2dJQ0FnSkhWeWJDQTlJ * RzV6ZFY5aFpGOXpiRzkwWDNWeWJDZ2tkQ2s3Q2lBZ0lDQnBaaUFvSkhWeWJDQTlQVDBnSnljcElISmxkSFZ5YmlBbkp6c0tJQ0Fn * SUhKbGRIVnliaUFuUEdFZ1kyeGhjM005SW01ekxXRmtMWFJ2ZDJWeUlpQmtZWFJoTFhOcFpHVTlJaWNnTGlBa2MybGtaU0F1SUNj * aUlHaHlaV1k5SW1oMGRIQnpPaTh2Ym05emFXZHVkWEF1ZEhKaFpHVXZJaUIwWVhKblpYUTlJbDlpYkdGdWF5SWdjbVZzUFNKdWIy * OXdaVzVsY2lJK1BHbHRaeUJoYkhROUltRmtJaUJ6Y21NOUlpY2dMaUJvZEcxc2MzQmxZMmxoYkdOb1lYSnpLQ1IxY213c0lFVk9W * RjlSVlU5VVJWTXNJQ2RWVkVZdE9DY3BJQzRnSnlJZ2NtVm1aWEp5WlhKd2IyeHBZM2s5SW01dkxYSmxabVZ5Y21WeUlqNDhMMkUr * SnpzS2ZRb0tMeW9xSUVGa0xYSmxjRzl5ZENCbGJtUndiMmx1ZENCdmJteDVMaUJVY21Ga1pTQjJZV3hwWkdGMFpYTWdjMnh2ZEM5 * bGNHOWphQ0JoYm1RZ2IzZHVjeUJoWTJObGNIUmhibU5sTEFvZ0tpQmpiMjVtYVdkMWNtVmtJSFJoYTJWa2IzZHVJR0Z1WkNCemFH * OTNhVzVuY3lCemRHRjBaUzRnVTJWdVpHbHVaeUJoSUhKbGNHOXlkQ0JwY3lCdWIzUWdjSEp2YjJZZ2IyWWdZUW9nS2lCMFlXdGxa * RzkzYml3Z2IzZHVaWEp6YUdsd0lHOXlJSEpsWm5WdVpEc2dkR2hwY3lCb1pXeHdaWElnWjNKaGJuUnpJRzV2SUdWamIyNXZiV2xq * SUdGMWRHaHZjbWwwZVM0Z0tpOEtablZ1WTNScGIyNGdibk4xWDNKbGNHOXlkRjlsYm1Sd2IybHVkQ2dwT2lCemRISnBibWNnZXdv * Z0lDQWdKSFFnUFNCdWMzVmZZM0p2Y0Y5MGJHUW9LVHNLSUNBZ0lHbG1JQ2drZENBOVBUMGdKeWNnZkh3Z0lXbHVYMkZ5Y21GNUtD * UjBMQ0J1YzNWZllXUmZjMmwwWlhNb0tTd2dkSEoxWlNrcElISmxkSFZ5YmlBbkp6c0tJQ0FnSUhKbGRIVnliaUJ1YzNWZmRISmha * R1ZmYjNKcFoybHVLQ2tnTGlBbkx6OWhjR2s5WVdSZmNtVndiM0owSm1OeWIzQTlKeUF1SUhWeWJHVnVZMjlrWlNna2RDazdDbjBL * THlvZ1BUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlJRVZPUkNCRlEwOU9UMDFaSzBsRVJVNVVTVlJaSUZSRlRFOU5SVkpGSUQw * OVBUMDlQVDA5UFQwOVBUMDlQVDA5UFNBcUx3PT0ifV0sInByZXZpb3VzX3NvdXJjZV9zaGEyNTYiOiIwMzcyNjI4NTk0NDNiMjJm * ODVlMzJkOWM2NmM4NTFhMjUxZjJkM2I0NDNjMmFjNjc2MWM1MjFkNWQxYjMwZTgxIiwic291cmNlX3NoYTI1Nl9yb2xlIjoicmV2 * aWV3ZWQgcGFyZW50LCBub3QgYSBzZWxmLXJlZmVyZW50aWFsIGNhbmRpZGF0ZSBoYXNoIiwicmV2aXNpb24iOiJSMDUgcmV2aWV3 * ZWQgUjA0IHJldHVybjsgYm91bmRlZCByZXBhaXJzLCBpc29sYXRlZCBldmlkZW5jZSwgcHJlc2VydmVkIGFyY2hpdGVjdHVyZS9V * SSJ9 * NSU-RECOVERY-V1-END *//** * CURRENT REVISION 2026-10-13 / R05: Keep this block in place. The paired * caps govern architecture/UI and minimum necessary change. Prove the defect; * preserve correct behavior; add no speculative state, timer or control. * Revise mutable details with evidence, not deletion or blind reversion. * Keep this site's work/evidence isolated and bound to the tested source hash. * * FINAL-PACK MAINTENANCE CHARTER -- DO NOT DELETE. * * CURRENT PRODUCT AMENDMENT 2026-10-03 - owner-directed entrance round. * Purpose: Trade NSU. Place ads. Test forecasts. * Use one readable proportional entrance family, carrying this face's * own motif and palette. Lead with useful plain words; preserve the * meaning of consent, risk and actual capability limits. Fit normal * views without scrollbars; retain an accessible zoom/reflow fallback. * Shared wallet access is not a new face ledger, mint or account. * Product tools are Ads / Predictor / Orders. Wallet and identity belong * to the shared wallet control, not duplicate product tabs. Preserve * order-book access, exact-pair truth and existing payment deep links. * The qualified PhiGraph tool (R35) belongs in this same PHP as a bounded * research view; the older INFO-only placement plan is superseded. * Forecasts are testable hypotheses, never profit or price guarantees. * Keep purchase/LTC/paid execution gates closed until their own proof. * Source authority: verified current root 4.php. * Old cells/sync-pack pointers are historical; those cells are absent. * Reconcile live/raw/source bytes before writing; do not revive old packs. * This amendment supersedes conflicting old layout/roadmap prose only. * Preserved history is not a task queue or proof of implemented work. * * This comment keeps this file maintainable when it is the only artifact left. * Repair measured failures or narrow security/honesty defects; do not invent a * new financial product. * * UNIVERSAL LAW: one stock-PHP file; no composer/npm/database dependency; no * visitor signup, recovery account, general-purpose tracking profile, shared * bank, SSO, or paywall on basic use. Keep ?src=1 and ?download=1 auditable. * This host owns only its own data, vault, and operator panel. A panel is never * a visitor identity. Trade alone may keep the purpose-bound first-party * public wallet-locator rows described below; they are not named accounts * or tracking profiles. Older plaintext seed assignments are legacy exposure. Public status must never disclose vault/filesystem * detail, and admin passwords belong in POST bodies, never URLs. * * ===================================================================== * CURRENT LAW (Prompt 20 · 2026-09-11) — active implementation authority. * Historical prose below may remain for forensics; it is NOT a todo list. * Prefer this block + executable code over older "Today:" / KEEP WORKING lines. * --------------------------------------------------------------------- * AUTH: Normal visitor mutations require ECDSA P-256 v2 (`ecdsa_p256_v2` / * `auth_required`). Seed is NOT accepted on normal mutation routes. * Bounded one-shot legacy path only: `api=legacy_wallet_migrate` (register * signer for an old funded seed-addr wallet). Do NOT re-enable raw seed * fallback on transfer/order/cancel/fill/accrue merely to green stale tests. * MONEY CAP: 100_000_000 NSU once at birth via nst_mint_guarded('genesis', …). * After birth remaining issuance is 0. No daily drip. * KING / MINTER: KING-only mint; renters never mint; no visitor-triggered mint. * EMISSION: NST_EMISSION_PAUSED = true; nst_emission_run_daily is a no-op. * Crop-share / drip / mirror-faucet emission machinery is DORMANT HISTORY. * ANNUAL RESET: Balances survive; orders / ads / profit-rent-period burn/reset. * SSOT copy: NST_DISCLOSE_YEARLY_WIPE* (Prompt 19). No remint / re-anchor. * SUPERSEDED (do not resurrect as current): seed-on-wire as normal auth, * browser_hmac_v1 + ecdsa_p256_v1 "Today" dual-path spends, daily emission / * ten-crop drip as live issuance, population re-anchor mint, visitor faucet * claim, LORD rent-credit treasury→LORD grants. * ===================================================================== * * SOUL: money/buy rails. The browser creates a fresh local wallet or opens * available local words; a public locator alone cannot recover the secret. * A visitor can preserve the words offline, use the book, send NSU, or post * a signed intent in a transparent on-site book; any foreign settlement stays * directly between people. The system is experimental and honest about it. * KEEP WORKING: automatic pre-modal wallet path, wallet restore/create before * ENTER, ECDSA P-256 v2 mutation auth (see CURRENT LAW), free basic use, * transfers, book, source/download, local advisory microbrains that are * transparent and fail-open, yearly wipe semantics, and site-local panel. * (HISTORICAL phrase "seed-is-wallet authority" meant custody/identity of the * 12 words — SUPERSEDED BY CURRENT LAW for mutation auth: seed not accepted * on normal spends.) * NEVER BECOME: an operator spending/signing or escrow product, a CEX, * market-maker authority, price oracle, automatic accept/reject, shared sister * ledger, pay-to-use rails, seed recovery desk, named account, assignment * history, third-party tracker, behavioral dossier, cross-site profile, renter * mint, TradingView/Haveno cosplay, or a claim that an unproven security path * is proven. * * DEVICE ASSIGNMENT (amended 2026-08-12; auth clarified Prompt 20 · 2026-09-11): * - IDENTITY / CUSTODY: The normalized twelve-word seed identifies the wallet * and remains recovery material the visitor must protect. The first-party * browser token and its row are only a convenience locator. * - MUTATION AUTH (CURRENT): Normal spends/orders use ECDSA P-256 v2 signatures. * Seed is not accepted on normal mutation routes. SUPERSEDED BY CURRENT LAW * above — do not read the older "sole spending authority" wording as an * instruction to restore seed-on-wire auth. Bounded exception: * `legacy_wallet_migrate` only. * - Current device binding stores a public address, signing-key fingerprint * and revision for the token. Fresh words are generated and held in the * browser; public signer registration and signed bind carry no fresh seed. * Rebinding changes that locator, not other bindings or wallet balances. * - Missing/cleared storage or a fresh private-browsing identity gets a fresh * token and wallet. IP, User-Agent, language, timezone, screen, canvas, * behavior, and similarity scores never recover, merge, or select a wallet; * passive signals, if present at all, are metadata-only. * - Legacy plaintext assignment rows may still expose old secrets to the * host; one-time migration cannot undo that past exposure. Do not extend * this legacy warning into a claim that new local-only wallets upload words, * or claim that legacy wallets were always non-custodial. No operator spend, * seed browser, escrow, recovery desk or secret logging is authorized. * - Restore/create and pure address derivation complete before entrance-modal * interaction. They grant no NSU and never call the economic api=addr path, * mint, accrue, transfer, order, fill, cancel, or mutate chain/KING state. * - Wallet Details reveals words actually held by this browser; anyone * who sees them can spend. Preserve the separate legacy-exposure warning. * If local words are missing, request restore honestly; a public locator * is not a recovery secret. Personal device and offline backup are required. * * MONETARY CONSTITUTION (amended 2026-08-03 — see NSU-DNA/08-BRIDGE-SPEC.md §7): * - CEILING: NST_MAX_SUPPLY = 100_000_000 NSU, identical in every empire. No * country can out-print a sister; an empire wins by being USED, not by * printing. Never make the cap hardware-relative or per-host — symmetry is * what lets two kings verify they are on the same money. * - THE CEILING IS NOT THE TREASURY. What the KING holds is a balance; the cap * is a limit on all issuance for all time. These were once the same field and * "max supply" moved whenever the treasury re-anchored. Keep them separate. * - ONE MINT AT BIRTH: the entire 100 million cap (NST_MAX_SUPPLY micros) is * credited once to the king wallet through nst_mint_guarded('genesis', ...). * After that, nothing is ever printed. No daily drip. NST_EMISSION_PAUSED * stays true; nst_emission_run_daily is a no-op. Remaining supply after birth * is 0, so later emission cannot print even if someone flips the pause. * Utterly fair except the king starts with 100 million. * - NO PARTICIPANT MAY TRIGGER A MINT. Any issuance a user can cause by * transacting becomes infinite the moment they control both sides. This is * exactly why the KING takes a TAX on receipts and never a DUPLICATE of them: * a mirrored fee is free money to whoever owns a lord crop. * - TEN CROPS / EMISSION SPLIT — HISTORICAL DORMANT MACHINERY (not live issuance). * SUPERSEDED BY CURRENT LAW: king-cap-at-birth + NST_EMISSION_PAUSED. * When emission was designed, the day's emission would divide evenly among * ten crop lord wallets (no protocol UBI / contribution pools — those were * constant addresses no key could open). Code paths may still exist as * no-ops; do NOT treat this bullet as an instruction to resume drip. * - NEVER PAY PER HEAD. If a lord funds a basic income from their own stream, * it must divide a fixed amount among claimants, never pay a fixed amount to * each. Paying per visitor makes issuance = participants x amount and the * attacker chooses the participant count — one residential IPv6 /64 is 18 * quintillion addresses. An IP is not a scarce resource. * - THE KING'S CUT IS ONE RULE: a percentage off the top of every LORD RECEIPT * (tax, never duplicate). Historical note: when emission ran, crop shares were * also lord receipts under this rule — SUPERSEDED AS ISSUANCE PATH by paused * emission / zero remaining supply; tax-on-receipt rule itself remains. * - NEVER TAX A USER-TO-USER TRADE. An order-book fill or a plain transfer * landing in a lord's wallet is movement, not revenue; taxing it would skim a * lord for buying NSU with their own money. Taking a slice of two strangers * settling their own terms is a gatekeeper's cut, which the ethos refuses. * - ADS BUY THEIR OWN REAL ESTATE, NEVER RANK. Paid prominence inside a * dedicated ad slot is a sink; paid position in someone else's organic * results is a platform algorithm deciding who gets seen. Refused. * - Every mint type must be declared in nst_conservation_replay() or the E2 * freeze halts all chain appends. That is the correct failure mode: a supply * bug must stop the economy, not quietly inflate it. * CHANGE BOUNDARY: preserve money behavior while the owner-directed * Ads/Predictor/Orders tools replace duplicate wallet tabs. Product selection * remains zero-or-one where the existing shell uses it. Clicking the * active control returns to the public book/order-entry landing. Shared * Wallet owns send/receive; Orders owns order actions. Current/local pair * data stays inspectable and exact-pair local. * The future pair crawler/bridge is deferred; no external chart/rate origin. * Preserve the distinction between browser evidence and real e2e proof. * Advisory text may explain local signals but may not decide a trade. * PROOF: PHP lint; assignment isolation/overwrite/security fixtures; automatic * pre-modal boot with no economy delta; tab 0→1→0 and 1→2 sequences; pair-local * truth; numbered/pack parity; existing security/lock behavior; live parity * only after separate deployment authority. */ /* NS_CHARTER_HONESTY 2026-09-26 */ /** * CURRENT REVISION 2026-10-13 / R05: Keep this block in place. The paired * caps govern architecture/UI and minimum necessary change. Prove the defect; * preserve correct behavior; add no speculative state, timer or control. * Revise mutable details with evidence, not deletion or blind reversion. * Keep this site's work/evidence isolated and bound to the tested source hash. * * DO NOT DELETE/REMOVE THIS BLOCK - NOSIGNUP.TRADE PRODUCT CONTRACT. * AUTHORITATIVE product sentences (must stay true in UI, not only Discord): * * ORIGINAL LAYOUT DNA (conform loosely — edit > invent): * - Classic three-band stage: Top ~65vh (search/wallet/book surface), Middle ~25vh (coach/stats), * Bottom ~10vh (unnumbered Ads / Predictor / Orders tools). White hairlines, black void. * - Slide-in animations (left/right/up). DNA line: TRADE · money/buy · orange burn. * - Empire entrance (chat DNA): Options · Mirrors + I AGREE ENTER; wipe facts in modal/coach * AND a centered top chrome banner (service+pricing reset; coins stay). Tabs never numbered 1/2/3. * * 0 PRIME: One file. No signup. Hard to kill. No course. No paywall on basics. * 1 FIRST RUN: Before entrance-modal interaction, create or restore the current * token's wallet and load it without help. This grants no starter NSU and * makes no mint, faucet, modifier, chain, order, transfer, or KING mutation. * Wallet Details shows locally held words and warns that anyone seeing them * can spend. Missing local words require restore, not server-secret recovery. * 2 SEED / AUTH: Seed remains wallet identity + recovery material (write offline). * MUTATION AUTH (CURRENT LAW): ECDSA P-256 v2 on normal routes; seed not * accepted there. Bounded legacy: `legacy_wallet_migrate` once for old * funded seed-addr wallets. HISTORICAL wording "sole spending authority" / * seed-on-wire dual-path — SUPERSEDED BY CURRENT LAW (Prompt 20). * One first-party browser token maps to a public address/signing-key locator. * Fresh words stay in the browser; normal registration/bind uses public * signer material and signatures. Several devices can deliberately use * one wallet, but passive signals never recover or merge identities. * Legacy assignment secrets were server-exposed; migration cannot undo * that history. Losing the local words requires the visitor's offline copy; * a public locator alone cannot spend or recover a wallet. * 3 NSU: King holds the whole 100 million at birth. After that nothing is * printed. No daily drip. Emission stays paused. Visitor faucet is 410 * retired — no free claim. Buy on the book. Utterly fair except the king * starts with 100 million (amended 2026-08-28 — king-cap-at-birth / faucet 410). * Renters must NOT receive treasury.secret / site wallet seed. * Optional donate path may grant NSU later; never a required purchase. * 4 RESET: Yearly wipe. Wallet balances and access records survive (2026-09-29). Profit/ads/bids/ * orders/modifiers burn. Treasury does NOT re-anchor or remint; after birth * nothing is printed (amended 2026-08-28 — king holds the cap; no drip). * Wipe facts live in entry modal + coach AND a centered top chrome banner * (Nosignup.trade - Service and pricing reset in N days · coins stay · orders/ads/profit burn). * Early window restarts. * RENT SIPHON (scaffold): at wipe, remaining renter-side profit/ad piles burn; * renters never mint. No faucet remint. * 5 BOOK: Signed intents; NSU leg on-site; foreign settle off-platform P2P. * Empty book after genesis/wipe → loose bootstrap spread (kind=bootstrap_spread) * around temp anchor 1.0 NOTE/NSU so discovery has a mid; makers tighten. * Market P = book converge; donate buyback arms at P/2 (never fixed sticker). * 6 NO THROAT: No visitor signup, named account, or promised recovery desk. * Historical plaintext assignment rows exposed legacy seeds to the host; * preserve that warning without claiming fresh browser-only wallets upload * their words. Migration does not erase historical exposure. No operator * spend/escrow or recovery product follows from retained legacy data. Site-local renter panel is app-god-mode (incl. replace THIS file), * not OS root. Independent wallets remain independent (seed = identity/ * recovery; mutation auth = ECDSA v2 per CURRENT LAW). * 7 SECURITY: Experimental until proven. Honest warnings > false guarantees. * CURRENT (Prompt 20): `ecdsa_p256_v2` / `auth_required` on normal mutations; * seed rejected except `legacy_wallet_migrate`. State honesty may still * expose capability flags; do not treat flags as permission to restore seed. * HISTORICAL (SUPERSEDED BY CURRENT LAW): "Today: browser_hmac_v1 + * ecdsa_p256_v1" dual-path spends including faucet claim — not active auth. * Visitor faucet claim remains 410 retired. * 8 MIRRORS: Swarm later; this node is disposable; source must stay auditable. * 9 PANEL: /controlpanel or ?controlpanel=1 — paste THIS site's wallet seed * (same as operator/treasury seed). No recovery desk. No password product path. * Unlock: seed derives treasury_addr (owner). Later rent may use economy.operator_addr. * 10 ECONOMY: site-local ads; donate→NSU via book mid×k then 50% bid / 50% profit. * 11 SHELL: Ads / Predictor / Orders are product tools; wallet access is shared. * Unchecked landing is the NSU order book (bids / asks / spread / last). * Clicking the active control returns to zero. Zero shows the useful * baseplate: public selected-pair book and order entry, with wallet send/ * receive in the common wallet and order actions in Orders. Exact pair * equality governs every quote, * book row, fill, and sparkline; no global midpoint masquerades as pair truth. * Future LLM pair crawling/bridge expansion is deferred and adds no origin now. * * King-cap-at-birth NSU · no drip · disposable seed wallets · transfers · order book * ?src=1 / ?download=1 · no Monero/Haveno · no TradingView · no CDN · no DB daemon * law amendment 2026-08-12: device assignment honesty + zero-tab baseplate * rev 2026-07-13g - wave7: chain RMW lock transfer/order/admin_pay/extract/lazy_accrue TOCTOU */ /** * LATER-SLOTS (groundwork 2026-08-27, zero live; not a charter rewrite) * HISTORICAL / DORMANT — SUPERSEDED BY CURRENT LAW (emission paused; no drip): * MIRROR FAUCET sketch: NEVER PAY PER HEAD. IP is not scarce. Any later share * would be of EXISTING circulating value, not a new mint, not ?api=faucet (410). * Do not read as an instruction to resume daily emission. * DEAD-KING VOTE: PHP MUST NOT be election authority. No tally, no probe. * Browsers/mirrors decide later if king silent X time. (Live succession election * routes remain retired — see ledger R-V8-001.) */ /** * FILE MAP (current root 4.php — orientation, not a second spec): * charter + PRODUCT CONTRACT ...... law / keep-working / never-become (above) * constants + paths ............... NST_* · $ROOT/$DATA/$CHAIN/$VAULT * identity / auth helpers ......... addr_from_seed · mac · ecdsa · nst_addr_v2 scaffold * brains + wipe copy .............. nst_brain_* · nst_ephemerality_reel_segments * chain / book / economy .......... locks · balances · bootstrap · faucet · ads * E2 conservation ............... nst_conservation_* · freeze file · write gates * panel / vault / admin ........... panel_seed_ok · require_admin · vault notes * routes .......................... ?api=* · ?src/?download · ?controlpanel=1 (early exit) * main HTML/CSS ................... Top/Mid/Bottom DNA · entry gate · unnumbered tabs * client JS IIFE .................. wallet/session · book · ads · coach · gate boot * economy+identity telomere ...... IDENTITY pointers (mill/JS left in place) + ECONOMY ask-trade * Edit verified current root 4.php. The old cells/trade.php and global * sync-pack path are absent; never replace current bytes with a stale pack. * * SECTION MAP (maintainers index — approximate roles; comments only): * ~L203–~L1400 ...... mirror/paths · identity/ECDSA · nst_actor / nst_auth_sig (API actor/auth) * ~L1400–~L11200 .... chain/book/economy · mint · conservation · money-law · panel helpers * ~L11374–~L14000 ... visitor+admin book APIs (state/transfer/order/fill/cancel/migrate/inbox/…) * ~L14000–~L18830 ... HTML landing · CSS · entry gate (NS_ETHOS_GATE) * ~L18900–~L21820 ... Trade client IIFE (wallet · book · toasts · WORDS→window.__NS_NSU_WORDS__) * ~L21827–~L22865 ... empire-wallet (data-ns-empire-wallet; reuses __NS_NSU_WORDS__) * ~L22870–EOF ....... ECONOMY+IDENTITY TELOMERE … END TELOMERE (byte-preserve) */ declare(strict_types=1); /* TR-R1: stock-PHP /predict compatibility. The old tree is not required. In-route /predict, /predict/ and /predict/index.php: src wins, else download, else whitelisted symbol/range/interval on /?tool=predictor. Any other /predict path rolls back to /?tool=predictor with no query. Never an open redirect. */ function nst_predict_compat_send(string $target): void { if ($target === '' || $target[0] !== '/' || strpos($target, '//') !== false) return; if (preg_match('#^[a-z][a-z0-9+.-]*:#i', ltrim($target, '/')) === 1) return; header('Cache-Control: no-store'); header('Referrer-Policy: no-referrer'); header('X-Content-Type-Options: nosniff'); header('Location: ' . $target, true, 302); exit; } function nst_predict_compat_maybe_redirect(): void { $uri = (string)($_SERVER['REQUEST_URI'] ?? ''); $path = parse_url($uri, PHP_URL_PATH); if (!is_string($path) || $path === '') return; $path = str_replace('\\', '/', $path); if (preg_match('#^/predict(?:/|$)#', $path) !== 1) return; $inRoute = ($path === '/predict' || $path === '/predict/' || $path === '/predict/index.php'); if (!$inRoute) { nst_predict_compat_send('/?tool=predictor'); return; } if (array_key_exists('src', $_GET)) { nst_predict_compat_send('/?src=1'); return; } if (array_key_exists('download', $_GET)) { nst_predict_compat_send('/?download=1'); return; } $q = ['tool' => 'predictor']; $sym = strtoupper(trim((string)($_GET['symbol'] ?? ''))); if ($sym !== '' && preg_match('/^[A-Z0-9.^=\-]{1,24}$/', $sym) === 1) $q['symbol'] = $sym; $range = (string)($_GET['range'] ?? ''); if (in_array($range, ['6mo', '1y', '2y', '5y', '10y', 'max'], true)) $q['range'] = $range; $interval = (string)($_GET['interval'] ?? ''); if (in_array($interval, ['1d', '1wk'], true)) $q['interval'] = $interval; nst_predict_compat_send('/?' . http_build_query($q, '', '&', PHP_QUERY_RFC3986)); } nst_predict_compat_maybe_redirect(); /* NS_TRADE_PHIGRAPH_VIEW: same-file R35 research organ; no monetary route or startup dependency. */ if (!isset($_GET['src']) && !isset($_GET['download']) && ($_GET['api'] ?? '') !== 'src' && (($_GET['view'] ?? '') === 'phigraph' || ($_GET['api'] ?? '') === 'phigraph_history' || ($_GET['api'] ?? '') === 'history')) { if (($_GET['api'] ?? '') === 'phigraph_history') $_GET['api'] = 'history'; $nstPgNonce = base64_encode(random_bytes(18)); header('Content-Type: text/html; charset=utf-8'); header('Cache-Control: no-store'); header('X-Content-Type-Options: nosniff'); header('Referrer-Policy: no-referrer'); header('X-Frame-Options: SAMEORIGIN'); header("Content-Security-Policy: default-src 'none'; script-src 'nonce-" . $nstPgNonce . "' blob:; worker-src blob:; style-src 'unsafe-inline'; img-src data:; connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'"); /* PhiGraph R35 — tested research release, derived from the user-supplied R34. R34 SHA-256: 2d3b68d16932a9803ced7e91c1155b9cd965c409b3f945cb99e8023b1013efb9 Provenance: PhiGraph North Star reconstruction; original v8.3 source remains missing. Mountain-state x/y/z/s, canonical primitive grammar, Zipf weighting and evolution are hypotheses, not a proof of market predictability or a universal optimum. R35: one predictor for scoring and drawing; worker cancellation; chronological TRAIN / SELECT / CALIBRATE / HOLDOUT; honest uncertainty and residuals; opt-in contexts; copied/exported full state; forecast issue/settlement ledger. All provider history is retrospectively obtained, not a point-in-time corporate-action archive. No authentication secrets belong in this file: ?src and ?download expose its source. */ define('PHIGRAPH_VERSION', 'truth-kernel-r35-tested-research-2026-10-04'); define('MAX_HISTORY_ROWS', 6000); define('PG_MAX_BYTES', 8388608); /* ===================== PhiGraph R35: JSON reply + allowlisted provider fetch ===================== */ function pg_json(array $value, int $status = 200): never { http_response_code($status); header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-store'); echo json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR); exit; } function pg_symbol(string $s): string { $s = strtoupper(trim($s)); if (!preg_match('/^[A-Z0-9.^=\-]{1,24}$/D', $s)) throw new InvalidArgumentException('Invalid symbol.'); return $s; } function pg_http_get(string $url, int $timeout = 12): array { $p = parse_url($url); $allow = ['query1.finance.yahoo.com', 'query2.finance.yahoo.com', 'stooq.com', 'api.exchange.coinbase.com']; if (($p['scheme'] ?? '') !== 'https' || !in_array($p['host'] ?? '', $allow, true) || isset($p['user']) || isset($p['port'])) throw new RuntimeException('Provider URL is not allowlisted.'); $headers = ['User-Agent: Mozilla/5.0 PhiGraph-R35', 'Accept: application/json,text/csv,text/plain', 'Connection: close']; if (function_exists('curl_init')) { $ch = curl_init($url); $body = ''; curl_setopt_array($ch, [ CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => $timeout, CURLOPT_HTTPHEADER => $headers, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_WRITEFUNCTION => static function ($ch, string $chunk) use (&$body): int { if (strlen($body) + strlen($chunk) > PG_MAX_BYTES) return 0; $body .= $chunk; return strlen($chunk); } ]); $ok = curl_exec($ch); $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $error = curl_error($ch); curl_close($ch); return ['ok' => $ok !== false && $status >= 200 && $status < 300, 'status' => $status, 'body' => $body, 'error' => $error ?: "HTTP $status"]; } $ctx = stream_context_create([ 'http' => ['timeout' => $timeout, 'ignore_errors' => true, 'follow_location' => 0, 'header' => implode("\r\n", $headers) . "\r\n"], 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true] ]); $body = @file_get_contents($url, false, $ctx, 0, PG_MAX_BYTES + 1); $status = 0; foreach (($http_response_header ?? []) as $h) if (preg_match('~^HTTP/\S+\s+(\d+)~', $h, $m)) { $status = (int)$m[1]; break; } if (is_string($body) && strlen($body) > PG_MAX_BYTES) return ['ok' => false, 'status' => $status, 'body' => '', 'error' => 'Provider response too large.']; return [ 'ok' => $body !== false && $status >= 200 && $status < 300, 'status' => $status, 'body' => is_string($body) ? $body : '', 'error' => "HTTP $status or transport failure" ]; } /* ===================== PhiGraph R35: bar normalisation + provider parsers ===================== */ function pg_finish(array $rows, string $symbol, string $source, string $basis, string $interval, string $currency = '', array $warnings = [], ?int $now = null): array { $now ??= time(); $span = $interval === '1wk' ? 604800 : 86400; $valid = []; $duplicates = 0; $unfinished = 0; foreach ($rows as $r) { $t = (int)($r['t'] ?? 0); $c = $r['close'] ?? null; if ($t <= 0 || !is_numeric($c) || !is_finite((float)$c) || (float)$c <= 0) continue; $available = (int)($r['available_at'] ?? ($t + $span)); if ($available > $now) { $unfinished++; continue; } if (isset($valid[$t])) { $duplicates++; if (abs($valid[$t]['close'] - (float)$c) > 1e-9) throw new RuntimeException('Conflicting duplicate price timestamps.'); continue; } $valid[$t] = ['t' => $t, 'date' => gmdate('Y-m-d', $t), 'available_at' => $available, 'close' => (float)$c, 'volume' => isset($r['volume']) && is_numeric($r['volume']) ? (float)$r['volume'] : null]; } ksort($valid, SORT_NUMERIC); $rows = array_values($valid); $dropped = max(0, count($rows) - MAX_HISTORY_ROWS); if ($dropped) $rows = array_slice($rows, -MAX_HISTORY_ROWS); if (count($rows) < 120) throw new RuntimeException('Fewer than 120 valid completed bars. Choose a longer range or use CSV.'); if ($unfinished) $warnings[] = "$unfinished unfinished/recent bars omitted using a conservative completion rule."; if ($dropped) $warnings[] = "$dropped oldest bars omitted by the explicit 6000-bar limit."; return [ 'ok' => true, 'symbol' => $symbol, 'source' => $source, 'price_basis' => $basis, 'currency' => $currency, 'interval' => $interval, 'rows' => $rows, 'fetched_at' => gmdate('c', $now), 'available_time_rule' => 'bar-start + 24 hours (daily) / 7 days (weekly), conservative, not exact exchange-close timestamps', 'warnings' => $warnings, 'quality' => ['duplicates_removed' => $duplicates, 'unfinished_removed' => $unfinished, 'oldest_removed' => $dropped] ]; } function pg_parse_yahoo(string $body, string $symbol, string $interval = '1d', ?int $now = null): array { $j = json_decode($body, true, 512, JSON_THROW_ON_ERROR); $r = $j['chart']['result'][0] ?? null; if (!is_array($r)) throw new RuntimeException('Yahoo did not return a chart.'); $ts = $r['timestamp'] ?? []; $q = $r['indicators']['quote'][0] ?? []; $adj = $r['indicators']['adjclose'][0]['adjclose'] ?? null; $hasAdj = is_array($adj) && count(array_filter($adj, 'is_numeric')) > 0; $prices = $hasAdj ? $adj : ($q['close'] ?? []); $rows = []; foreach ($ts as $i => $t) if (isset($prices[$i])) $rows[] = ['t' => $t, 'close' => $prices[$i], 'volume' => $q['volume'][$i] ?? null]; $meta = $r['meta'] ?? []; $out = pg_finish($rows, $symbol, 'Yahoo Finance chart (unofficial endpoint)', $hasAdj ? 'ADJUSTED_CLOSE' : 'RAW_CLOSE', $interval, (string)($meta['currency'] ?? ''), [], $now); $out['exchange'] = $meta['exchangeName'] ?? ''; $out['timezone'] = $meta['exchangeTimezoneName'] ?? ''; return $out; } function pg_range_start(string $range, ?int $now = null): int { $now ??= time(); $days = ['6mo' => 184, '1y' => 366, '2y' => 732, '5y' => 1830, '10y' => 3660, 'max' => 8000]; if (!isset($days[$range])) throw new InvalidArgumentException('Unsupported range.'); return $now - $days[$range] * 86400; } function pg_parse_stooq(string $body, string $symbol, string $range = '5y', ?int $now = null): array { $now ??= time(); $lines = preg_split('/\r\n|\n|\r/', trim($body)); $head = str_getcsv(array_shift($lines), ',', '"', ''); $map = []; foreach ($head as $i => $h) $map[strtolower(trim($h))] = $i; if (!isset($map['date'], $map['close'])) throw new RuntimeException('Stooq did not return a price CSV.'); $rows = []; $start = pg_range_start($range, $now); foreach ($lines as $l) { $v = str_getcsv($l, ',', '"', ''); $date = $v[$map['date']] ?? ''; if (!preg_match('/^\d{4}-\d{2}-\d{2}$/D', $date)) continue; $t = strtotime($date . ' UTC'); if (!$t || $t < $start) continue; $rows[] = ['t' => $t, 'close' => $v[$map['close']] ?? null, 'volume' => $v[$map['volume'] ?? -1] ?? null]; } return pg_finish($rows, $symbol, 'Stooq daily CSV', 'PROVIDER_CLOSE_ADJUSTMENT_UNVERIFIED', '1d', '', ['Yahoo unavailable; provider and adjustment basis differ. Do not pool this feed with Yahoo forecasts without checking comparability.'], $now); } function pg_coinbase(string $symbol, string $range, string $interval): array { if (!preg_match('/^[A-Z0-9]{2,10}-(USD|USDC|EUR|GBP)$/D', $symbol) || $interval !== '1d') throw new RuntimeException('Coinbase fallback supports daily quoted crypto pairs only.'); $now = time(); $start = pg_range_start($range, $now); $rows = []; $end = $now; $deadline = microtime(true) + 45; $pages = 0; while ($end > $start && count($rows) < MAX_HISTORY_ROWS && $pages < 21) { if (microtime(true) > $deadline) throw new RuntimeException('Crypto history fetch deadline reached; no partial history was silently substituted.'); $from = max($start, $end - 299 * 86400); $url = 'https://api.exchange.coinbase.com/products/' . rawurlencode($symbol) . '/candles?' . http_build_query(['granularity' => 86400, 'start' => gmdate('c', $from), 'end' => gmdate('c', $end)]); $r = pg_http_get($url, 10); if (!$r['ok']) throw new RuntimeException('Coinbase ' . $r['error']); $a = json_decode($r['body'], true, 512, JSON_THROW_ON_ERROR); if (!is_array($a) || isset($a['message'])) throw new RuntimeException('Coinbase candle response unavailable.'); foreach ($a as $x) if (is_array($x) && count($x) >= 6 && (int)$x[0] >= $start) $rows[] = ['t' => (int)$x[0], 'close' => $x[4], 'volume' => $x[5]]; $pages++; $end = $from; usleep(120000); } return pg_finish($rows, $symbol, 'Coinbase Exchange candles', 'RAW_EXCHANGE_CLOSE', '1d', explode('-', $symbol)[1], ['Yahoo unavailable; prices are specific to Coinbase Exchange.'], $now); } /* ===================== PhiGraph R35: history orchestration (10-minute cache, provider fallbacks) ===================== */ function pg_history(string $symbol, string $range, string $interval): array { pg_range_start($range); if (!in_array($interval, ['1d', '1wk'], true)) throw new InvalidArgumentException('Unsupported interval.'); $dir = sys_get_temp_dir() . '/phigraph-r35-public-cache'; $key = hash('sha256', __FILE__ . '|' . $symbol . '|' . $range . '|' . $interval); $path = $dir . '/' . $key . '.json'; if (is_file($path) && filemtime($path) > time() - 600) { $v = json_decode((string)file_get_contents($path), true); if (is_array($v) && !empty($v['ok'])) { $v['cache_hit'] = true; return $v; } } $errors = []; $value = null; foreach (['query1.finance.yahoo.com', 'query2.finance.yahoo.com'] as $host) { try { $r = pg_http_get('https://' . $host . '/v8/finance/chart/' . rawurlencode($symbol) . '?' . http_build_query(['range' => $range, 'interval' => $interval, 'events' => 'history', 'includeAdjustedClose' => 'true'])); if (!$r['ok']) throw new RuntimeException($r['error']); $value = pg_parse_yahoo($r['body'], $symbol, $interval); break; } catch (Throwable $e) { $errors[] = $host . ': ' . $e->getMessage(); } } if (!$value && preg_match('/^[A-Z]{1,6}$/D', $symbol) && $interval === '1d') try { $r = pg_http_get('https://stooq.com/q/d/l/?s=' . strtolower($symbol) . '.us&i=d&d1=' . gmdate('Ymd', pg_range_start($range)) . '&d2=' . gmdate('Ymd')); if (!$r['ok']) throw new RuntimeException($r['error']); $value = pg_parse_stooq($r['body'], $symbol, $range); } catch (Throwable $e) { $errors[] = 'Stooq: ' . $e->getMessage(); } if (!$value && str_contains($symbol, '-')) try { $value = pg_coinbase($symbol, $range, $interval); } catch (Throwable $e) { $errors[] = $e->getMessage(); } if (!$value) throw new RuntimeException('Live history unavailable. CSV remains supported. ' . implode(' | ', $errors)); $value['version'] = PHIGRAPH_VERSION; $value['cache_hit'] = false; $value['transport_errors'] = $errors; if (!is_dir($dir)) @mkdir($dir, 0700, true); if (is_dir($dir) && is_writable($dir)) { $tmp = @tempnam($dir, 'pg-'); if ($tmp !== false) { file_put_contents($tmp, json_encode($value), LOCK_EX); @rename($tmp, $path); } } return $value; } /* ===================== PhiGraph R35: JSON API (?api=health, ?api=history) ===================== */ if (isset($_GET['api'])) { try { if ($_GET['api'] === 'health') pg_json([ 'ok' => true, 'version' => PHIGRAPH_VERSION, 'source_sha256' => hash_file('sha256', __FILE__), 'php_version' => PHP_VERSION, 'https_transport' => function_exists('curl_init') ? 'curl' : 'streams', 'network_probe' => 'NOT_RUN' ]); if ($_GET['api'] !== 'history') throw new InvalidArgumentException('Unknown API operation.'); pg_json(pg_history(pg_symbol((string)($_GET['symbol'] ?? 'AAPL')), (string)($_GET['range'] ?? '5y'), (string)($_GET['interval'] ?? '1d'))); } catch (InvalidArgumentException $e) { pg_json(['ok' => false, 'error' => $e->getMessage()], 400); } catch (Throwable $e) { pg_json(['ok' => false, 'error' => $e->getMessage(), 'version' => PHIGRAPH_VERSION], 502); } } ?> PhiGraph R35 — Research control panel

PhiGraph

Single-file research control panel ·
Canonical φ / exp · ln · sin/cos Zipf 1 · 1/2 · 1/3 focal support · residuals train · select · calibrate · holdout

Data + run controls

Live history is fetched server-side by this same file. Stocks/ETFs use symbols such as AAPL or SPY; crypto uses Yahoo-style symbols such as BTC-USD or ETH-USD. If providers change or block the request, import CSV below.

SOLO · TARGET ONLY

Analysis inputs

Solo uses only the target symbol's own past data. This is the default.

Peer Group is optional. Enter related symbols such as AMC, GME, BB. Their robust aggregate becomes a candidate feature; it is never forced into the forecast.

Broad market and risk context are separate optional candidate sources.

All context is past-only and aligned from observations available at or before each target timestamp.

Forecast path

The observed past and modeled future are always visually separate.

Future bars are independently trained horizon forecasts. Peaks and valleys are hypotheses, not known future prices.

Flashing is visual only; it never changes calculations or evidence.

Skill is relative error improvement: +10% means less error than the strongest declared comparison; negative means worse. Focal support is exploratory, not a probability of profit. The 80% band is nominal and per horizon, not a guarantee that the entire path lies inside it.

Load market history. Before prediction starts, the future is a flat prior-horizon scaffold only.

The flat line is only a prior horizon marker. BEGIN PREDICTING runs the declared pass budget. Ghost traces record real model revisions, never fabricated movement. An unsupported focal candidate is red and the path is dashed.

IMPORT CSV / RUN BUNDLE
MULTI-ASSET TOURNAMENT
No tournament run.

The symbol list and settings are snapshotted at start. Each fresh test is revealed once and burned. Previously burned symbols are skipped as untouched evidence until genuinely later data exist.

Load data to begin.
SEARCH / RUN SETTINGS

Pass budget and search bounds are fixed at start. More iterations do not establish greater accuracy. Stop remains responsive during computation and downloads.

MODEL CONTRACT
Recovered contract
q = (t, x, y, z, s); r = signed return
TRAIN 60% · SELECT 15% · CALIBRATE 10% · HOLDOUT 15%
Zipf rank weights = 1, 1/2, 1/3; geometric turns are candidates

Projection

Last completed close—
Supported focal—
Direction—
Focal candidate price—
Candidate skill—
Evidence status—
No data.
observed history
future / forecast
prior forecast traces
focal candidate
nominal 80% marginal band
Observed market history is drawn separately from future model output. Build the multi-horizon path to show independently predicted future anchors, including model-supported hills and valleys.

Top resonators

#FormulaGroupScale|fit|Score
No model yet.

Transparent state

No model.
Interpretation boundaries

Fitting, focal selection, interval calibration and holdout labels are separated chronologically. Held-out data are withheld from fitting, not guaranteed unknown to the researcher. Local seen-period records survive reset; storage can still be erased. No display state is proof of market skill.

View source · Download source

Truth Kernel claim ledger

Tournament ledger

SymbolStatuswTestRivalMAE skillNet
No tournament run.

Horizon evidence

Future barForecastSELECT skillApprox. lower supportSELECT nCAL nNominal 80% interval

Issued forecast ledger

No ledger loaded.
Residual diagnostics · not a Brownian proof

Debug text · Ctrl-A then Ctrl-C

Ctrl-A outside a form field selects a complete diagnostic bundle, including full formulas, data, context, logs and ledger. Native editing shortcuts remain unchanged.
PhiGraph R35 is a tested research application, not a validated investment strategy. Predictions and ledger timestamps are local; publication or profitable live deployment is not claimed. Market downloads are user-triggered. Every forecast and all results can be exported.
'','faces'=>[],'complete'=>false]; if (!is_file($path) || is_link($path)) return $out; $raw = @file_get_contents($path); if (!is_string($raw) || $raw === '' || strlen($raw) > 262144) return $out; if (preg_match('/^EMPIRE_FINGERPRINT\s+([0-9a-f]{64})\s*$/mi', $raw, $m)) $out['fingerprint'] = strtolower($m[1]); if (preg_match_all('/^([0-9a-f]{64})\s+nosignup\.(com|org|net|trade|chat|work|market|date|fun|info)\s*$/mi', $raw, $mm, PREG_SET_ORDER)) { foreach ($mm as $x) $out['faces']['nosignup.'.strtolower($x[2])] = strtolower($x[1]); } $need = ['nosignup.com','nosignup.org','nosignup.net','nosignup.trade','nosignup.chat','nosignup.work','nosignup.market','nosignup.date','nosignup.fun','nosignup.info']; $out['complete'] = ($out['fingerprint'] !== ''); foreach ($need as $k) if (!isset($out['faces'][$k])) { $out['complete']=false; break; } return $out; } function nsu_mirror_hello_payload(): array { $mf = nsu_mirror_manifest_state(); $actual = strtolower((string)@hash_file('sha256', __FILE__)); $keys = ['nosignup.trade']; $expected = ''; foreach ($keys as $k) { if (isset($mf['faces'][$k])) { $expected=(string)$mf['faces'][$k]; break; } } $faceMatch = ($actual !== '' && $expected !== '' && hash_equals($expected, $actual)); $manual = ((string)getenv('NSU_MIRROR_OVERLOAD') === '1') || is_file(__DIR__ . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'mirror.pause'); $loadRatio = null; $over = $manual; if (function_exists('sys_getloadavg')) { $la = @sys_getloadavg(); if (is_array($la) && isset($la[0])) { $cpus = 1; if (is_file('/proc/cpuinfo')) { $c=@file_get_contents('/proc/cpuinfo'); if (is_string($c)) { $n=preg_match_all('/^processor\s*:/m',$c); if ($n>0) $cpus=$n; } } $loadRatio = round(((float)$la[0]) / max(1,$cpus), 3); $mx = (float)(getenv('NSU_MIRROR_LOAD_RATIO_MAX') ?: 1.50); if ($mx > 0 && $loadRatio >= $mx) $over = true; } } $compatible = !empty($mf['complete']) && $faceMatch; return [ 'ok'=>true,'schema'=>'nsu-mirror-hello-v1','node_kind'=>'mirror','face'=>NSU_MIRROR_FACE, 'face_hash'=>$actual,'empire_fingerprint'=>(string)$mf['fingerprint'], 'complete_organism'=>(bool)$mf['complete'],'manifest_face_match'=>$faceMatch, 'compatible_for_service'=>$compatible,'accepting_traffic'=>$compatible && !$over, 'overloaded'=>$over,'load_ratio'=>$loadRatio, 'economy_mesh'=>false, 'economy_note'=>'Service mirror only. Decentralized NSU consensus is not claimed by this build.', 'capacity_authority'=>false, ]; } if ((string)($_GET['api'] ?? '') === 'mirror_hello') { if (PHP_SAPI !== 'cli') { header('Content-Type: application/json; charset=UTF-8'); header('Cache-Control: no-store, max-age=0'); header('Access-Control-Allow-Origin: *'); header('X-Content-Type-Options: nosniff'); header('Referrer-Policy: no-referrer'); } echo json_encode(nsu_mirror_hello_payload(), JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE); exit; } /* === /NSU V8 MIRROR SERVICE SUBSTRATE ===================================== */ if (preg_match('/^www\.(.+)$/i', preg_replace('/:\d+$/', '', (string)($_SERVER['HTTP_HOST'] ?? '')), $m)) { header('Location: //' . strtolower($m[1]) . (string)($_SERVER['REQUEST_URI'] ?? '/'), true, 301); exit; } /* Stock PHP 7.4 compatibility for the small PHP 8 string helper surface. */ if (!function_exists('str_contains')) { function str_contains(string $haystack, string $needle): bool { return $needle === '' || strpos($haystack, $needle) !== false; } } if (!function_exists('str_starts_with')) { function str_starts_with(string $haystack, string $needle): bool { return $needle === '' || strncmp($haystack, $needle, strlen($needle)) === 0; } } if (!function_exists('str_ends_with')) { function str_ends_with(string $haystack, string $needle): bool { if ($needle === '') return true; $length = strlen($needle); return $length <= strlen($haystack) && substr($haystack, -$length) === $needle; } } const NST_VERSION = '2026-07-13g'; /** * A new world starts with ALL coins already in the king wallet: the entire * 100 million NSU cap. After that, nothing is ever printed. No daily drip. * Public faucet stays 410 — buy on the book. Utterly fair except the king * starts with 100 million. Yearly wipe does not re-anchor or mint. * User balances never seized. Population crawl is metadata only. * Micros = whole_NSU × 1e6. */ const NST_DECIMALS = 6; const NST_MAX_SUPPLY = '100000000000000'; // 100_000_000 NSU - hard ceiling on all issuance const NST_GENESIS_TREASURY = NST_MAX_SUPPLY; // king holds the whole cap at birth /* ── MONETARY CONSTITUTION ──────────────────────────────────────────────────── * Every empire carries the SAME cap. No country can inflate its way to power * over a sister; an empire wins by being used, not by printing. That symmetry * is what makes a cross-empire rate mean anything, and it is why this value * must stay a fixed constant - never hardware-relative, never per-host. * Changing anything in this block changes the POLICY fingerprint. That is the * point: a king comparing empires reads the money, not the marketing. * ──────────────────────────────────────────────────────────────────────────── */ /* Emission machinery (paused). After birth remaining supply is 0, so this * cannot print even if someone flips NST_EMISSION_PAUSED. Kept as a no-op * path; nst_emission_run_daily returns immediately while paused. * Dormant formula: E(today) = (NST_MAX_SUPPLY - minted_to_date) * rate * A FIXED DAILY AMOUNT split among claimants - never a payment per claimant. * Paying per head makes issuance = participants x amount, and an attacker picks * the participant count (one IPv6 /64 is 18 quintillion addresses). Splitting a * fixed pool means extra identities dilute only the attacker. */ const NST_EMISSION_DAILY_RATE_PPM = 100; // 100 ppm = 0.01% of remaining, per day const NST_EMISSION_PAUSED = true; // 2026-08-27 hold: no daily mint, no crop/mirror drip /* THE DAY'S EMISSION SPLITS TEN WAYS - ONE SHARE PER CROP. NO POOLS. * * There used to be a 55/35/10 split into a UBI pool, a contribution pool and the * King. Both pools were constant strings rather than seed-derived addresses, so * no key could ever open them, and nothing in the code ever read them: 90% of * every coin minted went somewhere nobody could spend from. It was recoverable * (balances replay from the chain, so a payout added later reaches back) but it * was 90% of issuance parked behind an undesigned distribution rule. * * Operator decision, 2026-08-04: delete the pools. Each of the ten crops gets an * equal tenth of the day's emission, paid straight to that crop's lord wallet, * and the lord decides what to fund with it - advertising is the service every * crop offers from day one, others come later. Protocol-level UBI is gone; a * lord may choose to run one out of their own stream. * * The split is EQUAL and hard-coded at one-tenth on purpose. A percentage table * is a thing somebody has to maintain, argue about and get wrong; ten equal * shares is a thing nobody has to think about again. */ const NST_EMISSION_CROP_COUNT = 10; /* KING'S CUT - ONE RULE, APPLIED TO EVERY LORD RECEIPT. * * The King does not get a carve-out of emission and does not need a conditional * in each fee path. Instead x% comes off the top of ANY revenue landing in a * lord's wallet - their emission share, ad money, and whatever services get * built later. Because a crop's emission share is itself a lord receipt, this * single rule already delivers "a percent of everything issued from the faucet" * without a second mechanism to keep in sync. * * NEVER a user-to-user trade. An order-book fill or a plain transfer landing in * a lord's wallet is NOT revenue - taxing it would skim a lord for buying NSU * with their own money, which is precisely the gatekeeper's cut the ethos * refuses. The tax attaches to revenue row types, never to movement. * * Fees are TAXED, never DUPLICATED. Mirroring a fee into the King's faucet would * be an infinite mint: pay a lord you control, mirror it, repeat at zero cost. * * ON THE RATE. Operator-locked 2026-08-22: 100 ppm = 0.01%. The King already * receives a full crop share as lord of trade, so this tax only has to cover * the MARGINAL cost of the shared parts - the chain, the mint, propagation - * above what one crop's share already pays for. Tune this single constant; * nothing else needs to move. */ const NST_KING_TAX_PPM = 100; // 0.01% off the top of every lord receipt /** Marketplace cut when a buyer pays for an ad/service on another cell. * 100 ppm = 0.01%. Not applied when the purchase is ON trade itself * (Trade-as-lord already takes the remainder; no second marketplace cut). */ const NST_TRADE_CUT_PPM = 100; // 0.01% trade cut on other-site purchases /** Legacy metadata constant only (old pop-scale docs); liquid target ignores this. */ const NST_OPTIMUM_MICROS_PER_HUMAN = '1000000000000'; /** Fail-open world population if crawl/cache unavailable (UN-scale estimate). */ const NST_POP_FALLBACK = 8200000000; /** Cache TTL for crawled population (seconds). */ const NST_POP_CACHE_SECS = 604800; // 7 days const NST_FAUCET_MICROS = '5000000'; const NST_ORDER_MAX = 200; const NST_ORDER_MIN_NSU = '0.000001'; // one micro — smallest book size const NST_DATA = 'data'; const NST_MOD_DAILY_MICROS = '100000'; // 0.1 NSU per day const NST_RESET_SECS = 31536000; // 365 days - yearly superstructure const NST_EARLY_WINDOW_SECS = 2592000; // 30 days after each yearly reset /** King silence: 30 days without a money append. Do NOT reuse NST_EARLY_WINDOW_SECS. */ const NST_KING_SILENCE_SECS = 2592000; /** Operator-filled replica-holding trade peers. This host is always included in code. */ const NST_KNOWN_TRADE_NODES = []; /** P3a rent quote: price = RENT_NSU_PER_DAY * days_left (whole NSU → micros). Payment settle = later residual. */ const NST_RENT_NSU_PER_DAY = 100; // epoch rent R: Rent = 100 NSU x days left (not an ad-impression rate) const NST_RENT_QUOTE_TTL_SECS = 1800; // 30 minutes /** Dead address for treasury excess burn on re-anchor (not a user). */ const NST_BURN_ADDR = 'nsu-burn-v1-population-anchor'; /** When book mid missing: gift_nsu = external_amount * K (external in same unit as mid quote). */ const NST_DONATE_K = '1'; const NST_AD_MAX_PNG = 819200; // bytes — hard byte cap (800KB = 800*1024) const NST_AD_MAX_W = 800; // longest-side cap: max(w,h) ≤ 800 const NST_AD_MAX_H = 800; const NST_AD_REPORT_THRESHOLD = 5; // community reports to trigger takedown window const NST_AD_REPORT_SHOWINGS_WINDOW = 50; // takedown only while showings < 50 const NST_AD_MIN_W = 16; const NST_AD_MIN_H = 16; /** Public static donate rails (tentative; vault holds private seeds). */ const NST_DONATE_BTC = 'bc1qqnu6n0jztxl4f6krv7klradghle09uhyu7uymz'; const NST_DONATE_XMR = '8Ab24DppUvcdtHfm7K8gTqdBTmPCBiak1GwxgPm1C3osYVQL2QdC1C8GMwggKF77RKKzDgP2R8E3VH8ifetsKms5AqkVyVg'; const NST_DONATE_LTC = 'ltc1qlpdy8qzejcmjdn6vwarpyz8djdlk780w4qkwyp'; /** Operator / recruit contact (not an account desk). */ const NST_CONTACT_EMAIL = 'ads@nosignup.trade'; const NST_PROMO_MAILBOX = 'ads@nosignup.trade'; const NST_PROMO_AD_CREDIT_MICROS = '1000000000'; // 1000 NSU × 1e6 const NST_PROMO_AD_MAX_CLAIMS = 100; // FCFS cap; stop with a clear err when full /** * LORD rent GRANT path — RETIRED 2026-07-16 (NSU_VIABILITY_V1 CUT #4 / atomic P2). * Rent is PAYMENT TO treasury only (pay-to-treasury + payer-key bind = P3, not this ship). * Constants below are DEAD; nst_lord_rent_credit_compute is dead code; admin_rent_claim returns 410. * Do not re-enable treasury→LORD credit or server-generated LORD seeds. * * KING birth pile = the whole 100 million cap (no drip after). */ const LORD_RENT_CREDIT_LEGACY = 10000; // DEAD — retired flat placeholder (whole unit) const NST_KING_FAUCET_START_NSU = 100000000; // whole NSU — king holds the whole cap at birth /** @deprecated DEAD grant formula — do not use for product rent */ const LORD_RENT_PCT_BPS = 50; /** @deprecated DEAD grant formula */ const LORD_RENT_FLAT_BASE_NSU = 2500; /** @deprecated DEAD grant formula */ const LORD_RENT_FLAT_PERF_BPS = 100; /** @deprecated DEAD grant formula */ const LORD_RENT_FLAT_PERF_CAP_NSU = 50000; /** @deprecated DEAD grant formula */ const LORD_RENT_MIN_NSU = 1000; /** @deprecated DEAD grant formula */ const LORD_RENT_MAX_NSU = 500000; /** @deprecated DEAD — was API field compat for grant credit */ const LORD_RENT_CREDIT = LORD_RENT_CREDIT_LEGACY; /** * BOARD CAPACITY. People buy % share of the WHOLE board. * 100 NSU (in micros, NST_DECIMALS=6) = 100%. 1 NSU = 1% on an empty board. * Unsold remainder is a placeholder that redirects to https://nosignup.trade * so they can purchase ads. When paid stakes >= this, placeholder chance is 0 * and the lottery is relative among buyers. Not a second mint — spend still * circulates (lord + King tax). */ const NST_AD_BOARD_FULL_MICROS = '100000000'; // 100 NSU × 1e6 micros const NST_AD_SHARES_CAP = 10000; // one site board = 10000 shares const NST_AD_SHARE_FULL_YEAR_MICROS = '10000'; // 0.01 NSU per share at year start const NST_VANITY_FULL_YEAR_MICROS = '1000000'; // 1 NSU quote at year start (apply may stay 501) const NST_RENAME_COST_NSU = '10'; // displayed name-change cost; apply stays fail-closed until honest debit const NST_ID_PRESENCE_TTL_SECS = 2592000; // 30d inactivity for gossip-of-presence rows ONLY (never wallets/chain) /** * AD SPEND BUYS A SHARE OF THE EPOCH. THERE IS NO METER AND NO REFUND. * * The money is SPENT, not held: it leaves the buyer at purchase and lands with * the lord (the King's cut comes off the top of that receipt). What it buys is a * proportional chance of being the ad shown, across the whole empire, for the * REST OF THE EPOCH. Nothing is ever given back. * * PRICE DECAYS ON ITS OWN, WHICH IS THE POINT. A buy in month one purchases * twelve months of share; the same coins in month eleven purchase one. Nobody * sets that curve and no formula encodes it - late buyers are simply purchasing * less remaining time, so a rational bidder pays less as the wipe approaches. * More competition pushes the same way, since share is your spend over the * board's total. * * WHY NOT A PER-IMPRESSION METER. A build on 2026-08-04 replaced this with a * fixed rate (1 NSU = 1000 impressions) drawn down as ads were served. It looked * more honest and was strictly worse: a meter makes timing irrelevant, because * one NSU buys the same thousand impressions in January or December. That * deletes the decay above - the one signal that makes the board price itself - * and it invents a price the operator would then have to guess and maintain. * It also turned the busiest read in the empire into a locked disk write. * See NSU-DNA/02-REGRESSION-LEDGER.md R-010 (raised, then reversed). * * The ten crops that carry the board. Shown to a buyer so "a share of the * empire" is a checkable claim rather than a slogan. */ const NST_EMPIRE_DOMAINS = 'nosignup.com, nosignup.org, nosignup.net, nosignup.trade, ' . 'nosignup.chat, nosignup.work, nosignup.market, nosignup.date, nosignup.fun, nosignup.info'; /** * Bootstrap discovery book (temp anchor only — market P still converges from live book). * Anchor 1.0 NOTE/NSU = 1_000_000 micros; loose BUY half / SELL 1.5x. * Size each side ≈ treasury / NST_BOOTSTRAP_DENOM (0.1% when denom=1000). */ const NST_BOOTSTRAP_ANCHOR_PX = '1000000'; // 1.0 NOTE per NSU (micros) const NST_BOOTSTRAP_BUY_PX = '500000'; // 0.5 const NST_BOOTSTRAP_SELL_PX = '1500000'; // 1.5 const NST_BOOTSTRAP_DENOM = 1000; // amount = floor(treasury/denom) per side /** * OPERATOR_GO §4 / E2a: KING real fillable SELL ladder (NOT bootstrap_spread). * Three rungs around discovery anchor; small fixed size so book has TAKE-able asks. * Each SELL arms C2 half-BID via nst_treasury_half_bid_from_sell. */ const NST_LADDER_AMT_NSU = '100'; // 100 NSU per rung (micros via parse_amt) const NST_LADDER_AMT_MAX_NSU = '1000'; // hard cap per rung (anti-drain) const NST_LADDER_PX_LO = '1000000'; // 1.0 NOTE/NSU const NST_LADDER_PX_MID = '1250000'; // 1.25 const NST_LADDER_PX_HI = '1500000'; // 1.5 /** Public landing pair: NSU/LTC. Price = LTC per NSU. Sticker 0.001 = 1000 NSU for 1 LTC. */ const NST_PUBLIC_PAIR = 'NSU/LTC'; const NST_PUBLIC_PAIR_LABEL = 'LTC / NSU'; const NST_LTC_LOT_NSU = '1000'; const NST_LTC_LOT_PX_FMT = '0.001'; const NST_LTC_CONFIRMS = 1; const NST_LTC_TICKET_TTL = 2700; // 45 minutes /** One successful-or-attempted mutating trade per 5 seconds per actor. Server only. */ const NST_TX_GAP_SECS = 5; /** * GENESIS ONLY landing book (NSU/LTC). Price = LTC per NSU. * King ASKs list available treasury Zipf — fat at cheap 0.0001, thin at 0.1. * King bids ~1 LTC Zipf across 0.00001 … 0.0001. Geometric price steps (~24 rungs). * Does not remint. Does not cancel live orders. */ const NST_GENESIS_ASK_PX_LO_FMT = '0.0001'; const NST_GENESIS_ASK_PX_HI_FMT = '0.1'; const NST_GENESIS_BID_PX_LO_FMT = '0.00001'; const NST_GENESIS_BID_PX_HI_FMT = '0.0001'; const NST_GENESIS_BID_LTC_NOTIONAL = '1'; const NST_GENESIS_LADDER_RUNGS = 24; /** * Empire setup console (post-nuke / first host): * While data/empire_setup.json filled≠true, public visitors are redirected here. * Sister crops redirect to this host's ?empire_setup=1 (NST_EMPIRE_SETUP_PUBLIC_URL). * KING seeds treasury ladder + marks filled. AI never holds treasury seed. */ const NST_EMPIRE_SETUP_PUBLIC_URL = 'https://nosignup.trade/?empire_setup=1'; /** E2: throttle full conservation replay (seconds). Freeze file always honored. */ const NST_CONSERVATION_RECHECK_SECS = 300; /** Wallet Inbox v0: short-lived sealed notices (mail≠mint). Aligned with bid ephemerality. */ const NST_INBOX_TTL_SECS = 604800; // 7 days default const NST_INBOX_MAX_CT_HEX = 8192; const NST_INBOX_ALG = 'ecdh_p256_hkdf_sha256_aes256gcm_v1'; /** Device-wallet binding store: bounded, line-oriented, and isolated from money state. */ const NST_DEVICE_WALLET_BODY_MAX = 2048; const NST_DEVICE_WALLET_FILE_MAX = 16777216; const NST_DEVICE_WALLET_ROWS_MAX = 50000; const NST_DEVICE_WALLET_LINE_MAX = 384; const NST_DEVICE_WALLET_SEED_RAW_MAX = 256; const NST_DEVICE_WALLET_SEED_CANON_MAX = 255; const NST_DEVICE_WALLET_PROFILE_FIELDS_MAX = 16; const NST_DEVICE_WALLET_PROFILE_KEY_MAX = 32; const NST_DEVICE_WALLET_PROFILE_VALUE_MAX = 128; const NST_DEVICE_WALLET_COOKIE = 'nst_trade_device_v1'; const NST_DEVICE_WALLET_COOKIE_MAX_AGE = 31536000; // 365d rolling access lifetime; independent of service reset const NST_PUBLIC_BOOK_SIDE_MAX = 500; const NST_PUBLIC_RECENT_FILL_MAX = 200; $ROOT = __DIR__; /* Disk fallback ./data. Sibling vault is optional; one public_html is enough. */ $DATA = $ROOT . DIRECTORY_SEPARATOR . NST_DATA; $CHAIN = $DATA . DIRECTORY_SEPARATOR . 'chain.jsonl'; $CHAIN_TXN = $DATA . DIRECTORY_SEPARATOR . 'chain.txn'; $TFILE = $DATA . DIRECTORY_SEPARATOR . 'treasury.secret'; // KING vault seed (only minter) $SITE_SEED_FILE = $DATA . DIRECTORY_SEPARATOR . 'site.seed'; // trade panel wallet seed (not mint) $LTC_SECRET_FILE = $DATA . DIRECTORY_SEPARATOR . 'ltc.secret'; // KING LTC HD seed (trade console / SSH only) $LTC_WALLET_FILE = $DATA . DIRECTORY_SEPARATOR . 'ltc-wallet.json'; // HD index + tickets (no seed) $META = $DATA . DIRECTORY_SEPARATOR . 'meta.json'; $ECON = $DATA . DIRECTORY_SEPARATOR . 'economy.json'; $ADS = $DATA . DIRECTORY_SEPARATOR . 'ads'; $ADMIN_HASH_FILE = $DATA . DIRECTORY_SEPARATOR . 'admin.pass.hash'; $ADMIN_PASS_FILE = $DATA . DIRECTORY_SEPARATOR . 'admin.pass.txt'; // legacy plaintext migrate $MODS_FILE = $DATA . DIRECTORY_SEPARATOR . 'modifiers.json'; $OP_ADDR_FILE = $DATA . DIRECTORY_SEPARATOR . 'operator.addr'; // site panel addr (≠ treasury/KING on fresh genesis) /** P3b: durable open rent quotes (JSON map). Not chain; settle residual reads this. */ $RENT_QUOTES_FILE = $DATA . DIRECTORY_SEPARATOR . 'rent_quotes.json'; /** * Local/CLI execution must not escape the source tree. A normal web deployment keeps * the existing sibling-vault separation; Desktop/NSU and isolated candidate runs do not. */ function nst_source_local_runtime_required(string $root): bool { if (PHP_SAPI === 'cli' || PHP_SAPI === 'cli-server') { return true; } if ((string)getenv('NST_SOURCE_LOCAL_RUNTIME') === '1') { return true; } $paths = [$root]; $real = realpath($root); if (is_string($real) && $real !== '') { $paths[] = $real; } foreach ($paths as $path) { $normalized = str_replace('\\', '/', $path); if (preg_match('~(?:^|/)Desktop/NSU(?:/|$)~i', $normalized)) { return true; } if (preg_match('~(?:^|/)(?:baseline|candidate|candidates)(?:/|$)~i', $normalized)) { return true; } } return false; } $NST_SOURCE_LOCAL_RUNTIME = nst_source_local_runtime_required($ROOT); $VAULT_BASE = $NST_SOURCE_LOCAL_RUNTIME ? $ROOT : dirname($ROOT); $VAULT = $VAULT_BASE . DIRECTORY_SEPARATOR . 'vault'; /** Never let a front controller turn private state paths into a 200 product page. */ function nst_reject_private_http_path(): void { if (PHP_SAPI === 'cli') return; $uri = (string)($_SERVER['REQUEST_URI'] ?? ''); $path = (string)(@parse_url($uri, PHP_URL_PATH) ?? ''); for ($i = 0; $i < 2; $i++) { $decoded = rawurldecode($path); if ($decoded === $path) break; $path = $decoded; } $path = str_replace('\\', '/', $path); if (str_contains($path, "\0") || preg_match('~(?:^|/)(?:vault|data)(?:/|$)~i', $path)) { http_response_code(404); header('Content-Type: text/plain; charset=UTF-8'); header('Cache-Control: private, no-store, max-age=0, must-revalidate'); header('Pragma: no-cache'); header('Expires: 0'); header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: DENY'); echo "Not found\n"; exit; } } nst_reject_private_http_path(); nst_ad_try_public_serve(); /* PHP's development server can otherwise emit an uncaught exception page with * HTTP 200 and an absolute filesystem path. Every unhandled product failure is * a closed 503 with a non-secret body; CLI diagnostics retain a nonzero exit. */ set_exception_handler(static function (Throwable $error): void { if (PHP_SAPI === 'cli') { fwrite(STDERR, "nosignup.trade unavailable\n"); exit(1); } if (!headers_sent()) { http_response_code(503); header('Cache-Control: private, no-store, max-age=0, must-revalidate'); header('Pragma: no-cache'); header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: DENY'); } $api = (string)($_GET['api'] ?? $_POST['api'] ?? ''); if ($api !== '') { if (!headers_sent()) header('Content-Type: application/json; charset=UTF-8'); echo json_encode(['ok' => false, 'err' => 'service_unavailable'], JSON_UNESCAPED_SLASHES); } else { if (!headers_sent()) header('Content-Type: text/plain; charset=UTF-8'); echo "Service unavailable\n"; } exit; }); function j($x): string { return json_encode($x, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } /** PHP 8.0-compatible replacement for array_is_list(), added in PHP 8.1. */ function nst_is_list_compat(array $value): bool { if (function_exists('array_is_list')) { return array_is_list($value); } $expected = 0; foreach (array_keys($value) as $key) { if ($key !== $expected) { return false; } $expected++; } return true; } function norm_seed(string $s): string { return strtolower(trim(preg_replace('/\s+/', ' ', $s) ?? '')); } /** Path under durable $DATA — same path strings for L0/L1 money/identity (never RAM). */ function nst_data_file(string $name): string { global $DATA; return $DATA . DIRECTORY_SEPARATOR . $name; } /** * Ephemeral dir for regenerable L3 only (world_pop cache). * Prefer /dev/shm when present+writable; else durable $DATA/ephemeral/. * DENYLIST stays on disk: chain, treasury.secret, site.seed, ltc.secret, ltc-wallet.json, ltc-headers, * rent_quotes, economy, modifiers, operator.addr, admin.pass.hash, conservation.freeze, pubkey SoT. */ function nst_ephemeral_dir(): string { global $DATA, $NST_SOURCE_LOCAL_RUNTIME; static $cached = null; if ($cached !== null) { return $cached; } if (!$NST_SOURCE_LOCAL_RUNTIME && is_dir('/dev/shm') && is_writable('/dev/shm')) { $d = '/dev/shm/nosignup_trade_ephemeral'; if ((is_dir($d) || @mkdir($d, 0700, true)) && is_writable($d)) { return $cached = $d; } } $d = $DATA . DIRECTORY_SEPARATOR . 'ephemeral'; if (!is_dir($d)) { @mkdir($d, 0755, true); } /* not-ok if returned dir is missing or not writable — ns_worktable_banner reports it */ return $cached = $d; } /** Worktable health after mkdir. Banner only if the folder we use is missing/unwritable. Fail-open. */ function ns_worktable_problems(): array { global $DATA; if (!is_dir($DATA)) @mkdir($DATA, 0755, true); $disk = (string)$DATA; $ram = nst_ephemeral_dir(); $out = []; if (!ns_worktable_folder_ok($disk)) $out[] = ['kind' => 'disk', 'msg' => 'Disk folder cannot be created.', 'path' => $disk]; if (!ns_worktable_folder_ok($ram)) $out[] = ['kind' => 'ram', 'msg' => 'Memory folder cannot be created.', 'path' => $ram]; return $out; } function ns_worktable_folder_ok(string $d): bool { return $d !== '' && is_dir($d) && @is_writable($d); } function ns_worktable_banner(bool $public = true): string { $probs = ns_worktable_problems(); if (!$probs) return ''; $which = []; $paths = []; foreach ($probs as $p) { $which[] = $p['msg']; if (!$public && !empty($p['path'])) $paths[] = $p['path']; } $msg = 'This host cannot keep a work folder. Saves may not stick.'; if ($which) $msg .= ' ' . implode(' ', array_unique($which)); $extra = ''; if (!$public && $paths) { $extra = ' ' . htmlspecialchars(implode(' · ', $paths), ENT_QUOTES) . ''; } return '
' . htmlspecialchars($msg, ENT_QUOTES) . $extra . '
'; } /** * Server-only 1-per-5s mutating trade gate. * Key by wallet addr when present; else a cheap IP/connection key so omitting * addr is not a bypass. RAM worktable only. Fail-open if that folder is missing. * No cookie/header/token the client can omit. Stamp on attempt. */ function nst_tx_gate_peek_addr(): string { foreach (['from', 'addr'] as $k) { $a = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST[$k] ?? '')) ?? ''); if (strlen($a) === 64) return $a; } $seedRaw = $_POST['seed'] ?? null; if (function_exists('nst_device_wallet_normalize_seed_value')) { $norm = nst_device_wallet_normalize_seed_value($seedRaw); if (is_string($norm) && $norm !== '') { $a = addr_from_seed($norm); if (strlen($a) === 64) return $a; } } return ''; } function nst_tx_gate_key(string $addr): string { $addr = strtolower(preg_replace('/[^a-f0-9]/', '', $addr) ?? ''); $ip = function_exists('nst_king_client_ip') ? nst_king_client_ip() : strtolower(trim((string)($_SERVER['REMOTE_ADDR'] ?? ''))); if ($ip === '') $ip = '0'; // SA-02: the gate runs before authentication, so a claimed address is only a hint. // Bucket = claimed wallet + connection, so a stranger elsewhere cannot spend a known wallet's slot. if (strlen($addr) === 64) return 'a_' . hash('sha256', 'nst-tx-actor-v2|' . $addr . '|' . $ip); return 'i_' . hash('sha256', 'nst-tx-ip|' . $ip); } function nst_tx_gate_dir(): ?string { $ram = nst_ephemeral_dir(); if (!ns_worktable_folder_ok($ram)) return null; $d = $ram . DIRECTORY_SEPARATOR . 'txgate'; if (!is_dir($d)) @mkdir($d, 0700, true); if (!ns_worktable_folder_ok($d)) return null; return $d; } function nst_tx_too_fast_msg(string $addr): string { $addr = strtolower(preg_replace('/[^a-f0-9]/', '', $addr) ?? ''); $tid = ''; if (strlen($addr) === 64 && function_exists('nst_ticket_lookup_ticket')) { $have = nst_ticket_lookup_ticket($addr); if (is_string($have) && $have !== '') $tid = $have; } return $tid !== '' ? ('Apologies, ' . $tid . ", You're going too fast") : "Apologies, You're going too fast"; } function nst_tx_api_is_gated(string $api, string $method): bool { if (strtoupper($method) !== 'POST') return false; static $set = null; if ($set === null) { $set = array_fill_keys([ 'addr', 'accrue', 'register_pubkey', 'register_enc_pubkey', 'legacy_wallet_migrate', 'transfer', 'faucet', 'order', 'cancel', 'fill', 'ltc_buy_ticket', 'ad_stake', 'slowgpt_reserve', 'ad_swap', 'ad_give', 'vanity_apply', 'admin_pay', 'admin_treasury_ladder', 'admin_donate_credit', 'admin_extract_profit', 'admin_ltc_lot', 'admin_ltc_lot_cancel', 'admin_ad_creative', 'admin_rent_claim', 'admin_ltc_create', 'rent_quote', 'rent_settle', 'id', 'device_wallet', 'ad_promo_claim', 'job_admit', 'job_complete', 'job_cancel', ], true); } return isset($set[$api]); } function nst_tx_gate_enforce(string $addr = '', string $scope = ''): void { $dir = nst_tx_gate_dir(); if ($dir === null) return; $key = nst_tx_gate_key($addr); // Setup admission stays five seconds, independently of actual monetary operations. if ($scope !== '') $key = 'a_' . hash('sha256', 'nst-tx-scope-v1|' . $scope . '|' . $key); if (!preg_match('/^[ai]_[a-f0-9]{16,64}$/', $key)) return; $path = $dir . DIRECTORY_SEPARATOR . $key . '.ts'; $now = time(); $fh = @fopen($path, 'c+'); if (!is_resource($fh)) return; if (!@flock($fh, LOCK_EX)) { @fclose($fh); return; } $raw = stream_get_contents($fh); $last = (is_string($raw) && preg_match('/^[0-9]{1,12}$/', trim($raw))) ? (int)trim($raw) : 0; $gap = defined('NST_TX_GAP_SECS') ? (int)NST_TX_GAP_SECS : 5; if ($gap < 1) $gap = 5; if ($last > 0 && ($now - $last) < $gap) { @flock($fh, LOCK_UN); @fclose($fh); $msg = nst_tx_too_fast_msg($addr); if (function_exists('api_out')) { api_out(['ok' => false, 'err' => $msg, 'too_fast' => true], 429); } http_response_code(429); header('Content-Type: application/json; charset=UTF-8'); echo '{"ok":false,"err":' . json_encode($msg) . ',"too_fast":true}'; exit; } rewind($fh); ftruncate($fh, 0); fwrite($fh, (string)$now); fflush($fh); @flock($fh, LOCK_UN); @fclose($fh); } /** First two hex chars of addr (256 fan-out). Fallback crc32%256 if short. */ function nst_addr_shard2(string $addr): string { $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; if (strlen($a) >= 2) { return substr($a, 0, 2); } return sprintf('%02x', abs(crc32($a)) % 256); } /** Legacy addr (v1): hash of seed. Future dual: ECDSA P-256 pubkey-hash addr + this legacy. */ /** * AN EMPTY SEED HAS NO ADDRESS. * * This used to hash the empty string like any other input, producing the fixed * constant sha256("nsu-addr-v1|") = b6a89c23... That address looks completely * ordinary and no 12-word seed can ever derive it, so every coin sent there is * burned with no way back. * * It reached production. When trade could not read data/treasury.secret, * file_get_contents() returned false, trim(false) became '', and treasury_addr() * handed back that constant - so ?api=state reported a healthy-looking treasury * that held nothing and could never be spent. A missing key read as a valid * wallet instead of as an error. * * Returning '' makes the failure visible: callers already test for the empty * address (nst_treasury_addr_safe, the emission guard, the ad_stake fallback), * and an empty address can never be an output leg. */ function addr_from_seed(string $seed): string { $n = norm_seed($seed); if ($n === '') return ''; return hash('sha256', 'nsu-addr-v1|' . $n); } /** HMAC for browser_hmac_v1 (key includes seed → seed still needed to verify). */ function mac_sign(string $seed, string $msg): string { return hash_hmac('sha256', $msg, 'nsu-mac-v1|' . norm_seed($seed)); } /** ECDSA P-256 dual-path: seed-off-wire spend after pubkey registered for legacy addr. */ function nst_pubkey_path(string $addr): string { global $DATA; $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; $aa = nst_addr_shard2($a); $dir = $DATA . DIRECTORY_SEPARATOR . 'keys' . DIRECTORY_SEPARATOR . $aa; if (!is_dir($dir)) { @mkdir($dir, 0755, true); } return $dir . DIRECTORY_SEPARATOR . 'pubkey_' . $a . '.spki'; } /** Pre-footprint flat path (read fallback only; writes go to keys/{aa}/). */ function nst_pubkey_path_legacy(string $addr): string { global $DATA; return $DATA . DIRECTORY_SEPARATOR . 'pubkey_' . preg_replace('/[^a-f0-9]/', '', strtolower($addr)) . '.spki'; } function nst_addr_v2_from_spki_bin(string $spki): string { return hash('sha256', 'nsu-addr-v2|' . bin2hex($spki)); } /** IEEE P1363 (r||s, 64 bytes for P-256) → DER SEQUENCE for openssl_verify. */ function nst_ecdsa_p1363_to_der(string $p1363): ?string { if (strlen($p1363) !== 64) return null; $encInt = function (string $x): string { $x = ltrim($x, "\x00"); if ($x === '') $x = "\x00"; if ((ord($x[0]) & 0x80) !== 0) $x = "\x00" . $x; return "\x02" . chr(strlen($x)) . $x; }; $r = $encInt(substr($p1363, 0, 32)); $s = $encInt(substr($p1363, 32, 32)); $seq = $r . $s; return "\x30" . chr(strlen($seq)) . $seq; } /** True when host can verify ECDSA P-256 (OpenSSL extension). */ function nst_ecdsa_openssl_ready(): bool { return function_exists('openssl_verify') && function_exists('openssl_pkey_get_public'); } function nst_ecdsa_verify_p256(string $spkiBin, string $msg, string $sigP1363Hex): bool { if (!nst_ecdsa_openssl_ready()) return false; $sigRaw = @hex2bin(strtolower(preg_replace('/[^a-f0-9]/', '', $sigP1363Hex) ?? '')); if ($sigRaw === false || strlen($sigRaw) !== 64) return false; $der = nst_ecdsa_p1363_to_der($sigRaw); if ($der === null) return false; $pem = "-----BEGIN PUBLIC KEY-----\n" . chunk_split(base64_encode($spkiBin), 64, "\n") . "-----END PUBLIC KEY-----\n"; $pub = @openssl_pkey_get_public($pem); if ($pub === false) return false; $ok = openssl_verify($msg, $der, $pub, OPENSSL_ALGO_SHA256); return $ok === 1; } function nst_load_registered_spki(string $addr): ?string { $p = nst_pubkey_path($addr); if (!is_file($p)) { $p = nst_pubkey_path_legacy($addr); } if (!is_file($p)) return null; $hex = trim((string)@file_get_contents($p)); $bin = @hex2bin(strtolower(preg_replace('/[^a-f0-9]/', '', $hex) ?? '')); return ($bin !== false && strlen($bin) > 40) ? $bin : null; } function nst_save_registered_spki(string $addr, string $spkiBin): bool { if (strlen($spkiBin) < 40) return false; // SA-01: first binding wins atomically. A concurrent different key cannot overwrite it; // the same key re-registering stays idempotent. Every caller only saves when none exists. $path = nst_pubkey_path($addr); $tmp = $path . '.tmp-' . bin2hex(random_bytes(6)); if (@file_put_contents($tmp, bin2hex($spkiBin), LOCK_EX) === false) { @unlink($tmp); return false; } $linked = @link($tmp, $path); @unlink($tmp); if (!$linked && !file_exists($path)) { $fh = @fopen($path, 'x'); if (!is_resource($fh)) return false; $ok = @fwrite($fh, bin2hex($spkiBin)) === strlen(bin2hex($spkiBin)); @fflush($fh); @fclose($fh); if (!$ok) return false; } $now = nst_load_registered_spki($addr); return $now !== null && hash_equals($now, $spkiBin); } /** Wallet authority v2 — public SPKI fingerprint (hex of DER SPKI hex string? No: sha256 of SPKI bytes). */ function nst_wallet_sign_fp_from_spki_bin(string $spkiBin): string { return 'sha256:' . hash('sha256', $spkiBin); } function nst_wallet_sign_fp_from_spki_hex(string $spkiHex): string { $bin = @hex2bin(strtolower(preg_replace('/[^a-f0-9]/', '', $spkiHex) ?? '')); if ($bin === false || strlen($bin) < 40) return ''; return nst_wallet_sign_fp_from_spki_bin($bin); } function nst_empire_fp_public(): string { try { if (function_exists('nsu_mirror_manifest_state')) { $mf = nsu_mirror_manifest_state(); if (is_array($mf) && !empty($mf['fingerprint']) && preg_match('/\A[0-9a-f]{64}\z/D', (string)$mf['fingerprint'])) { return strtolower((string)$mf['fingerprint']); } } } catch (Throwable $e) {} // Fallback: policy hash label when empire manifest absent (still binds messages on this face). if (function_exists('nsu_policy_hash')) { $h = nsu_policy_hash(); if (is_string($h) && preg_match('/\A[0-9a-f]{64}\z/D', $h)) return $h; } return ''; } /** True when addr already has spendable balance, open orders, or any chain row touching it. */ function nst_addr_has_economic_history(string $addr): bool { $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; if (strlen($a) !== 64) return false; try { $chain = read_chain(false); $bal = balances($chain); if (dec_cmp((string)($bal[$a] ?? '0'), '0') > 0) return true; foreach (open_orders($chain) as $o) { if (hash_equals(strtolower((string)($o['addr'] ?? $o['from'] ?? '')), $a)) return true; } foreach ($chain as $tx) { if (!is_array($tx)) continue; foreach (['from', 'to', 'addr', 'rater', 'rated', 'actor', 'taker', 'maker'] as $k) { if (isset($tx[$k]) && hash_equals(strtolower((string)$tx[$k]), $a)) return true; } } } catch (Throwable $e) { // Fail closed: treat as having history so fresh self-register cannot take over ambiguously. return true; } return false; } function nst_wallet_meta_path(string $addr): string { global $DATA; $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; $aa = nst_addr_shard2($a); $dir = $DATA . DIRECTORY_SEPARATOR . 'keys' . DIRECTORY_SEPARATOR . $aa; if (!is_dir($dir)) @mkdir($dir, 0755, true); return $dir . DIRECTORY_SEPARATOR . 'walletmeta_' . $a . '.json'; } function nst_wallet_meta_load(string $addr): ?array { $p = nst_wallet_meta_path($addr); if (!is_file($p)) return null; $j = json_decode((string)@file_get_contents($p), true); return is_array($j) ? $j : null; } function nst_wallet_meta_save(string $addr, array $meta): bool { $meta['schema'] = 'nsu-wallet-key-v2'; $meta['addr'] = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; $meta['version'] = 2; $raw = json_encode($meta, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if (!is_string($raw)) return false; return @file_put_contents(nst_wallet_meta_path($addr), $raw, LOCK_EX) !== false; } function nst_wallet_register_msg_v2(string $empire, string $addr, string $spkiHex, string $nonce, string $ts): string { $spkiHash = hash('sha256', strtolower(preg_replace('/[^a-f0-9]/', '', $spkiHex) ?? '')); return 'NSU|WALLET-REGISTER|2|empire=' . $empire . '|addr=' . $addr . '|spki_sha256=' . $spkiHash . '|nonce=' . $nonce . '|ts=' . $ts; } function nst_wallet_enc_register_msg_v2(string $empire, string $addr, string $encSpkiHex, string $nonce, string $ts): string { $h = hash('sha256', strtolower(preg_replace('/[^a-f0-9]/', '', $encSpkiHex) ?? '')); return 'NSU|ENC-REGISTER|2|empire=' . $empire . '|addr=' . $addr . '|enc_spki_sha256=' . $h . '|nonce=' . $nonce . '|ts=' . $ts; } /** * WALLET INBOX v0 — address-keyed sealed mail (edit>invent on ECDSA/SPKI DNA). * KEY SEPARATION (documented): * - Signing key (ECDSA P-256): existing register_pubkey path (random key, AES-wrapped * at salt nsu-ecdsa-wrap-v1). Signs spends AND signs sealed inbox blobs. * - Encryption key (ECDH P-256): NEW, seed-derived (PBKDF2 salt nsu-ecdh-enc-v1, * 120000 iters → 32-byte scalar → PKCS8). Registered at encpub_.spki. * Distinct file + salt so sign≠encrypt. Recipient unlocks seed → re-derives priv → decrypts. * TRUE E2E: sender (KING panel client) encrypts; server stores OPAQUE ciphertext only. * Honesty: experimental until isolated e2e proof; never claim proven if plaintext touched server. * Mail ≠ mint: no NSU mint/balance change on seal/list/fetch/decrypt. */ function nst_encpub_path(string $addr): string { global $DATA; $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; $aa = nst_addr_shard2($a); $dir = $DATA . DIRECTORY_SEPARATOR . 'keys' . DIRECTORY_SEPARATOR . $aa; if (!is_dir($dir)) { @mkdir($dir, 0755, true); } return $dir . DIRECTORY_SEPARATOR . 'encpub_' . $a . '.spki'; } /** Pre-footprint flat path (read fallback only). */ function nst_encpub_path_legacy(string $addr): string { global $DATA; return $DATA . DIRECTORY_SEPARATOR . 'encpub_' . preg_replace('/[^a-f0-9]/', '', strtolower($addr)) . '.spki'; } function nst_load_registered_enc_spki(string $addr): ?string { $p = nst_encpub_path($addr); if (!is_file($p)) { $p = nst_encpub_path_legacy($addr); } if (!is_file($p)) return null; $hex = trim((string)@file_get_contents($p)); $bin = @hex2bin(strtolower(preg_replace('/[^a-f0-9]/', '', $hex) ?? '')); return ($bin !== false && strlen($bin) > 40) ? $bin : null; } function nst_save_registered_enc_spki(string $addr, string $spkiBin): bool { if (strlen($spkiBin) < 40) return false; return @file_put_contents(nst_encpub_path($addr), bin2hex($spkiBin), LOCK_EX) !== false; } /** * Wallet inbox blob path — sharded under data/inbox/{aa}/ (256 fan-out). * Filename keeps inbox_{addr}_{id}.blob so list/burn globs stay unambiguous. */ function nst_inbox_blob_path(string $addr, string $id): string { global $DATA; $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; $i = preg_replace('/[^a-f0-9]/', '', strtolower($id)) ?? ''; $aa = nst_addr_shard2($a); $dir = $DATA . DIRECTORY_SEPARATOR . 'inbox' . DIRECTORY_SEPARATOR . $aa; if (!is_dir($dir)) { @mkdir($dir, 0755, true); } return $dir . DIRECTORY_SEPARATOR . 'inbox_' . $a . '_' . $i . '.blob'; } /** Pre-footprint flat blob path (list/burn migration only). */ function nst_inbox_blob_path_legacy(string $addr, string $id): string { global $DATA; $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; $i = preg_replace('/[^a-f0-9]/', '', strtolower($id)) ?? ''; return $DATA . DIRECTORY_SEPARATOR . 'inbox_' . $a . '_' . $i . '.blob'; } function nst_inbox_sign_body(array $b): string { return implode('|', [ 'inbox_seal', '1', (string)($b['to'] ?? ''), (string)($b['from'] ?? ''), (string)($b['id'] ?? ''), (string)($b['ts'] ?? ''), (string)($b['exp'] ?? ''), (string)($b['eph_pub'] ?? ''), (string)($b['iv'] ?? ''), (string)($b['ct'] ?? ''), ]); } /** Collect inbox blob paths for addr: sharded inbox/{aa}/ + legacy flat (migration). */ function nst_inbox_glob_for_addr(string $addr): array { global $DATA; $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; if (strlen($a) !== 64) return []; $files = []; $aa = nst_addr_shard2($a); $shardDir = $DATA . DIRECTORY_SEPARATOR . 'inbox' . DIRECTORY_SEPARATOR . $aa; if (is_dir($shardDir)) { foreach (glob($shardDir . DIRECTORY_SEPARATOR . 'inbox_' . $a . '_*.blob') ?: [] as $f) { $files[] = $f; } } // Legacy flat data/inbox_{addr}_*.blob (pre-footprint) foreach (glob($DATA . DIRECTORY_SEPARATOR . 'inbox_' . $a . '_*.blob') ?: [] as $f) { $files[] = $f; } return array_values(array_unique($files)); } /** List non-expired sealed blobs for addr (metadata only; ct still opaque). */ function nst_inbox_list_for_addr(string $addr): array { $a = preg_replace('/[^a-f0-9]/', '', strtolower($addr)) ?? ''; if (strlen($a) !== 64) return []; $out = []; $now = time(); foreach (nst_inbox_glob_for_addr($a) as $f) { $raw = (string)@file_get_contents($f); $j = json_decode($raw, true); if (!is_array($j)) continue; $exp = (int)($j['exp'] ?? 0); if ($exp > 0 && $exp < $now) { @unlink($f); continue; } $out[] = [ 'id' => (string)($j['id'] ?? ''), 'to' => (string)($j['to'] ?? ''), 'from' => (string)($j['from'] ?? ''), 'ts' => (int)($j['ts'] ?? 0), 'exp' => $exp, 'kind' => (string)($j['kind'] ?? 'king_note'), 'alg' => (string)($j['alg'] ?? ''), 'bytes' => strlen($raw), 'honesty' => (string)($j['honesty'] ?? 'experimental · sealed mail · not proven e2e'), ]; } usort($out, static fn($x, $y) => ($y['ts'] ?? 0) <=> ($x['ts'] ?? 0)); return $out; } /** * Yearly wipe / epoch burn: sealed notices die with bids (balances survive). * CRITICAL: must recurse inbox/{aa}/ shards — missed burn = stale blob regression. * Also burns legacy flat data/inbox_*.blob. */ function nst_inbox_burn_all(): int { global $DATA; $n = 0; // Legacy flat foreach (glob($DATA . DIRECTORY_SEPARATOR . 'inbox_*.blob') ?: [] as $f) { if (@unlink($f)) { $n++; } } // Sharded tree: data/inbox/{aa}/inbox_*.blob $root = $DATA . DIRECTORY_SEPARATOR . 'inbox'; if (is_dir($root)) { foreach (glob($root . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR) ?: [] as $dir) { foreach (glob($dir . DIRECTORY_SEPARATOR . 'inbox_*.blob') ?: [] as $f) { if (@unlink($f)) { $n++; } } } // Any blobs directly under inbox/ (defensive) foreach (glob($root . DIRECTORY_SEPARATOR . 'inbox_*.blob') ?: [] as $f) { if (@unlink($f)) { $n++; } } } return $n; } /** * INBOX auto-wire (money-adjacent): SERVER-COMPOSED notices of PUBLIC book events. * Honesty floor: this is NOT private E2E — the server sees the (public) content and * ECDH-encrypts for delivery only. KING hand-sealed client-encrypt notes remain the * only true-E2E path. Mail ≠ mint: never writes chain balances. * Money-safety: call ONLY after money ops commit; always fail-open (try/catch). */ function nst_inbox_ec_ready(): bool { return function_exists('openssl_pkey_new') && function_exists('openssl_pkey_derive') && function_exists('openssl_encrypt') && function_exists('hash_hkdf') && defined('OPENSSL_KEYTYPE_EC'); } /** Export P-256 public key as SPKI hex (uncompressed). Fail → ''. */ function nst_inbox_spki_hex_from_pkey($pkey): string { if ($pkey === false || $pkey === null) return ''; $det = @openssl_pkey_get_details($pkey); if (!is_array($det)) return ''; if (isset($det['ec']['x'], $det['ec']['y'])) { $x = $det['ec']['x']; $y = $det['ec']['y']; if (strlen($x) < 32) $x = str_pad($x, 32, "\0", STR_PAD_LEFT); if (strlen($y) < 32) $y = str_pad($y, 32, "\0", STR_PAD_LEFT); if (strlen($x) > 32) $x = substr($x, -32); if (strlen($y) > 32) $y = substr($y, -32); $hdr = @hex2bin('3059301306072a8648ce3d020106082a8648ce3d030107034200'); if ($hdr === false) return ''; return bin2hex($hdr . "\x04" . $x . $y); } $pem = (string)($det['key'] ?? ''); $b64 = preg_replace('/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\s+/', '', $pem) ?? ''; $bin = base64_decode($b64, true); return ($bin !== false && strlen($bin) > 40) ? bin2hex($bin) : ''; } /** * Server-side ECDH→HKDF→AES-GCM seal (same wire as client inbox v0). * @return array{eph_pub:string,iv:string,ct:string}|null */ function nst_inbox_server_ecdh_seal(string $recipientSpkiBin, string $plaintext): ?array { if (!nst_inbox_ec_ready() || strlen($recipientSpkiBin) < 50) return null; try { $pem = "-----BEGIN PUBLIC KEY-----\n" . chunk_split(base64_encode($recipientSpkiBin), 64, "\n") . "-----END PUBLIC KEY-----\n"; $their = @openssl_pkey_get_public($pem); if ($their === false) return null; $opts = ['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']; $cnf = getenv('OPENSSL_CONF'); if (is_string($cnf) && $cnf !== '' && is_file($cnf)) { $opts['config'] = $cnf; } $eph = @openssl_pkey_new($opts); if ($eph === false) return null; $shared = @openssl_pkey_derive($their, $eph); if ($shared === false || $shared === '') return null; $key = hash_hkdf('sha256', $shared, 32, 'nsu-inbox-v1', ''); if (!is_string($key) || strlen($key) !== 32) return null; $iv = random_bytes(12); $tag = ''; $ct = @openssl_encrypt($plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, '', 16); if ($ct === false || strlen($tag) !== 16) return null; $ephHex = nst_inbox_spki_hex_from_pkey($eph); if (strlen($ephHex) < 100) return null; return [ 'eph_pub' => strtolower($ephHex), 'iv' => bin2hex($iv), 'ct' => bin2hex($ct . $tag), ]; } catch (Throwable $e) { return null; } } /** * Best-effort deliver a server_notice to $toAddr if enc-pubkey registered. * Fail-open: any error → skipped; never throws; never touches balances. * @param array $publicFacts public book facts only (no seeds/keys) * @return array{ok:bool,skipped?:bool,reason?:string,id?:string} */ function nst_inbox_deliver_server_notice(string $toAddr, string $event, array $publicFacts): array { try { $to = preg_replace('/[^a-f0-9]/', '', strtolower($toAddr)) ?? ''; if (strlen($to) !== 64) { return ['ok' => false, 'skipped' => true, 'reason' => 'bad_addr']; } $spki = nst_load_registered_enc_spki($to); if ($spki === null) { return ['ok' => false, 'skipped' => true, 'reason' => 'no_enc_pubkey']; } // Strip anything that must never appear in a notice (defense in depth). $safeFacts = []; foreach ($publicFacts as $k => $v) { $kk = preg_replace('/[^a-z0-9_]/', '', strtolower((string)$k)) ?? ''; if ($kk === '' || in_array($kk, ['seed', 'secret', 'treasury_secret', 'priv', 'private_key', 'pkcs8'], true)) { continue; } if (is_scalar($v) || $v === null) { $safeFacts[$kk] = $v; } else { $safeFacts[$kk] = substr(j($v), 0, 400); } } $payload = [ 'kind' => 'server_notice', 'event' => $event, 'facts' => $safeFacts, 'note' => 'server-composed notice of a public book event — encrypted for delivery, not a private secret; the event is public on the book', 'e2e' => false, 'server_composed' => true, 'mail_not_mint' => true, ]; $plaintext = j($payload); if (strlen($plaintext) > 1800) { $plaintext = substr($plaintext, 0, 1800); } $sealed = nst_inbox_server_ecdh_seal($spki, $plaintext); if ($sealed === null) { return ['ok' => false, 'skipped' => true, 'reason' => 'crypto_unavailable']; } if (strlen($sealed['ct']) > NST_INBOX_MAX_CT_HEX) { return ['ok' => false, 'skipped' => true, 'reason' => 'ct_too_large']; } $ts = time(); $ttl = NST_INBOX_TTL_SECS; $exp = $ts + $ttl; $factId = (string)($safeFacts['id'] ?? $safeFacts['order_id'] ?? $safeFacts['quote_id'] ?? ''); $id = substr(hash('sha256', 'server_notice|v1|' . $event . '|' . $to . '|' . $factId . '|' . (string)$ts . '|' . bin2hex(random_bytes(4))), 0, 32); $from = ''; try { $from = strtolower(addr_from_seed(treasury_secret())); } catch (Throwable $e) { $from = str_repeat('0', 64); } $blob = [ 'v' => 1, 'id' => $id, 'to' => $to, 'from' => $from, 'ts' => $ts, 'ttl' => $ttl, 'exp' => $exp, 'eph_pub' => $sealed['eph_pub'], 'iv' => $sealed['iv'], 'ct' => $sealed['ct'], 'alg' => NST_INBOX_ALG, 'kind' => 'server_notice', 'event' => $event, 'mail_not_mint' => true, 'server_composed' => true, 'honesty' => 'server-composed notice of a public book event — encrypted for delivery, not a private secret; the event is public on the book', 'sig_mode' => 'server_mac', ]; try { $sec = treasury_secret(); $blob['sig'] = mac_sign($sec, nst_inbox_sign_body($blob)); } catch (Throwable $e) { $blob['sig'] = ''; } $path = nst_inbox_blob_path($to, $id); if (@file_put_contents($path, j($blob), LOCK_EX) === false) { return ['ok' => false, 'skipped' => true, 'reason' => 'storage']; } return ['ok' => true, 'id' => $id, 'to' => $to, 'kind' => 'server_notice', 'event' => $event]; } catch (Throwable $e) { return ['ok' => false, 'skipped' => true, 'reason' => 'exception']; } } /** After fill commits: notify maker + taker (public fill facts). Fail-open. */ function nst_inbox_notify_fill(array $fill): void { try { $facts = [ 'event' => 'fill', 'order_id' => (string)($fill['order_id'] ?? ''), 'pair' => (string)($fill['pair'] ?? ''), 'side' => (string)($fill['side'] ?? ''), 'amount' => (string)($fill['amount_fmt'] ?? $fill['amount'] ?? ''), 'price' => (string)($fill['price_fmt'] ?? $fill['price'] ?? ''), 'maker' => (string)($fill['maker'] ?? ''), 'taker' => (string)($fill['taker'] ?? ''), 'ts' => (int)($fill['ts'] ?? time()), 'id' => (string)($fill['order_id'] ?? '') . '|' . (string)($fill['ts'] ?? '') . '|' . (string)($fill['taker'] ?? ''), ]; $maker = preg_replace('/[^a-f0-9]/', '', strtolower((string)($fill['maker'] ?? ''))) ?? ''; $taker = preg_replace('/[^a-f0-9]/', '', strtolower((string)($fill['taker'] ?? ''))) ?? ''; if (strlen($maker) === 64) { nst_inbox_deliver_server_notice($maker, 'fill', $facts); } if (strlen($taker) === 64 && $taker !== $maker) { nst_inbox_deliver_server_notice($taker, 'fill', $facts); } } catch (Throwable $e) { // fail-open } } /** After half-BID arms: notify treasury-sell context (public book floor). Fail-open. */ function nst_inbox_notify_half_bid(?array $sellOrder, array $halfBid): void { try { if (!empty($halfBid['skipped'])) { return; } $facts = [ 'event' => 'half_bid_armed', 'half_bid_id' => (string)($halfBid['id'] ?? ''), 'paired_sell_id' => (string)($halfBid['paired_sell_id'] ?? ($sellOrder['id'] ?? '')), 'pair' => (string)($halfBid['pair'] ?? ($sellOrder['pair'] ?? '')), 'amount' => (string)($halfBid['amount_fmt'] ?? $halfBid['amount'] ?? ''), 'price' => (string)($halfBid['price_fmt'] ?? $halfBid['price'] ?? ''), 'side' => 'buy', 'order_kind' => 'treasury_half_bid', 'ts' => (int)($halfBid['ts'] ?? time()), 'id' => (string)($halfBid['id'] ?? ''), ]; $to = preg_replace('/[^a-f0-9]/', '', strtolower((string)($sellOrder['addr'] ?? $halfBid['addr'] ?? ''))) ?? ''; if (strlen($to) === 64) { nst_inbox_deliver_server_notice($to, 'half_bid_armed', $facts); } } catch (Throwable $e) { // fail-open } } /** After rent settle commits: notify payer. Fail-open. */ function nst_inbox_notify_rent_settle(array $settle): void { try { if (empty($settle['settled'])) { return; } $payer = preg_replace('/[^a-f0-9]/', '', strtolower((string)($settle['payer'] ?? ''))) ?? ''; $lease = is_array($settle['lease'] ?? null) ? $settle['lease'] : []; $facts = [ 'event' => 'rent_settle', 'site' => (string)($settle['site'] ?? $lease['site'] ?? ''), 'quote_id' => (string)($settle['quote_id'] ?? $lease['quote_id'] ?? ''), 'payer' => $payer, 'amount' => (string)($lease['amount_fmt'] ?? $lease['amount'] ?? ''), 'ts' => (int)($lease['rented_at'] ?? time()), 'id' => (string)($settle['quote_id'] ?? ''), ]; if (strlen($payer) === 64) { nst_inbox_deliver_server_notice($payer, 'rent_settle', $facts); } } catch (Throwable $e) { // fail-open } } /** After rent refund commits: notify payer. Fail-open. */ function nst_inbox_notify_rent_refund(?array $refund): void { try { if (!is_array($refund) || empty($refund['refunded'])) { return; } $tx = is_array($refund['tx'] ?? null) ? $refund['tx'] : []; $payer = preg_replace('/[^a-f0-9]/', '', strtolower((string)($tx['to'] ?? ''))) ?? ''; $facts = [ 'event' => 'rent_refund', 'quote_id' => (string)($refund['quote_id'] ?? $tx['quote_id'] ?? ''), 'reason' => (string)($refund['reason'] ?? $tx['rent_refund_reason'] ?? ''), 'amount' => (string)($tx['amount_fmt'] ?? $tx['amount'] ?? ''), 'payer' => $payer, 'ts' => (int)($tx['ts'] ?? time()), 'id' => (string)($tx['rent_refund_of_nonce'] ?? $tx['nonce'] ?? ''), ]; if (strlen($payer) === 64) { nst_inbox_deliver_server_notice($payer, 'rent_refund', $facts); } } catch (Throwable $e) { // fail-open } } /** * Visitor actor (wallet authority v2): ECDSA P-256 required for normal mutations. * Seed fallback is intentionally disabled here — use api=legacy_wallet_migrate only * when an old funded wallet still needs first-time signer registration. * @return array{addr:string,seed:?string,ecdsa:bool,spki_hex:string,ecdsa_sig:string} */ function nst_actor(string $addrKey = 'from', bool $allowSeedFallback = false): array { $seedRaw = trim((string)($_POST['seed'] ?? '')); $spkiHex = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['pubkey'] ?? '')) ?? ''); $ecdsaSig = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['ecdsa_sig'] ?? '')) ?? ''); if ($spkiHex !== '' && $ecdsaSig !== '') { $addr = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST[$addrKey] ?? ''))) ?? ''; if (strlen($addr) !== 64) { $addr = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['from'] ?? ''))) ?? ''; } if (strlen($addr) !== 64) { $addr = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['addr'] ?? ''))) ?? ''; } if (strlen($addr) !== 64) { api_out(['ok' => false, 'err' => 'Need a 64-char hex wallet address'], 400); } $actor = [ 'addr' => $addr, 'seed' => null, 'ecdsa' => true, 'spki_hex' => $spkiHex, 'ecdsa_sig' => $ecdsaSig, ]; nst_device_wallet_guard_actor($actor); return $actor; } if (!$allowSeedFallback) { if ($seedRaw !== '') { api_out([ 'ok' => false, 'err' => 'Seed not accepted on this route — use ECDSA v2 signatures (or legacy_wallet_migrate once)', 'auth_required' => 'ecdsa_p256_v2', ], 401); } api_out([ 'ok' => false, 'err' => 'Need ECDSA signature (pubkey + ecdsa_sig) — seed fallback removed for normal spends', 'auth_required' => 'ecdsa_p256_v2', ], 401); } [$seed, $addr] = require_seed(); $actor = [ 'addr' => $addr, 'seed' => $seed, 'ecdsa' => false, 'spki_hex' => '', 'ecdsa_sig' => '', ]; nst_device_wallet_guard_actor($actor); return $actor; } /** * Verify signature over $body for an nst_actor() result. * ECDSA: registered SPKI + P-256 P1363 sig (override via $ecdsaSigOverride for secondary legs). * HMAC: optional client_sig ($clientSigKey) → browser_hmac_v1; else server_mac. * * @return array{sig_mode:string,sig:string,pubkey:string,seed_on_wire:bool} */ function nst_auth_sig(array $actor, string $body, string $clientSigKey = 'client_sig', ?string $ecdsaSigOverride = null): array { if (!empty($actor['ecdsa'])) { $spkiHex = (string)($actor['spki_hex'] ?? ''); $ecdsaSig = $ecdsaSigOverride !== null ? strtolower(preg_replace('/[^a-f0-9]/', '', $ecdsaSigOverride) ?? '') : (string)($actor['ecdsa_sig'] ?? ''); $spki = @hex2bin($spkiHex); if ($spki === false || strlen($spki) < 50) { api_out(['ok' => false, 'err' => 'Bad public key — check the key and try again'], 400); } $reg = nst_load_registered_spki((string)$actor['addr']); if ($reg === null || !hash_equals($reg, $spki)) { api_out(['ok' => false, 'err' => 'pubkey not registered — run wallet register / legacy_wallet_migrate'], 403); } if ($ecdsaSig === '' || !nst_ecdsa_verify_p256($spki, $body, $ecdsaSig)) { api_out(['ok' => false, 'err' => 'bad ecdsa signature'], 400); } return [ 'sig_mode' => 'ecdsa_p256_v2', 'sig' => $ecdsaSig, 'pubkey' => $spkiHex, 'seed_on_wire' => false, ]; } // Seed/HMAC path only when caller explicitly allowed seed fallback (legacy migrate). $seed = (string)($actor['seed'] ?? ''); if ($seed === '') { api_out(['ok' => false, 'err' => 'Need ECDSA signature — seed fallback disabled'], 401); } $expect = mac_sign($seed, $body); $clientSig = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST[$clientSigKey] ?? ''))) ?? ''; $sigMode = 'server_mac'; if ($clientSig !== '') { if (strlen($clientSig) !== 64 || !hash_equals($expect, $clientSig)) { api_out(['ok' => false, 'err' => 'bad client signature — re-unlock and retry'], 400); } $sigMode = 'browser_hmac_v1'; } return [ 'sig_mode' => $sigMode, 'sig' => $expect, 'pubkey' => '', 'seed_on_wire' => true, ]; } /* NS_A1_ECDSA_V2_LABELS 2026-09-26 */ function nst_sig_is_ecdsa(string $mode): bool { return str_starts_with(strtolower($mode), 'ecdsa_p256'); } function dec_ok(string $a): bool { return (bool)preg_match('/^[0-9]+$/', $a); } function dec_norm(string $a): string { $a = ltrim($a, '0'); return $a === '' ? '0' : $a; } function dec_cmp(string $a, string $b): int { $a = dec_norm($a); $b = dec_norm($b); if (strlen($a) !== strlen($b)) return strlen($a) < strlen($b) ? -1 : 1; return $a <=> $b; } function dec_add(string $a, string $b): string { $a = dec_norm($a); $b = dec_norm($b); if (function_exists('bcadd')) return bcadd($a, $b, 0); $i = strlen($a) - 1; $j = strlen($b) - 1; $c = 0; $r = ''; while ($i >= 0 || $j >= 0 || $c) { $s = $c + ($i >= 0 ? (int)$a[$i--] : 0) + ($j >= 0 ? (int)$b[$j--] : 0); $r = (string)($s % 10) . $r; $c = intdiv($s, 10); } return dec_norm($r); } function dec_sub(string $a, string $b): string { if (dec_cmp($a, $b) < 0) return '-1'; if (function_exists('bcsub')) return bcsub(dec_norm($a), dec_norm($b), 0); $a = str_pad(dec_norm($a), max(strlen($a), strlen($b)), '0', STR_PAD_LEFT); $b = str_pad(dec_norm($b), strlen($a), '0', STR_PAD_LEFT); $c = 0; $r = ''; for ($i = strlen($a) - 1; $i >= 0; $i--) { $d = (int)$a[$i] - $c - (int)$b[$i]; if ($d < 0) { $d += 10; $c = 1; } else { $c = 0; } $r = (string)$d . $r; } return dec_norm($r); } function dec_mul_small(string $a, int $m): string { if (function_exists('bcmul')) return bcmul(dec_norm($a), (string)$m, 0); $a = dec_norm($a); $c = 0; $r = ''; for ($i = strlen($a) - 1; $i >= 0; $i--) { $p = (int)$a[$i] * $m + $c; $r = (string)($p % 10) . $r; $c = intdiv($p, 10); } while ($c > 0) { $r = (string)($c % 10) . $r; $c = intdiv($c, 10); } return dec_norm($r); } function fmt_amt(string $micros): string { $micros = dec_norm($micros); if (strlen($micros) <= NST_DECIMALS) { $whole = '0'; $frac = str_pad($micros, NST_DECIMALS, '0', STR_PAD_LEFT); } else { $whole = substr($micros, 0, -NST_DECIMALS); $frac = substr($micros, -NST_DECIMALS); } $frac = rtrim($frac, '0'); return $frac === '' ? $whole : "$whole.$frac"; } function parse_amt(string $h): ?string { $h = trim($h); if (!preg_match('/^\d+(\.\d{1,' . NST_DECIMALS . '})?$/', $h)) return null; if (strpos($h, '.') === false) return dec_mul_small($h, (int)str_pad('1', NST_DECIMALS + 1, '0')); [$w, $f] = explode('.', $h, 2); $f = str_pad($f, NST_DECIMALS, '0', STR_PAD_RIGHT); return dec_add(dec_mul_small($w === '' ? '0' : $w, (int)str_pad('1', NST_DECIMALS + 1, '0')), dec_norm($f)); } /* ==== BEGIN WAVEB-TR05-TR14 MONEY AUTHORITY (do not split) ==== */ /** * WAVEB-TR05..14 — Trade economy V2 money/transaction authority helpers. * Injected into 4.php. Purchase gates remain CLOSED until TR-10/11 acceptance passes. * Preserve: money law, ECDSA-v2, no seed-on-wire, 100M once-at-birth, no faucet/remint. */ /** TR-05: require a scalar string input (reject arrays/objects/bool/null). */ function nst_req_scalar_string(mixed $raw, string $field = 'input'): array { if ($raw === null) { return ['ok' => false, 'err' => $field . ' missing', 'value' => null]; } if (is_bool($raw) || is_array($raw) || is_object($raw)) { return ['ok' => false, 'err' => $field . ' must be a scalar string', 'value' => null]; } if (is_int($raw) || is_float($raw)) { // Numeric POST types: only accept finite ints that stringify cleanly; reject floats/exponents upstream. if (is_float($raw) && (!is_finite($raw) || floor($raw) !== $raw)) { return ['ok' => false, 'err' => $field . ' must be a decimal string', 'value' => null]; } $raw = (string)(int)$raw; } if (!is_string($raw)) { return ['ok' => false, 'err' => $field . ' must be a string', 'value' => null]; } return ['ok' => true, 'err' => '', 'value' => $raw]; } /** * TR-05: strict money parse → canonical nonnegative integer-micro string. * Supported human grammar: optional leading zeros integer, optional '.' + 1..NST_DECIMALS digits. * Rejects: signs, whitespace interior, scientific notation, Unicode digits, >6 decimals, empty, NaN words. * @return array{ok:bool,err:string,micros:?string,canonical_human:?string} */ function nst_money_parse_input(mixed $raw, bool $requirePositive = false): array { $s = nst_req_scalar_string($raw, 'amount'); if (!$s['ok']) { return ['ok' => false, 'err' => $s['err'], 'micros' => null, 'canonical_human' => null]; } $h = trim((string)$s['value']); if ($h === '') { return ['ok' => false, 'err' => 'amount empty', 'micros' => null, 'canonical_human' => null]; } // Bound length (human form) — protects against pathological digit strings. if (strlen($h) > 48) { return ['ok' => false, 'err' => 'amount too long', 'micros' => null, 'canonical_human' => null]; } $lower = strtolower($h); if ($lower === 'nan' || $lower === 'inf' || $lower === 'infinity' || $lower === '-inf' || $lower === '-infinity') { return ['ok' => false, 'err' => 'amount not a number', 'micros' => null, 'canonical_human' => null]; } // Reject any non-ASCII digit / sign / exponent marker early. if (preg_match('/[^\d.]/', $h)) { return ['ok' => false, 'err' => 'amount grammar rejected', 'micros' => null, 'canonical_human' => null]; } if (substr_count($h, '.') > 1) { return ['ok' => false, 'err' => 'amount grammar rejected', 'micros' => null, 'canonical_human' => null]; } if (!preg_match('/^\d+(\.\d{1,' . NST_DECIMALS . '})?$/', $h)) { return ['ok' => false, 'err' => 'amount grammar rejected', 'micros' => null, 'canonical_human' => null]; } $micros = parse_amt($h); if ($micros === null || !nst_int_micros_ok($micros)) { return ['ok' => false, 'err' => 'amount parse failed', 'micros' => null, 'canonical_human' => null]; } if ($requirePositive && dec_cmp($micros, '0') <= 0) { return ['ok' => false, 'err' => 'amount must be positive', 'micros' => null, 'canonical_human' => null]; } // Canonical human without leading-zero weirdness (except "0"). $canon = fmt_amt($micros); return ['ok' => true, 'err' => '', 'micros' => $micros, 'canonical_human' => $canon]; } /** TR-05: 64-hex lowercase address. */ function nst_addr64_ok(string $addr): bool { return (bool)preg_match('/^[a-f0-9]{64}$/', $addr); } function nst_addr64_parse(mixed $raw, string $field = 'addr'): array { $s = nst_req_scalar_string($raw, $field); if (!$s['ok']) { return ['ok' => false, 'err' => $s['err'], 'addr' => '']; } $hex = strtolower(preg_replace('/[^a-f0-9]/', '', (string)$s['value']) ?? ''); if (!nst_addr64_ok($hex)) { return ['ok' => false, 'err' => $field . ' must be 64-char hex', 'addr' => '']; } return ['ok' => true, 'err' => '', 'addr' => $hex]; } function nst_nonce_parse(mixed $raw, string $field = 'nonce'): array { $s = nst_req_scalar_string($raw, $field); if (!$s['ok']) { return ['ok' => false, 'err' => $s['err'], 'nonce' => '']; } $n = substr(trim((string)$s['value']), 0, 64); if ($n === '' || !preg_match('/^[A-Za-z0-9._:-]+$/', $n)) { return ['ok' => false, 'err' => $field . ' invalid', 'nonce' => '']; } return ['ok' => true, 'err' => '', 'nonce' => $n]; } function nst_epoch_n_parse(mixed $raw, string $field = 'epoch_n'): array { $s = nst_req_scalar_string($raw, $field); if (!$s['ok']) { // allow plain int if (is_int($raw) && $raw > 0) { return ['ok' => true, 'err' => '', 'epoch_n' => $raw]; } return ['ok' => false, 'err' => $s['err'], 'epoch_n' => 0]; } $t = trim((string)$s['value']); if (!preg_match('/^[1-9]\d{0,8}$/', $t)) { return ['ok' => false, 'err' => $field . ' invalid', 'epoch_n' => 0]; } return ['ok' => true, 'err' => '', 'epoch_n' => (int)$t]; } /** * TR-06: canonical signed economic intent v2 encoding. * Fixed field order, types as strings, deterministic UTF-8. No delimiter-smash of free text. * Body version prefix "nsu-econ-intent-v2". */ function nst_econ_intent_v2_fields(array $f): array { $keys = [ 'schema', 'op', 'network', 'payer', 'site', 'epoch_n', 'quote_id', 'amount_micros', 'creative_sha256', 'href', 'label', 'slot_mode', 'nonce', 'ts', 'exp', ]; $out = []; foreach ($keys as $k) { $out[$k] = array_key_exists($k, $f) ? (string)$f[$k] : ''; } if ($out['schema'] === '') { $out['schema'] = 'nsu-econ-intent-v2'; } return $out; } function nst_econ_intent_v2_canonical(array $f): string { $n = nst_econ_intent_v2_fields($f); // JSON with fixed key order, no slash escape, unicode unescaped. return json_encode($n, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } function nst_econ_intent_v2_digest(array $f): string { return hash('sha256', nst_econ_intent_v2_canonical($f)); } /** Signing body for ECDSA-v2 auth helper (pipe form kept for nst_auth_sig compatibility). */ function nst_econ_intent_v2_auth_body(array $f): string { $n = nst_econ_intent_v2_fields($f); return implode('|', [ 'ad_purchase_v2', '2', $n['payer'], $n['amount_micros'], $n['site'], $n['epoch_n'], $n['quote_id'], $n['creative_sha256'], $n['nonce'], $n['ts'], $n['exp'], $n['slot_mode'], hash('sha256', $n['href'] . "\0" . $n['label']), ]); } /** * TR-07: versioned quote with identity + short expiry. Advisory until rechecked under lock. */ function nst_ad_quote_rule_ver(): string { return 'slot-day-v1'; } function nst_ad_quote_v2_build(string $site, ?int $now = null): array { $site = nst_ad_site_norm($site); $now = $now ?? time(); $ep = nst_epoch_info_read_only(); $epoch_n = (int)($ep['epoch_n'] ?? 1); $days = nst_ad_slot_days_left(); $nsu = nst_ad_slot_price_nsu(); $micros = parse_amt((string)$nsu); if ($micros === null) { $micros = '0'; } $exp = $now + 120; // short quote TTL $rule = nst_ad_quote_rule_ver(); $payload = [ 'schema' => 'nsu-ad-quote-v2', 'site' => $site, 'epoch_n' => (string)$epoch_n, 'price_micros' => $micros, 'days_left' => (string)$days, 'rule_ver' => $rule, 'issued_at' => (string)$now, 'exp' => (string)$exp, 'nsu_per_day' => (string)(int)NST_RENT_NSU_PER_DAY, ]; $quote_id = hash('sha256', json_encode($payload, JSON_UNESCAPED_SLASHES)); $payload['quote_id'] = $quote_id; $ready = $site !== '' ? nst_ad_spend_ready($site) : ['ok' => false, 'why' => 'unknown site']; $payload['buyable'] = !empty($ready['ok']); // WAVEB2: mirrors spend_ready after TR-10/11 green $payload['spend_ready'] = !empty($ready['ok']); $payload['why'] = !empty($ready['ok']) ? '' : (string)($ready['why'] ?? 'unavailable'); return $payload; } function nst_ad_quote_v2_recheck(array $quote, string $site, string $amountMicros, ?int $now = null): array { $now = $now ?? time(); if (($quote['schema'] ?? '') !== 'nsu-ad-quote-v2') { return ['ok' => false, 'code' => 'quote_invalid', 'err' => 'Unknown quote schema']; } if ((string)($quote['site'] ?? '') !== nst_ad_site_norm($site)) { return ['ok' => false, 'code' => 'quote_changed', 'err' => 'Quote site mismatch']; } if ((int)($quote['exp'] ?? 0) < $now) { return ['ok' => false, 'code' => 'quote_expired', 'err' => 'Quote expired']; } if ((string)($quote['rule_ver'] ?? '') !== nst_ad_quote_rule_ver()) { return ['ok' => false, 'code' => 'quote_changed', 'err' => 'Quote rule version changed']; } $fresh = nst_ad_quote_v2_build($site, $now); if ((string)($quote['epoch_n'] ?? '') !== (string)$fresh['epoch_n']) { return ['ok' => false, 'code' => 'quote_changed', 'err' => 'Epoch changed', 'new_quote' => $fresh]; } if ((string)($quote['price_micros'] ?? '') !== (string)$fresh['price_micros']) { return ['ok' => false, 'code' => 'quote_changed', 'err' => 'Price changed', 'new_quote' => $fresh]; } if (dec_cmp((string)$amountMicros, (string)$quote['price_micros']) !== 0) { return ['ok' => false, 'code' => 'quote_changed', 'err' => 'Signed amount does not match quote']; } // Recompute quote_id over the same fields the issuer used (without buyable flags). $check = $quote; unset($check['buyable'], $check['spend_ready'], $check['why'], $check['quote_id']); $expectId = hash('sha256', json_encode([ 'schema' => 'nsu-ad-quote-v2', 'site' => (string)($check['site'] ?? ''), 'epoch_n' => (string)($check['epoch_n'] ?? ''), 'price_micros' => (string)($check['price_micros'] ?? ''), 'days_left' => (string)($check['days_left'] ?? ''), 'rule_ver' => (string)($check['rule_ver'] ?? ''), 'issued_at' => (string)($check['issued_at'] ?? ''), 'exp' => (string)($check['exp'] ?? ''), 'nsu_per_day' => (string)($check['nsu_per_day'] ?? ''), ], JSON_UNESCAPED_SLASHES)); if (!hash_equals($expectId, (string)($quote['quote_id'] ?? ''))) { return ['ok' => false, 'code' => 'quote_invalid', 'err' => 'Quote identity mismatch']; } return ['ok' => true, 'code' => 'ok', 'err' => '', 'quote' => $quote]; } /** * TR-02: server-issued ad purchase quotes. This file is not the money chain. * A missing chain.jsonl is never created here. */ function nst_ad_purchase_quote_store_path(): string { return nst_data_file('ad_purchase_quotes.json'); } function nst_ad_purchase_quote_canonical_body(array $quote): array { return [ 'schema' => 'nsu-ad-quote-v2', 'site' => (string)($quote['site'] ?? ''), 'epoch_n' => (string)($quote['epoch_n'] ?? ''), 'price_micros' => (string)($quote['price_micros'] ?? ''), 'days_left' => (string)($quote['days_left'] ?? ''), 'rule_ver' => (string)($quote['rule_ver'] ?? ''), 'issued_at' => (string)($quote['issued_at'] ?? ''), 'exp' => (string)($quote['exp'] ?? ''), 'nsu_per_day' => (string)($quote['nsu_per_day'] ?? ''), ]; } function nst_ad_purchase_quote_expected_id(array $quote): string { $body = nst_ad_purchase_quote_canonical_body($quote); $json = json_encode($body, JSON_UNESCAPED_SLASHES); if ($json === false) { return ''; } return hash('sha256', $json); } function nst_ad_purchase_quote_persist(array $quote): array { $body = nst_ad_purchase_quote_canonical_body($quote); if ($body['schema'] !== 'nsu-ad-quote-v2' || $body['site'] === '' || $body['epoch_n'] === '' || $body['price_micros'] === '' || $body['rule_ver'] === '' || $body['exp'] === '' || $body['issued_at'] === '') { return ['ok' => false, 'err' => 'quote incomplete']; } $id = nst_ad_purchase_quote_expected_id($body); $given = strtolower(trim((string)($quote['quote_id'] ?? ''))); if ($given !== '' && !hash_equals($id, $given)) { return ['ok' => false, 'err' => 'quote identity mismatch']; } $row = $body; $row['quote_id'] = $id; $row['currency'] = 'NSU'; $sha = strtolower(trim((string)($quote['creative_sha256'] ?? ''))); if ($sha !== '' && preg_match('/^[a-f0-9]{64}$/', $sha) === 1) { $row['creative_sha256'] = $sha; } $path = nst_ad_purchase_quote_store_path(); $ret = nst_with_file_lock($path, static function ($fh) use ($row, $id) { rewind($fh); $raw = stream_get_contents($fh); $quotes = []; if (is_string($raw) && trim($raw) !== '') { $j = json_decode($raw, true); if (is_array($j) && isset($j['quotes']) && is_array($j['quotes'])) { $quotes = $j['quotes']; } } $now = time(); $kept = []; foreach ($quotes as $qid => $old) { if (!is_array($old)) { continue; } $exp = (int)($old['exp'] ?? 0); if ($exp > 0 && $exp < ($now - 7 * 86400)) { continue; } $kept[(string)($old['quote_id'] ?? $qid)] = $old; } $kept[$id] = $row; if (count($kept) > 400) { $kept = array_slice($kept, -400, null, true); } $state = [ 'schema' => 'nsu-ad-purchase-quotes-v1', 'quotes' => $kept, 'updated' => $now, ]; $blob = json_encode($state, JSON_UNESCAPED_SLASHES); if ($blob === false) { return ['ok' => false, 'err' => 'quote encode failed']; } rewind($fh); ftruncate($fh, 0); if (fwrite($fh, $blob) === false) { return ['ok' => false, 'err' => 'quote write failed']; } fflush($fh); return ['ok' => true, 'quote' => $row]; }); if (!is_array($ret) || empty($ret['ok'])) { return ['ok' => false, 'err' => is_array($ret) ? (string)($ret['err'] ?? 'quote store unavailable') : 'quote store unavailable']; } return $ret; } function nst_ad_purchase_quote_lookup(string $quoteId): ?array { $quoteId = strtolower(trim($quoteId)); if (preg_match('/^[a-f0-9]{64}$/', $quoteId) !== 1) { return null; } $path = nst_ad_purchase_quote_store_path(); if (!is_file($path)) { return null; } $found = null; nst_with_file_lock($path, static function ($fh) use ($quoteId, &$found) { rewind($fh); $raw = stream_get_contents($fh); $j = is_string($raw) ? json_decode($raw, true) : null; $row = (is_array($j) && isset($j['quotes'][$quoteId]) && is_array($j['quotes'][$quoteId])) ? $j['quotes'][$quoteId] : null; $found = $row; return $j; }); return is_array($found) ? $found : null; } /** Bind the first creative that passes quote checks. A later different image fails. Does not touch the chain. */ function nst_ad_purchase_quote_bind_creative(string $quoteId, string $creativeSha): array { $quoteId = strtolower(trim($quoteId)); $creativeSha = strtolower(trim($creativeSha)); if (preg_match('/^[a-f0-9]{64}$/', $quoteId) !== 1 || preg_match('/^[a-f0-9]{64}$/', $creativeSha) !== 1) { return ['ok' => false, 'err' => 'creative binding invalid']; } $path = nst_ad_purchase_quote_store_path(); $ret = nst_with_file_lock($path, static function ($fh) use ($quoteId, $creativeSha) { rewind($fh); $raw = stream_get_contents($fh); $j = is_string($raw) ? json_decode($raw, true) : null; if (!is_array($j) || !isset($j['quotes']) || !is_array($j['quotes']) || !isset($j['quotes'][$quoteId]) || !is_array($j['quotes'][$quoteId])) { return ['ok' => false, 'err' => 'Unknown quote']; } $row = $j['quotes'][$quoteId]; $have = strtolower(trim((string)($row['creative_sha256'] ?? ''))); if ($have !== '') { if (!hash_equals($have, $creativeSha)) { return ['ok' => false, 'code' => 'quote_changed', 'err' => 'Creative does not match the issued quote']; } return ['ok' => true, 'quote' => $row]; } $row['creative_sha256'] = $creativeSha; $j['quotes'][$quoteId] = $row; $j['updated'] = time(); $blob = json_encode($j, JSON_UNESCAPED_SLASHES); if ($blob === false) { return ['ok' => false, 'err' => 'quote encode failed']; } rewind($fh); ftruncate($fh, 0); if (fwrite($fh, $blob) === false) { return ['ok' => false, 'err' => 'quote write failed']; } fflush($fh); return ['ok' => true, 'quote' => $row]; }); if (!is_array($ret)) { return ['ok' => false, 'err' => 'quote store unavailable']; } return $ret; } function nst_ad_purchase_quote_client_conflict(array $client, array $server, string $creativeSha): string { $allowed = [ 'schema' => 'schema', 'site' => 'site', 'epoch_n' => 'epoch_n', 'price_micros' => 'price_micros', 'days_left' => 'days_left', 'rule_ver' => 'rule_ver', 'issued_at' => 'issued_at', 'exp' => 'exp', 'nsu_per_day' => 'nsu_per_day', 'quote_id' => 'quote_id', 'currency' => 'currency', 'creative_sha256' => 'creative_sha256', ]; foreach ($client as $k => $v) { if (!is_string($k) || is_array($v) || is_object($v)) { return 'malformed quote field'; } if (!isset($allowed[$k])) { return 'quote field cannot satisfy the check'; } $sv = trim((string)$v); if ($sv === '') { continue; } if ($k === 'currency') { if (strtoupper($sv) !== 'NSU') { return 'quote currency disagreement'; } $have = strtoupper((string)($server['currency'] ?? 'NSU')); if ($have !== 'NSU') { return 'quote currency disagreement'; } continue; } if ($k === 'creative_sha256') { $got = strtolower($sv); if (!hash_equals($got, strtolower($creativeSha))) { return 'quote creative disagreement'; } $bound = strtolower(trim((string)($server['creative_sha256'] ?? ''))); if ($bound !== '' && !hash_equals($got, $bound)) { return 'quote creative disagreement'; } continue; } $have = (string)($server[$allowed[$k]] ?? ''); if (!hash_equals($have, $sv)) { return 'quote disagreement'; } } return ''; } /** * TR-02: on a NEW intent, the server store is the only quote. Client quote_* fields cannot satisfy it. * Returns an error array, or null when the issued quote may proceed. Does not stage, write an outcome, or touch the chain. */ function nst_ad_purchase_quote_enforce_new(array $p, string $site, string $amountMicros, string $creativeSha): ?array { $fail = static function (string $code, string $err): array { return ['ok' => false, 'code' => $code, 'err' => $err, 'state' => 'rejected_before_charge']; }; $id = strtolower(trim((string)($p['quote_id'] ?? ''))); if ($id === '' || preg_match('/^[a-f0-9]{64}$/', $id) !== 1) { return $fail('quote_invalid', 'Quote required'); } $server = nst_ad_purchase_quote_lookup($id); if (!is_array($server)) { return $fail('quote_invalid', 'Unknown quote'); } $expect = nst_ad_purchase_quote_expected_id($server); if ($expect === '' || !hash_equals($expect, (string)($server['quote_id'] ?? ''))) { return $fail('quote_invalid', 'Malformed quote'); } foreach (['schema', 'site', 'epoch_n', 'price_micros', 'rule_ver', 'issued_at', 'exp', 'days_left', 'nsu_per_day'] as $need) { if (trim((string)($server[$need] ?? '')) === '') { return $fail('quote_invalid', 'Quote is missing binding fields'); } } if ((string)($server['schema'] ?? '') !== 'nsu-ad-quote-v2') { return $fail('quote_invalid', 'Malformed quote'); } $currency = strtoupper(trim((string)($server['currency'] ?? 'NSU'))); if ($currency !== 'NSU') { return $fail('quote_invalid', 'Quote currency is not NSU'); } $client = null; if (array_key_exists('client_quote', $p) && $p['client_quote'] !== null) { if (!is_array($p['client_quote'])) { return $fail('quote_invalid', 'Malformed quote'); } $client = $p['client_quote']; } if (array_key_exists('quote', $p) && $p['quote'] !== null) { if (!is_array($p['quote'])) { return $fail('quote_invalid', 'Malformed quote'); } $client = array_merge($p['quote'], is_array($client) ? $client : []); } if (is_array($client)) { $why = nst_ad_purchase_quote_client_conflict($client, $server, $creativeSha); if ($why !== '') { return $fail('quote_invalid', $why); } } $qcheck = nst_ad_quote_v2_recheck($server, $site, $amountMicros); if (empty($qcheck['ok'])) { return $fail((string)($qcheck['code'] ?? 'quote_invalid'), (string)($qcheck['err'] ?? 'Quote refused')); } $bound = nst_ad_purchase_quote_bind_creative($id, strtolower($creativeSha)); if (empty($bound['ok'])) { $code = (string)($bound['code'] ?? 'quote_changed'); if ($code !== 'quote_changed' && $code !== 'quote_invalid') { $code = 'quote_changed'; } return $fail($code, (string)($bound['err'] ?? 'Creative does not match the issued quote')); } return null; } /** * TR-08: stable economy transaction lock (never rename this file while held). * Ordering convention: economy_tx lock BEFORE chain lock. Writers must not invert. */ function nst_economy_tx_lock_path(): string { return nst_data_file('economy.tx.lock'); } function nst_with_economy_tx_lock(callable $fn, int $waitMs = 2500) { $path = nst_economy_tx_lock_path(); $dir = dirname($path); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } $fh = @fopen($path, 'c+b'); if (!$fh) { return ['ok' => false, 'code' => 'lock_unavailable', 'err' => 'Could not open economy tx lock']; } $deadline = microtime(true) + max(0, $waitMs) / 1000.0; $got = false; while (microtime(true) <= $deadline) { if (flock($fh, LOCK_EX | LOCK_NB)) { $got = true; break; } usleep(20000); } if (!$got) { fclose($fh); return ['ok' => false, 'code' => 'busy', 'err' => 'Economy transaction busy — retry same intent']; } try { $result = $fn(); if (!is_array($result)) { return ['ok' => true, 'result' => $result]; } if (!array_key_exists('ok', $result)) { $result['ok'] = true; } return $result; } finally { flock($fh, LOCK_UN); fclose($fh); } } /** * TR-09: immutable creative staging under content/tx id. Verified byte count + sha256. * Staging alone never creates ownership or debit. */ function nst_ad_stage_root(): string { $dir = nst_data_file('ads_stage'); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } $ht = $dir . DIRECTORY_SEPARATOR . '.htaccess'; if (!is_file($ht)) { @file_put_contents($ht, "Require all denied\nDeny from all\n", LOCK_EX); } return $dir; } function nst_ad_stage_creative(string $txId, string $bin, string $ext): array { $txId = strtolower(preg_replace('/[^a-f0-9]/', '', $txId) ?? ''); if (strlen($txId) < 32) { return ['ok' => false, 'err' => 'stage tx id invalid']; } $ext = strtolower(preg_replace('/[^a-z0-9]/', '', $ext) ?? ''); if (!in_array($ext, ['png', 'jpg', 'jpeg', 'gif', 'webp'], true)) { return ['ok' => false, 'err' => 'stage ext invalid']; } $brain = nst_brain_ad_image($bin); if (empty($brain['ok'])) { return ['ok' => false, 'err' => (string)($brain['err'] ?? 'image rejected'), 'brain' => $brain]; } $sha = hash('sha256', $bin); $dir = nst_ad_stage_root() . DIRECTORY_SEPARATOR . substr($txId, 0, 2); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } $final = $dir . DIRECTORY_SEPARATOR . $txId . '.' . $ext; $tmp = $final . '.tmp.' . bin2hex(random_bytes(4)); $n = @file_put_contents($tmp, $bin, LOCK_EX); if ($n === false || (int)$n !== strlen($bin)) { @unlink($tmp); return ['ok' => false, 'err' => 'stage write incomplete']; } // Best-effort flush. $fh = @fopen($tmp, 'rb'); if ($fh) { @fflush($fh); fclose($fh); } $readBack = @file_get_contents($tmp); if ($readBack === false || strlen($readBack) !== strlen($bin) || hash('sha256', $readBack) !== $sha) { @unlink($tmp); return ['ok' => false, 'err' => 'stage verify failed']; } if (!@rename($tmp, $final)) { @unlink($tmp); return ['ok' => false, 'err' => 'stage publish rename failed']; } return [ 'ok' => true, 'path' => $final, 'sha256' => $sha, 'bytes' => strlen($bin), 'ext' => (string)($brain['ext'] ?? $ext), 'w' => (int)($brain['w'] ?? 0), 'h' => (int)($brain['h'] ?? 0), 'staged' => true, 'ownership' => false, 'debit' => false, ]; } /** * TR-12: validate conserved outputs before commit/replay acceptance. * Every positive output needs a valid addr; totals must equal debit exactly. */ function nst_econ_validate_outputs(mixed $outputs, string $debitMicros): array { if (!is_array($outputs)) { return ['ok' => false, 'err' => 'outputs not a list', 'outputs' => [], 'findings' => ['not_array']]; } if (!nst_int_micros_ok($debitMicros) || dec_cmp($debitMicros, '0') < 0) { return ['ok' => false, 'err' => 'debit invalid', 'outputs' => [], 'findings' => ['debit']]; } $findings = []; $merged = []; $sum = '0'; foreach ($outputs as $i => $o) { if (!is_array($o)) { $findings[] = 'item_' . $i . '_not_object'; continue; } $addr = strtolower(trim((string)($o['addr'] ?? ''))); $amt = $o['amount'] ?? null; if (is_array($amt) || is_object($amt) || is_bool($amt) || $amt === null) { $findings[] = 'item_' . $i . '_amount_type'; continue; } $am = trim((string)$amt); if (!nst_int_micros_ok($am)) { $findings[] = 'item_' . $i . '_amount_grammar'; continue; } if (dec_cmp($am, '0') < 0) { $findings[] = 'item_' . $i . '_negative'; continue; } if (dec_cmp($am, '0') === 0) { continue; // zero outputs ignored } if (!nst_addr64_ok($addr)) { $findings[] = 'item_' . $i . '_addr'; continue; } if (!isset($merged[$addr])) { $merged[$addr] = '0'; } $merged[$addr] = dec_add($merged[$addr], $am); $sum = dec_add($sum, $am); } if ($findings) { return ['ok' => false, 'err' => 'invalid outputs', 'outputs' => [], 'findings' => $findings, 'sum' => $sum]; } if (dec_cmp($sum, $debitMicros) !== 0) { return ['ok' => false, 'err' => 'outputs do not equal debit', 'outputs' => [], 'findings' => ['sum_mismatch'], 'sum' => $sum]; } ksort($merged); $outs = []; foreach ($merged as $a => $m) { $outs[] = ['addr' => $a, 'amount' => $m]; } return ['ok' => true, 'err' => '', 'outputs' => $outs, 'findings' => [], 'sum' => $sum]; } /** TR-11/14: durable outcome index keyed by epoch|payer|nonce (not transport). */ function nst_econ_outcome_dir(): string { $dir = nst_data_file('econ_outcomes'); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } return $dir; } function nst_econ_outcome_key(int $epochN, string $payer, string $nonce): string { return hash('sha256', 'nsu-econ-outcome-v1|' . $epochN . '|' . strtolower($payer) . '|' . $nonce); } function nst_econ_outcome_path(int $epochN, string $payer, string $nonce): string { $k = nst_econ_outcome_key($epochN, $payer, $nonce); $dir = nst_econ_outcome_dir() . DIRECTORY_SEPARATOR . substr($k, 0, 2); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } return $dir . DIRECTORY_SEPARATOR . $k . '.json'; } function nst_econ_outcome_read(int $epochN, string $payer, string $nonce): array { $path = nst_econ_outcome_path($epochN, $payer, $nonce); if (!is_file($path)) { return ['status' => 'unknown_intent', 'record' => null]; } $raw = @file_get_contents($path); if ($raw === false) { return ['status' => 'unreadable', 'record' => null]; } $j = json_decode((string)$raw, true); if (!is_array($j)) { return ['status' => 'corrupt', 'record' => null]; } return ['status' => (string)($j['state'] ?? 'unknown'), 'record' => $j]; } function nst_econ_outcome_write(array $rec): array { $epochN = (int)($rec['epoch_n'] ?? 0); $payer = strtolower((string)($rec['payer'] ?? '')); $nonce = (string)($rec['nonce'] ?? ''); if ($epochN < 1 || !nst_addr64_ok($payer) || $nonce === '') { return ['ok' => false, 'err' => 'outcome identity incomplete']; } $digest = (string)($rec['request_digest'] ?? ''); $existing = nst_econ_outcome_read($epochN, $payer, $nonce); if (is_array($existing['record'] ?? null)) { $prev = $existing['record']; if ((string)($prev['request_digest'] ?? '') !== '' && $digest !== '' && !hash_equals((string)$prev['request_digest'], $digest)) { return ['ok' => false, 'code' => 'conflict', 'err' => 'Nonce reused with different request digest', 'record' => $prev]; } // Exact retry: keep terminal/pending original. return ['ok' => true, 'code' => 'idempotent', 'record' => $prev]; } $rec['schema'] = 'nsu-econ-outcome-v1'; $rec['updated_at'] = time(); $path = nst_econ_outcome_path($epochN, $payer, $nonce); $tmp = $path . '.tmp.' . bin2hex(random_bytes(3)); $json = json_encode($rec, JSON_UNESCAPED_SLASHES); if ($json === false || @file_put_contents($tmp, $json, LOCK_EX) === false) { @unlink($tmp); return ['ok' => false, 'err' => 'outcome write failed']; } if (!@rename($tmp, $path)) { @unlink($tmp); return ['ok' => false, 'err' => 'outcome rename failed']; } return ['ok' => true, 'code' => 'written', 'record' => $rec]; } /** * TR-10: build a versioned paid-purchase event (authority record). Does not append by itself. * type remains 'ad_stake' for balances compatibility with ver=2 extras. */ function nst_ad_purchase_event_v2_build(array $p): array { $outs = nst_econ_validate_outputs($p['outputs'] ?? [], (string)($p['amount_micros'] ?? '')); if (empty($outs['ok'])) { return ['ok' => false, 'err' => (string)($outs['err'] ?? 'outputs'), 'findings' => $outs['findings'] ?? []]; } $payer = strtolower((string)($p['payer'] ?? '')); if (!nst_addr64_ok($payer)) { return ['ok' => false, 'err' => 'payer invalid']; } $row = [ 'type' => 'ad_stake', 'ver' => 2, 'schema' => 'nsu-ad-purchase-v2', 'from' => $payer, 'amount' => (string)$p['amount_micros'], 'amount_fmt' => fmt_amt((string)$p['amount_micros']), 'outputs' => $outs['outputs'], 'crop' => (string)($p['site'] ?? ''), 'site' => (string)($p['site'] ?? ''), 'epoch_n' => (int)($p['epoch_n'] ?? 0), 'slot' => (string)($p['slot'] ?? ''), 'quote_id' => (string)($p['quote_id'] ?? ''), 'request_digest' => (string)($p['request_digest'] ?? ''), 'creative_sha256' => (string)($p['creative_sha256'] ?? ''), 'creative_ext' => (string)($p['creative_ext'] ?? ''), 'href' => (string)($p['href'] ?? ''), 'label' => (string)($p['label'] ?? ''), 'nonce' => (string)($p['nonce'] ?? ''), 'ts' => (int)($p['ts'] ?? 0), 'exp' => (int)($p['exp'] ?? 0), 'tx_id' => (string)($p['tx_id'] ?? ''), 'delivery' => (string)($p['delivery'] ?? 'pending'), 'lord_micros' => (string)($p['lord_micros'] ?? '0'), 'king_tax_micros' => (string)($p['king_tax_micros'] ?? '0'), 'trade_cut_micros' => (string)($p['trade_cut_micros'] ?? '0'), 'note' => 'ad purchase v2: durable event; conserved outputs; no mint', ]; if ($row['tx_id'] === '') { $row['tx_id'] = hash('sha256', $row['request_digest'] . '|' . $row['nonce'] . '|' . $row['from']); } return ['ok' => true, 'row' => $row]; } /** * TR-13: classify money-bearing event — transfer vs issuance. Wave B adds no issuance. */ function nst_econ_event_issuance_class(string $type, int $ver = 1): string { if (function_exists('nst_is_mint_type') && nst_is_mint_type($type)) { return 'issuance'; } if ($type === 'ad_stake' || $type === 'slowgpt_reserve' || $type === 'transfer' || $type === 'fill' || $type === 'order' || $type === 'cancel') { return 'transfer_existing'; } return 'unknown'; } /** * TR-13 helper: independent conserved-split check for a purchase amount. * Uses existing nst_ad_purchase_split + output validator. */ function nst_econ_purchase_conservation_check(string $micros, bool $otherSite, string $lord, string $king, string $tradeLord): array { if (!nst_int_micros_ok($micros) || dec_cmp($micros, '0') <= 0) { return ['ok' => false, 'err' => 'amount']; } $split = nst_ad_purchase_split($micros, $otherSite); $outs = []; $add = static function (array &$outs, string $addr, string $amt): void { if ($addr === '' || dec_cmp($amt, '0') <= 0) return; foreach ($outs as $i => $o) { if (($o['addr'] ?? '') === $addr) { $outs[$i]['amount'] = dec_add((string)$o['amount'], $amt); return; } } $outs[] = ['addr' => $addr, 'amount' => $amt]; }; $add($outs, $lord, (string)$split['lord']); if ($otherSite) { $add($outs, $tradeLord, (string)$split['trade']); } $add($outs, $king, (string)$split['king']); $v = nst_econ_validate_outputs($outs, $micros); if (empty($v['ok'])) { return ['ok' => false, 'err' => 'split/outputs', 'split' => $split, 'validate' => $v]; } // Class must be transfer_existing (no remint). if (nst_econ_event_issuance_class('ad_stake', 2) !== 'transfer_existing') { return ['ok' => false, 'err' => 'issuance class']; } return ['ok' => true, 'split' => $split, 'outputs' => $v['outputs']]; } /** * TR-14: authenticated purchase-status view from outcome index (no private paths). */ function nst_ad_purchase_status_for(string $payer, string $nonce, int $epochN): array { if (!nst_addr64_ok($payer) || $nonce === '' || $epochN < 1) { return [ 'ok' => false, 'schema' => 'nsu-ad-purchase-status-v1', 'state' => 'rejected_before_charge', 'err' => 'Malformed status key', ]; } $got = nst_econ_outcome_read($epochN, $payer, $nonce); $rec = $got['record']; if (!is_array($rec)) { $st = (string)($got['status'] ?? 'unknown_intent'); if ($st === 'unknown_intent') { return [ 'ok' => true, 'schema' => 'nsu-ad-purchase-status-v1', 'state' => 'unknown_intent', 'epoch_n' => $epochN, 'payer' => $payer, 'nonce' => $nonce, 'err' => '', 'hint' => 'No durable intent found. Do not buy again until verified; refresh status.', ]; } return [ 'ok' => false, 'schema' => 'nsu-ad-purchase-status-v1', 'state' => 'repair_required', 'err' => 'Outcome store ' . $st, ]; } // Actor mismatch protection: caller must already be authenticated as payer. return [ 'ok' => true, 'schema' => 'nsu-ad-purchase-status-v1', 'state' => (string)($rec['state'] ?? 'unknown'), 'tx_id' => (string)($rec['tx_id'] ?? ''), 'epoch_n' => (int)($rec['epoch_n'] ?? $epochN), 'site' => (string)($rec['site'] ?? ''), 'slot' => (string)($rec['slot'] ?? ''), 'amount_micros' => (string)($rec['amount_micros'] ?? ''), 'amount_fmt' => isset($rec['amount_micros']) ? fmt_amt((string)$rec['amount_micros']) : '', 'request_digest' => (string)($rec['request_digest'] ?? ''), 'delivery' => (string)($rec['delivery'] ?? ''), 'err' => (string)($rec['err'] ?? ''), ]; } /** Marker for unit register / smoke */ function nst_waveb_money_authority_present(): bool { return true; } /* ==== END WAVEB-TR05-TR14 MONEY AUTHORITY ==== */ /* ==== BEGIN WAVEB2-DURABLE-COMMIT-TR10-TR24 (do not split) ==== */ /** * WAVEB2 durable purchase commit + TR-24 public ad state (read-only). * Injected into 4.php. Purchase gate stays CLOSED until TR-10/11 acceptance passes. * Lab crash injection: getenv('NST_CRASH_AFTER') = stage|outcome_pending|chain|slot|publish|before_delivered */ /** TR-11: allow same-digest state progression under tx lock (not a new charge). */ function nst_econ_outcome_update(array $rec, bool $allowProgress = true): array { $epochN = (int)($rec['epoch_n'] ?? 0); $payer = strtolower((string)($rec['payer'] ?? '')); $nonce = (string)($rec['nonce'] ?? ''); if ($epochN < 1 || !nst_addr64_ok($payer) || $nonce === '') { return ['ok' => false, 'err' => 'outcome identity incomplete']; } $digest = (string)($rec['request_digest'] ?? ''); $existing = nst_econ_outcome_read($epochN, $payer, $nonce); if (is_array($existing['record'] ?? null)) { $prev = $existing['record']; $prevDigest = (string)($prev['request_digest'] ?? ''); if ($prevDigest !== '' && $digest !== '' && !hash_equals($prevDigest, $digest)) { return ['ok' => false, 'code' => 'conflict', 'err' => 'Nonce reused with different request digest', 'record' => $prev]; } if (!$allowProgress) { return ['ok' => true, 'code' => 'idempotent', 'record' => $prev]; } // Merge progress fields onto previous terminal/pending record. $merged = $prev; foreach ($rec as $k => $v) { if ($k === 'schema') continue; $merged[$k] = $v; } $merged['schema'] = 'nsu-econ-outcome-v1'; $merged['updated_at'] = time(); $path = nst_econ_outcome_path($epochN, $payer, $nonce); $tmp = $path . '.tmp.' . bin2hex(random_bytes(3)); $json = json_encode($merged, JSON_UNESCAPED_SLASHES); if ($json === false || @file_put_contents($tmp, $json, LOCK_EX) === false) { @unlink($tmp); return ['ok' => false, 'err' => 'outcome update write failed']; } if (!@rename($tmp, $path)) { @unlink($tmp); return ['ok' => false, 'err' => 'outcome update rename failed']; } return ['ok' => true, 'code' => 'updated', 'record' => $merged]; } return nst_econ_outcome_write($rec); } function nst_crash_lab_checkpoint(string $point): void { $want = (string)getenv('NST_CRASH_AFTER'); if ($want !== '' && hash_equals($want, $point)) { // Hard stop for crash/restart acceptance (lab only). if (getenv('NST_CRASH_MARKER')) { @file_put_contents((string)getenv('NST_CRASH_MARKER'), $point . "\n", LOCK_EX); } // Use SIGKILL-style exit without shutdown writes when possible. if (function_exists('posix_kill') && function_exists('getmypid')) { @posix_kill(getmypid(), 9); } exit(99); } } /** * Publish staged creative bytes into the active slot path without deleting the stage object. * Does not invent ownership — caller must mark_sold under the same tx lock. */ function nst_ad_publish_staged_to_slot(string $stagePath, string $site, int $n, string $ext, string $owner = ''): ?string { if (!is_file($stagePath)) return null; $bin = @file_get_contents($stagePath); if ($bin === false || $bin === '') return null; return nst_ad_write_slot($site, $n, $bin, $ext, $owner); } /** * TR-10/11: one durable paid purchase under economy_tx lock. * Success is derived from durable outcome + delivered projection. * Still invoked only after nst_ad_spend_ready (gate) passes in the HTTP handler. * * @param array $p Keys: payer,site,epoch_n,amount_micros,outputs,quote_id,request_digest,nonce,ts,exp, * creative_bin,creative_ext,creative_sha256,href,label,lord_micros,king_tax_micros,trade_cut_micros, * auth_sig,auth_sig_mode,other_site,cat_name,split_note,king_tax_ppm,trade_cut_ppm */ function nst_ad_purchase_commit_v2(array $p): array { $payer = strtolower((string)($p['payer'] ?? '')); $nonce = (string)($p['nonce'] ?? ''); $epochN = (int)($p['epoch_n'] ?? 0); $site = nst_ad_site_norm((string)($p['site'] ?? '')); $digest = (string)($p['request_digest'] ?? ''); $micros = (string)($p['amount_micros'] ?? ''); $imgBin = (string)($p['creative_bin'] ?? ''); $ext = strtolower((string)($p['creative_ext'] ?? 'png')); if ($ext === 'jpeg') $ext = 'jpg'; if (!nst_addr64_ok($payer) || $nonce === '' || $epochN < 1 || $site === '' || $digest === '') { return ['ok' => false, 'code' => 'rejected_before_charge', 'err' => 'Purchase identity incomplete']; } if ($imgBin === '' || !nst_int_micros_ok($micros) || dec_cmp($micros, '0') <= 0) { return ['ok' => false, 'code' => 'rejected_before_charge', 'err' => 'Creative or amount missing']; } // TR-02: client _resume/resume is ignored. Only the pending record below may set _resume. unset($p['_resume'], $p['resume']); return nst_with_economy_tx_lock(function () use ($p, $payer, $nonce, $epochN, $site, $digest, $micros, $imgBin, $ext) { // WAVEC-TR17-EPOCH-BOUNDARY: stale uncommitted quote rejects without charge. if (function_exists('nst_econ_epoch_boundary_check')) { $eb = nst_econ_epoch_boundary_check($epochN); if (empty($eb['ok'])) { return ['ok' => false, 'code' => (string)($eb['code'] ?? 'quote_changed'), 'err' => (string)($eb['err'] ?? 'epoch changed'), 'epoch_n' => (int)($eb['epoch_n'] ?? 0)]; } } // Exact retry / conflict resolution first. $prior = nst_econ_outcome_read($epochN, $payer, $nonce); if (is_array($prior['record'] ?? null)) { $rec = $prior['record']; $prevDigest = (string)($rec['request_digest'] ?? ''); if ($prevDigest !== '' && !hash_equals($prevDigest, $digest)) { return ['ok' => false, 'code' => 'conflict', 'err' => 'Nonce reused with different request', 'outcome' => $rec]; } $st = (string)($rec['state'] ?? ''); if ($st === 'delivered' || $st === 'committed_pending_delivery') { // Resume delivery if needed, else return original receipt. if ($st === 'delivered') { return [ 'ok' => true, 'code' => 'idempotent', 'state' => 'delivered', 'tx_id' => (string)($rec['tx_id'] ?? ''), 'slot' => (int)($rec['slot'] ?? -1), 'slot_pad' => isset($rec['slot']) ? nst_ad_slot_pad((int)$rec['slot']) : '', 'site' => (string)($rec['site'] ?? $site), 'amount_micros' => (string)($rec['amount_micros'] ?? $micros), 'outcome' => $rec, 'idempotent' => true, ]; } // Fall through to complete pending delivery using stored fields. $p['tx_id'] = (string)($rec['tx_id'] ?? $p['tx_id'] ?? ''); $p['creative_sha256'] = (string)($rec['creative_sha256'] ?? $p['creative_sha256'] ?? ''); $p['stage_path'] = (string)($rec['stage_path'] ?? ''); if (isset($rec['slot']) && (int)$rec['slot'] >= 0) { $p['slot_hint'] = (int)$rec['slot']; } $p['_resume'] = true; } elseif (in_array($st, ['rejected_before_charge', 'repair_required'], true)) { return ['ok' => false, 'code' => $st, 'err' => (string)($rec['err'] ?? 'prior rejection'), 'outcome' => $rec]; } } // TR-02: NEW intent must match the quote this server issued for quote_id. // Client quote_exp / quote_* cannot satisfy or override that lookup. // Delivered retries and server-side pending recovery already returned or set _resume. if (empty($p['_resume'])) { $creativeForQuote = (string)($p['creative_sha256'] ?? ''); if ($creativeForQuote === '') { $creativeForQuote = hash('sha256', $imgBin); } $qRefuse = nst_ad_purchase_quote_enforce_new($p, $site, $micros, $creativeForQuote); if (is_array($qRefuse)) { return $qRefuse; } } // Missing, empty, or corrupt ledger: refuse before creative stage and before any outcome file. // A missing chain file is not created. Resume of an already-charged purchase is unchanged. if (empty($p['_resume'])) { global $CHAIN; if (is_link($CHAIN) || (file_exists($CHAIN) && !is_file($CHAIN))) { return ['ok' => false, 'code' => 'rejected_before_charge', 'err' => 'chain unreadable', 'state' => 'rejected_before_charge']; } if (!file_exists($CHAIN)) { return ['ok' => false, 'code' => 'rejected_before_charge', 'err' => 'chain lock unavailable', 'state' => 'rejected_before_charge']; } if (file_exists($CHAIN)) { if (is_link($CHAIN) || !is_file($CHAIN)) { return ['ok' => false, 'code' => 'rejected_before_charge', 'err' => 'chain unreadable', 'state' => 'rejected_before_charge']; } try { $preRows = read_chain(); } catch (Throwable $e) { $msg = $e->getMessage(); $allowed = ['chain_invalid', 'chain_missing_established_state', 'chain_unavailable', 'chain_commit_failed', 'chain_recovery_required']; $err = in_array($msg, $allowed, true) ? $msg : 'chain unreadable'; return ['ok' => false, 'code' => 'rejected_before_charge', 'err' => $err, 'state' => 'rejected_before_charge']; } if ($preRows === []) { return ['ok' => false, 'code' => 'rejected_before_charge', 'err' => 'empty ledger', 'state' => 'rejected_before_charge']; } } } $txId = (string)($p['tx_id'] ?? ''); if ($txId === '') { $txId = hash('sha256', $digest . '|' . $nonce . '|' . $payer); } $creativeSha = (string)($p['creative_sha256'] ?? ''); if ($creativeSha === '') { $creativeSha = hash('sha256', $imgBin); } // Stage creative (idempotent if stage file already present for tx). $stagePath = (string)($p['stage_path'] ?? ''); if ($stagePath === '' || !is_file($stagePath)) { $staged = nst_ad_stage_creative($txId, $imgBin, $ext); if (empty($staged['ok'])) { return ['ok' => false, 'code' => 'rejected_before_charge', 'err' => (string)($staged['err'] ?? 'stage failed')]; } $stagePath = (string)$staged['path']; $creativeSha = (string)$staged['sha256']; $ext = (string)($staged['ext'] ?? $ext); } nst_crash_lab_checkpoint('stage'); $pending = [ 'epoch_n' => $epochN, 'payer' => $payer, 'nonce' => $nonce, 'request_digest' => $digest, 'tx_id' => $txId, 'site' => $site, 'amount_micros' => $micros, 'creative_sha256' => $creativeSha, 'creative_ext' => $ext, 'stage_path' => $stagePath, 'quote_id' => (string)($p['quote_id'] ?? ''), 'href' => (string)($p['href'] ?? ''), 'label' => (string)($p['label'] ?? ''), 'state' => 'prepared_uncharged', 'delivery' => 'pending', 'err' => '', ]; $ow = nst_econ_outcome_update($pending, true); if (empty($ow['ok'])) { return ['ok' => false, 'code' => (string)($ow['code'] ?? 'rejected_before_charge'), 'err' => (string)($ow['err'] ?? 'outcome')]; } // If first write returned prior delivered, surface it. if (($ow['code'] ?? '') === 'idempotent' && (string)(($ow['record']['state'] ?? '')) === 'delivered') { $rec = $ow['record']; return [ 'ok' => true, 'code' => 'idempotent', 'state' => 'delivered', 'tx_id' => (string)($rec['tx_id'] ?? $txId), 'slot' => (int)($rec['slot'] ?? -1), 'slot_pad' => isset($rec['slot']) ? nst_ad_slot_pad((int)$rec['slot']) : '', 'site' => (string)($rec['site'] ?? $site), 'amount_micros' => (string)($rec['amount_micros'] ?? $micros), 'outcome' => $rec, 'idempotent' => true, ]; } nst_crash_lab_checkpoint('outcome_pending'); $outputs = $p['outputs'] ?? []; $vov = nst_econ_validate_outputs($outputs, $micros); if (empty($vov['ok'])) { nst_econ_outcome_update(array_merge($pending, [ 'state' => 'rejected_before_charge', 'err' => 'outputs failed conservation', 'delivery' => 'none', ]), true); return ['ok' => false, 'code' => 'rejected_before_charge', 'err' => 'Purchase outputs failed conservation checks']; } $outputs = $vov['outputs']; $eventBuild = nst_ad_purchase_event_v2_build([ 'payer' => $payer, 'amount_micros' => $micros, 'outputs' => $outputs, 'site' => $site, 'epoch_n' => $epochN, 'slot' => '', 'quote_id' => (string)($p['quote_id'] ?? ''), 'request_digest' => $digest, 'creative_sha256' => $creativeSha, 'creative_ext' => $ext, 'href' => (string)($p['href'] ?? ''), 'label' => (string)($p['label'] ?? ''), 'nonce' => $nonce, 'ts' => (int)($p['ts'] ?? 0), 'exp' => (int)($p['exp'] ?? 0), 'tx_id' => $txId, 'delivery' => 'pending', 'lord_micros' => (string)($p['lord_micros'] ?? '0'), 'king_tax_micros' => (string)($p['king_tax_micros'] ?? '0'), 'trade_cut_micros' => (string)($p['trade_cut_micros'] ?? '0'), ]); if (empty($eventBuild['ok'])) { return ['ok' => false, 'code' => 'rejected_before_charge', 'err' => (string)($eventBuild['err'] ?? 'event')]; } $row = $eventBuild['row']; $row['sig'] = (string)($p['auth_sig'] ?? ''); $row['sig_mode'] = (string)($p['auth_sig_mode'] ?? ''); $alreadyCharged = !empty($p['_resume']); $chainRes = nst_with_chain_lock(function (array $chain, callable $append) use ( $payer, $nonce, $micros, $row, &$alreadyCharged ) { foreach ($chain as $prior) { if (($prior['type'] ?? '') === 'ad_stake' && (string)($prior['from'] ?? '') === $payer && (string)($prior['nonce'] ?? '') === $nonce) { $alreadyCharged = true; return [ 'ok' => true, 'idempotent' => true, 'row' => $prior, ]; } } if ($alreadyCharged) { return ['ok' => true, 'idempotent' => true, 'row' => $row]; } $bal = balances($chain); $have = (string)($bal[$payer] ?? '0'); $av = available_nsu($bal, $chain, $payer); if (dec_cmp($av, $micros) < 0) { return ['ok' => false, 'code' => 'rejected_before_charge', 'err' => 'Not enough free NSU — you hold ' . fmt_amt($have) . ', free ' . fmt_amt($av) . ' (the rest backs open sell orders)']; } if (!$append($row)) { return ['ok' => false, 'code' => 'repair_required', 'err' => 'Could not save to the server — try again in a moment']; } return ['ok' => true, 'idempotent' => false, 'row' => $row]; }); if (!is_array($chainRes) || empty($chainRes['ok'])) { $err = is_array($chainRes) ? (string)($chainRes['err'] ?? 'chain failed') : 'chain lock unavailable'; $code = is_array($chainRes) ? (string)($chainRes['code'] ?? 'rejected_before_charge') : 'rejected_before_charge'; if ($code === 'rejected_before_charge') { nst_econ_outcome_update(array_merge($pending, [ 'state' => 'rejected_before_charge', 'err' => $err, 'delivery' => 'none', ]), true); } return ['ok' => false, 'code' => $code, 'err' => $err]; } nst_crash_lab_checkpoint('chain'); // Mark committed pending delivery BEFORE projection writes. $slotHint = isset($p['slot_hint']) ? (int)$p['slot_hint'] : -1; nst_econ_outcome_update(array_merge($pending, [ 'state' => 'committed_pending_delivery', 'delivery' => 'pending', 'tx_id' => $txId, 'creative_sha256' => $creativeSha, 'stage_path' => $stagePath, 'slot' => $slotHint >= 0 ? $slotHint : null, 'err' => '', ]), true); // Assign or reuse slot. $slotN = $slotHint; if ($slotN < 0) { $slotN = nst_ad_slot_assign($site); } if ($slotN === null || $slotN < 0) { nst_econ_outcome_update(array_merge($pending, [ 'state' => 'committed_pending_delivery', 'delivery' => 'pending', 'tx_id' => $txId, 'err' => 'Paid, but no free slot remained', ]), true); return [ 'ok' => false, 'code' => 'committed_pending_delivery', 'err' => 'Paid, but no free slot remained — status will recover this intent; do not pay again', 'tx_id' => $txId, 'state' => 'committed_pending_delivery', ]; } nst_crash_lab_checkpoint('slot'); $saved = nst_ad_publish_staged_to_slot($stagePath, $site, $slotN, $ext, $payer); if ($saved === null) { nst_econ_outcome_update(array_merge($pending, [ 'state' => 'committed_pending_delivery', 'delivery' => 'pending', 'tx_id' => $txId, 'slot' => $slotN, 'err' => 'Paid, but the image was not saved', ]), true); return [ 'ok' => false, 'code' => 'committed_pending_delivery', 'err' => 'Paid, but the image was not saved — refresh status; do not pay again', 'tx_id' => $txId, 'slot' => $slotN, 'state' => 'committed_pending_delivery', ]; } nst_crash_lab_checkpoint('publish'); nst_ad_mark_sold($site, $slotN, $ext, $payer); // Economy bids projection (presentation; not ownership authority). $ts = (int)($p['ts'] ?? time()); $href = (string)($p['href'] ?? ''); $label = (string)($p['label'] ?? ''); $e = load_economy(); if (!isset($e['bids']) || !is_array($e['bids'])) $e['bids'] = []; $bidId = hash('sha256', $payer . '|' . $ts . '|' . $micros . '|' . $nonce); $merged = false; foreach ($e['bids'] as $i => $b) { if (!is_array($b) || ($b['owner'] ?? '') !== $payer) continue; if (function_exists('ad_bid_crop') ? ad_bid_crop($b) !== $site : (($b['crop'] ?? '') !== $site)) continue; $e['bids'][$i]['stake_micros'] = dec_add((string)($b['stake_micros'] ?? '0'), $micros); $e['bids'][$i]['stake_fmt'] = fmt_amt((string)$e['bids'][$i]['stake_micros']); $e['bids'][$i]['active'] = true; $e['bids'][$i]['ts'] = $ts; $e['bids'][$i]['crop'] = $site; if ($href !== '') $e['bids'][$i]['href'] = $href; if ($label !== '') $e['bids'][$i]['label'] = $label; $bidId = (string)($b['id'] ?? $bidId); $merged = true; break; } if (!$merged) { $e['bids'][] = [ 'id' => $bidId, 'owner' => $payer, 'crop' => $site, 'stake_micros' => $micros, 'stake_fmt' => fmt_amt($micros), 'ts' => $ts, 'active' => true, 'from_donate' => false, 'href' => $href, 'label' => $label, 'creative_url' => '', 'creative_file' => '', 'tx_id' => $txId, 'slot' => $slotN, ]; } $e['ad_bid_micros'] = dec_add((string)($e['ad_bid_micros'] ?? '0'), empty($p['_resume']) ? $micros : '0'); // WAVEC-TR20-BIDS-RECENT: presentation feed only; ownership via index/outcomes. if (count($e['bids']) > 200) { $e['bids'] = array_slice($e['bids'], -200); $e['bids_feed_note'] = 'recent_activity_cap_200_not_total_ownership'; } save_economy($e); nst_crash_lab_checkpoint('before_delivered'); $final = [ 'epoch_n' => $epochN, 'payer' => $payer, 'nonce' => $nonce, 'request_digest' => $digest, 'tx_id' => $txId, 'site' => $site, 'slot' => $slotN, 'amount_micros' => $micros, 'creative_sha256' => $creativeSha, 'creative_ext' => $ext, 'stage_path' => $stagePath, 'quote_id' => (string)($p['quote_id'] ?? ''), 'href' => $href, 'label' => $label, 'state' => 'delivered', 'delivery' => 'committed', 'err' => '', 'bid_id' => $bidId, ]; nst_econ_outcome_update($final, true); return [ 'ok' => true, 'code' => !empty($chainRes['idempotent']) || !empty($p['_resume']) ? 'idempotent' : 'delivered', 'state' => 'delivered', 'tx_id' => $txId, 'slot' => $slotN, 'slot_pad' => nst_ad_slot_pad($slotN), 'site' => $site, 'amount_micros' => $micros, 'amount_fmt' => fmt_amt($micros), 'creative_sha256' => $creativeSha, 'public_url' => nst_ad_public_url($site, $slotN), 'bid_id' => $bidId, 'outcome' => $final, 'idempotent' => !empty($chainRes['idempotent']) || !empty($p['_resume']), ]; }, 5000); } /** * TR-24: read-only public ad slot state (nsu-ad-state-v1 / nsu-ad-batch-v1). * Never moves money. Missing/corrupt/pending → unknown, not house. */ function nst_ad_slot_state_record(string $site, int $n, int $epochN, array $idx): array { $pad = nst_ad_slot_pad($n); $base = [ 'schema' => 'nsu-ad-state-v1', 'site' => $site, 'slot' => $pad, 'epoch' => $epochN, ]; $idxEpoch = (int)($idx['epoch_n'] ?? 0); if ($idxEpoch !== $epochN) { return $base + ['state' => 'unknown', 'delivery' => '']; } $meta = $idx['sites'][$site][(string)$n] ?? $idx['sites'][$site][$pad] ?? null; if (!is_array($meta)) { // Established empty inventory for current epoch → house. // Unknown only when index itself is unusable (handled by caller). return $base + ['state' => 'house', 'delivery' => '']; } $owner = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($meta['owner'] ?? '')) ?? ''); $ext = strtolower((string)($meta['ext'] ?? '')); if ($owner === '' || !preg_match('/^[a-f0-9]{64}$/', $owner)) { return $base + ['state' => 'unknown', 'delivery' => '']; } // Confirm a creative file exists for committed delivery. $found = false; if (function_exists('nst_ad_slot_candidates')) { foreach (nst_ad_slot_candidates($site, $n) as $cand) { if (is_file($cand)) { $found = true; break; } } } else { $found = true; // index claims ownership; file probe optional } if (!$found) { return $base + ['state' => 'unknown', 'delivery' => 'pending']; } // WAVEE-TR24-CREATIVE-HASH: versioned public creative identity (no owner/paths). $sha = ''; $pubExt = $ext !== '' ? $ext : 'png'; if ($ext === 'jpeg') $pubExt = 'jpg'; if (isset($meta['creative_sha256']) && is_string($meta['creative_sha256']) && preg_match('/^[a-f0-9]{64}$/', strtolower($meta['creative_sha256']))) { $sha = strtolower($meta['creative_sha256']); } elseif (function_exists('nst_ad_slot_candidates')) { foreach (nst_ad_slot_candidates($site, $n) as $cand) { if (is_file($cand)) { $sha = hash_file('sha256', $cand); $fe = strtolower(pathinfo($cand, PATHINFO_EXTENSION)); if ($fe === 'jpeg') $fe = 'jpg'; if ($fe !== '') $pubExt = $fe; break; } } } $gen = ''; if ($sha !== '') $gen = substr($sha, 0, 16); elseif (!empty($meta['ts'])) $gen = 'ts' . (string)(int)$meta['ts']; // Community moderation: takedown preserves owner; public state is "takedown". if (!empty($meta['takedown'])) { $out = $base + ['state' => 'takedown', 'delivery' => 'committed']; if ($sha !== '') $out['creative_sha256'] = $sha; if ($pubExt !== '') $out['creative_ext'] = $pubExt !== '' ? $pubExt : 'png'; if ($gen !== '') $out['content_gen'] = $gen; return $out; } $out = $base + ['state' => 'creative', 'delivery' => 'committed']; if ($sha !== '') $out['creative_sha256'] = $sha; if ($pubExt !== '') $out['creative_ext'] = $pubExt; if ($gen !== '') $out['content_gen'] = $gen; return $out; } function nst_ad_slot_state_batch(string $site, array $slots, ?int $epochN = null): array { $site = nst_ad_site_norm($site); if ($site === '') { return ['ok' => false, 'err' => 'Unknown site', 'schema' => 'nsu-ad-batch-v1']; } if ($epochN === null) { $epochN = (int)(nst_epoch_info_read_only()['epoch_n'] ?? 1); } if ($epochN < 1) { return ['ok' => false, 'err' => 'Invalid epoch', 'schema' => 'nsu-ad-batch-v1']; } // Dedupe preserving order; max 64. $seen = []; $norm = []; foreach ($slots as $s) { $n = nst_ad_slot_norm_n($s); if ($n === null) continue; if (isset($seen[$n])) continue; $seen[$n] = true; $norm[] = $n; // WAVEE-TR34-BATCH-QUOTA if (function_exists('nst_quota_check_batch_slots')) { $qc = nst_quota_check_batch_slots(count($norm)); if (empty($qc['ok'])) { return ['ok' => false, 'err' => (string)($qc['err'] ?? 'batch too large'), 'code' => 'batch_too_large', 'schema' => 'nsu-ad-batch-v1', 'retry_after' => (int)($qc['retry_after'] ?? 1)]; } } elseif (count($norm) > 64) { return ['ok' => false, 'err' => 'Maximum 64 slots per request', 'schema' => 'nsu-ad-batch-v1']; } } if (!$norm) { return ['ok' => false, 'err' => 'No valid slots', 'schema' => 'nsu-ad-batch-v1']; } $load = function_exists('nst_ad_index_load_result') ? nst_ad_index_load_result() : null; $items = []; if (!is_array($load) || ($load['status'] ?? '') === 'corrupt' || ($load['status'] ?? '') === 'unreadable') { foreach ($norm as $n) { $items[] = [ 'schema' => 'nsu-ad-state-v1', 'site' => $site, 'slot' => nst_ad_slot_pad($n), 'epoch' => $epochN, 'state' => 'unknown', 'delivery' => '', ]; } } else { $idx = is_array($load['payload'] ?? null) ? $load['payload'] : (function_exists('nst_ad_index_load') ? nst_ad_index_load() : ['epoch_n' => $epochN, 'sites' => []]); // Typed statuses that must not advertise house: $status = (string)($load['status'] ?? 'valid-current'); $unavailable = in_array($status, ['corrupt', 'unreadable', 'missing-established', 'valid-old-epoch', 'unsupported-version'], true); foreach ($norm as $n) { if ($unavailable) { $items[] = [ 'schema' => 'nsu-ad-state-v1', 'site' => $site, 'slot' => nst_ad_slot_pad($n), 'epoch' => $epochN, 'state' => 'unknown', 'delivery' => '', ]; } else { $items[] = nst_ad_slot_state_record($site, $n, $epochN, $idx); } } } return [ 'ok' => true, 'schema' => 'nsu-ad-batch-v1', 'site' => $site, 'epoch' => $epochN, 'items' => $items, ]; } function nst_ad_slot_state_handle(): void { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET'); header('Access-Control-Allow-Headers: Content-Type'); header('Cache-Control: no-store'); if (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) !== 'GET') { nst_ticket_json(['ok' => false, 'err' => 'GET only', 'schema' => 'nsu-ad-batch-v1'], 405); } $site = nst_ad_site_norm((string)($_GET['site'] ?? $_GET['crop'] ?? '')); $epochIn = $_GET['epoch'] ?? $_GET['epoch_n'] ?? null; $epochN = null; if ($epochIn !== null && $epochIn !== '') { if (function_exists('nst_epoch_n_parse')) { $ep = nst_epoch_n_parse($epochIn); if (empty($ep['ok'])) { nst_ticket_json(['ok' => false, 'err' => 'Invalid epoch', 'schema' => 'nsu-ad-batch-v1'], 400); } $epochN = (int)$ep['epoch_n']; } else { $epochN = (int)$epochIn; } } $slotsRaw = (string)($_GET['slots'] ?? $_GET['slot'] ?? ''); $slots = []; if ($slotsRaw !== '') { foreach (preg_split('/[,\s]+/', $slotsRaw) as $p) { $p = trim($p); if ($p !== '') $slots[] = $p; } } // Also accept slot0..slotN style? Keep simple CSV / single. if (isset($_GET['n']) && $_GET['n'] !== '') { $slots[] = (string)$_GET['n']; } $batch = nst_ad_slot_state_batch($site, $slots, $epochN); nst_ticket_json($batch, !empty($batch['ok']) ? 200 : 400); } /** Marker */ function nst_waveb2_durable_commit_present(): bool { return true; } /* ==== END WAVEB2-DURABLE-COMMIT-TR10-TR24 ==== */ /* ==== BEGIN WAVEC-TR17-TR31 OWNERSHIP-LEGACY-OPS (do not split) ==== */ /** * WAVE C+D green slices: TR-17..22, TR-25..27, TR-29 (+TR-31 backup helpers). * Injected after WAVEB2 block. No vaults/seeds. Money law + ECDSA-v2 preserved. * Gates: all-site stays CLOSED; SlowGPT paid stays CLOSED. */ /** TR-17: epoch identity for quotes/outcomes/public state. */ function nst_econ_current_epoch_n(): int { $ep = nst_epoch_info_read_only(); $n = (int)($ep['epoch_n'] ?? 1); return $n < 1 ? 1 : $n; } /** * TR-17: inside economy_tx lock, reject purchase if intent epoch ≠ live epoch. * Stale uncommitted quote → no charge. Committed prior-epoch outcomes remain readable. */ function nst_econ_epoch_boundary_check(int $intentEpoch): array { $cur = nst_econ_current_epoch_n(); if ($intentEpoch < 1) { return ['ok' => false, 'code' => 'rejected_before_charge', 'err' => 'Invalid epoch', 'epoch_n' => $cur]; } if ($intentEpoch !== $cur) { return [ 'ok' => false, 'code' => 'quote_changed', 'err' => 'Epoch changed — get a fresh quote; prior-epoch receipt stays valid for recovery', 'epoch_n' => $cur, 'intent_epoch_n' => $intentEpoch, ]; } return ['ok' => true, 'epoch_n' => $cur]; } /** TR-17: retired outcome archive root (survives presentation wipe). */ function nst_econ_outcome_retired_root(): string { $d = nst_data_file('econ.outcomes.retired'); if (!is_dir($d)) @mkdir($d, 0700, true); return $d; } /** * TR-17: expire presentation inventory under economy_tx lock without destroying * recovery journals. Outcomes for from-epoch move to retired archive. */ function nst_epoch_expire_presentation(int $fromN, int $toN): array { // TR-RESET: this presentation roll must keep prior-epoch ad owners, crop // files, wallet rows, and committed outcome files. It does not publish an // empty current epoch and it does not move or delete those rows. Profit, // bids, open-order cancellation, and the chain row stay in // economy_on_epoch_roll, which this candidate does not run. if ($fromN < 1 || $toN <= $fromN) { return ['ok' => false, 'err' => 'bad epoch roll bounds', 'moved_outcomes' => 0, 'from' => $fromN, 'to' => $toN, 'owners_kept' => true, 'wiped_index' => false]; } return ['ok' => true, 'moved_outcomes' => 0, 'from' => $fromN, 'to' => $toN, 'owners_kept' => true, 'wiped_index' => false]; } /** * TR-17: serialize epoch presentation expire with economic writes. * Does NOT mint, does NOT touch chain balances. Call from economy_on_epoch_roll. */ function nst_epoch_roll_presentation_guarded(int $fromN, int $toN): array { if ($fromN < 1 || $toN <= $fromN) { return ['ok' => false, 'err' => 'bad epoch roll bounds']; } if (!function_exists('nst_with_economy_tx_lock')) { return nst_epoch_expire_presentation($fromN, $toN); } return nst_with_economy_tx_lock(function () use ($fromN, $toN) { return nst_epoch_expire_presentation($fromN, $toN); }); } function nst_ad_slot_shard_safe(int $n): string { $pad = function_exists('nst_ad_slot_pad') ? nst_ad_slot_pad($n) : str_pad((string)$n, 4, '0', STR_PAD_LEFT); return substr($pad, 0, 2); } /* ===================== TR-18 legacy reconciliation ===================== */ /** * Classify one site+slot against index, filesystem, and optional chain hints. * Never mutates. Classifications per briefing. */ function nst_legacy_classify_slot(string $site, int $n, array $idx, int $curEpoch, string $adsRoot): array { $site = function_exists('nst_ad_site_norm') ? nst_ad_site_norm($site) : strtolower($site); $pad = function_exists('nst_ad_slot_pad') ? nst_ad_slot_pad($n) : str_pad((string)$n, 4, '0', STR_PAD_LEFT); $meta = $idx['sites'][$site][(string)$n] ?? $idx['sites'][$site][$pad] ?? null; $idxEpoch = (int)($idx['epoch_n'] ?? 0); $owner = is_array($meta) ? strtolower((string)($meta['owner'] ?? '')) : ''; $ext = is_array($meta) ? strtolower((string)($meta['ext'] ?? 'png')) : ''; $found = null; // Always resolve under the provided isolated $adsRoot (never ambient ads_dir). $exts = $ext !== '' ? [$ext, 'png', 'jpg', 'jpeg', 'webp', 'gif'] : ['png', 'jpg', 'jpeg', 'webp', 'gif']; $names = [$pad, (string)$n]; foreach ($exts as $e) { foreach ($names as $name) { $cands = [ $adsRoot . DIRECTORY_SEPARATOR . $site . DIRECTORY_SEPARATOR . $name . '.' . $e, $adsRoot . DIRECTORY_SEPARATOR . $site . DIRECTORY_SEPARATOR . nst_ad_slot_shard_safe($n) . DIRECTORY_SEPARATOR . $name . '.' . $e, ]; foreach ($cands as $p) { if (is_file($p) && filesize($p) > 8) { $found = $p; break 3; } } } } if ($idxEpoch > 0 && $idxEpoch !== $curEpoch) { if ($found) { return ['class' => 'confirmed-expired', 'site' => $site, 'slot' => $n, 'owner' => $owner, 'path' => $found]; } return ['class' => 'confirmed-expired', 'site' => $site, 'slot' => $n, 'owner' => $owner, 'path' => null]; } if (is_array($meta) && $owner !== '' && $found) { return ['class' => 'confirmed-current', 'site' => $site, 'slot' => $n, 'owner' => $owner, 'path' => $found, 'sha256' => hash_file('sha256', $found)]; } if (is_array($meta) && $owner !== '' && !$found) { return ['class' => 'missing-creative', 'site' => $site, 'slot' => $n, 'owner' => $owner]; } if (!is_array($meta) && $found) { return ['class' => 'unlinked-creative', 'site' => $site, 'slot' => $n, 'path' => $found, 'sha256' => hash_file('sha256', $found)]; } if (is_array($meta) && $owner === '' && $found) { return ['class' => 'unresolvable-history', 'site' => $site, 'slot' => $n, 'path' => $found]; } return ['class' => 'empty', 'site' => $site, 'slot' => $n]; } /** * TR-18: scan isolated copy only. Idempotent classification plan. * $root must be a disposable fixture path (lab guard rejects Desktop/canonical). */ function nst_legacy_reconcile_plan(string $root, ?array $idx = null, ?int $curEpoch = null): array { if (function_exists('nst_lab_reject_canonical_roots')) { nst_lab_reject_canonical_roots($root); } $curEpoch = $curEpoch ?? nst_econ_current_epoch_n(); if ($idx === null) { $path = rtrim($root, '/\\') . DIRECTORY_SEPARATOR . 'ad.slots.json'; $raw = is_file($path) ? @file_get_contents($path) : false; $idx = is_string($raw) ? json_decode($raw, true) : null; if (!is_array($idx)) $idx = ['epoch_n' => $curEpoch, 'sites' => []]; } $adsRoot = rtrim($root, '/\\') . DIRECTORY_SEPARATOR . 'ads'; $counts = [ 'confirmed-current' => 0, 'confirmed-expired' => 0, 'unlinked-creative' => 0, 'missing-creative' => 0, 'conflicting-owner' => 0, 'unresolvable-history' => 0, 'empty' => 0, ]; $rows = []; $ownerSeen = []; // site|slot => owner $sites = function_exists('nst_ad_slot_sites') ? nst_ad_slot_sites() : array_keys($idx['sites'] ?? []); foreach ($sites as $site) { $site = (string)$site; $slotMap = $idx['sites'][$site] ?? []; if (!is_array($slotMap)) continue; foreach ($slotMap as $k => $meta) { $n = function_exists('nst_ad_slot_norm_n') ? nst_ad_slot_norm_n($k) : (int)$k; if ($n === null || $n < 0) continue; $key = $site . '|' . $n; $owner = is_array($meta) ? strtolower((string)($meta['owner'] ?? '')) : ''; if (isset($ownerSeen[$key]) && $ownerSeen[$key] !== '' && $owner !== '' && $ownerSeen[$key] !== $owner) { $row = ['class' => 'conflicting-owner', 'site' => $site, 'slot' => $n, 'owners' => [$ownerSeen[$key], $owner]]; $rows[] = $row; $counts['conflicting-owner']++; continue; } if ($owner !== '') $ownerSeen[$key] = $owner; $row = nst_legacy_classify_slot($site, $n, $idx, $curEpoch, $adsRoot); $rows[] = $row; $c = (string)($row['class'] ?? 'empty'); if (!isset($counts[$c])) $counts[$c] = 0; $counts[$c]++; } // Unlinked files under site dir $siteDir = $adsRoot . DIRECTORY_SEPARATOR . $site; if (is_dir($siteDir)) { $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($siteDir, FilesystemIterator::SKIP_DOTS)); foreach ($it as $f) { if (!$f->isFile()) continue; $name = $f->getBasename(); if (!preg_match('/^(\d{1,4})\.(png|jpe?g|webp|gif)$/i', $name, $m)) continue; $n = (int)$m[1]; $key = $site . '|' . $n; if (isset($slotMap[(string)$n]) || isset($slotMap[str_pad((string)$n, 4, '0', STR_PAD_LEFT)])) continue; $row = ['class' => 'unlinked-creative', 'site' => $site, 'slot' => $n, 'path' => $f->getPathname(), 'sha256' => hash_file('sha256', $f->getPathname())]; $rows[] = $row; $counts['unlinked-creative']++; } } } $planHash = hash('sha256', j(['epoch' => $curEpoch, 'counts' => $counts, 'n' => count($rows)])); return [ 'ok' => true, 'schema' => 'nsu-legacy-reconcile-plan-v1', 'epoch_n' => $curEpoch, 'counts' => $counts, 'rows' => $rows, 'plan_hash' => $planHash, 'mutable' => false, 'note' => 'Read-only plan; ambiguous rows preserved, never deleted by this scan', ]; } /* ===================== TR-19 rebuildable index ===================== */ function nst_ad_index_schema_ver(): string { return 'nsu-ad-index-v2'; } /** * Rebuild site+slot ownership projection from committed outcomes + optional legacy index. * Deterministic. Does not invent owners from truncated bids. */ function nst_ad_index_rebuild(array $opts = []): array { $epochN = (int)($opts['epoch_n'] ?? nst_econ_current_epoch_n()); $sites = function_exists('nst_ad_slot_sites') ? nst_ad_slot_sites() : ['trade']; $out = [ 'schema' => nst_ad_index_schema_ver(), 'epoch_n' => $epochN, 'generation' => (int)($opts['generation'] ?? time()), 'sites' => [], 'watermark' => '', 'built_from' => [], ]; foreach ($sites as $s) $out['sites'][$s] = []; // 1) Committed delivered outcomes for this epoch $outcomeRoot = nst_data_file('econ.outcomes'); $eventParts = []; if (is_dir($outcomeRoot)) { $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($outcomeRoot, FilesystemIterator::SKIP_DOTS)); foreach ($it as $f) { if (!$f->isFile()) continue; $raw = @file_get_contents($f->getPathname()); $j = is_string($raw) ? json_decode($raw, true) : null; if (!is_array($j)) continue; if ((int)($j['epoch_n'] ?? 0) !== $epochN) continue; if ((string)($j['state'] ?? '') !== 'delivered') continue; $site = function_exists('nst_ad_site_norm') ? nst_ad_site_norm((string)($j['site'] ?? '')) : (string)($j['site'] ?? ''); $slot = (int)($j['slot'] ?? -1); $owner = strtolower((string)($j['payer'] ?? '')); if ($site === '' || $slot < 0 || $slot > 9999 || $owner === '') continue; if (!isset($out['sites'][$site])) $out['sites'][$site] = []; $ext = strtolower((string)($j['creative_ext'] ?? 'png')); $out['sites'][$site][(string)$slot] = [ 'owner' => $owner, 'ext' => $ext, 'tx_id' => (string)($j['tx_id'] ?? ''), 'creative_sha256' => (string)($j['creative_sha256'] ?? ''), ]; $eventParts[] = $site . '|' . $slot . '|' . $owner . '|' . (string)($j['tx_id'] ?? ''); $out['built_from'][] = 'outcome:' . basename($f->getPathname()); } } // 2) Explicit legacy mappings (confirmed-current only) — never conflicting $legacy = $opts['legacy_index'] ?? null; if (is_array($legacy) && (int)($legacy['epoch_n'] ?? 0) === $epochN) { foreach (($legacy['sites'] ?? []) as $site => $slots) { if (!is_array($slots)) continue; $site = function_exists('nst_ad_site_norm') ? nst_ad_site_norm((string)$site) : (string)$site; if (!isset($out['sites'][$site])) $out['sites'][$site] = []; foreach ($slots as $k => $meta) { $n = function_exists('nst_ad_slot_norm_n') ? nst_ad_slot_norm_n($k) : (int)$k; if ($n === null) continue; if (isset($out['sites'][$site][(string)$n])) continue; // outcomes win if (!is_array($meta)) continue; $owner = strtolower((string)($meta['owner'] ?? '')); if ($owner === '') continue; $out['sites'][$site][(string)$n] = [ 'owner' => $owner, 'ext' => strtolower((string)($meta['ext'] ?? 'png')), 'tx_id' => (string)($meta['tx_id'] ?? ''), 'legacy' => true, ]; $eventParts[] = 'legacy|' . $site . '|' . $n . '|' . $owner; } } } sort($eventParts); $out['watermark'] = hash('sha256', 'nsu-ad-index-v2|' . $epochN . '|' . implode(';', $eventParts)); return $out; } /** * TR-19: write complete replacement to temp, verify, publish under lock. */ function nst_ad_index_publish_atomic(array $idx): array { $path = function_exists('nst_ad_index_path') ? nst_ad_index_path() : nst_data_file('ad.slots.json'); if (!isset($idx['schema'])) $idx['schema'] = nst_ad_index_schema_ver(); if (!isset($idx['watermark']) || $idx['watermark'] === '') { $idx['watermark'] = hash('sha256', j($idx)); } $payload = j($idx); $tmp = $path . '.tmp.' . getmypid() . '.' . bin2hex(random_bytes(4)); $n = @file_put_contents($tmp, $payload, LOCK_EX); if ($n === false || (int)$n !== strlen($payload)) { @unlink($tmp); return ['ok' => false, 'err' => 'temp write failed']; } $check = @file_get_contents($tmp); if (!is_string($check) || !hash_equals(hash('sha256', $check), hash('sha256', $payload))) { @unlink($tmp); return ['ok' => false, 'err' => 'temp verify failed']; } $decoded = json_decode($check, true); if (!is_array($decoded) || !isset($decoded['sites']) || !isset($decoded['epoch_n'])) { @unlink($tmp); return ['ok' => false, 'err' => 'temp schema verify failed']; } // Publish if (!@rename($tmp, $path)) { // Windows-ish fallback @unlink($path); if (!@rename($tmp, $path)) { @unlink($tmp); return ['ok' => false, 'err' => 'publish rename failed']; } } @chmod($path, 0600); return ['ok' => true, 'path' => $path, 'watermark' => (string)$idx['watermark'], 'bytes' => strlen($payload)]; } /** * Reader: reject partial/unverified generation (missing watermark on v2). */ function nst_ad_index_load_verified(): array { $r = function_exists('nst_ad_index_load_result') ? nst_ad_index_load_result() : ['status' => 'unreadable', 'payload' => null]; if (($r['status'] ?? '') !== 'valid-current' || !is_array($r['payload'] ?? null)) { return $r; } $p = $r['payload']; $schema = (string)($p['schema'] ?? ''); if ($schema === nst_ad_index_schema_ver() && empty($p['watermark'])) { return ['status' => 'corrupt', 'payload' => null, 'err' => 'v2 index missing watermark']; } return $r; } /* ===================== TR-20 scalable read models ===================== */ /** Presentation activity cap — never claim this equals total ownership. */ function nst_econ_activity_feed_cap(): int { return 200; } /** * Per-owner current-slot view from committed ownership index (not truncated bids). * Counts independent of activity feed. */ function nst_owner_slots_view(string $owner, ?array $idx = null): array { $owner = strtolower(preg_replace('/[^a-f0-9]/', '', $owner) ?? ''); if (strlen($owner) !== 64) { return ['ok' => false, 'err' => 'bad owner', 'slots' => [], 'count' => '0']; } if ($idx === null) { $load = nst_ad_index_load_verified(); if (($load['status'] ?? '') !== 'valid-current') { return ['ok' => false, 'err' => 'index unavailable', 'status' => (string)($load['status'] ?? ''), 'slots' => [], 'count' => '0']; } $idx = $load['payload']; } $slots = []; foreach (($idx['sites'] ?? []) as $site => $map) { if (!is_array($map)) continue; foreach ($map as $k => $meta) { if (!is_array($meta)) continue; if (strtolower((string)($meta['owner'] ?? '')) !== $owner) continue; $n = function_exists('nst_ad_slot_norm_n') ? nst_ad_slot_norm_n($k) : (int)$k; if ($n === null) continue; $slots[] = [ 'site' => (string)$site, 'slot' => $n, 'slot_pad' => function_exists('nst_ad_slot_pad') ? nst_ad_slot_pad($n) : (string)$n, 'ext' => (string)($meta['ext'] ?? 'png'), 'tx_id' => (string)($meta['tx_id'] ?? ''), ]; } } usort($slots, static function ($a, $b) { $c = strcmp($a['site'], $b['site']); return $c !== 0 ? $c : ($a['slot'] <=> $b['slot']); }); return [ 'ok' => true, 'owner' => $owner, 'epoch_n' => (int)($idx['epoch_n'] ?? 0), 'slots' => $slots, 'count' => (string)count($slots), // exact string total — not feed length 'activity_feed_is_not_total' => true, ]; } /** * Bounded recent activity from economy bids — labeled recent, never total. */ function nst_econ_recent_activity(?array $economy = null, int $limit = 0): array { if ($limit <= 0) $limit = nst_econ_activity_feed_cap(); if ($economy === null && function_exists('load_economy')) { $economy = load_economy(); } if (!is_array($economy)) $economy = []; $bids = is_array($economy['bids'] ?? null) ? $economy['bids'] : []; $totalKnown = count($bids); $slice = array_slice($bids, -$limit); return [ 'ok' => true, 'kind' => 'recent_activity', 'limit' => $limit, 'returned' => count($slice), 'feed_truncated' => $totalKnown >= $limit, 'note' => 'Recent activity only — use nst_owner_slots_view for total ownership counts', 'items' => array_values($slice), ]; } /** Checkpoint: event position + digest for read-model reuse. */ function nst_econ_read_checkpoint_path(): string { return nst_data_file('econ.read.checkpoint.json'); } function nst_econ_read_checkpoint_write(string $chainHead, int $eventPos, string $digest): array { $rec = [ 'schema' => 'nsu-econ-read-checkpoint-v1', 'chain_head' => $chainHead, 'event_pos' => $eventPos, 'digest' => $digest, 'ts' => time(), ]; $path = nst_econ_read_checkpoint_path(); $ok = @file_put_contents($path, j($rec), LOCK_EX) !== false; return ['ok' => $ok, 'record' => $rec]; } function nst_econ_read_checkpoint_validate(string $expectHead): array { $path = nst_econ_read_checkpoint_path(); if (!is_file($path)) return ['ok' => false, 'err' => 'missing']; $j = json_decode((string)@file_get_contents($path), true); if (!is_array($j)) return ['ok' => false, 'err' => 'corrupt']; if ((string)($j['chain_head'] ?? '') !== $expectHead) { return ['ok' => false, 'err' => 'chain_head_mismatch', 'record' => $j]; } return ['ok' => true, 'record' => $j]; } /* ===================== TR-21 honest inventory presentation ===================== */ function nst_ad_delivery_model_copy(): array { return [ 'model' => 'random_slot_10000', 'slots_per_site' => 10000, 'assignment' => 'one assigned slot among a site\'s 10,000 rolls', 'duration' => 'until yearly epoch reset', 'price_rule' => '100 NSU × days left in epoch (not divided by 10000)', 'not_promised' => [ 'guaranteed impression count', 'revenue or return', 'weighted-bid share percentage as delivery odds', 'all ten faces from a single-site purchase', ], 'cross_site' => 'Same slot number on different sites are independent purchases', 'probability_note' => 'Each face independently rolls 0–9999 and fetches that site\'s creative; owning N slots on one site means N of 10000 rolls can hit your creative on that face only', ]; } /** Attach honest labels onto a quote payload (TR-21). */ function nst_ad_quote_attach_honest_copy(array $quote): array { $quote['delivery_model'] = nst_ad_delivery_model_copy(); $quote['inventory_truth'] = 'sold/remain counts are per-site slot occupancy, not legacy weighted shares'; // Relabel misleading empire "all" buyable if present — all-site still gated elsewhere. if (isset($quote['sites']['all']) && is_array($quote['sites']['all'])) { $quote['sites']['all']['name'] = 'all ten faces (unavailable until atomic group commit)'; $quote['sites']['all']['delivery_note'] = 'Not a single-site receipt — requires TR-16 all-or-nothing'; } $quote['decay'] = 'one slot = 100 NSU × days left (not divided by 10000)'; $quote['roll'] = 'each face rolls 0–9999 and fetches nosignup.trade/{site}/{n}'; return $quote; } /* ===================== TR-22 orphan cleanup ===================== */ function nst_ad_stage_root_safe(): string { if (function_exists('nst_ad_stage_root')) return nst_ad_stage_root(); $d = nst_data_file('ad.stage'); if (!is_dir($d)) @mkdir($d, 0700, true); return $d; } /** * Snapshot authoritative creative references (current index + active outcomes + stages referenced). */ function nst_ad_reference_snapshot(): array { $refs = []; // abs path => reason $load = function_exists('nst_ad_index_load_result') ? nst_ad_index_load_result() : null; if (is_array($load) && ($load['status'] ?? '') === 'valid-current' && is_array($load['payload'] ?? null)) { foreach (($load['payload']['sites'] ?? []) as $site => $map) { if (!is_array($map)) continue; foreach ($map as $k => $meta) { $n = function_exists('nst_ad_slot_norm_n') ? nst_ad_slot_norm_n($k) : (int)$k; if ($n === null) continue; if (function_exists('nst_ad_slot_find')) { $p = nst_ad_slot_find((string)$site, $n); if ($p) $refs[$p] = 'current-creative'; } } } } $outcomeRoot = nst_data_file('econ.outcomes'); if (is_dir($outcomeRoot)) { $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($outcomeRoot, FilesystemIterator::SKIP_DOTS)); foreach ($it as $f) { if (!$f->isFile()) continue; $j = json_decode((string)@file_get_contents($f->getPathname()), true); if (!is_array($j)) continue; $st = (string)($j['state'] ?? ''); $sp = (string)($j['stage_path'] ?? ''); if ($sp !== '' && is_file($sp) && in_array($st, ['prepared_uncharged', 'committed_pending_delivery', 'delivered'], true)) { $refs[$sp] = 'active-intent-stage'; } } } return $refs; } /** * Resolve path under intended root; reject traversal/symlink escape. */ function nst_ad_path_under_root(string $path, string $root): ?string { $rootReal = realpath($root); if ($rootReal === false) return null; $real = realpath($path); if ($real === false) { // For not-yet-existing, normalize $norm = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path); if (str_contains($norm, '..' . DIRECTORY_SEPARATOR) || str_contains($norm, DIRECTORY_SEPARATOR . '..')) { return null; } return null; } if ($real === $rootReal || str_starts_with($real, $rootReal . DIRECTORY_SEPARATOR)) { return $real; } return null; } /** * TR-22: dry-run or delete verified orphans under ads + stage with grace. * @param array{dry_run?:bool,grace_secs?:int,now?:int} $opts */ function nst_ad_orphan_cleanup(array $opts = []): array { $dry = !array_key_exists('dry_run', $opts) || !empty($opts['dry_run']); $grace = (int)($opts['grace_secs'] ?? 3600); $now = (int)($opts['now'] ?? time()); $refs = nst_ad_reference_snapshot(); $eligible = []; $kept = []; $ads = function_exists('ads_dir') ? ads_dir() : nst_data_file('ads'); $stage = nst_ad_stage_root_safe(); $scanRoots = []; if (is_dir($ads)) $scanRoots[] = ['root' => $ads, 'class' => 'ads']; if (is_dir($stage)) $scanRoots[] = ['root' => $stage, 'class' => 'stage']; foreach ($scanRoots as $sr) { $root = $sr['root']; $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS)); foreach ($it as $f) { if (!$f->isFile()) continue; $path = $f->getPathname(); $base = $f->getBasename(); if ($base === '_placeholder.png' || str_starts_with($base, '.')) { $kept[] = ['path_hash' => hash('sha256', $path), 'reason' => 'retained-system']; continue; } if (str_contains($path, DIRECTORY_SEPARATOR . '_house' . DIRECTORY_SEPARATOR)) { $kept[] = ['path_hash' => hash('sha256', $path), 'reason' => 'house-cache']; continue; } $under = nst_ad_path_under_root($path, $root); if ($under === null) { $kept[] = ['path_hash' => hash('sha256', $path), 'reason' => 'path-escape-rejected']; continue; } if (isset($refs[$path]) || isset($refs[$under])) { $kept[] = ['path_hash' => hash('sha256', $under), 'reason' => $refs[$path] ?? $refs[$under]]; continue; } $mtime = (int)$f->getMTime(); if (($now - $mtime) < $grace) { $kept[] = ['path_hash' => hash('sha256', $under), 'reason' => 'grace']; continue; } $eligible[] = [ 'path_hash' => hash('sha256', $under), 'bytes' => (int)$f->getSize(), 'mtime' => $mtime, 'retention_class' => 'verified-orphan', 'area' => $sr['class'], '_path' => $under, // stripped from public receipt below ]; } } $deleted = []; if (!$dry) { // Re-check references before each delete $refs2 = nst_ad_reference_snapshot(); foreach ($eligible as $e) { $p = (string)($e['_path'] ?? ''); if ($p === '' || isset($refs2[$p])) continue; if (@unlink($p)) { $deleted[] = ['path_hash' => $e['path_hash'], 'bytes' => $e['bytes']]; } } } $receipt = [ 'schema' => 'nsu-orphan-cleanup-receipt-v1', 'dry_run' => $dry, 'grace_secs' => $grace, 'eligible_count' => count($eligible), 'deleted_count' => count($deleted), 'kept_count' => count($kept), 'eligible' => array_map(static function ($e) { unset($e['_path']); return $e; }, $eligible), 'deleted' => $deleted, 'ts' => $now, ]; return ['ok' => true, 'receipt' => $receipt]; } /* ===================== TR-25 image serve vs committed state ===================== */ /** * Resolve creative for public serve from verified projection. * Unknown/old-epoch → unavailable (no cross-site fallback). */ function nst_ad_serve_slot_from_projection(string $site, int $n): array { $site = function_exists('nst_ad_site_norm') ? nst_ad_site_norm($site) : strtolower($site); if ($site === '' || $n < 0 || $n > 9999) { return ['ok' => false, 'code' => 'not_found']; } $epochN = nst_econ_current_epoch_n(); $load = function_exists('nst_ad_index_load_result') ? nst_ad_index_load_result() : null; $status = is_array($load) ? (string)($load['status'] ?? '') : 'unreadable'; if (in_array($status, ['corrupt', 'unreadable', 'missing-established', 'valid-old-epoch', 'unsupported-version'], true)) { return ['ok' => false, 'code' => 'unavailable', 'status' => $status, 'epoch' => $epochN]; } $idx = is_array($load['payload'] ?? null) ? $load['payload'] : null; if (!is_array($idx)) { // missing-new-install (never-established index): same as empty inventory → house. // Aligns public PNG serve with ad_slot_state (house), without weakening // corrupt/unreadable/missing-established/old-epoch/unsupported → 503 above. if ($status === 'missing-new-install') { return ['ok' => true, 'kind' => 'house', 'site' => $site, 'slot' => $n, 'epoch' => $epochN]; } return ['ok' => false, 'code' => 'unavailable', 'status' => 'missing', 'epoch' => $epochN]; } $idxEpoch = (int)($idx['epoch_n'] ?? 0); if ($idxEpoch !== $epochN) { return ['ok' => false, 'code' => 'unavailable', 'status' => 'valid-old-epoch', 'epoch' => $epochN]; } $pad = function_exists('nst_ad_slot_pad') ? nst_ad_slot_pad($n) : str_pad((string)$n, 4, '0', STR_PAD_LEFT); $meta = $idx['sites'][$site][(string)$n] ?? $idx['sites'][$site][$pad] ?? null; if (!is_array($meta) || empty($meta['owner'])) { // Established empty → house return ['ok' => true, 'kind' => 'house', 'site' => $site, 'slot' => $n, 'epoch' => $epochN]; } $found = function_exists('nst_ad_slot_find') ? nst_ad_slot_find($site, $n) : null; if (!$found || !is_file($found)) { return ['ok' => false, 'code' => 'unavailable', 'status' => 'missing-creative', 'epoch' => $epochN]; } // Cache key includes content generation $gen = (string)($idx['watermark'] ?? $idx['generation'] ?? $epochN); $sha = hash_file('sha256', $found); $isTd = !empty($meta['takedown']); return [ 'ok' => true, 'kind' => $isTd ? 'takedown' : 'owned', 'site' => $site, 'slot' => $n, 'path' => $found, 'ext' => strtolower(pathinfo($found, PATHINFO_EXTENSION) ?: (string)($meta['ext'] ?? 'png')), 'epoch' => $epochN, 'cache_gen' => $gen, 'content_sha256' => $sha, ]; } /* ===================== TR-26 authenticated receipt ===================== */ function nst_ad_receipt_export(string $payer, string $nonce, int $epochN): array { if (!function_exists('nst_ad_purchase_status_for')) { return ['ok' => false, 'err' => 'status unavailable']; } $st = nst_ad_purchase_status_for($payer, $nonce, $epochN); $rec = is_array($st['outcome'] ?? null) ? $st['outcome'] : (is_array($st['record'] ?? null) ? $st['record'] : null); // Redact private paths if (is_array($rec)) { unset($rec['stage_path'], $rec['_path']); } return [ 'ok' => !empty($st['ok']) || in_array((string)($st['state'] ?? ''), ['delivered', 'committed_pending_delivery', 'rejected_before_charge'], true), 'schema' => 'nsu-ad-receipt-v1', 'payer' => strtolower($payer), 'nonce' => $nonce, 'epoch_n' => $epochN, 'tx_id' => is_array($rec) ? (string)($rec['tx_id'] ?? '') : (string)($st['tx_id'] ?? ''), 'site' => is_array($rec) ? (string)($rec['site'] ?? '') : '', 'slot' => is_array($rec) && isset($rec['slot']) ? (int)$rec['slot'] : null, 'amount_micros' => is_array($rec) ? (string)($rec['amount_micros'] ?? '') : '', 'state' => (string)($st['state'] ?? $st['status'] ?? 'unknown'), 'delivery' => is_array($rec) ? (string)($rec['delivery'] ?? '') : '', 'request_digest' => is_array($rec) ? (string)($rec['request_digest'] ?? '') : '', 'creative_sha256' => is_array($rec) ? (string)($rec['creative_sha256'] ?? '') : '', 'rule_ver' => function_exists('nst_ad_quote_rule_ver') ? nst_ad_quote_rule_ver() : 'nsu-ad-quote-v2', 'redacted' => true, 'note' => 'Evidence for reconciliation — not spending authority', ]; } /* ===================== TR-27 checkout confirmation ===================== */ function nst_ad_checkout_confirm_required(array $post): array { $c = (string)($post['checkout_confirm'] ?? $post['confirm'] ?? ''); if ($c !== '1' && strtoupper($c) !== 'BUY') { return [ 'ok' => false, 'code' => 'confirm_required', 'err' => 'Explicit checkout confirmation required (checkout_confirm=1)', 'confirm_required' => '1', ]; } return ['ok' => true]; } /* ===================== TR-29 cancellation / settlement transitions ===================== */ /** * Transition table (no auto-mint refunds). Refunds require authorized funding + linked debit. */ function nst_econ_transition_table(): array { return [ 'schema' => 'nsu-econ-transition-v1', 'rows' => [ ['from' => 'intent_unsigned', 'to' => 'abandoned', 'money' => 'none', 'note' => 'Cancel before commitment'], ['from' => 'prepared_uncharged', 'to' => 'rejected_before_charge', 'money' => 'none', 'note' => 'Validation/stage failure'], ['from' => 'prepared_uncharged', 'to' => 'committed_pending_delivery', 'money' => 'debited', 'note' => 'Chain append success'], ['from' => 'committed_pending_delivery', 'to' => 'delivered', 'money' => 'unchanged', 'note' => 'Publish+mark_sold'], ['from' => 'committed_pending_delivery', 'to' => 'repair_required', 'money' => 'debited', 'note' => 'Needs recovery; do not re-pay'], ['from' => 'delivered', 'to' => 'delivered', 'money' => 'unchanged', 'note' => 'Idempotent retry'], ['from' => 'delivered', 'to' => 'revoked_with_refund', 'money' => 'refund_linked', 'note' => 'Explicit authorized refund only; not automatic'], ], 'forbidden' => [ 'delivered-and-fully-refunded without documented revocation', 'minting compensation from thin air', 'treasury secret as convenience refund source in tests', ], ]; } function nst_econ_transition_allowed(string $from, string $to): bool { foreach (nst_econ_transition_table()['rows'] as $r) { if (($r['from'] ?? '') === $from && ($r['to'] ?? '') === $to) return true; } return false; } /* ===================== TR-31 backup / restore (lab) ===================== */ /** * Consistent backup of economy projections into isolated directory. * Never includes vault secrets. Restore target must be disposable. */ function nst_econ_backup_create(string $destDir, array $opts = []): array { if (function_exists('nst_lab_reject_canonical_roots')) { nst_lab_reject_canonical_roots($destDir); } if (!is_dir($destDir)) @mkdir($destDir, 0700, true); $files = []; $copy = static function (string $src, string $name) use ($destDir, &$files): void { if (!is_file($src)) return; $dest = $destDir . DIRECTORY_SEPARATOR . $name; if (@copy($src, $dest)) { $files[$name] = hash_file('sha256', $dest); } }; $copy(nst_data_file('ad.slots.json'), 'ad.slots.json'); $copy(nst_data_file('economy.json'), 'economy.json'); $copy(nst_data_file('ad.slots.init.json'), 'ad.slots.init.json'); // outcomes tree $outSrc = nst_data_file('econ.outcomes'); $outDest = $destDir . DIRECTORY_SEPARATOR . 'econ.outcomes'; $outcomeCount = 0; if (is_dir($outSrc)) { @mkdir($outDest, 0700, true); $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($outSrc, FilesystemIterator::SKIP_DOTS)); foreach ($it as $f) { if (!$f->isFile()) continue; $rel = $f->getFilename(); $d = $outDest . DIRECTORY_SEPARATOR . $rel; if (@copy($f->getPathname(), $d)) { $files['econ.outcomes/' . $rel] = hash_file('sha256', $d); $outcomeCount++; } } } $manifest = [ 'schema' => 'nsu-econ-backup-v1', 'ts' => time(), 'epoch_n' => nst_econ_current_epoch_n(), 'files' => $files, 'outcome_count' => $outcomeCount, 'watermark' => hash('sha256', j($files)), 'includes_secrets' => false, ]; @file_put_contents($destDir . DIRECTORY_SEPARATOR . 'MANIFEST.json', j($manifest), LOCK_EX); return ['ok' => true, 'manifest' => $manifest, 'dir' => $destDir]; } function nst_econ_backup_restore(string $backupDir, string $destRoot): array { if (function_exists('nst_lab_reject_canonical_roots')) { nst_lab_reject_canonical_roots($backupDir); nst_lab_reject_canonical_roots($destRoot); } $manPath = rtrim($backupDir, '/\\') . DIRECTORY_SEPARATOR . 'MANIFEST.json'; if (!is_file($manPath)) return ['ok' => false, 'err' => 'missing manifest']; $man = json_decode((string)@file_get_contents($manPath), true); if (!is_array($man) || ($man['schema'] ?? '') !== 'nsu-econ-backup-v1') { return ['ok' => false, 'err' => 'bad manifest']; } // Verify checksums foreach (($man['files'] ?? []) as $name => $sha) { $p = rtrim($backupDir, '/\\') . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, (string)$name); if (!is_file($p) || !hash_equals((string)$sha, hash_file('sha256', $p))) { return ['ok' => false, 'err' => 'checksum fail: ' . $name]; } } if (!is_dir($destRoot)) @mkdir($destRoot, 0700, true); foreach (($man['files'] ?? []) as $name => $_) { $src = rtrim($backupDir, '/\\') . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, (string)$name); $dest = rtrim($destRoot, '/\\') . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, (string)$name); $dir = dirname($dest); if (!is_dir($dir)) @mkdir($dir, 0700, true); if (!@copy($src, $dest)) return ['ok' => false, 'err' => 'copy fail: ' . $name]; } @copy($manPath, rtrim($destRoot, '/\\') . DIRECTORY_SEPARATOR . 'MANIFEST.json'); return ['ok' => true, 'restored_to' => $destRoot, 'watermark' => (string)($man['watermark'] ?? '')]; } function nst_wavec_helpers_present(): bool { return true; } /* ==== END WAVEC-TR17-TR31 OWNERSHIP-LEGACY-OPS ==== */ /* ==== BEGIN WAVEE-TR30-TR36 OPS (do not split) ==== */ /** * WAVE E operating helpers: TR-30, TR-32..TR-36 (+ TR-28 admission prep, closed). * No vaults/seeds. Money law + ECDSA-v2 preserved. * Gates: all-site CLOSED; SlowGPT paid CLOSED until INFO agrees. */ /** TR-30: nonsecret protocol capability record tied to source generation. */ function nst_econ_capability_schema(): string { return 'nsu-econ-capability-v1'; } function nst_econ_source_generation(): string { static $g = null; if (is_string($g)) return $g; $self = (string)(defined('NST_SELF_PATH') ? NST_SELF_PATH : (__FILE__)); // Prefer the product file when helpers are embedded in 4.php / index.php. $cand = [ (string)($_SERVER['SCRIPT_FILENAME'] ?? ''), dirname(__DIR__) . '/index.php', __FILE__, ]; foreach ($cand as $p) { if (is_string($p) && $p !== '' && is_file($p) && filesize($p) > 100000) { $g = hash_file('sha256', $p); return $g; } } $g = hash('sha256', 'nsu-econ-capability|' . (string)@filemtime(__FILE__)); return $g; } /** * Publish supported read schemas, write intent versions, and enabled product ops. * Disabled ops stay disabled regardless of parser support. */ function nst_econ_protocol_capability(): array { $paidAds = false; // Capability requires affirmative readiness evidence. if (function_exists('nst_ad_spend_ready')) { // Missing, failed or malformed readiness must not advertise availability. try { $sr = nst_ad_spend_ready('trade'); if (is_array($sr)) $paidAds = ($sr['ok'] ?? null) === true; } catch (Throwable $e) { $paidAds = false; } } $slowPaid = function_exists('nst_slowgpt_paid_enabled') ? (bool)nst_slowgpt_paid_enabled() : false; $allSite = false; // TR-16 remains closed $enabled = ['ad_quote', 'ad_slot_state', 'ad_purchase_status', 'ad_receipt_export', 'ad_report', 'single_site_swap', 'single_site_give', 'transfer', 'order', 'fill', 'cancel']; if ($paidAds) $enabled[] = 'ad_stake'; $disabled = ['all_site_swap', 'all_site_give']; if (!$slowPaid) $disabled[] = 'slowgpt_paid_reserve'; if (!$paidAds) { $disabled[] = 'ad_stake'; $enabled = array_values(array_filter($enabled, static fn($x) => $x !== 'ad_stake')); } return [ 'ok' => true, 'schema' => nst_econ_capability_schema(), 'source_generation' => nst_econ_source_generation(), 'read_schemas' => [ 'nsu-ad-state-v1', 'nsu-ad-batch-v1', 'nsu-ad-quote-v2', 'nsu-econ-capability-v1', 'nsu-ad-receipt-v1', 'nsu-econ-health-v1', 'nsu-ad-report-v1', ], 'write_intent_versions' => [ 'nsu-econ-intent-v2', 'nsu-ad-purchase-event-v2', ], 'enabled_ops' => array_values(array_unique($enabled)), 'disabled_ops' => array_values(array_unique($disabled)), 'products' => [ 'ad_purchase' => $paidAds ? 'enabled' : 'disabled', 'slowgpt_paid' => $slowPaid ? 'enabled' : 'disabled', 'all_site_ownership' => $allSite ? 'enabled' : 'disabled', 'free_info_bringup' => 'enabled', ], 'checkout_confirm_required' => true, 'notes' => [ 'unknown_schema_versions_must_be_handled_neutrally', 'disabled_ops_reject_writes_even_if_parser_exists', 'free_product_remains_when_optional_economy_unavailable', ], ]; } function nst_econ_capability_accepts_read(string $schema): bool { $cap = nst_econ_protocol_capability(); return in_array($schema, $cap['read_schemas'] ?? [], true); } function nst_econ_capability_write_allowed(string $op, string $intentVer = 'nsu-econ-intent-v2'): array { $cap = nst_econ_protocol_capability(); if (in_array($op, $cap['disabled_ops'] ?? [], true)) { return ['ok' => false, 'code' => 'op_disabled', 'err' => 'Operation unavailable', 'op' => $op]; } if (!in_array($op, $cap['enabled_ops'] ?? [], true)) { return ['ok' => false, 'code' => 'op_unknown', 'err' => 'Unknown operation', 'op' => $op]; } if (!in_array($intentVer, $cap['write_intent_versions'] ?? [], true)) { return ['ok' => false, 'code' => 'intent_version_unsupported', 'err' => 'Refresh client — unsupported intent version', 'intent_ver' => $intentVer]; } return ['ok' => true, 'op' => $op, 'intent_ver' => $intentVer]; } function nst_econ_capability_handle(): void { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET'); header('Cache-Control: no-store'); if (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) !== 'GET') { nst_ticket_json(['ok' => false, 'err' => 'GET only', 'schema' => nst_econ_capability_schema()], 405); } nst_ticket_json(nst_econ_protocol_capability(), 200); } /** TR-32: restartable schema migration beside old projections. */ function nst_schema_migration_path(): string { return nst_data_file('econ.schema.migration.json'); } function nst_schema_version_pointer_path(): string { return nst_data_file('econ.schema.pointer.json'); } function nst_schema_pointer_read(): array { $p = nst_schema_version_pointer_path(); if (!is_file($p)) { return ['ok' => true, 'schema' => 'nsu-ad-index-v2', 'event_min_reader' => 'nsu-ad-purchase-event-v2', 'migrating' => false]; } $j = json_decode((string)@file_get_contents($p), true); if (!is_array($j)) return ['ok' => false, 'err' => 'corrupt pointer']; return array_merge(['ok' => true], $j); } function nst_schema_pointer_write(array $ptr): array { $path = nst_schema_version_pointer_path(); $tmp = $path . '.tmp.' . bin2hex(random_bytes(4)); $ptr['schema_ptr'] = 'nsu-schema-pointer-v1'; $ptr['ts'] = time(); $bytes = j($ptr); if (@file_put_contents($tmp, $bytes, LOCK_EX) === false) { return ['ok' => false, 'err' => 'pointer write failed']; } if (function_exists('nst_fsync_file')) { @nst_fsync_file($tmp); } if (!@rename($tmp, $path)) { @unlink($tmp); return ['ok' => false, 'err' => 'pointer rename failed']; } return ['ok' => true, 'pointer' => $ptr]; } function nst_schema_migration_begin(string $targetSchema, string $inputWatermark): array { $state = [ 'schema' => 'nsu-schema-migration-v1', 'target' => $targetSchema, 'input_watermark' => $inputWatermark, 'completed_partitions' => [], 'verification_digest' => '', 'status' => 'running', 'quarantine' => [], 'started_ts' => time(), ]; $path = nst_schema_migration_path(); if (@file_put_contents($path, j($state), LOCK_EX) === false) { return ['ok' => false, 'err' => 'cannot start migration']; } return ['ok' => true, 'state' => $state]; } function nst_schema_migration_checkpoint(string $partition, string $digest): array { $path = nst_schema_migration_path(); if (!is_file($path)) return ['ok' => false, 'err' => 'no migration']; $state = json_decode((string)@file_get_contents($path), true); if (!is_array($state) || ($state['status'] ?? '') !== 'running') { return ['ok' => false, 'err' => 'migration not running']; } $parts = $state['completed_partitions'] ?? []; if (!in_array($partition, $parts, true)) $parts[] = $partition; $state['completed_partitions'] = $parts; $state['verification_digest'] = hash('sha256', ($state['verification_digest'] ?? '') . '|' . $partition . '|' . $digest); $state['last_checkpoint_ts'] = time(); if (@file_put_contents($path, j($state), LOCK_EX) === false) { return ['ok' => false, 'err' => 'checkpoint write failed']; } return ['ok' => true, 'state' => $state]; } function nst_schema_migration_quarantine(string $recordId, string $reason): array { $path = nst_schema_migration_path(); if (!is_file($path)) return ['ok' => false, 'err' => 'no migration']; $state = json_decode((string)@file_get_contents($path), true); if (!is_array($state)) return ['ok' => false, 'err' => 'bad state']; $q = $state['quarantine'] ?? []; $q[] = ['id' => $recordId, 'reason' => $reason, 'ts' => time()]; $state['quarantine'] = $q; @file_put_contents($path, j($state), LOCK_EX); return ['ok' => true, 'quarantined' => count($q)]; } function nst_schema_migration_complete(string $finalDigest): array { $path = nst_schema_migration_path(); if (!is_file($path)) return ['ok' => false, 'err' => 'no migration']; $state = json_decode((string)@file_get_contents($path), true); if (!is_array($state) || ($state['status'] ?? '') !== 'running') { return ['ok' => false, 'err' => 'migration not running']; } $state['status'] = 'complete'; $state['verification_digest'] = $finalDigest !== '' ? $finalDigest : (string)($state['verification_digest'] ?? ''); $state['completed_ts'] = time(); @file_put_contents($path, j($state), LOCK_EX); $sw = nst_schema_pointer_write([ 'schema' => (string)($state['target'] ?? 'nsu-ad-index-v2'), 'event_min_reader' => 'nsu-ad-purchase-event-v2', 'migrating' => false, 'migration_digest' => $state['verification_digest'], 'input_watermark' => (string)($state['input_watermark'] ?? ''), ]); if (empty($sw['ok'])) return $sw; return ['ok' => true, 'state' => $state, 'pointer' => $sw['pointer'] ?? []]; } function nst_schema_migration_reject_unsupported(string $futureVer): array { return [ 'ok' => false, 'code' => 'unsupported_future_version', 'err' => 'Cannot read schema ' . $futureVer . ' — upgrade required; writes refused', 'writes_allowed' => false, 'recovery' => 'forward_fix_or_compatible_reader', ]; } function nst_schema_reader_allows_event(string $eventSchema): array { $ptr = nst_schema_pointer_read(); if (empty($ptr['ok'])) return ['ok' => false, 'err' => 'bad pointer']; $min = (string)($ptr['event_min_reader'] ?? 'nsu-ad-purchase-event-v2'); // Additive: known current + prior intent schemas allowed. $known = ['nsu-ad-purchase-event-v2', 'ad_stake', 'nsu-econ-intent-v2']; if (!in_array($eventSchema, $known, true) && $eventSchema !== $min) { return nst_schema_migration_reject_unsupported($eventSchema); } return ['ok' => true, 'schema' => $eventSchema]; } /** TR-33: bounded financial observability — no secrets. */ function nst_fin_obs_dir(): string { $d = nst_data_file('econ.obs'); if (!is_dir($d)) @mkdir($d, 0700, true); return $d; } function nst_fin_obs_counters_path(): string { return nst_fin_obs_dir() . DIRECTORY_SEPARATOR . 'counters.json'; } function nst_fin_obs_scrub(string $s): string { // Strip hex64 seeds/addrs? Keep short hashes only — redact long hex blobs that look like seeds/sigs. $s = preg_replace('/\b[a-f0-9]{128,}\b/i', '[redacted_long_hex]', $s) ?? $s; $s = preg_replace('/\b(seed|private[_-]?key|mnemonic|client_sig|ecdsa_sig)\b\s*[:=]\s*\S+/i', '$1=[redacted]', $s) ?? $s; $s = preg_replace('#/(?:Users|home|var/www)/[^\s"\']+#', '[redacted_path]', $s) ?? $s; return $s; } function nst_fin_obs_counters_load(): array { $p = nst_fin_obs_counters_path(); if (!is_file($p)) { return [ 'pending_delivery' => 0, 'replay_conflicts' => 0, 'corrupt_reads' => 0, 'lock_timeouts' => 0, 'write_failures' => 0, 'recovery_success' => 0, 'freeze' => 0, 'schema' => 'nsu-econ-obs-counters-v1', ]; } $j = json_decode((string)@file_get_contents($p), true); return is_array($j) ? $j : ['schema' => 'nsu-econ-obs-counters-v1']; } function nst_fin_obs_counters_save(array $c): void { $c['schema'] = 'nsu-econ-obs-counters-v1'; $c['updated_ts'] = time(); @file_put_contents(nst_fin_obs_counters_path(), j($c), LOCK_EX); } function nst_fin_obs_bump(string $key, int $by = 1): void { $c = nst_fin_obs_counters_load(); $c[$key] = (int)($c[$key] ?? 0) + $by; nst_fin_obs_counters_save($c); } /** * Emit one bounded operational event. Never logs seeds/sigs/bodies/images. */ function nst_fin_obs_emit(string $unit, string $stage, string $resultCode, array $opts = []): array { $corr = substr(preg_replace('/[^a-f0-9]/', '', (string)($opts['correlation'] ?? '')) ?? '', 0, 64); $safeHashes = []; foreach (($opts['hashes'] ?? []) as $k => $v) { $kk = preg_replace('/[^a-z0-9_]/', '', strtolower((string)$k)) ?? ''; $vv = preg_replace('/[^a-f0-9]/', '', strtolower((string)$v)) ?? ''; if ($kk !== '' && preg_match('/^[a-f0-9]{64}$/', $vv)) $safeHashes[$kk] = $vv; } $evt = [ 'schema' => 'nsu-econ-obs-v1', 'unit' => substr($unit, 0, 32), 'version' => (string)($opts['version'] ?? '1'), 'stage' => substr($stage, 0, 48), 'result' => substr($resultCode, 0, 48), 'correlation' => $corr, 'duration_ms' => (int)($opts['duration_ms'] ?? 0), 'hashes' => $safeHashes, 'ts' => time(), ]; // Rotate: keep last 200 event lines in a single file. $logPath = nst_fin_obs_dir() . DIRECTORY_SEPARATOR . 'events.jsonl'; $line = nst_fin_obs_scrub(j($evt)) . "\n"; // Reject if secret markers slipped in. if (stripos($line, 'seed=') !== false || stripos($line, '[fixture_secret]') !== false) { $line = j(['schema' => 'nsu-econ-obs-v1', 'result' => 'redacted_block', 'ts' => time()]) . "\n"; } @file_put_contents($logPath, $line, FILE_APPEND | LOCK_EX); // Bound retention: keep last 200 events (also cap by size). if (is_file($logPath)) { $rows = @file($logPath, FILE_IGNORE_NEW_LINES); if (is_array($rows) && (count($rows) > 200 || filesize($logPath) > 256000)) { $rows = array_slice($rows, -200); @file_put_contents($logPath, implode("\n", $rows) . "\n", LOCK_EX); } } $map = [ 'pending_delivery' => 'pending_delivery', 'replay_conflict' => 'replay_conflicts', 'corrupt_read' => 'corrupt_reads', 'lock_timeout' => 'lock_timeouts', 'write_failure' => 'write_failures', 'recovery_success' => 'recovery_success', 'freeze' => 'freeze', ]; if (isset($map[$resultCode])) nst_fin_obs_bump($map[$resultCode]); return ['ok' => true, 'event' => $evt]; } function nst_fin_obs_health_public(): array { $c = nst_fin_obs_counters_load(); $frozen = function_exists('nst_conservation_is_frozen') ? (bool)nst_conservation_is_frozen() : ((int)($c['freeze'] ?? 0) > 0); return [ 'ok' => true, 'schema' => 'nsu-econ-health-v1', 'available' => !$frozen, 'freeze' => $frozen, 'pending_delivery_signal' => ((int)($c['pending_delivery'] ?? 0) > 0), // Aggregates only — no private correlation. 'ops' => [ 'corrupt_reads' => (int)($c['corrupt_reads'] ?? 0), 'lock_timeouts' => (int)($c['lock_timeouts'] ?? 0), 'write_failures' => (int)($c['write_failures'] ?? 0), 'recovery_success' => (int)($c['recovery_success'] ?? 0), ], ]; } function nst_fin_obs_health_handle(): void { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET'); header('Cache-Control: no-store'); if (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) !== 'GET') { nst_ticket_json(['ok' => false, 'err' => 'GET only', 'schema' => 'nsu-econ-health-v1'], 405); } nst_ticket_json(nst_fin_obs_health_public(), 200); } /** TR-34: quotas / bounded reads. */ function nst_quota_batch_slots_max(): int { return 64; } function nst_quota_history_page_max(): int { return 100; } function nst_quota_status_poll_min_ms(): int { return 500; } function nst_quota_check_batch_slots(int $count): array { if ($count > nst_quota_batch_slots_max()) { return ['ok' => false, 'code' => 'batch_too_large', 'err' => 'Maximum ' . nst_quota_batch_slots_max() . ' slots per request', 'retry_after' => 1]; } return ['ok' => true]; } function nst_quota_check_history_page(int $limit): array { $max = nst_quota_history_page_max(); if ($limit > $max) { return ['ok' => true, 'capped' => true, 'limit' => $max, 'requested' => $limit]; } if ($limit < 1) return ['ok' => true, 'capped' => true, 'limit' => 1, 'requested' => $limit]; return ['ok' => true, 'capped' => false, 'limit' => $limit]; } function nst_quota_status_poll_gate(string $actorKey, ?int $now = null): array { $now = $now ?? time(); $dir = nst_fin_obs_dir(); $safe = substr(hash('sha256', 'poll|' . $actorKey), 0, 32); $p = $dir . DIRECTORY_SEPARATOR . 'poll.' . $safe; $minMs = nst_quota_status_poll_min_ms(); $last = is_file($p) ? (int)@file_get_contents($p) : 0; $elapsedMs = (int)(($now - $last) * 1000); if ($last > 0 && $elapsedMs < $minMs) { $wait = max(1, (int)ceil(($minMs - $elapsedMs) / 1000)); return ['ok' => false, 'code' => 'busy', 'err' => 'Status poll rate limited', 'retry_after' => $wait, 'intent_preserved' => true]; } @file_put_contents($p, (string)$now, LOCK_EX); return ['ok' => true]; } /** TR-35: receipt verification key registry (verification material only — no private keys). */ function nst_receipt_key_registry_path(): string { return nst_data_file('econ.receipt.keys.json'); } function nst_receipt_key_registry_load(): array { $p = nst_receipt_key_registry_path(); if (!is_file($p)) { return ['schema' => 'nsu-receipt-keys-v1', 'keys' => [], 'issuer' => 'nosignup.trade']; } $j = json_decode((string)@file_get_contents($p), true); return is_array($j) ? $j : ['schema' => 'nsu-receipt-keys-v1', 'keys' => []]; } function nst_receipt_key_registry_save(array $reg): array { $reg['schema'] = 'nsu-receipt-keys-v1'; // Strip any accidental private material keys. foreach (['private_key', 'seed', 'sk', 'signing_key'] as $bad) { unset($reg[$bad]); } if (isset($reg['keys']) && is_array($reg['keys'])) { foreach ($reg['keys'] as &$k) { if (!is_array($k)) continue; unset($k['private_key'], $k['seed'], $k['sk'], $k['signing_key']); } unset($k); } $path = nst_receipt_key_registry_path(); if (@file_put_contents($path, j($reg), LOCK_EX) === false) { return ['ok' => false, 'err' => 'registry save failed']; } return ['ok' => true]; } /** * Register ephemeral verification-only key material (lab/tests). Never accepts private keys. */ function nst_receipt_key_register(array $entry): array { $kid = preg_replace('/[^a-zA-Z0-9._-]/', '', (string)($entry['kid'] ?? '')) ?? ''; $algo = (string)($entry['algo'] ?? 'ECDSA_P256'); $verifySpkiHex = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($entry['verify_spki_hex'] ?? '')) ?? ''); $validFrom = (int)($entry['valid_from'] ?? time()); $validTo = (int)($entry['valid_to'] ?? (time() + 86400 * 90)); $issuer = (string)($entry['issuer'] ?? 'nosignup.trade'); $revoked = !empty($entry['revoked']); if ($kid === '' || strlen($verifySpkiHex) < 64) { return ['ok' => false, 'err' => 'kid and verify_spki_hex required']; } if (isset($entry['private_key']) || isset($entry['seed']) || isset($entry['sk'])) { return ['ok' => false, 'err' => 'private material refused']; } $reg = nst_receipt_key_registry_load(); $keys = $reg['keys'] ?? []; $keys[$kid] = [ 'kid' => $kid, 'algo' => $algo, 'verify_spki_hex' => $verifySpkiHex, 'valid_from' => $validFrom, 'valid_to' => $validTo, 'issuer' => $issuer, 'revoked' => $revoked, 'network' => (string)($entry['network'] ?? 'nosignup'), ]; $reg['keys'] = $keys; $reg['issuer'] = $issuer; $s = nst_receipt_key_registry_save($reg); if (empty($s['ok'])) return $s; return ['ok' => true, 'kid' => $kid]; } function nst_receipt_key_select(string $kid, ?int $now = null): array { $now = $now ?? time(); $reg = nst_receipt_key_registry_load(); $keys = $reg['keys'] ?? []; if (!isset($keys[$kid]) || !is_array($keys[$kid])) { return ['ok' => false, 'code' => 'unknown_kid', 'err' => 'Unknown receipt key id']; } $k = $keys[$kid]; if (!empty($k['revoked'])) { return ['ok' => false, 'code' => 'revoked', 'err' => 'Receipt key revoked', 'kid' => $kid]; } if ($now < (int)($k['valid_from'] ?? 0)) { return ['ok' => false, 'code' => 'not_yet_valid', 'err' => 'Receipt key not yet valid', 'kid' => $kid]; } if ($now > (int)($k['valid_to'] ?? 0)) { return ['ok' => false, 'code' => 'expired', 'err' => 'Receipt key expired', 'kid' => $kid]; } return ['ok' => true, 'key' => $k]; } /** * Verify a receipt payload against allowlisted kid. Does not authorize spend. * $sigHex is P1363 hex; message is canonical receipt body. */ function nst_receipt_verify(array $receipt, string $sigHex, ?int $now = null): array { $kid = (string)($receipt['kid'] ?? ''); $issuer = (string)($receipt['issuer'] ?? ''); $sel = nst_receipt_key_select($kid, $now); if (empty($sel['ok'])) return $sel; $k = $sel['key']; if ($issuer !== '' && $issuer !== (string)($k['issuer'] ?? '')) { return ['ok' => false, 'code' => 'wrong_issuer', 'err' => 'Issuer mismatch']; } $body = (string)($receipt['canonical'] ?? ''); if ($body === '') { // Build from common fields $body = implode('|', [ (string)($receipt['schema'] ?? 'nsu-slowgpt-receipt-v1'), (string)($receipt['payer'] ?? ''), (string)($receipt['job'] ?? ''), (string)($receipt['product'] ?? ''), (string)($receipt['site'] ?? ''), (string)($receipt['epoch_n'] ?? ''), (string)($receipt['parallel'] ?? ''), (string)($receipt['amount_micros'] ?? ''), (string)($receipt['receipt_id'] ?? ''), (string)($receipt['exp'] ?? ''), ]); } $spkiHex = (string)($k['verify_spki_hex'] ?? ''); $spki = @hex2bin($spkiHex); if ($spki === false || $spki === '') { return ['ok' => false, 'code' => 'bad_verify_material', 'err' => 'Bad verify key']; } if (!function_exists('nst_ecdsa_verify_p256')) { return ['ok' => false, 'code' => 'verifier_unavailable', 'err' => 'ECDSA verifier unavailable']; } $ok = nst_ecdsa_verify_p256($spki, $body, strtolower(preg_replace('/[^a-f0-9]/', '', $sigHex) ?? '')); if (!$ok) { nst_fin_obs_emit('TR-35', 'receipt_verify', 'verify_fail', ['correlation' => hash('sha256', $kid . '|' . $body)]); return ['ok' => false, 'code' => 'bad_signature', 'err' => 'Receipt signature invalid', 'kid' => $kid]; } nst_fin_obs_emit('TR-35', 'receipt_verify', 'verify_ok', ['correlation' => hash('sha256', $kid)]); return ['ok' => true, 'kid' => $kid, 'issuer' => (string)($k['issuer'] ?? ''), 'authorizes_new_delivery' => true]; } /** * TR-28 admission prep (CLOSED): schema + hook readiness note only. * Do NOT enable nst_slowgpt_paid_enabled. */ function nst_slowgpt_receipt_contract_schema(): string { return 'nsu-slowgpt-receipt-v1'; } function nst_slowgpt_admission_hook_ready(): bool { // Producer-side receipt verify helper exists; INFO admission still required. return function_exists('nst_receipt_verify') && function_exists('nst_receipt_key_register'); } function nst_slowgpt_admission_status(): array { return [ 'ok' => true, 'schema' => nst_slowgpt_receipt_contract_schema(), 'paid_enabled' => function_exists('nst_slowgpt_paid_enabled') ? nst_slowgpt_paid_enabled() : false, 'hook_ready' => nst_slowgpt_admission_hook_ready(), 'note' => 'Paid SlowGPT stays CLOSED until INFO server admission agrees. Free 1-4 bring-up preserved.', ]; } /** TR-36: release gate summary for finish-line evidence (local). */ function nst_econ_release_gate_summary(): array { $cap = nst_econ_protocol_capability(); return [ 'ok' => true, 'schema' => 'nsu-econ-release-gates-v1', 'source_generation' => $cap['source_generation'] ?? '', 'gates' => [ 'nst_ad_spend_ready' => (($cap['products']['ad_purchase'] ?? '') === 'enabled') ? 'OPEN' : 'CLOSED', 'nst_slowgpt_paid_enabled' => 'CLOSED', 'all_site_swap_give' => 'CLOSED', 'checkout_confirm' => 'REQUIRED', ], 'holds' => [ 'TR-16' => 'all-site atomic group commit not completed — leave CLOSED', 'TR-28' => 'paid SlowGPT awaiting INFO admission agreement', ], 'wave_e' => ['TR-30', 'TR-32', 'TR-33', 'TR-34', 'TR-35', 'TR-36'], 'money_law' => 'integer_micros_100M_once_no_remint', 'auth' => 'ECDSA-P256-v2', 'telomere' => 'preserved', ]; } function nst_wavee_helpers_present(): bool { return true; } /* ==== END WAVEE-TR30-TR36 OPS ==== */ /* ==== BEGIN COMMUNITY-AD-MODERATION-V1 (do not split) ==== */ /** * Community ad report / showings / takedown. Trade owns the ledger. * Chat POSTs site+slot+epoch only (no peer/wallet/seed). No ownership change, * no credit/refund. TR-16 all-site and TR-28 SlowGPT paid stay CLOSED. * Schema: nsu-ad-report-v1. Store: data/ad.reports.json (file lock). * Takedown: ≥5 accepted reports while showings < 50 for that creative. * New creative sha → new counter+showings bucket. Soft IP+day fingerprint. */ function nst_ad_report_schema(): string { return 'nsu-ad-report-v1'; } function nst_ad_report_store_path(): string { return nst_data_file('ad.reports.json'); } function nst_ad_report_threshold(): int { return defined('NST_AD_REPORT_THRESHOLD') ? (int)NST_AD_REPORT_THRESHOLD : 5; } function nst_ad_report_showings_window(): int { return defined('NST_AD_REPORT_SHOWINGS_WINDOW') ? (int)NST_AD_REPORT_SHOWINGS_WINDOW : 50; } function nst_ad_report_placeholder_text(): string { return 'This advertisement was reported too many times'; } function nst_ad_buyer_report_warning(): string { return 'Max 800KB · longest side ≤800px · png/jpg/gif/webp. If the community repeatedly reports you, your ad is automatically removed (5 reports while under 50 showings → takedown; reupload a different creative). Ownership for the year is kept. Check My Ads from time to time to see if your ads are okay; impressions are shown there.'; } function nst_ad_report_bucket_key(string $site, int $slot, int $epoch, string $creativeSha): string { $sha = strtolower(preg_replace('/[^a-f0-9]/', '', $creativeSha) ?? ''); if (!preg_match('/^[a-f0-9]{64}$/', $sha)) $sha = 'unknown'; return $site . '|' . (string)$slot . '|' . (string)$epoch . '|' . $sha; } function nst_ad_report_client_fingerprint(string $site, int $slot, int $epoch): string { $ip = (string)($_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? ''); if (str_contains($ip, ',')) $ip = trim(explode(',', $ip, 2)[0]); $ip = preg_replace('/[^0-9a-fA-F:.\-]/', '', $ip) ?? ''; $day = gmdate('Y-m-d'); // UTC day bucket; documented soft anti-spam only return hash('sha256', $ip . '|' . $site . '|' . (string)$slot . '|' . (string)$epoch . '|' . $day); } function nst_ad_report_store_empty(): array { return ['schema' => 'nsu-ad-report-store-v1', 'buckets' => []]; } function nst_ad_report_store_read_fh($fh): array { rewind($fh); $raw = stream_get_contents($fh); if (!is_string($raw) || trim($raw) === '') return nst_ad_report_store_empty(); $j = json_decode($raw, true); if (!is_array($j)) return nst_ad_report_store_empty(); if (!isset($j['buckets']) || !is_array($j['buckets'])) $j['buckets'] = []; $j['schema'] = 'nsu-ad-report-store-v1'; return $j; } function nst_ad_report_store_write_fh($fh, array $store): void { rewind($fh); ftruncate($fh, 0); fwrite($fh, j($store)); fflush($fh); } /** Best-effort unlocked read of ad.reports store (console / peek only). */ function nst_ad_report_store_read_unlocked(): array { $path = nst_ad_report_store_path(); if (!is_file($path)) return nst_ad_report_store_empty(); $raw = @file_get_contents($path); if (!is_string($raw) || trim($raw) === '') return nst_ad_report_store_empty(); $j = json_decode($raw, true); if (!is_array($j)) return nst_ad_report_store_empty(); if (!isset($j['buckets']) || !is_array($j['buckets'])) $j['buckets'] = []; $j['schema'] = 'nsu-ad-report-store-v1'; return $j; } /** Single durable impressions digit for one creative bucket (+ takedown flag). */ function nst_ad_report_bucket_peek(string $site, int $n, int $epoch, string $creativeSha, ?array $store = null): array { $out = ['showings' => 0, 'takedown' => false, 'reports' => 0]; if ($creativeSha === '' || !preg_match('/^[a-f0-9]{64}$/', $creativeSha)) return $out; if ($store === null) $store = nst_ad_report_store_read_unlocked(); $key = nst_ad_report_bucket_key($site, $n, $epoch, $creativeSha); $b = $store['buckets'][$key] ?? null; if (!is_array($b)) return $out; $out['showings'] = (int)($b['showings'] ?? 0); $out['takedown'] = !empty($b['takedown']); $out['reports'] = (int)($b['count'] ?? 0); return $out; } function nst_ad_report_resolve_creative_sha(string $site, int $n, ?array $meta): string { if (is_array($meta) && isset($meta['creative_sha256']) && is_string($meta['creative_sha256'])) { $sha = strtolower(preg_replace('/[^a-f0-9]/', '', $meta['creative_sha256']) ?? ''); if (preg_match('/^[a-f0-9]{64}$/', $sha)) return $sha; } if (function_exists('nst_ad_slot_find')) { $found = nst_ad_slot_find($site, $n); if ($found && is_file($found)) { $sha = hash_file('sha256', $found); if (is_string($sha) && preg_match('/^[a-f0-9]{64}$/', $sha)) return $sha; } } return ''; } /** GD placeholder with exact phrase; face dims when known. */ function nst_ad_takedown_png_bin(string $site, int $n): string { if (!function_exists('imagecreatetruecolor')) return ''; $dims = function_exists('nst_ad_house_dims') ? nst_ad_house_dims($site) : ['w' => 640, 'h' => 256]; $W = max(16, (int)($dims['w'] ?? 640)); $H = max(16, (int)($dims['h'] ?? 256)); $im = imagecreatetruecolor($W, $H); if ($im === false) return ''; $bg = imagecolorallocate($im, 28, 18, 18); $border = imagecolorallocate($im, 120, 60, 60); $white = imagecolorallocate($im, 240, 230, 230); imagefilledrectangle($im, 0, 0, $W - 1, $H - 1, $bg); imagerectangle($im, 3, 3, $W - 4, $H - 4, $border); $text = nst_ad_report_placeholder_text(); $font = function_exists('nst_ad_house_font') ? nst_ad_house_font('bold') : ''; if ($font === '') { // Wrap with imagestring $words = explode(' ', $text); $lines = []; $cur = ''; foreach ($words as $w) { $try = $cur === '' ? $w : ($cur . ' ' . $w); if (strlen($try) > max(12, (int)($W / 8))) { if ($cur !== '') $lines[] = $cur; $cur = $w; } else { $cur = $try; } } if ($cur !== '') $lines[] = $cur; $ystep = 14; $y = (int)max(4, ($H - $ystep * count($lines)) / 2); foreach ($lines as $line) { $tw = imagefontwidth(3) * strlen($line); imagestring($im, 3, (int)max(4, ($W - $tw) / 2), $y, $line, $white); $y += $ystep; } } else { $scale = ($W < $H) ? ($W / 160.0) : ($H / 256.0); if ($scale < 0.25) $scale = 0.25; $sz = max(8.0, 22.0 * $scale); // Word-wrap via TTF measure $words = explode(' ', $text); $lines = []; $cur = ''; $inner = max(8, $W - 24); foreach ($words as $w) { $try = $cur === '' ? $w : ($cur . ' ' . $w); $bbox = imagettfbbox($sz, 0, $font, $try); $tw = is_array($bbox) ? (int)abs($bbox[2] - $bbox[0]) : strlen($try) * 10; if ($tw > $inner && $cur !== '') { $lines[] = $cur; $cur = $w; } else { $cur = $try; } } if ($cur !== '') $lines[] = $cur; $boxes = []; $total_h = 0; $gap = max(2, (int)round(8 * $scale)); foreach ($lines as $line) { $bbox = imagettfbbox($sz, 0, $font, $line); if ($bbox === false) { imagedestroy($im); return ''; } $w = (int)abs($bbox[2] - $bbox[0]); $h = (int)abs($bbox[7] - $bbox[1]); $boxes[] = [$line, $w, $h, $bbox]; $total_h += $h; } $total_h += $gap * max(0, count($boxes) - 1); $y = (int)(($H - $total_h) / 2); foreach ($boxes as $i => $b) { [$line, $w, $h, $bbox] = $b; $x = (int)(($W - $w) / 2); // baseline adjustment $base = $y + $h; imagettftext($im, $sz, 0, $x, $base, $white, $font, $line); $y += $h + $gap; } } ob_start(); imagepng($im); $bin = (string)ob_get_clean(); imagedestroy($im); return str_starts_with($bin, "\x89PNG") ? $bin : ''; } function nst_ad_takedown_apply(string $site, int $n, string $owner, string $prevSha): array { $site = function_exists('nst_ad_site_norm') ? nst_ad_site_norm($site) : strtolower($site); $bin = nst_ad_takedown_png_bin($site, $n); if ($bin === '') { return ['ok' => false, 'err' => 'takedown png failed']; } // Same owner only. write_slot refuses a different owner and does not unlink another crop's file. $path = null; if (function_exists('nst_ad_write_slot')) { $path = nst_ad_write_slot($site, $n, $bin, 'png', $owner); } if (!$path || !is_file($path)) { return ['ok' => false, 'err' => 'takedown write failed']; } $sha = hash_file('sha256', $path); // Update index: keep owner, set takedown flag, refresh creative sha. $idxPath = function_exists('nst_ad_index_path') ? nst_ad_index_path() : nst_data_file('ad.slots.json'); $ret = nst_with_file_lock($idxPath, static function ($fh) use ($site, $n, $owner, $sha) { $raw = stream_get_contents($fh); $idx = is_string($raw) && $raw !== '' ? json_decode($raw, true) : null; if (!is_array($idx)) $idx = function_exists('nst_ad_index_empty') ? nst_ad_index_empty() : ['epoch_n' => 1, 'sites' => []]; if (!isset($idx['sites'][$site]) || !is_array($idx['sites'][$site])) $idx['sites'][$site] = []; $prev = $idx['sites'][$site][(string)$n] ?? []; if (!is_array($prev)) $prev = []; $own = $owner !== '' ? $owner : (string)($prev['owner'] ?? ''); $idx['sites'][$site][(string)$n] = [ 'ext' => 'png', 'owner' => $own, 'ts' => time(), 'takedown' => true, 'creative_sha256' => $sha, ]; rewind($fh); ftruncate($fh, 0); fwrite($fh, j($idx)); fflush($fh); return true; }); if ($ret !== true) { return ['ok' => false, 'err' => 'index update failed', 'path' => $path, 'creative_sha256' => $sha]; } return ['ok' => true, 'path' => $path, 'creative_sha256' => $sha, 'prev_sha' => $prevSha]; } function nst_ad_report_showing_inc(string $site, int $n, int $epoch, string $creativeSha): void { if ($creativeSha === '' || !preg_match('/^[a-f0-9]{64}$/', $creativeSha)) return; $key = nst_ad_report_bucket_key($site, $n, $epoch, $creativeSha); $path = nst_ad_report_store_path(); try { nst_with_file_lock($path, static function ($fh) use ($key, $site, $n, $epoch, $creativeSha) { $store = nst_ad_report_store_read_fh($fh); $b = $store['buckets'][$key] ?? null; if (!is_array($b)) { $b = [ 'site' => $site, 'slot' => $n, 'epoch' => $epoch, 'creative_sha256' => $creativeSha, 'count' => 0, 'showings' => 0, 'takedown' => false, 'fps' => [], 'ts_first' => time(), 'ts_last' => time(), ]; } // Count owned + post-takedown placeholder views (My Ads digit). // Unsold house has no owner bucket and never reaches this path. $b['showings'] = (int)($b['showings'] ?? 0) + 1; $b['ts_last'] = time(); $store['buckets'][$key] = $b; nst_ad_report_store_write_fh($fh, $store); return true; }); } catch (Throwable $e) { // best-effort; never break image serve } } /** * Core report accept. No money movement. Returns response array + http hint. */ function nst_ad_report_accept(string $site, $slotRaw, $epochRaw, ?string $fp = null): array { $schema = nst_ad_report_schema(); $site = function_exists('nst_ad_site_norm') ? nst_ad_site_norm($site) : strtolower(trim($site)); if ($site === '') { return ['http' => 400, 'body' => ['ok' => false, 'err' => 'bad site', 'schema' => $schema]]; } $n = function_exists('nst_ad_slot_norm_n') ? nst_ad_slot_norm_n($slotRaw) : null; if ($n === null) { return ['http' => 400, 'body' => ['ok' => false, 'err' => 'bad slot', 'schema' => $schema]]; } $epochN = 0; if (function_exists('nst_epoch_n_parse')) { $ep = nst_epoch_n_parse($epochRaw); if (empty($ep['ok'])) { return ['http' => 400, 'body' => ['ok' => false, 'err' => 'bad epoch', 'schema' => $schema]]; } $epochN = (int)$ep['epoch_n']; } else { $epochN = (int)$epochRaw; if ($epochN < 1) { return ['http' => 400, 'body' => ['ok' => false, 'err' => 'bad epoch', 'schema' => $schema]]; } } $threshold = nst_ad_report_threshold(); $win = nst_ad_report_showings_window(); $pad = function_exists('nst_ad_slot_pad') ? nst_ad_slot_pad($n) : str_pad((string)$n, 4, '0', STR_PAD_LEFT); $load = function_exists('nst_ad_index_load_result') ? nst_ad_index_load_result() : null; $status = is_array($load) ? (string)($load['status'] ?? '') : 'unreadable'; $idx = is_array($load) && is_array($load['payload'] ?? null) ? $load['payload'] : null; if ($status !== 'valid-current' || !is_array($idx)) { // missing-new-install / empty → house; nothing to report return ['http' => 409, 'body' => [ 'ok' => false, 'err' => 'nothing to report', 'code' => 'house_or_unavailable', 'schema' => $schema, 'site' => $site, 'slot' => $pad, 'epoch' => $epochN, 'takedown' => false, ]]; } $idxEpoch = (int)($idx['epoch_n'] ?? 0); if ($idxEpoch !== $epochN) { return ['http' => 409, 'body' => [ 'ok' => false, 'err' => 'epoch mismatch', 'code' => 'epoch_mismatch', 'schema' => $schema, 'site' => $site, 'slot' => $pad, 'epoch' => $epochN, 'takedown' => false, ]]; } $meta = $idx['sites'][$site][(string)$n] ?? $idx['sites'][$site][$pad] ?? null; if (!is_array($meta) || empty($meta['owner'])) { return ['http' => 409, 'body' => [ 'ok' => false, 'err' => 'nothing to report', 'code' => 'house', 'schema' => $schema, 'site' => $site, 'slot' => $pad, 'epoch' => $epochN, 'takedown' => false, ]]; } $owner = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($meta['owner'] ?? '')) ?? ''); $alreadyTd = !empty($meta['takedown']); $creativeSha = nst_ad_report_resolve_creative_sha($site, $n, $meta); if ($creativeSha === '') $creativeSha = 'unknown'; if ($fp === null || $fp === '') { $fp = nst_ad_report_client_fingerprint($site, $n, $epochN); } $path = nst_ad_report_store_path(); $result = nst_with_file_lock($path, static function ($fh) use ( $site, $n, $epochN, $creativeSha, $fp, $threshold, $win, $alreadyTd, $owner, $pad, $schema, $meta ) { $store = nst_ad_report_store_read_fh($fh); $key = nst_ad_report_bucket_key($site, $n, $epochN, $creativeSha); $b = $store['buckets'][$key] ?? null; if (!is_array($b)) { $b = [ 'site' => $site, 'slot' => $n, 'epoch' => $epochN, 'creative_sha256' => $creativeSha, 'count' => 0, 'showings' => 0, 'takedown' => false, 'fps' => [], 'ts_first' => time(), 'ts_last' => time(), ]; } if ($alreadyTd || !empty($b['takedown'])) { $b['takedown'] = true; if ((int)($b['count'] ?? 0) < $threshold) $b['count'] = $threshold; $store['buckets'][$key] = $b; nst_ad_report_store_write_fh($fh, $store); return [ 'http' => 200, 'body' => [ 'ok' => true, 'schema' => $schema, 'site' => $site, 'slot' => $pad, 'epoch' => $epochN, 'count' => (int)$b['count'], 'showings' => (int)($b['showings'] ?? 0), 'threshold' => $threshold, 'showings_window' => $win, 'status' => 'takedown', 'takedown' => true, 'creative_sha256' => $creativeSha === 'unknown' ? null : $creativeSha, ], 'do_takedown' => false, ]; } $fps = is_array($b['fps'] ?? null) ? $b['fps'] : []; if (isset($fps[$fp])) { // Soft anti-spam: same client+day already counted $store['buckets'][$key] = $b; nst_ad_report_store_write_fh($fh, $store); $count = (int)($b['count'] ?? 0); $showings = (int)($b['showings'] ?? 0); $status = 'duplicate'; $td = false; if ($count >= $threshold && $showings < $win) { $status = 'takedown'; $td = !empty($b['takedown']); } elseif ($count >= $threshold && $showings >= $win) { $status = 'window_closed'; } return [ 'http' => 200, 'body' => [ 'ok' => true, 'schema' => $schema, 'site' => $site, 'slot' => $pad, 'epoch' => $epochN, 'count' => $count, 'showings' => $showings, 'threshold' => $threshold, 'showings_window' => $win, 'status' => $status, 'takedown' => $td, 'note' => 'duplicate fingerprint today — count unchanged', 'creative_sha256' => $creativeSha === 'unknown' ? null : $creativeSha, ], 'do_takedown' => false, ]; } $fps[$fp] = time(); // Bound fps map if (count($fps) > 64) { asort($fps); $fps = array_slice($fps, -48, null, true); } $b['fps'] = $fps; $b['count'] = (int)($b['count'] ?? 0) + 1; $b['ts_last'] = time(); if (empty($b['ts_first'])) $b['ts_first'] = time(); $count = (int)$b['count']; $showings = (int)($b['showings'] ?? 0); $doTd = false; $status = 'accepted'; $tdFlag = false; if ($count >= $threshold) { if ($showings < $win) { $status = 'takedown'; $tdFlag = true; $doTd = true; $b['takedown'] = true; } else { $status = 'window_closed'; $tdFlag = false; } } $store['buckets'][$key] = $b; nst_ad_report_store_write_fh($fh, $store); return [ 'http' => 200, 'body' => [ 'ok' => true, 'schema' => $schema, 'site' => $site, 'slot' => $pad, 'epoch' => $epochN, 'count' => $count, 'showings' => $showings, 'threshold' => $threshold, 'showings_window' => $win, 'status' => $status, 'takedown' => $tdFlag, 'creative_sha256' => $creativeSha === 'unknown' ? null : $creativeSha, ], 'do_takedown' => $doTd, 'owner' => $owner, 'creative_sha' => $creativeSha, 'slot_n' => $n, 'site' => $site, ]; }); if (!is_array($result)) { return ['http' => 503, 'body' => ['ok' => false, 'err' => 'report store busy', 'schema' => $schema]]; } if (!empty($result['do_takedown'])) { $td = nst_ad_takedown_apply( (string)$result['site'], (int)$result['slot_n'], (string)($result['owner'] ?? ''), (string)($result['creative_sha'] ?? '') ); if (empty($td['ok'])) { $result['body']['takedown_err'] = (string)($td['err'] ?? 'takedown failed'); // Still report accepted count; status stays takedown intent } else { $result['body']['placeholder_sha256'] = $td['creative_sha256'] ?? null; } } return ['http' => (int)$result['http'], 'body' => $result['body']]; } function nst_ad_report_handle(): void { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type'); header('Cache-Control: no-store'); $method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')); if ($method === 'OPTIONS') { http_response_code(204); exit; } if ($method !== 'POST') { nst_ticket_json(['ok' => false, 'err' => 'POST only', 'schema' => nst_ad_report_schema()], 405); } $site = (string)($_POST['site'] ?? $_POST['crop'] ?? $_GET['site'] ?? $_GET['crop'] ?? ''); $slot = $_POST['slot'] ?? $_POST['n'] ?? $_GET['slot'] ?? $_GET['n'] ?? ''; $epoch = $_POST['epoch'] ?? $_POST['epoch_n'] ?? $_GET['epoch'] ?? $_GET['epoch_n'] ?? ''; $ctype = strtolower((string)($_SERVER['CONTENT_TYPE'] ?? $_SERVER['HTTP_CONTENT_TYPE'] ?? '')); if ($site === '' || $slot === '' || $epoch === '' || str_contains($ctype, 'application/json')) { $raw = (string)@file_get_contents('php://input'); if ($raw !== '') { $j = json_decode($raw, true); if (is_array($j)) { if ($site === '') $site = (string)($j['site'] ?? $j['crop'] ?? ''); if ($slot === '' || $slot === null) $slot = $j['slot'] ?? $j['n'] ?? ''; if ($epoch === '' || $epoch === null) $epoch = $j['epoch'] ?? $j['epoch_n'] ?? ''; } } } // Also accept crop= query alias as site when body omitted site if ($site === '') $site = (string)($_GET['crop'] ?? ''); $out = nst_ad_report_accept($site, $slot, $epoch, null); nst_ticket_json($out['body'], (int)$out['http']); } function nst_ad_report_selftest(): void { global $DATA, $ADS; $chk = static function (string $name, bool $ok, string $detail = ''): void { echo ($ok ? 'PASS' : 'FAIL') . "\t" . $name . ($detail !== '' ? ("\t" . $detail) : '') . "\n"; if (!$ok) { fwrite(STDERR, "selftest failed: $name $detail\n"); exit(1); } }; $chk('placeholder text', nst_ad_report_placeholder_text() === 'This advertisement was reported too many times'); $chk('threshold', nst_ad_report_threshold() === 5); $chk('showings window', nst_ad_report_showings_window() === 50); $chk('max side 800', NST_AD_MAX_W === 800 && NST_AD_MAX_H === 800); $chk('byte cap 800k', NST_AD_MAX_PNG === 819200); // Isolated fixture under lab data already pointed by $DATA/$ADS $site = 'chat'; $slot = 4242; $epoch = function_exists('nst_econ_current_epoch_n') ? nst_econ_current_epoch_n() : 1; $owner = str_repeat('ab', 32); // Tiny valid PNG via GD $bin = ''; if (function_exists('imagecreatetruecolor')) { $im = imagecreatetruecolor(64, 48); $bg = imagecolorallocate($im, 20, 80, 120); imagefilledrectangle($im, 0, 0, 63, 47, $bg); ob_start(); imagepng($im); $bin = (string)ob_get_clean(); imagedestroy($im); } $chk('fixture png', $bin !== '' && str_starts_with($bin, "\x89PNG")); $path = nst_ad_write_slot($site, $slot, $bin, 'png', $owner); $chk('write slot', is_string($path) && is_file($path)); $sha = hash_file('sha256', $path); $chk('mark sold', nst_ad_mark_sold($site, $slot, 'png', $owner, $sha)); // Build index epoch alignment $idx = nst_ad_index_load(); $chk('index has slot', isset($idx['sites'][$site][(string)$slot])); // House reject $house = nst_ad_report_accept($site, 1, $epoch, hash('sha256', 'house-fp')); $chk('house reject', (int)$house['http'] === 409 && ($house['body']['code'] ?? '') !== ''); // Bad site $bad = nst_ad_report_accept('nope', $slot, $epoch, hash('sha256', 'x')); $chk('bad site', (int)$bad['http'] === 400); // Ramp reports with distinct fingerprints (showings 0 < 50) $last = null; for ($i = 1; $i <= 5; $i++) { $last = nst_ad_report_accept($site, $slot, $epoch, hash('sha256', 'fp-' . $i)); $chk("report $i http", (int)$last['http'] === 200, j($last['body'])); $chk("report $i count", (int)($last['body']['count'] ?? 0) === $i, j($last['body'])); } $chk('takedown status', ($last['body']['status'] ?? '') === 'takedown' && !empty($last['body']['takedown'])); $idx2 = nst_ad_index_load(); $meta2 = $idx2['sites'][$site][(string)$slot] ?? []; $chk('meta takedown', !empty($meta2['takedown'])); $chk('owner preserved', (string)($meta2['owner'] ?? '') === $owner); $rec = nst_ad_slot_state_record($site, $slot, (int)($idx2['epoch_n'] ?? $epoch), $idx2); $chk('state takedown', ($rec['state'] ?? '') === 'takedown'); $proj = nst_ad_serve_slot_from_projection($site, $slot); $chk('proj takedown', ($proj['kind'] ?? '') === 'takedown' && !empty($proj['path'])); // Placeholder file contains phrase (GD text may not be OCR'd; check bin non-empty PNG) $tdBin = (string)@file_get_contents((string)$proj['path']); $chk('placeholder png', str_starts_with($tdBin, "\x89PNG")); // Idempotent $again = nst_ad_report_accept($site, $slot, $epoch, hash('sha256', 'fp-more')); $chk('idempotent takedown', (int)$again['http'] === 200 && !empty($again['body']['takedown'])); // Window-closed path: new creative + high showings $im2 = imagecreatetruecolor(80, 40); $c2 = imagecolorallocate($im2, 200, 40, 40); imagefilledrectangle($im2, 0, 0, 79, 39, $c2); ob_start(); imagepng($im2); $bin2 = (string)ob_get_clean(); imagedestroy($im2); $path2 = nst_ad_write_slot($site, $slot, $bin2, 'png', $owner); $sha2 = hash_file('sha256', $path2); $chk('new creative', $sha2 !== $sha); nst_ad_mark_sold($site, $slot, 'png', $owner, $sha2); for ($s = 0; $s < 50; $s++) { nst_ad_report_showing_inc($site, $slot, $epoch, $sha2); } $last2 = null; for ($i = 1; $i <= 5; $i++) { $last2 = nst_ad_report_accept($site, $slot, $epoch, hash('sha256', 'win-fp-' . $i)); } $chk('window closed', ($last2['body']['status'] ?? '') === 'window_closed' && empty($last2['body']['takedown']), j($last2['body'])); $idx3 = nst_ad_index_load(); $meta3 = $idx3['sites'][$site][(string)$slot] ?? []; $chk('no takedown after window', empty($meta3['takedown'])); echo "ad_report selftest OK\n"; } /* ==== END COMMUNITY-AD-MODERATION-V1 ==== */ /* ---- TINY PURPOSE BRAINS (hand-crafted, in-file, no external deps) ---- * Each brain does ONE job. Sentience = many small coherent specialists, * not one cloud oracle. Fail open when unsure. */ /** Brain: memo_risk - bag-of-words scam/phishing score in [0,1]. Fail open. */ function nst_brain_memo_risk(string $text): array { $t = strtolower($text); $hits = []; $bad = [ 'seed phrase' => 0.35, 'private key' => 0.4, 'send all' => 0.25, 'double your' => 0.3, 'airdrop claim' => 0.25, 'connect wallet' => 0.2, 'verify wallet' => 0.25, 'urgent' => 0.1, 'guaranteed' => 0.15, '100x' => 0.2, 'giveaway' => 0.1, ]; $score = 0.0; foreach ($bad as $k => $w) { if ($k !== '' && str_contains($t, $k)) { $score += $w; $hits[] = $k; } } if ($score > 1.0) $score = 1.0; return ['brain' => 'memo_risk', 'score' => round($score, 3), 'hits' => $hits, 'flag' => $score >= 0.45]; } /** Brain: order_sanity - soft checks on side/amount/price; never blocks alone. */ function nst_brain_order_sanity(string $side, string $amount, string $price): array { $notes = []; $score = 0.0; if (!in_array($side, ['buy', 'sell'], true)) { $notes[] = 'side'; $score += 0.5; } if (dec_cmp($amount, '0') <= 0) { $notes[] = 'amount'; $score += 0.4; } if (dec_cmp($price, '0') <= 0) { $notes[] = 'price'; $score += 0.4; } // huge size relative to max supply micros is absurd if (strlen(dec_norm($amount)) > 18) { $notes[] = 'amount_huge'; $score += 0.3; } if ($score > 1.0) $score = 1.0; return ['brain' => 'order_sanity', 'score' => round($score, 3), 'notes' => $notes, 'flag' => $score >= 0.5]; } /* NS_YEARLY_WIPE_COPY 2026-09-26 */ /** * Brain: wipe_copy + ephemerality reel - explicit yearly law for news-reel banner. * Segments are short so the reel can use larger type on Android (screen efficiency). * Coherence: same facts as gate / coach / top banner — service+pricing reset; coins stay; orders/ads/profit burn. * * @return list */ function nst_ephemerality_reel_segments(int $daysLeft, ?array $extra = null): array { $d = max(0, $daysLeft); $segs = [ 'RESET | Service and pricing reset in ' . $d . ' days', 'EPHEMERALITY | this crop forgets on purpose every year', 'SURVIVES | your NSU balances and existing wallet-access records', 'BURNS | orders | ads | profit | creatives | mods | panel loot', 'RE-KEYS | legacy panel password can die at wipe; site wallet seed is NOT auto-rotated (rotate yourself if leaked)', 'TREASURY | king starts with 100 million | after that nothing is printed | no drip', 'VALUE | THIS server owner sets local k / value note | not mirrors | not a global bank', 'MIRRORS | equal service peers | complete ten-face spore | capacity earns through traffic served | capacity is never authority | exact build identity required', 'EARLY DRIP | retired - protocol does not pay per head', 'SEED | your 12 words are the only password | no desk | no refunds', ]; if (is_array($extra)) { foreach ($extra as $s) { $s = trim((string)$s); if ($s !== '') $segs[] = $s; } } return $segs; } /** Single-line join (legacy callers / static SSR). */ function nst_brain_wipe_copy(int $daysLeft): string { return implode(' | ', nst_ephemerality_reel_segments($daysLeft)); } /** * Brain: ad_png - PNG magic + IHDR geometry + byte budget. * Hard reject: corrupt / wrong magic / out-of-range dimensions / oversize bytes. * Soft flag: extreme aspect or huge pixel count (still may serve). Fail open on soft only. */ function nst_brain_ad_png(string $bin): array { $len = strlen($bin); if ($len < 33) { return ['brain' => 'ad_png', 'ok' => false, 'err' => 'png too small', 'score' => 1.0, 'notes' => ['tiny'], 'flag' => true]; } if ($len > NST_AD_MAX_PNG) { return ['brain' => 'ad_png', 'ok' => false, 'err' => 'png too big (max ' . NST_AD_MAX_PNG . 'b)', 'score' => 1.0, 'notes' => ['bytes'], 'flag' => true, 'bytes' => $len]; } if (substr($bin, 0, 8) !== "\x89PNG\r\n\x1a\n") { return ['brain' => 'ad_png', 'ok' => false, 'err' => 'not a PNG', 'score' => 1.0, 'notes' => ['magic'], 'flag' => true]; } if (substr($bin, 12, 4) !== 'IHDR') { return ['brain' => 'ad_png', 'ok' => false, 'err' => 'missing IHDR', 'score' => 1.0, 'notes' => ['ihdr'], 'flag' => true]; } $w = unpack('N', substr($bin, 16, 4))[1]; $h = unpack('N', substr($bin, 20, 4))[1]; $bit = ord($bin[24]); $color = ord($bin[25]); if ($w < NST_AD_MIN_W || $h < NST_AD_MIN_H) { return ['brain' => 'ad_png', 'ok' => false, 'err' => 'min ' . NST_AD_MIN_W . 'x' . NST_AD_MIN_H, 'w' => $w, 'h' => $h, 'score' => 1.0, 'notes' => ['min'], 'flag' => true]; } if ($w > NST_AD_MAX_W || $h > NST_AD_MAX_H) { return ['brain' => 'ad_png', 'ok' => false, 'err' => 'longest side max ' . NST_AD_MAX_W . 'px', 'w' => $w, 'h' => $h, 'score' => 1.0, 'notes' => ['max'], 'flag' => true]; } $notes = []; $score = 0.0; $ratio = $w / max(1, $h); if ($ratio > 12.0 || $ratio < (1.0 / 12.0)) { $notes[] = 'extreme_aspect'; $score += 0.35; } if (($w * $h) > 2000000) { $notes[] = 'megapixels'; $score += 0.25; } if (!in_array($color, [0, 2, 3, 4, 6], true)) { $notes[] = 'color_type'; $score += 0.5; } if ($bit !== 8 && $bit !== 16 && $bit !== 4 && $bit !== 2 && $bit !== 1) { $notes[] = 'bit_depth'; $score += 0.2; } // Prefer board-friendly widths (soft) if ($w < 120 && $h < 60) { $notes[] = 'tiny_display'; $score += 0.15; } if ($score > 1.0) { $score = 1.0; } // WAVEA-TR03-IMAGE-ADMISSION: IHDR-only / no IDAT+IEND is not a complete PNG. if (strpos($bin, 'IDAT') === false || strpos($bin, 'IEND') === false) { return ['brain' => 'ad_png', 'ok' => false, 'err' => 'png missing IDAT/IEND', 'w' => $w, 'h' => $h, 'score' => 1.0, 'notes' => ['struct'], 'flag' => true, 'bytes' => $len]; } return [ 'brain' => 'ad_png', 'ok' => true, 'score' => round($score, 3), 'notes' => $notes, 'flag' => $score >= 0.5, 'w' => $w, 'h' => $h, 'bit_depth' => $bit, 'color_type' => $color, 'bytes' => $len, 'ratio' => round($ratio, 3), ]; } /** Brain: href_risk - click-through URL scheme + soft phishing tokens. Hard reject non-http(s). */ function nst_brain_href_risk(string $href): array { $href = trim($href); if ($href === '') { return ['brain' => 'href_risk', 'ok' => true, 'score' => 0.0, 'hits' => [], 'flag' => false]; } if (preg_match('#^(javascript|data|file|vbscript):#i', $href)) { return ['brain' => 'href_risk', 'ok' => false, 'err' => 'blocked scheme', 'score' => 1.0, 'hits' => ['scheme'], 'flag' => true]; } if (!preg_match('#^https?://#i', $href)) { return ['brain' => 'href_risk', 'ok' => false, 'err' => 'href must be http(s)', 'score' => 1.0, 'hits' => ['scheme'], 'flag' => true]; } $t = strtolower($href); $hits = []; $bad = [ 'seed' => 0.2, 'privatekey' => 0.3, 'wallet-connect' => 0.15, 'airdrop' => 0.15, 'double-your' => 0.25, 'free-money' => 0.2, 'bit.ly' => 0.1, 'tinyurl' => 0.1, ]; $score = 0.0; foreach ($bad as $k => $w) { if (str_contains($t, $k)) { $score += $w; $hits[] = $k; } } if ($score > 1.0) { $score = 1.0; } return ['brain' => 'href_risk', 'ok' => true, 'score' => round($score, 3), 'hits' => $hits, 'flag' => $score >= 0.45]; } /** Brain: pair_sane - soft book pair text (NSU/NOTE etc); never blocks alone. */ function nst_brain_pair_sane(string $pair): array { $p = strtoupper(trim($pair)); $notes = []; $score = 0.0; if ($p === '' || !preg_match('#^[A-Z0-9]{2,12}/[A-Z0-9]{2,12}$#', $p)) { $notes[] = 'format'; $score += 0.4; } if (str_contains($p, 'SEED') || str_contains($p, 'HTTP')) { $notes[] = 'token'; $score += 0.4; } if ($score > 1.0) { $score = 1.0; } return ['brain' => 'pair_sane', 'score' => round($score, 3), 'notes' => $notes, 'flag' => $score >= 0.5, 'pair' => $p]; } function boot_data(): void { global $DATA; if (!is_dir($DATA)) @mkdir($DATA, 0755, true); admin_pass_burn(); $ht = $DATA . '/.htaccess'; if (!is_file($ht)) @file_put_contents($ht, "Require all denied\nDeny from all\n"); $idx = $DATA . '/index.html'; if (!is_file($idx)) @file_put_contents($idx, ''); if (function_exists('nst_ltc_headers_boot')) nst_ltc_headers_boot(); } function nst_genesis_pending_path(): string { global $DATA; return $DATA . DIRECTORY_SEPARATOR . 'genesis.pending'; } /** Established sidecars make a missing/empty ledger data loss, not virginity. */ function nst_chain_established_state_present(): bool { foreach (['TFILE', 'SITE_SEED_FILE', 'META', 'ECON', 'OP_ADDR_FILE', 'ADMIN_HASH_FILE', 'MODS_FILE'] as $name) { $path = $GLOBALS[$name] ?? ''; if (is_string($path) && $path !== '' && file_exists($path)) return true; } return false; } function nst_genesis_pending_valid(): bool { $path = nst_genesis_pending_path(); if (is_link($path) || !is_file($path)) return false; $raw = @file_get_contents($path); return is_string($raw) && hash_equals("NST-TRADE-GENESIS-PENDING-V1\n", $raw); } /** Called only while genesis.lock is held. */ function nst_genesis_pending_begin(): void { $path = nst_genesis_pending_path(); if (nst_genesis_pending_valid()) return; if (file_exists($path) || is_link($path) || nst_chain_established_state_present()) { throw new RuntimeException('chain_missing_established_state'); } $fh = @fopen($path, 'x+b'); if (!is_resource($fh)) throw new RuntimeException('genesis_unavailable'); try { @chmod($path, 0600); if (!nst_stream_write_all($fh, "NST-TRADE-GENESIS-PENDING-V1\n") || !nst_stream_flush_durable($fh) ) { throw new RuntimeException('genesis_unavailable'); } } finally { @fclose($fh); } } function nst_genesis_pending_finish(): void { $path = nst_genesis_pending_path(); if (!file_exists($path) && !is_link($path)) return; if (!nst_genesis_pending_valid() || !@unlink($path)) { throw new RuntimeException('genesis_unavailable'); } } /** Called only while genesis.lock is held, before the first strict chain read. */ function nst_chain_create_for_genesis(): void { global $CHAIN; if (file_exists($CHAIN)) { if (is_link($CHAIN) || !is_file($CHAIN)) throw new RuntimeException('chain_invalid'); $size = @filesize($CHAIN); if ($size === 0 && nst_chain_established_state_present() && !nst_genesis_pending_valid()) { throw new RuntimeException('chain_missing_established_state'); } return; } if (is_link($CHAIN) || (nst_chain_established_state_present() && !nst_genesis_pending_valid())) { throw new RuntimeException('chain_missing_established_state'); } $fh = @fopen($CHAIN, 'x+b'); if (!is_resource($fh)) throw new RuntimeException('genesis_unavailable'); try { @chmod($CHAIN, 0600); if (!nst_stream_flush_durable($fh)) throw new RuntimeException('genesis_unavailable'); } finally { @fclose($fh); } } /** Write every byte or fail; fwrite() may legally return a short count. */ function nst_stream_write_all($fh, string $bytes): bool { $length = strlen($bytes); $offset = 0; while ($offset < $length) { $written = @fwrite($fh, substr($bytes, $offset)); if (!is_int($written) || $written < 1) return false; $offset += $written; } return true; } /** Flush PHP and, where PHP exposes it, ask the OS to flush the file too. */ function nst_stream_flush_durable($fh): bool { if (!@fflush($fh)) return false; return !function_exists('fsync') || @fsync($fh); } /** * Strict JSONL reader for an already locked chain handle. * Blank, partial, malformed, or link-broken rows are corruption, never rows to * silently skip. $byteLimit reads exactly a known-good journal prefix. */ function nst_chain_read_locked_handle($fh, ?int $byteLimit = null, bool $verifyLinks = true): array { if (@rewind($fh) === false) throw new RuntimeException('chain_invalid'); $rows = []; $read = 0; while ($byteLimit === null || $read < $byteLimit) { $line = @fgets($fh); if ($line === false) { if (!@feof($fh) || ($byteLimit !== null && $read !== $byteLimit)) { throw new RuntimeException('chain_invalid'); } break; } $lineBytes = strlen($line); $read += $lineBytes; if (($byteLimit !== null && $read > $byteLimit) || $lineBytes < 2 || substr($line, -1) !== "\n" || str_contains($line, "\r") ) { throw new RuntimeException('chain_invalid'); } try { $decoded = json_decode(substr($line, 0, -1), true, 64, JSON_THROW_ON_ERROR); } catch (Throwable $e) { throw new RuntimeException('chain_invalid'); } if (!is_array($decoded) || $decoded === [] || nst_is_list_compat($decoded) || !is_string($decoded['type'] ?? null) || preg_match('/\A[a-z][a-z0-9_]{0,63}\z/D', (string)$decoded['type']) !== 1 ) { throw new RuntimeException('chain_invalid'); } foreach (array_keys($decoded) as $key) { if (!is_string($key)) throw new RuntimeException('chain_invalid'); } $rows[] = $decoded; } if ($byteLimit !== null && $read !== $byteLimit) { throw new RuntimeException('chain_invalid'); } if ($verifyLinks) { $link = nst_chain_link_verify($rows); if (empty($link['ok'])) throw new RuntimeException('chain_invalid'); } return $rows; } /** Remove only abandoned, uniquely named chain-journal staging files. */ function nst_chain_cleanup_journal_temps(): void { global $CHAIN_TXN; foreach ((array)(glob($CHAIN_TXN . '.*.tmp') ?: []) as $path) { if (!preg_match('/\Achain\.txn\.[0-9a-f]{24}\.tmp\z/D', basename($path))) continue; if (is_file($path) && !is_link($path)) @unlink($path); } } /** Remove a completed intent record or fail before reporting commit success. */ function nst_chain_remove_journal(): void { global $CHAIN_TXN; if (!file_exists($CHAIN_TXN)) { if (is_link($CHAIN_TXN)) throw new RuntimeException('chain_invalid'); return; } if (is_link($CHAIN_TXN) || !is_file($CHAIN_TXN) || !@unlink($CHAIN_TXN) || file_exists($CHAIN_TXN) || is_link($CHAIN_TXN) ) { throw new RuntimeException('chain_commit_failed'); } } /** Publish a durable intent record before touching chain.jsonl. */ function nst_chain_write_journal(array $journal): void { global $CHAIN_TXN; if (file_exists($CHAIN_TXN) || is_link($CHAIN_TXN)) { throw new RuntimeException('chain_recovery_required'); } nst_chain_cleanup_journal_temps(); $bytes = j($journal) . "\n"; $tmp = $CHAIN_TXN . '.' . bin2hex(random_bytes(12)) . '.tmp'; $fh = @fopen($tmp, 'x+b'); if (!is_resource($fh)) throw new RuntimeException('chain_commit_failed'); $ok = false; try { @chmod($tmp, 0600); if (!nst_stream_write_all($fh, $bytes) || !nst_stream_flush_durable($fh)) { throw new RuntimeException('chain_commit_failed'); } @fclose($fh); $fh = null; if (!@rename($tmp, $CHAIN_TXN)) throw new RuntimeException('chain_commit_failed'); $tmp = ''; @chmod($CHAIN_TXN, 0600); $saved = @file_get_contents($CHAIN_TXN); if (!is_string($saved) || !hash_equals($bytes, $saved)) { throw new RuntimeException('chain_commit_failed'); } $ok = true; } finally { if (is_resource($fh)) @fclose($fh); if (!$ok && $tmp !== '' && is_file($tmp) && !is_link($tmp)) @unlink($tmp); } } /** * Roll a durable intent forward. This is idempotent across crashes before, * during, or after the chain append: an observed payload prefix is completed; * unrelated bytes or a changed base fail closed. */ function nst_chain_recover_locked($fh): void { global $CHAIN_TXN; nst_chain_cleanup_journal_temps(); if (!file_exists($CHAIN_TXN)) { if (is_link($CHAIN_TXN)) throw new RuntimeException('chain_invalid'); return; } if (is_link($CHAIN_TXN) || !is_file($CHAIN_TXN)) throw new RuntimeException('chain_invalid'); $raw = @file_get_contents($CHAIN_TXN); $journal = is_string($raw) ? json_decode($raw, true) : null; if (!is_array($journal) || (int)($journal['v'] ?? 0) !== 1 || !is_int($journal['base_size'] ?? null) || (int)$journal['base_size'] < 0 || !is_string($journal['base_head'] ?? null) || !preg_match('/\A[0-9a-f]{64}\z/D', (string)$journal['base_head']) || !is_string($journal['payload_b64'] ?? null) || !is_string($journal['payload_sha256'] ?? null) || !is_string($journal['expected_head'] ?? null) ) { throw new RuntimeException('chain_invalid'); } $payload = base64_decode((string)$journal['payload_b64'], true); if (!is_string($payload) || $payload === '' || strlen($payload) > 16777216 || substr($payload, -1) !== "\n" || !hash_equals((string)$journal['payload_sha256'], hash('sha256', $payload)) || !preg_match('/\A[0-9a-f]{64}\z/D', (string)$journal['expected_head']) ) { throw new RuntimeException('chain_invalid'); } $baseSize = (int)$journal['base_size']; $baseRows = nst_chain_read_locked_handle($fh, $baseSize); if (!hash_equals((string)$journal['base_head'], nst_chain_head($baseRows))) { throw new RuntimeException('chain_invalid'); } $stat = @fstat($fh); $size = is_array($stat) ? (int)($stat['size'] ?? -1) : -1; $expectedSize = $baseSize + strlen($payload); if ($size < $baseSize || $size > $expectedSize) throw new RuntimeException('chain_invalid'); if (@fseek($fh, $baseSize, SEEK_SET) !== 0) throw new RuntimeException('chain_invalid'); $haveLen = $size - $baseSize; $have = $haveLen > 0 ? @stream_get_contents($fh, $haveLen) : ''; if (!is_string($have) || strlen($have) !== $haveLen || !hash_equals(substr($payload, 0, $haveLen), $have) ) { throw new RuntimeException('chain_invalid'); } if ($haveLen < strlen($payload)) { if (@fseek($fh, 0, SEEK_END) !== 0 || !nst_stream_write_all($fh, substr($payload, $haveLen)) ) { throw new RuntimeException('chain_commit_failed'); } } /* Even a full-length payload may have reached only the OS cache before the * writer died or reported fsync failure. Re-confirm durability on every * recovery path before deleting the only intent record. */ if (!nst_stream_flush_durable($fh)) throw new RuntimeException('chain_commit_failed'); $rows = nst_chain_read_locked_handle($fh); if (!hash_equals((string)$journal['expected_head'], nst_chain_head($rows))) { throw new RuntimeException('chain_invalid'); } nst_chain_remove_journal(); } /** Commit one or more linked rows under the caller's exclusive chain lock. */ function nst_chain_commit_rows_locked($fh, array &$rows, array $newRows): bool { if ($newRows === []) return true; if (empty($GLOBALS['NST_KING_REPLACE_OK']) && function_exists('nst_conservation_is_frozen') && nst_conservation_is_frozen()) return false; if (empty($GLOBALS['NST_KING_REPLACE_OK']) && function_exists('nst_king_silence_frozen') && nst_king_silence_frozen($rows)) return false; $baseStat = @fstat($fh); $baseSize = is_array($baseStat) ? (int)($baseStat['size'] ?? -1) : -1; if ($baseSize < 0) return false; $working = $rows; $payload = ''; foreach ($newRows as $row) { if (!is_array($row) || $row === []) return false; /* Read the head THROUGH THE HANDLE WE ALREADY HOLD. * * The obvious version - nst_chain_head(read_chain()) - opens a SECOND handle * to a file this function has already flock'd exclusively. On Windows that * open fails, read_chain() returns [], and every row silently links to H0: * observed here as five consecutive rows all storing the genesis head, which * froze the chain on the next verify. It may well succeed on Linux, which is * worse - the bug would then only appear on somebody else's machine. * * $working was read from the open handle under LOCK_EX. The lock spans * head computation and write, so there is no TOCTOU. Do not call * read_chain() here. */ $head = nst_chain_head($working); if (array_key_exists('prev', $row)) { if (!is_string($row['prev']) || !hash_equals($head, $row['prev'])) return false; } else { $row['prev'] = $head; } $encoded = j($row) . "\n"; if (strlen($encoded) < 3) return false; $payload .= $encoded; if (strlen($payload) > 16777216) return false; $working[] = $row; } $journal = [ 'v' => 1, 'base_size' => $baseSize, 'base_head' => nst_chain_head($rows), 'payload_sha256' => hash('sha256', $payload), 'payload_b64' => base64_encode($payload), 'expected_head' => nst_chain_head($working), ]; nst_chain_write_journal($journal); if (@fseek($fh, 0, SEEK_END) !== 0 || !nst_stream_write_all($fh, $payload) || !nst_stream_flush_durable($fh) ) { // Durable intent now exists. Recovery either completes it or fails loud; // never report a clean rollback while the commit may still materialize. nst_chain_recover_locked($fh); } $verified = nst_chain_read_locked_handle($fh); if (!hash_equals((string)$journal['expected_head'], nst_chain_head($verified))) { throw new RuntimeException('chain_commit_failed'); } nst_chain_remove_journal(); $rows = $verified; if (function_exists('nst_king_heartbeat_after_commit')) nst_king_heartbeat_after_commit($newRows); return true; } function read_chain(bool $verifyLinks = true): array { global $CHAIN, $CHAIN_TXN; /* Reads must stay reads. A fresh pre-ENTER shell is allowed to observe an * empty ledger without manufacturing data/, chain.jsonl, or lock files. */ if (!file_exists($CHAIN)) { if (is_link($CHAIN) || file_exists($CHAIN_TXN) || is_link($CHAIN_TXN)) { throw new RuntimeException('chain_invalid'); } if (nst_chain_established_state_present() && !nst_genesis_pending_valid()) { throw new RuntimeException('chain_missing_established_state'); } return []; } if (is_link($CHAIN) || !is_file($CHAIN)) throw new RuntimeException('chain_invalid'); $fh = @fopen($CHAIN, 'r+b'); if (!is_resource($fh)) throw new RuntimeException('chain_unavailable'); if (!@flock($fh, LOCK_EX)) { @fclose($fh); throw new RuntimeException('chain_unavailable'); } try { nst_chain_recover_locked($fh); $rows = nst_chain_read_locked_handle($fh, null, $verifyLinks); if ($rows === [] && nst_chain_established_state_present() && !nst_genesis_pending_valid()) { throw new RuntimeException('chain_missing_established_state'); } return $rows; } finally { @flock($fh, LOCK_UN); @fclose($fh); } } function append_row(array $row): bool { $result = nst_with_chain_lock(static function (array $rows, callable $append) use ($row): bool { return $append($row); }); return $result === true; } /** * Exclusive chain read-modify-write. Holds LOCK_EX for the whole callback so * fill/faucet/transfer/order/admin_pay/extract/lazy_accrue races cannot double-spend. * Callback: fn(array $rows, callable(array):bool $append, callable(array):bool $appendBatch): mixed * Do NOT call append_row() inside — use $append only (same fd / non-deadlocking). * Returns null if the chain file cannot be opened/locked. */ function nst_with_chain_lock(callable $fn) { global $CHAIN; boot_data(); if (!file_exists($CHAIN) || is_link($CHAIN) || !is_file($CHAIN)) return null; $fh = @fopen($CHAIN, 'r+b'); if (!$fh) return null; if (!flock($fh, LOCK_EX)) { fclose($fh); return null; } try { nst_chain_recover_locked($fh); $rows = nst_chain_read_locked_handle($fh); if ($rows === [] && nst_chain_established_state_present() && !nst_genesis_pending_valid()) { throw new RuntimeException('chain_missing_established_state'); } $append = function (array $row) use ($fh, &$rows): bool { return nst_chain_commit_rows_locked($fh, $rows, [$row]); }; $appendBatch = function (array $batch) use ($fh, &$rows): bool { return nst_chain_commit_rows_locked($fh, $rows, $batch); }; return $fn($rows, $append, $appendBatch); } finally { flock($fh, LOCK_UN); fclose($fh); } } /** * Exclusive file lock helper (durable files: rent_quotes, etc.). * Callback: fn(resource $fh): mixed — file opened c+b, pointer undefined. * Free claim faucet retired 410 — no cooldown file. */ function nst_with_file_lock(string $path, callable $fn) { $dir = dirname($path); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } $fh = @fopen($path, 'c+b'); if (!$fh) return null; if (!flock($fh, LOCK_EX)) { fclose($fh); return null; } try { return $fn($fh); } finally { flock($fh, LOCK_UN); fclose($fh); } } function balances(array $rows): array { $b = []; foreach ($rows as $r) { $t = $r['type'] ?? ''; if (nst_is_mint_type($t)) { foreach ($r['outputs'] ?? [] as $o) { $a = $o['addr'] ?? ''; $m = $o['amount'] ?? '0'; if ($a && dec_ok($m)) $b[$a] = dec_add($b[$a] ?? '0', $m); } // explicit treasury burn leg (historical re-anchor excess; re-anchor stays retired) if ($t === 'treasury_reanchor' && !empty($r['burn_amount']) && dec_ok((string)$r['burn_amount'])) { $ta = (string)($r['treasury_addr'] ?? ''); $bm = (string)$r['burn_amount']; if ($ta !== '' && dec_cmp($b[$ta] ?? '0', $bm) >= 0) { $b[$ta] = dec_sub($b[$ta] ?? '0', $bm); $burn = NST_BURN_ADDR; $b[$burn] = dec_add($b[$burn] ?? '0', $bm); } } } elseif ($t === 'transfer') { $f = $r['from'] ?? ''; $to = $r['to'] ?? ''; $m = $r['amount'] ?? '0'; if (!$f || !$to || !dec_ok($m)) continue; if (dec_cmp($b[$f] ?? '0', $m) < 0) continue; // skip invalid historical $b[$f] = dec_sub($b[$f] ?? '0', $m); $b[$to] = dec_add($b[$to] ?? '0', $m); } elseif ($t === 'job_pay') { /* Funded-job settlement: one conserved leg sponsor -> worker drawn from a grant reservation. */ $f = $r['from'] ?? ''; $to = $r['to'] ?? ''; $m = $r['amount'] ?? '0'; if (!$f || !$to || !dec_ok($m) || $f === $to) continue; if (dec_cmp($b[$f] ?? '0', $m) < 0) continue; $b[$f] = dec_sub($b[$f] ?? '0', $m); $b[$to] = dec_add($b[$to] ?? '0', $m); } elseif ($t === 'ad_stake' || $t === 'slowgpt_reserve') { /* One payer, several payees (lord + King's tax). NOT a mint: the legs * must sum to exactly what the payer spends, and this branch is what * makes that true. Without it the row would be inert - the payer * never debited, the lord never paid - and the sink would be theatre. * The sum is re-checked here rather than trusted from the writer, * because balances() is replayed against chains this build did not * necessarily produce. */ /* WAVEB-TR12-OUTPUT-ADDR: positive outputs require valid 64-hex addresses. * Empty-address legs no longer count toward the conserved sum (they used * to debit the payer and credit nobody). Historical malformed fixtures * become skipped rows + independent audit findings, not silent burns. */ $f = $r['from'] ?? ''; $m = (string)($r['amount'] ?? '0'); if (!$f || !dec_ok($m)) continue; $outs = $r['outputs'] ?? []; if (function_exists('nst_econ_validate_outputs')) { $vov = nst_econ_validate_outputs($outs, $m); if (empty($vov['ok'])) continue; $outs = $vov['outputs']; } else { $sum = '0'; $bad = false; foreach ($outs as $o) { if (!is_array($o)) { $bad = true; break; } $am = (string)($o['amount'] ?? '0'); $a = strtolower(trim((string)($o['addr'] ?? ''))); if (!dec_ok($am)) { $bad = true; break; } if (dec_cmp($am, '0') === 0) continue; if ($a === '' || !preg_match('/^[a-f0-9]{64}$/', $a)) { $bad = true; break; } $sum = dec_add($sum, $am); } if ($bad || dec_cmp($sum, $m) !== 0) continue; } if (dec_cmp($b[$f] ?? '0', $m) < 0) continue; $b[$f] = dec_sub($b[$f] ?? '0', $m); foreach ($outs as $o) { $a = (string)($o['addr'] ?? ''); $am = (string)($o['amount'] ?? '0'); if ($a !== '' && dec_ok($am)) $b[$a] = dec_add($b[$a] ?? '0', $am); } } } return $b; } /* ======================================================================== * E2 CONSERVATION INVARIANT + FREEZE + int-micros audit (NSU-V1-SIMPLE STEP 1) * ADDITIVE ONLY. Does not alter balances()/transfer/faucet/order bodies. * On mismatch or non-integer money fields: sticky FREEZE file → all chain * writes reject → write APIs 503 conservation_freeze + loud red banner. * ======================================================================== */ /** True iff $m is a non-negative integer decimal string (micros). Same law as dec_ok. */ function nst_int_micros_ok(string $m): bool { return dec_ok($m); } /** * Audit money-bearing fields on chain rows for non-integer micros. * @return array{ok:bool,violations:list} */ function nst_audit_int_micros(array $rows): array { $violations = []; $check = static function (int $i, string $type, string $field, $raw) use (&$violations): void { if ($raw === null) { $violations[] = ['i' => $i, 'type' => $type, 'field' => $field, 'value' => '']; return; } $v = is_string($raw) || is_int($raw) ? (string)$raw : ''; if ($v === '' || !nst_int_micros_ok($v)) { $violations[] = ['i' => $i, 'type' => $type, 'field' => $field, 'value' => $v]; } }; foreach ($rows as $i => $r) { if (!is_array($r)) { continue; } $t = (string)($r['type'] ?? ''); if (nst_is_mint_type($t)) { foreach ($r['outputs'] ?? [] as $j => $o) { if (!is_array($o)) { continue; } $check((int)$i, $t, 'outputs[' . $j . '].amount', $o['amount'] ?? null); } if ($t === 'treasury_reanchor' && array_key_exists('burn_amount', $r)) { $check((int)$i, $t, 'burn_amount', $r['burn_amount']); } } elseif ($t === 'transfer' || $t === 'fill' || $t === 'job_grant' || $t === 'job_admit' || $t === 'job_pay' || $t === 'job_cancel') { if (array_key_exists('amount', $r)) { $check((int)$i, $t, 'amount', $r['amount']); } } elseif ($t === 'ad_stake' || $t === 'slowgpt_reserve') { /* Every money field, including each payout leg. A non-integer amount * anywhere is what the E2 freeze exists to catch. */ if (array_key_exists('amount', $r)) { $check((int)$i, $t, 'amount', $r['amount']); } foreach ($r['outputs'] ?? [] as $j => $o) { if (!is_array($o)) continue; $check((int)$i, $t, 'outputs[' . $j . '].amount', $o['amount'] ?? null); } } elseif ($t === 'order') { if (array_key_exists('amount', $r)) { $check((int)$i, $t, 'amount', $r['amount']); } if (array_key_exists('price', $r)) { $check((int)$i, $t, 'price', $r['price']); } } } return ['ok' => $violations === [], 'violations' => $violations]; } /** * Replay conservation: expected issued vs Σ balances(). * Expected = genesis outputs + treasury_reanchor credit outputs only. * Burn stays in total (NST_BURN_ADDR). Transfers conserve. * * @return array{ok:bool,observed_sum:string,expected_issued:string,balances_sum:string,int_ok:bool,detail:array} */ function nst_conservation_replay(array $rows): array { $expected = '0'; $genesisN = 0; $genesisSum = '0'; $reanchorCreditN = 0; $reanchorCreditSum = '0'; $emissionN = 0; $emissionSum = '0'; foreach ($rows as $r) { if (!is_array($r)) { continue; } $t = (string)($r['type'] ?? ''); /* Any declared mint type counts toward expected issuance. Using the * registry means a new mint type cannot be added without conservation * seeing it - the alternative desyncs the audit and trips the E2 freeze. */ if ($t === 'emission') { foreach ($r['outputs'] ?? [] as $o) { if (!is_array($o)) continue; $m = (string)($o['amount'] ?? '0'); if (!nst_int_micros_ok($m) || dec_cmp($m, '0') <= 0) continue; $expected = dec_add($expected, $m); $emissionSum = dec_add($emissionSum, $m); $emissionN++; } } elseif ($t === 'genesis') { $genesisN++; foreach ($r['outputs'] ?? [] as $o) { if (!is_array($o)) { continue; } $m = (string)($o['amount'] ?? '0'); if (!nst_int_micros_ok($m)) { continue; } $expected = dec_add($expected, $m); $genesisSum = dec_add($genesisSum, $m); } } elseif ($t === 'treasury_reanchor') { foreach ($r['outputs'] ?? [] as $o) { if (!is_array($o)) { continue; } $m = (string)($o['amount'] ?? '0'); if (!nst_int_micros_ok($m) || dec_cmp($m, '0') <= 0) { continue; } $expected = dec_add($expected, $m); $reanchorCreditSum = dec_add($reanchorCreditSum, $m); $reanchorCreditN++; } } } $bal = balances($rows); $observed = '0'; $nonzeroAddrs = 0; foreach ($bal as $addr => $amt) { $a = (string)$amt; if (!nst_int_micros_ok($a)) { return [ 'ok' => false, 'observed_sum' => '0', 'expected_issued' => $expected, 'balances_sum' => '0', 'int_ok' => false, 'detail' => [ 'err' => 'balance_non_int', 'addr' => (string)$addr, 'amount' => $a, ], ]; } $observed = dec_add($observed, $a); if (dec_cmp($a, '0') > 0) { $nonzeroAddrs++; } } $intAudit = nst_audit_int_micros($rows); $match = dec_cmp($observed, $expected) === 0; $ok = $match && !empty($intAudit['ok']); $diff = '0'; if (!$match) { $diff = dec_cmp($observed, $expected) > 0 ? dec_sub($observed, $expected) : ('-' . dec_sub($expected, $observed)); } return [ 'ok' => $ok, 'observed_sum' => $observed, 'expected_issued' => $expected, 'balances_sum' => $observed, 'int_ok' => !empty($intAudit['ok']), 'detail' => [ 'match' => $match, 'diff' => $diff, 'genesis_rows' => $genesisN, 'genesis_sum' => $genesisSum, 'reanchor_credit_legs' => $reanchorCreditN, 'reanchor_credit_sum' => $reanchorCreditSum, 'emission_legs' => $emissionN, 'emission_sum' => $emissionSum, 'nonzero_addrs' => $nonzeroAddrs, 'burn_addr' => NST_BURN_ADDR, 'burn_bal' => (string)($bal[NST_BURN_ADDR] ?? '0'), 'int_violations' => $intAudit['violations'], 'note' => 'expected=genesis+reanchor credits; burn moves to NST_BURN_ADDR (stays in total)', ], ]; } /* ======================================================================== * CHAIN LINKAGE — make the chain an actual chain. * * chain.jsonl was an append-only LOG, not a chain: every row was individually * signed, but the SEQUENCE was unauthenticated. No row committed to any earlier * row, so deleting line 4 left no evidence that line 4 had ever existed. * * Each row now carries prev = the running head before it: * * H0 = sha256("nsu-chain-v1") * Hn = sha256(H(n-1) | canonical(row_n)) * * WHAT THIS CATCHES, and therefore invalidates: modification, insertion, * mid-deletion and reordering. All four become arithmetic rather than opinion. * * WHAT IT DOES NOT CATCH: truncation of the TAIL. Chop the last N rows and what * remains is a shorter chain that verifies perfectly, because nothing left is * pointing at what was removed. Anyone claiming a hash chain alone prevents * rollback is wrong. That needs an external witness - the published head, and * counterparties who kept a copy. * * MIGRATION IS THE DANGEROUS PART. Chains written before this existed have no * prev field at all, and hard-failing on that would brick every running empire * on upgrade. So a row WITHOUT prev is treated as pre-linkage: it still advances * the head (or later rows could never link), it is simply not checked. Linkage * begins at the first row that carries it and is enforced from there on. * ======================================================================== */ /** Deterministic bytes for a row, excluding the link fields themselves. */ function nst_row_canonical(array $row): string { unset($row['prev']); ksort($row); // key order must not depend on insertion order return j($row); } /** Fold the running head over a list of rows. */ function nst_chain_head(array $rows): string { $h = hash('sha256', 'nsu-chain-v1'); foreach ($rows as $r) { if (!is_array($r)) continue; $h = hash('sha256', $h . '|' . nst_row_canonical($r)); } return $h; } /** * Verify linkage across a chain. * @return array{ok:bool,head:string,checked:int,legacy:int,break_at:?int,expected?:string,found?:string} */ function nst_chain_link_verify(array $rows): array { $h = hash('sha256', 'nsu-chain-v1'); $checked = 0; $legacy = 0; foreach ($rows as $i => $r) { if (!is_array($r)) continue; if (array_key_exists('prev', $r)) { if (!is_string($r['prev']) || !hash_equals($h, $r['prev'])) { return ['ok' => false, 'head' => $h, 'checked' => $checked, 'legacy' => $legacy, 'break_at' => (int)$i, 'expected' => $h, 'found' => (string)($r['prev'] ?? '')]; } $checked++; } else { $legacy++; // pre-linkage row: advances the head, not verified } $h = hash('sha256', $h . '|' . nst_row_canonical($r)); } return ['ok' => true, 'head' => $h, 'checked' => $checked, 'legacy' => $legacy, 'break_at' => null]; } /** * AUDIT ANY EMPIRE'S CHAIN WITH THE CODE THAT MAINTAINS YOUR OWN. * * Kings evolve their crops separately - that is the design - so you can never * verify a peer's CODE. ?api=selfhash returns a hash the audited file computes * about itself, which is worth nothing against a King who edits the file. Any * fingerprint, attestation or badge has the same flaw: it is self-report. * * So do not audit the code. Audit the LEDGER, which is arithmetic on data the * peer published: * * ceiling sum of every mint leg <= NST_MAX_SUPPLY * emission each day == (MAX - minted_before) * rate / 1e6 * split equal share per crop, last absorbing dust, King's cut off top * conservation sum of balances == sum of mints * linkage prev hashes fold correctly * * None of that is a vote, so no coalition can capture it: every reader checks * alone and reaches the same answer. This is also why POLICY should be OBSERVED * rather than declared - an emission row carries the rate and split it actually * used, which is testimony against interest, where a constants list a peer * hands you is just a claim. * * Two verdicts, deliberately separate: * internally_consistent the peer obeyed its OWN declared rules * policy_match those rules are also YOUR rules * A peer can be perfectly honest and still be on different money. Conflating * the two would either reject honest neighbours or accept forgers. * * WHAT THIS CANNOT DO: prove the history happened. A fabricated chain that * obeys every rule costs nothing to produce. Only a counterparty's own record * of your head over time can catch that, and only for the period they recorded. */ function nst_verify_chain(array $rows): array { $link = nst_chain_link_verify($rows); $cons = nst_conservation_replay($rows); $minted = '0'; $emissions = 0; $badFormula = []; $badSplit = []; $rateSeen = []; $cropCounts = []; $overCeiling = false; foreach ($rows as $i => $r) { if (!is_array($r) || ($r['type'] ?? '') !== 'emission') { if (is_array($r) && nst_is_mint_type($r['type'] ?? '')) { foreach ($r['outputs'] ?? [] as $o) { $m = (string)($o['amount'] ?? '0'); if (dec_ok($m)) $minted = dec_add($minted, $m); } } continue; } $emissions++; $rate = (int)($r['rate_ppm'] ?? 0); $rateSeen[$rate] = true; /* The formula, checked against the supply BEFORE this row. */ $expect = dec_div_small(dec_mul_small(dec_sub(NST_MAX_SUPPLY, $minted), $rate), 1000000); $legSum = '0'; foreach ($r['outputs'] ?? [] as $o) { $m = (string)($o['amount'] ?? '0'); if (dec_ok($m)) $legSum = dec_add($legSum, $m); } if (dec_cmp($legSum, $expect) !== 0) { $badFormula[] = ['row' => (int)$i, 'declared' => $legSum, 'formula' => $expect]; } /* The split: crop legs plus one aggregate King leg. */ $n = (int)($r['crops_paid'] ?? 0); $cropCounts[$n] = true; $per = (string)($r['per_crop_micros'] ?? '0'); $tax = (string)($r['king_tax_micros'] ?? '0'); if ($n > 0 && dec_ok($per)) { $grossAll = dec_mul_small($per, $n); /* King's declared take must match the declared tax rate on the gross. */ $taxPpm = (int)($r['king_tax_ppm'] ?? 0); $expectTax = dec_div_small(dec_mul_small($grossAll, $taxPpm), 1000000); /* Allow the per-share rounding floor: each share may round up by <1 micro. */ $slack = (string)$n; if (dec_cmp($tax, $expectTax) < 0 || dec_cmp(dec_sub($tax, $expectTax), $slack) > 0) { $badSplit[] = ['row' => (int)$i, 'king_tax' => $tax, 'expected_about' => $expectTax]; } } $minted = dec_add($minted, $legSum); } if (dec_cmp($minted, NST_MAX_SUPPLY) > 0) $overCeiling = true; $internal = $link['ok'] && !empty($cons['ok']) && !$badFormula && !$badSplit && !$overCeiling; $policy = (!$rateSeen || array_keys($rateSeen) === [NST_EMISSION_DAILY_RATE_PPM]) && (!$cropCounts || array_keys($cropCounts) === [NST_EMISSION_CROP_COUNT]); return [ 'rows' => count($rows), 'internally_consistent' => $internal, 'policy_match' => (bool)$policy, 'linkage' => ['ok' => $link['ok'], 'break_at' => $link['break_at'], 'checked' => $link['checked'], 'legacy' => $link['legacy'], 'head' => $link['head']], 'conservation' => ['ok' => !empty($cons['ok']), 'observed' => (string)$cons['observed_sum'], 'expected' => (string)$cons['expected_issued']], 'supply' => ['minted' => $minted, 'ceiling' => NST_MAX_SUPPLY, 'over_ceiling' => $overCeiling], 'emissions' => $emissions, 'observed_rate_ppm' => array_values(array_map('intval', array_keys($rateSeen))), 'observed_crop_count' => array_values(array_map('intval', array_keys($cropCounts))), 'my_rate_ppm' => NST_EMISSION_DAILY_RATE_PPM, 'my_crop_count' => NST_EMISSION_CROP_COUNT, 'formula_breaks' => $badFormula, 'split_breaks' => $badSplit, 'note' => 'internally_consistent = the peer obeyed its own declared rules. ' . 'policy_match = those rules are also ours. Neither proves the history happened; ' . 'only your own recorded heads over time can catch a fabricated past.', ]; } function nst_conservation_freeze_path(): string { global $DATA; return $DATA . DIRECTORY_SEPARATOR . 'conservation.freeze.json'; } /** @return array{frozen?:bool,ts?:int,reason?:string,observed?:string,expected?:string}|null */ function nst_conservation_freeze_read(): ?array { $p = nst_conservation_freeze_path(); if (!is_file($p)) { return null; } $j = json_decode((string)@file_get_contents($p), true); return is_array($j) ? $j : ['frozen' => true, 'reason' => 'unreadable_freeze_file']; } function nst_conservation_is_frozen(): bool { $j = nst_conservation_freeze_read(); if ($j === null) { return false; } // Fail closed: freeze file present with missing frozen key still blocks. if (array_key_exists('frozen', $j)) { return !empty($j['frozen']); } return true; } /** Sticky freeze until operator deletes file (no public unfreeze API). */ function nst_conservation_freeze_set(array $report, string $reason = 'conservation_mismatch'): bool { boot_data(); $payload = [ 'frozen' => true, 'ts' => time(), 'reason' => $reason, 'observed' => (string)($report['observed_sum'] ?? $report['observed'] ?? '0'), 'expected' => (string)($report['expected_issued'] ?? $report['expected'] ?? '0'), 'detail' => $report['detail'] ?? null, 'version' => NST_VERSION, ]; return @file_put_contents(nst_conservation_freeze_path(), j($payload), LOCK_EX) !== false; } /** Operator/test only — remove freeze file. Not auto-called on healthy check. */ function nst_conservation_freeze_clear(): bool { $p = nst_conservation_freeze_path(); if (!is_file($p)) { return true; } return @unlink($p); } /** * Run conservation replay; FREEZE on mismatch or int-micros fail. * Sticky: never auto-clears freeze (silent heal forbidden). */ function nst_conservation_check_and_maybe_freeze(array $rows): array { $rep = nst_conservation_replay($rows); $rep['checked_ts'] = time(); $rep['frozen'] = nst_conservation_is_frozen(); $rep['freeze_written'] = false; /* STRUCTURE BEFORE SUMS. * Conservation adds up the rows; there is no point adding up rows whose * ORDER you cannot trust. A linkage break means somebody edited, inserted, * reordered or removed history - which invalidates the sequence the totals * were computed over, even when the totals happen to still balance. So this * runs first and freezes on its own, with its own reason. */ $link = nst_chain_link_verify($rows); $rep['link'] = ['ok' => $link['ok'], 'head' => $link['head'], 'checked' => $link['checked'], 'legacy' => $link['legacy'], 'break_at' => $link['break_at']]; if (empty($link['ok'])) { $rep['ok'] = false; $rep['freeze_written'] = nst_conservation_freeze_set($rep, 'chain_link_break'); $rep['frozen'] = true; } if (empty($rep['ok']) && empty($rep['freeze_written'])) { $why = empty($rep['int_ok']) ? 'int_micros_violation' : 'conservation_mismatch'; $rep['freeze_written'] = nst_conservation_freeze_set($rep, $why); $rep['frozen'] = true; } // Best-effort meta stamp (freeze file is authority). $m = load_meta(); $m['last_conservation_ts'] = (int)$rep['checked_ts']; $m['last_conservation_ok'] = !empty($rep['ok']); $m['last_conservation_observed'] = (string)$rep['observed_sum']; $m['last_conservation_expected'] = (string)$rep['expected_issued']; save_meta($m); return $rep; } /** Early reject for write APIs. 503 + err conservation_freeze. */ function nst_require_writes_unfrozen(): void { if (nst_conservation_is_frozen()) { $fr = nst_conservation_freeze_read() ?? []; api_out([ 'ok' => false, 'err' => 'Trading is paused while the ledger is checked — try again shortly', 'frozen' => true, 'reason' => (string)($fr['reason'] ?? 'conservation_mismatch'), 'observed_fmt' => isset($fr['observed']) && nst_int_micros_ok((string)$fr['observed']) ? fmt_amt((string)$fr['observed']) : null, 'expected_fmt' => isset($fr['expected']) && nst_int_micros_ok((string)$fr['expected']) ? fmt_amt((string)$fr['expected']) : null, 'freeze_ts' => (int)($fr['ts'] ?? 0), 'note' => 'WRITES FROZEN — ledger conservation check failed on this mirror. King silence is never a monetary election or freeze condition.', ], 503); } } /** * LEGACY KING SUCCESSION (RETIRED LIVE PATH) — kept only for forensic migration context. * Ultralight: no cron. Freeze is a timestamp compare against the last successful * money append. Not conservation.freeze.json (that file never auto-clears). * Replica hash is nst_chain_head($rows), never sha256 of raw jsonl. */ function nst_king_heartbeat_path(): string { return nst_data_file('king_heartbeat.json'); } function nst_king_election_path(): string { return nst_data_file('king_election.json'); } function nst_king_succession_path(): string { return nst_data_file('king_succession.json'); } function nst_king_known_nodes_path(): string { return nst_data_file('known_nodes.json'); } /** Money-append types that prove the king is alive. Not HTML/state/book/audit/ad_pick/selfhash. */ function nst_king_is_heartbeat_type(string $t): bool { return in_array($t, ['transfer', 'order', 'fill', 'cancel', 'mint', 'admin_pay', 'ad_stake', 'slowgpt_reserve', 'emission'], true); } function nst_king_canon_node_url(string $raw): string { $raw = trim($raw); if ($raw === '' || strlen($raw) > 512) return ''; if (preg_match('/[\\x00-\\x20\\x7f]/', $raw)) return ''; $p = parse_url($raw); if (!is_array($p)) return ''; $scheme = strtolower((string)($p['scheme'] ?? '')); if ($scheme !== 'http' && $scheme !== 'https') return ''; $host = strtolower((string)($p['host'] ?? '')); if ($host === '' || !preg_match('/\\A[a-z0-9.-]{1,253}\\z/D', $host)) return ''; if (isset($p['user']) || isset($p['pass'])) return ''; $port = isset($p['port']) ? (int)$p['port'] : 0; $path = (string)($p['path'] ?? '/'); if ($path === '') $path = '/'; if (str_contains($path, '..')) return ''; $out = $scheme . '://' . $host; if ($port > 0 && $port !== 80 && $port !== 443) $out .= ':' . $port; $out .= $path; return $out; } function nst_king_self_url(): string { $https = false; if (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off') $https = true; if ((int)($_SERVER['SERVER_PORT'] ?? 0) === 443) $https = true; $host = strtolower((string)($_SERVER['HTTP_HOST'] ?? '')); $host = preg_replace('/:\\d+$/', '', $host) ?? ''; $path = (string)($_SERVER['SCRIPT_NAME'] ?? '/'); if ($path === '') $path = '/'; if ($host !== '') { $u = nst_king_canon_node_url(($https ? 'https://' : 'http://') . $host . $path); if ($u !== '') return $u; } if (function_exists('nsu_trade_origin')) { $u = nst_king_canon_node_url(rtrim(nsu_trade_origin(), '/') . '/'); if ($u !== '') return $u; } return 'https://nosignup.trade/'; } /** * Operator list of replica-holding TRADE peers. This host is always included. * Do not read chat/work gossip. File is created empty-except-self on first ping. */ function nst_king_known_nodes(): array { $self = nst_king_self_url(); $nodes = []; if ($self !== '') $nodes[$self] = true; if (defined('NST_KNOWN_TRADE_NODES') && is_array(NST_KNOWN_TRADE_NODES)) { foreach (NST_KNOWN_TRADE_NODES as $u) { $c = nst_king_canon_node_url((string)$u); if ($c !== '') $nodes[$c] = true; } } $p = nst_king_known_nodes_path(); if (is_file($p) && !is_link($p)) { $j = json_decode((string)@file_get_contents($p), true); if (is_array($j)) { foreach ($j as $u) { $c = nst_king_canon_node_url((string)$u); if ($c !== '') $nodes[$c] = true; } } } return array_keys($nodes); } function nst_king_known_nodes_ensure_file(): void { boot_data(); $p = nst_king_known_nodes_path(); if (is_file($p) || is_link($p)) return; $self = nst_king_self_url(); $list = $self !== '' ? [$self] : []; @file_put_contents($p, j($list) . "\n", LOCK_EX); } function nst_king_is_known_node(string $url): bool { $c = nst_king_canon_node_url($url); if ($c === '') return false; foreach (nst_king_known_nodes() as $n) { if (hash_equals($n, $c)) return true; } return false; } function nst_king_read_json_file(string $path): ?array { if (!is_file($path) || is_link($path)) return null; $j = json_decode((string)@file_get_contents($path), true); return is_array($j) ? $j : null; } function nst_king_heartbeat_ts_from_rows(?array $rows = null): int { $j = nst_king_read_json_file(nst_king_heartbeat_path()); if (is_array($j) && (int)($j['ts'] ?? 0) > 0) { return (int)$j['ts']; } if (!is_array($rows)) return 0; $best = 0; foreach ($rows as $r) { if (!is_array($r)) continue; if (!nst_king_is_heartbeat_type((string)($r['type'] ?? ''))) continue; $ts = (int)($r['ts'] ?? 0); if ($ts > $best) $best = $ts; } return $best; } function nst_king_heartbeat_ts(): int { $j = nst_king_read_json_file(nst_king_heartbeat_path()); if (is_array($j) && (int)($j['ts'] ?? 0) > 0) { return (int)$j['ts']; } return 0; } function nst_king_succession_read(): ?array { return nst_king_read_json_file(nst_king_succession_path()); } function nst_king_succession_is_reigning(?array $rows = null): bool { $s = nst_king_succession_read(); if (!is_array($s) || ($s['role'] ?? '') !== 'king') return false; $want = (string)($s['winning_hash'] ?? ''); if ($want === '' || !preg_match('/\\A[0-9a-f]{64}\\z/D', $want)) return false; if (!is_array($rows)) return true; return hash_equals($want, nst_chain_head($rows)); } function nst_king_succession_is_replica(): bool { $s = nst_king_succession_read(); return is_array($s) && ($s['role'] ?? '') === 'replica'; } /** True when money must refuse with reason king_silence_freeze. */ function nst_king_silence_frozen(?array $rows = null): bool { /* V8 LAW: a human operator going silent cannot grant traffic/replicas monetary authority. */ return false; } function nst_king_election_delete(): void { $p = nst_king_election_path(); if (is_file($p) && !is_link($p)) @unlink($p); } /** Heartbeat ONLY after a successful money append. Returning king deletes the election. */ function nst_king_heartbeat_after_commit(array $newRows): void { if (!empty($GLOBALS['NST_KING_REPLACE_OK'])) return; $hit = false; foreach ($newRows as $r) { if (!is_array($r)) continue; $t = (string)($r['type'] ?? ''); if (nst_king_is_heartbeat_type($t) || (string)($r['kind'] ?? '') === 'admin_pay') { $hit = true; break; } } if (!$hit) return; boot_data(); @file_put_contents(nst_king_heartbeat_path(), j(['v' => 1, 'ts' => time()]) . "\n", LOCK_EX); nst_king_election_delete(); } function nst_king_election_read(): array { $j = nst_king_read_json_file(nst_king_election_path()); if (!is_array($j)) { return ['v' => 1, 'votes' => []]; } if (!isset($j['votes']) || !is_array($j['votes'])) $j['votes'] = []; return $j; } function nst_king_tally(?array $el = null): ?array { $el = $el ?? nst_king_election_read(); $votes = $el['votes'] ?? []; if (!is_array($votes) || $votes === []) return null; $counts = []; foreach ($votes as $v) { if (!is_array($v)) continue; $h = strtolower((string)($v['hash'] ?? '')); if (preg_match('/\\A[0-9a-f]{64}\\z/D', $h) !== 1) continue; $counts[$h] = ($counts[$h] ?? 0) + 1; } if ($counts === []) return null; arsort($counts, SORT_NUMERIC); return $counts; } function nst_king_winning_hash(?array $el = null, string $localHead = ''): ?string { $tally = nst_king_tally($el); if ($tally === null) return null; $best = 0; $cands = []; foreach ($tally as $h => $n) { $n = (int)$n; if ($n > $best) { $best = $n; $cands = [$h]; } elseif ($n === $best) { $cands[] = $h; } } if ($cands === []) return null; if ($localHead !== '' && in_array($localHead, $cands, true)) return $localHead; sort($cands, SORT_STRING); return $cands[0]; } function nst_king_client_ip(): string { return strtolower(trim((string)($_SERVER['REMOTE_ADDR'] ?? ''))); } function nst_king_cors_audit_like(): void { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET'); } /** GET known-node ?api=audit only. Operator list, never a caller-supplied URL. */ function nst_king_http_get_known(string $url, int $timeoutSec = 8): ?string { $url = trim($url); if ($url === '' || strlen($url) > 1024) return null; $p = parse_url($url); if (!is_array($p)) return null; $base = nst_king_canon_node_url( (string)($p['scheme'] ?? '') . '://' . (string)($p['host'] ?? '') . (isset($p['port']) ? (':' . (int)$p['port']) : '') . (string)($p['path'] ?? '/') ); if ($base === '' || !nst_king_is_known_node($base)) return null; if (function_exists('curl_init')) { $ch = curl_init($url); if ($ch === false) return null; curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => $timeoutSec, CURLOPT_TIMEOUT => $timeoutSec, CURLOPT_USERAGENT => 'nosignup.trade-king/1.0', CURLOPT_SSL_VERIFYPEER => true, CURLOPT_PROTOCOLS => defined('CURLPROTO_HTTP') ? (CURLPROTO_HTTP | CURLPROTO_HTTPS) : 3, ]); $body = curl_exec($ch); $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($body === false || $code >= 400) return null; $s = (string)$body; return strlen($s) > 33554432 ? null : $s; } $ctx = stream_context_create([ 'http' => [ 'timeout' => $timeoutSec, 'follow_location' => 0, 'header' => "User-Agent: nosignup.trade-king/1.0\r\n", ], 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true], ]); $body = @file_get_contents($url, false, $ctx); if ($body === false) return null; $s = (string)$body; return strlen($s) > 33554432 ? null : $s; } function nst_king_audit_url(string $node, int $from = 0, int $limit = 1): string { $q = 'api=audit&from=' . max(0, $from) . '&limit=' . max(1, min(2000, $limit)); return $node . (str_contains($node, '?') ? '&' : '?') . $q; } /** Pull peer heads from ?api=audit. Never on HTML/state/book. */ function nst_king_pull_peer_heads(): array { $self = nst_king_self_url(); $out = []; foreach (nst_king_known_nodes() as $node) { if ($self !== '' && hash_equals($self, $node)) continue; $raw = nst_king_http_get_known(nst_king_audit_url($node, 0, 1), 6); if ($raw === null) continue; $j = json_decode($raw, true); if (!is_array($j) || empty($j['ok'])) continue; $h = strtolower((string)($j['head'] ?? '')); if (preg_match('/\\A[0-9a-f]{64}\\z/D', $h) !== 1) continue; $out[] = ['node' => $node, 'head' => $h, 'n' => (int)($j['n'] ?? $j['height'] ?? 0)]; } return $out; } function nst_king_pull_peer_rows(string $node): ?array { $rows = []; $from = 0; $guard = 0; while ($guard++ < 128) { $raw = nst_king_http_get_known(nst_king_audit_url($node, $from, 2000), 20); if ($raw === null) return null; $j = json_decode($raw, true); if (!is_array($j) || empty($j['ok']) || !is_array($j['rows'] ?? null)) return null; foreach ($j['rows'] as $r) { if (is_array($r)) $rows[] = $r; } $next = $j['next'] ?? null; if ($next === null) break; $from = (int)$next; if ($from < 1) break; } return $rows; } /** * Replace the local chain with $newRows via the existing journal/commit path. * Not a new ledger. Not genesis. Does not copy seeds. */ function nst_king_chain_replace_rows(array $newRows, string $wantHead): bool { global $CHAIN; $wantHead = strtolower($wantHead); if (preg_match('/\\A[0-9a-f]{64}\\z/D', $wantHead) !== 1) return false; if (!hash_equals($wantHead, nst_chain_head($newRows))) return false; boot_data(); if (!file_exists($CHAIN) || is_link($CHAIN) || !is_file($CHAIN)) return false; $fh = @fopen($CHAIN, 'r+b'); if (!$fh) return false; if (!flock($fh, LOCK_EX)) { fclose($fh); return false; } $ok = false; $GLOBALS['NST_KING_REPLACE_OK'] = true; try { nst_chain_recover_locked($fh); if (@ftruncate($fh, 0) !== true) return false; if (@rewind($fh) === false) return false; $rows = []; $batch = []; $size = 0; foreach ($newRows as $row) { if (!is_array($row) || $row === []) return false; $enc = j($row) . "\n"; if ($size + strlen($enc) > 16000000 && $batch !== []) { if (!nst_chain_commit_rows_locked($fh, $rows, $batch)) return false; $batch = []; $size = 0; } $batch[] = $row; $size += strlen($enc); } if ($batch !== [] && !nst_chain_commit_rows_locked($fh, $rows, $batch)) return false; if (!hash_equals($wantHead, nst_chain_head($rows))) return false; $ok = true; return true; } finally { $GLOBALS['NST_KING_REPLACE_OK'] = false; flock($fh, LOCK_UN); fclose($fh); if (!$ok) { /* leave recovery to the existing journal if a batch wrote intent */ } } } function nst_king_succession_write(string $role, string $hash): void { boot_data(); @file_put_contents(nst_king_succession_path(), j([ 'v' => 1, 'role' => $role, 'winning_hash' => $hash, 'applied_ts' => time(), ]) . "\n", LOCK_EX); } /** RETIRED: historical chain-election implementation. No live V8 endpoint calls this. */ function nst_king_maybe_apply_winner(array $localRows, string $localHead): array { $hb = nst_king_heartbeat_ts_from_rows($localRows); if ($hb < 1 || (time() - $hb) < NST_KING_SILENCE_SECS) { return ['applied' => false, 'reason' => 'before_day_30']; } if (nst_king_succession_read() !== null) { return ['applied' => false, 'reason' => 'already_applied']; } $el = nst_king_election_read(); $win = nst_king_winning_hash($el, $localHead); if ($win === null) { return ['applied' => false, 'reason' => 'no_winner']; } if (hash_equals($win, $localHead)) { nst_king_succession_write('king', $win); nst_king_election_delete(); return ['applied' => true, 'role' => 'king', 'hash' => $win]; } $peer = ''; foreach ($el['votes'] as $v) { if (!is_array($v)) continue; if (hash_equals(strtolower((string)($v['hash'] ?? '')), $win)) { $n = nst_king_canon_node_url((string)($v['node'] ?? '')); if ($n !== '' && nst_king_is_known_node($n)) { $peer = $n; break; } } } if ($peer === '') { foreach (nst_king_pull_peer_heads() as $ph) { if (hash_equals((string)$ph['head'], $win)) { $peer = (string)$ph['node']; break; } } } if ($peer === '') { return ['applied' => false, 'reason' => 'winner_peer_missing', 'hash' => $win]; } $rows = nst_king_pull_peer_rows($peer); if (!is_array($rows) || !hash_equals($win, nst_chain_head($rows))) { return ['applied' => false, 'reason' => 'winner_rows_mismatch', 'hash' => $win]; } if (!nst_king_chain_replace_rows($rows, $win)) { return ['applied' => false, 'reason' => 'replace_failed', 'hash' => $win]; } nst_king_succession_write('replica', $win); nst_king_election_delete(); return ['applied' => true, 'role' => 'replica', 'hash' => $win]; } function nst_king_local_rows_and_head(): array { try { $rows = file_exists($GLOBALS['CHAIN'] ?? '') ? read_chain(false) : []; } catch (Throwable $e) { $rows = []; } return [$rows, nst_chain_head($rows)]; } function nst_king_ping_api_out(): void { nst_king_cors_audit_like(); [$rows, $head] = nst_king_local_rows_and_head(); $hb = nst_king_heartbeat_ts_from_rows($rows); api_out([ 'ok' => true, 'head' => $head, 'heartbeat_ts' => $hb, 'age_secs' => $hb > 0 ? max(0, time() - $hb) : null, 'frozen' => false, 'succession_election' => 'retired', 'note' => 'Read-only legacy heartbeat diagnostic. Mirror capacity, traffic, silence, and replica count never elect monetary authority.', ]); } function nst_king_pull_api_out(): void { nst_king_cors_audit_like(); api_out(['ok'=>false,'err'=>'retired','succession_election'=>'retired','note'=>'Legacy peer-chain replacement is retired. Future NSU consensus must be a separately specified decentralized protocol.'], 410); } /** Vote = accept THIS node's replica hash. Same hash or invalid. One vote per IP, locked. */ function nst_king_vote_api_out(): void { api_out(['ok'=>false,'err'=>'retired','succession_election'=>'retired','note'=>'Traffic, browsers, mirrors and replica count do not vote monetary authority.'], 410); } /** * Throttled boot check. Always honors freeze file. * @return array public-safe conservation snapshot */ function nst_conservation_boot_check(array $rows, bool $force = false): array { $method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')); $write = ($method === 'POST'); if (!$write) { $force = false; } $frozen = nst_conservation_is_frozen(); $meta = load_meta(); $last = (int)($meta['last_conservation_ts'] ?? 0); $age = $last > 0 ? (time() - $last) : PHP_INT_MAX; $need = $force || $last < 1 || $age >= NST_CONSERVATION_RECHECK_SECS; if ($frozen && !$force) { $fr = nst_conservation_freeze_read() ?? []; return [ 'ok' => false, 'frozen' => true, 'sum_fmt' => isset($fr['observed']) && nst_int_micros_ok((string)$fr['observed']) ? fmt_amt((string)$fr['observed']) : null, 'expected_fmt' => isset($fr['expected']) && nst_int_micros_ok((string)$fr['expected']) ? fmt_amt((string)$fr['expected']) : null, 'checked_ts' => (int)($fr['ts'] ?? $last), 'throttled' => true, 'reason' => (string)($fr['reason'] ?? 'conservation_freeze'), ]; } if (!$need && !$frozen) { $obs = (string)($meta['last_conservation_observed'] ?? ''); $exp = (string)($meta['last_conservation_expected'] ?? ''); $okMeta = !empty($meta['last_conservation_ok']); if ($obs !== '' && $exp !== '' && nst_int_micros_ok($obs) && nst_int_micros_ok($exp)) { return [ 'ok' => $okMeta, 'frozen' => false, 'sum_fmt' => fmt_amt($obs), 'expected_fmt' => fmt_amt($exp), 'checked_ts' => $last, 'throttled' => true, ]; } } if (!$write) { $obs = (string)($meta['last_conservation_observed'] ?? ''); $exp = (string)($meta['last_conservation_expected'] ?? ''); $okMeta = !empty($meta['last_conservation_ok']); return [ 'ok' => $okMeta, 'frozen' => false, 'sum_fmt' => ($obs !== '' && nst_int_micros_ok($obs)) ? fmt_amt($obs) : null, 'expected_fmt' => ($exp !== '' && nst_int_micros_ok($exp)) ? fmt_amt($exp) : null, 'checked_ts' => $last, 'throttled' => true, ]; } $rep = nst_conservation_check_and_maybe_freeze($rows); return [ 'ok' => !empty($rep['ok']), 'frozen' => !empty($rep['frozen']), 'sum_fmt' => fmt_amt((string)$rep['observed_sum']), 'expected_fmt' => fmt_amt((string)$rep['expected_issued']), 'checked_ts' => (int)$rep['checked_ts'], 'throttled' => false, 'int_ok' => !empty($rep['int_ok']), 'detail' => $rep['detail'] ?? null, ]; } /** Public-facing conservation object for api=state / HTML. */ function nst_conservation_public_view(?array $bootSnap = null): array { if (is_array($bootSnap)) { return [ 'ok' => !empty($bootSnap['ok']), 'frozen' => !empty($bootSnap['frozen']) || nst_conservation_is_frozen(), 'sum_fmt' => $bootSnap['sum_fmt'] ?? null, 'expected_fmt' => $bootSnap['expected_fmt'] ?? null, 'checked_ts' => (int)($bootSnap['checked_ts'] ?? 0), ]; } $frozen = nst_conservation_is_frozen(); $fr = $frozen ? (nst_conservation_freeze_read() ?? []) : []; $meta = load_meta(); $obs = (string)($fr['observed'] ?? $meta['last_conservation_observed'] ?? ''); $exp = (string)($fr['expected'] ?? $meta['last_conservation_expected'] ?? ''); return [ 'ok' => !$frozen && !empty($meta['last_conservation_ok']), 'frozen' => $frozen, 'sum_fmt' => ($obs !== '' && nst_int_micros_ok($obs)) ? fmt_amt($obs) : null, 'expected_fmt' => ($exp !== '' && nst_int_micros_ok($exp)) ? fmt_amt($exp) : null, 'checked_ts' => (int)($fr['ts'] ?? $meta['last_conservation_ts'] ?? 0), ]; } /** * World population metadata only (not used for supply scale). * King holds the whole 100 million cap at birth. No drip. Pop is metadata only. * Fail-open sources: * 1) data/world_pop.override (operator integer — durable, not ephemeral) * 2) ephemeral world_pop.json cache if fresh (prefer /dev/shm; else data/ephemeral/) * 3) crawl allowlisted public stats (World Bank WLD SP.POP.TOTL) * 4) NST_POP_FALLBACK constant * Never blocks wipe/genesis if network dies — birth pile is the whole cap. * Cache is regenerable L3 only; losing it triggers recompute — never money SoT. * * @return array{pop:int,source:string,ts:int,cached:bool} */ function nst_world_pop_cache_path(): string { return nst_ephemeral_dir() . DIRECTORY_SEPARATOR . 'world_pop.json'; } function nst_world_population(): array { global $DATA; boot_data(); $override = $DATA . DIRECTORY_SEPARATOR . 'world_pop.override'; if (is_file($override)) { $n = (int)trim((string)@file_get_contents($override)); if ($n >= 1000000 && $n <= 50000000000) { return ['pop' => $n, 'source' => 'override', 'ts' => time(), 'cached' => false]; } } $cache = nst_world_pop_cache_path(); if (is_file($cache)) { $j = json_decode((string)@file_get_contents($cache), true); if (is_array($j) && !empty($j['pop']) && !empty($j['ts'])) { $age = time() - (int)$j['ts']; if ($age >= 0 && $age < NST_POP_CACHE_SECS && (int)$j['pop'] >= 1000000) { return [ 'pop' => (int)$j['pop'], 'source' => (string)($j['source'] ?? 'cache'), 'ts' => (int)$j['ts'], 'cached' => true, ]; } } } $crawled = nst_crawl_world_population(); if ($crawled !== null) { $edir = dirname($cache); if (!is_dir($edir)) { @mkdir($edir, 0755, true); } @file_put_contents($cache, j($crawled), LOCK_EX); return $crawled + ['cached' => false]; } return [ 'pop' => NST_POP_FALLBACK, 'source' => 'fallback_constant', 'ts' => time(), 'cached' => false, ]; } /** @return array{pop:int,source:string,ts:int}|null */ function nst_crawl_world_population(): ?array { if (!ini_get('allow_url_fopen') && !function_exists('curl_init')) { return null; } // World Bank: total population, world aggregate, most recent value $url = 'https://api.worldbank.org/v2/country/WLD/indicator/SP.POP.TOTL?format=json&per_page=5&mrv=5'; $raw = nst_http_get_limited($url, 6); if ($raw === null || $raw === '') return null; $j = json_decode($raw, true); if (!is_array($j) || !isset($j[1]) || !is_array($j[1])) return null; foreach ($j[1] as $row) { if (!is_array($row)) continue; $v = $row['value'] ?? null; if ($v === null || $v === '') continue; $n = (int)$v; if ($n >= 1000000000 && $n <= 50000000000) { return [ 'pop' => $n, 'source' => 'worldbank:SP.POP.TOTL', 'ts' => time(), ]; } } return null; } function nst_http_get_limited(string $url, int $timeoutSec): ?string { $host = parse_url($url, PHP_URL_HOST); if (!is_string($host) || !preg_match('/^(api\.worldbank\.org)$/i', $host)) { return null; // allowlist only - no open SSRF } if (function_exists('curl_init')) { $ch = curl_init($url); if ($ch === false) return null; curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_CONNECTTIMEOUT => $timeoutSec, CURLOPT_TIMEOUT => $timeoutSec, CURLOPT_USERAGENT => 'nosignup.trade-pop/1.0', CURLOPT_SSL_VERIFYPEER => true, ]); $body = curl_exec($ch); $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($body === false || $code >= 400) return null; return (string)$body; } $ctx = stream_context_create([ 'http' => [ 'timeout' => $timeoutSec, 'header' => "User-Agent: nosignup.trade-pop/1.0\r\n", ], 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true], ]); $body = @file_get_contents($url, false, $ctx); return $body === false ? null : (string)$body; } /** * Optimum liquid treasury micros = the whole cap (king holds it at birth). * $pop retained for API compatibility / metadata; not used for supply scale. */ function nst_optimum_treasury_micros(int $pop = 0): string { return NST_GENESIS_TREASURY; } /* ═══════════════════════════════════════════════════════════════════════════ * MONETARY SPINE — supply, emission, and the single gate every mint passes. * Design: NSU-DNA/08-BRIDGE-SPEC.md §7. Audit: NSU-DNA/09-EXPLOIT-AUDIT.md. * ═══════════════════════════════════════════════════════════════════════════ */ /** * THE canonical list of row types that create money. One definition, used * everywhere. Three separate places used to spell this out by hand — balances(), * the integer validator, and the conservation replay — so adding a mint type * meant remembering all three. Missing one does not fail loudly: it desyncs the * conservation audit and trips the E2 freeze, halting the whole economy. * * 'treasury_reanchor' STAYS in this list even though nothing creates one any * more. Chains already carry those rows; dropping the type would make replay * under-count historical issuance and freeze every live empire on upgrade. */ function nst_mint_types(): array { return ['genesis', 'treasury_reanchor', 'emission']; } function nst_is_mint_type(string $t): bool { return in_array($t, nst_mint_types(), true); } /** * How much money exists, REPLAYED FROM THE CHAIN — never stored. * * data/economy.json is destroyed and rebuilt at every yearly roll. A counter * kept there would read zero each new year and the empire would re-mint the * entire cap annually, forever, with the ceiling check agreeing because it too * would believe nothing had been minted. No attacker required; a page load * triggers the roll. See §7.8. balances() already works this way; so does this. * * @param array|null $rows chain rows, or null to read them */ function nst_minted_to_date(?array $rows = null): string { $rows = $rows ?? read_chain(); $sum = '0'; foreach ($rows as $r) { if (!is_array($r) || !nst_is_mint_type((string)($r['type'] ?? ''))) { continue; } foreach ($r['outputs'] ?? [] as $o) { if (!is_array($o)) continue; $m = (string)($o['amount'] ?? '0'); if (nst_int_micros_ok($m) && dec_cmp($m, '0') > 0) { $sum = dec_add($sum, $m); } } } return $sum; } /** Headroom under the ceiling. Zero means the tap is closed for good. */ function nst_supply_remaining(?array $rows = null): string { $rem = dec_sub(NST_MAX_SUPPLY, nst_minted_to_date($rows)); return dec_cmp($rem, '0') > 0 ? $rem : '0'; } /** * The day's emission: a fixed fraction of what is still unminted. * * E = (NST_MAX_SUPPLY - minted_to_date) * NST_EMISSION_DAILY_RATE_PPM / 1e6 * * Self-tapering, never exhausts, needs no schedule table, and is auditable from * two numbers anyone can read off the chain. Early participation is worth more * than late — the bootstrapping incentive — without a cliff at the end. */ function nst_emission_today(?array $rows = null): string { $rem = nst_supply_remaining($rows); if (dec_cmp($rem, '0') <= 0) return '0'; /* Multiply BEFORE dividing: these are arbitrary-precision strings, so the * big intermediate is free, and dividing first would round small remainders * to zero and silently close the tap early. */ $e = dec_div_small(dec_mul_small($rem, (int)NST_EMISSION_DAILY_RATE_PPM), 1000000); return dec_cmp($e, '0') > 0 ? $e : '0'; } /** * THE CHOKE POINT. Every row that creates money goes through here. * * Before this existed, NST_MAX_SUPPLY appeared only in display code — the * charter promised a "hard ceiling on all issuance" and nothing enforced it. * A mint that would breach the cap is CLAMPED to the remaining headroom, never * silently allowed and never partially written. * * @param string $type must be in nst_mint_types() * @param array $outputs * @param array $extra additional row fields (reason, ts, notes) * @return array{ok:bool,minted:string,clamped:bool,err?:string} */ function nst_mint_guarded(string $type, array $outputs, array $extra = []): array { if (!nst_is_mint_type($type)) { return ['ok' => false, 'minted' => '0', 'clamped' => false, 'err' => 'not a mint type: ' . $type]; } /* CHECK AND WRITE MUST BE ATOMIC. * append_row() locks only its own write. Reading the supply, deciding there * is headroom, and then appending is three steps — two concurrent requests * can both read the same total, both see room, and both append, putting * issuance over the ceiling. A cap that holds only when nobody is looking * twice at once is not a cap. This lock spans the whole decision. */ $lockPath = $GLOBALS['DATA'] . DIRECTORY_SEPARATOR . 'mint.lock'; $lock = @fopen($lockPath, 'cb'); if ($lock === false) { return ['ok' => false, 'minted' => '0', 'clamped' => false, 'err' => 'cannot open mint lock']; } if (!flock($lock, LOCK_EX)) { fclose($lock); return ['ok' => false, 'minted' => '0', 'clamped' => false, 'err' => 'Mint is busy — try again in a moment']; } try { return nst_mint_guarded_locked($type, $outputs, $extra); } finally { flock($lock, LOCK_UN); fclose($lock); } } /** Inner mint. NEVER call directly — nst_mint_guarded() holds the lock. */ function nst_mint_guarded_locked(string $type, array $outputs, array $extra = []): array { $rows = read_chain(); /* The mint lock must protect uniqueness as well as the ceiling. A caller's * pre-lock "already emitted" read can race another process and otherwise * mint the same UTC day twice while both rows remain under the cap. */ if ($type === 'emission') { $day = (string)($extra['day'] ?? ''); if (preg_match('/\A[0-9]{4}-[0-9]{2}-[0-9]{2}\z/D', $day) !== 1) { return ['ok' => false, 'minted' => '0', 'clamped' => false, 'err' => 'invalid emission day']; } foreach ($rows as $existing) { if (($existing['type'] ?? '') === 'emission' && ($existing['day'] ?? '') === $day) { return ['ok' => false, 'minted' => '0', 'clamped' => false, 'err' => 'already_emitted_today']; } } } $remaining = nst_supply_remaining($rows); $want = '0'; foreach ($outputs as $o) { /* NEVER MINT TO AN EMPTY ADDRESS. * nst_conservation_replay() counts every mint leg toward expected * issuance, but balances() skips a leg with no address - so one empty * addr makes observed < expected and the E2 freeze halts the whole * economy. The coins would also be unspendable, because no seed derives * the empty address. Refuse the mint instead: a caller that cannot name * the payee is not ready to pay. */ $a = (string)($o['addr'] ?? ''); if ($a === '') { return ['ok' => false, 'minted' => '0', 'clamped' => false, 'err' => 'mint output has no address']; } $m = (string)($o['amount'] ?? '0'); if (!nst_int_micros_ok($m) || dec_cmp($m, '0') <= 0) { return ['ok' => false, 'minted' => '0', 'clamped' => false, 'err' => 'bad output amount']; } $want = dec_add($want, $m); } if (dec_cmp($want, '0') <= 0) { return ['ok' => false, 'minted' => '0', 'clamped' => false, 'err' => 'nothing to mint']; } $clamped = false; if (dec_cmp($want, $remaining) > 0) { if (dec_cmp($remaining, '0') <= 0) { return ['ok' => false, 'minted' => '0', 'clamped' => true, 'err' => 'ceiling reached - no supply remains']; } /* Fill legs in order until the headroom is gone, truncating the one that * straddles the ceiling and dropping any after it. This happens at most * once in the empire's life — the final partial day — so exact * proportions matter less than never writing a row that breaches. */ $scaled = []; $left = $remaining; foreach ($outputs as $o) { if (dec_cmp($left, '0') <= 0) break; $m = (string)$o['amount']; $part = (dec_cmp($m, $left) > 0) ? $left : $m; $scaled[] = ['addr' => (string)$o['addr'], 'amount' => $part]; $left = dec_sub($left, $part); } $outputs = $scaled; $want = $remaining; $clamped = true; } $row = array_merge($extra, [ 'type' => $type, 'ver' => 2, 'ts' => $extra['ts'] ?? time(), 'outputs' => array_values($outputs), 'minted_before' => nst_minted_to_date($rows), 'ceiling' => NST_MAX_SUPPLY, 'clamped' => $clamped, ]); if (!append_row($row)) { return ['ok' => false, 'minted' => '0', 'clamped' => $clamped, 'err' => 'mint commit failed']; } return ['ok' => true, 'minted' => $want, 'clamped' => $clamped]; } /** * RETIRED 2026-08-03 — the yearly treasury re-anchor. * * It topped the KING treasury back up to a fixed target at every wipe, which * meant whatever the King spent during the year came back as newly minted coin: * he kept the goods AND got the money back, every year, unbounded. See * 09-EXPLOIT-AUDIT.md C-1. Coins now enter only through nst_emission_today(), * and the King is paid a declared share of that emission like everyone else. * * Kept as a no-op so old call sites report cleanly instead of fataling. It must * never mint again. * * @return array{pop:int,source:string,target:string,before:string,after:string,action:string} */ function nst_treasury_reanchor_to_population(string $reason): array { return [ 'pop' => 0, 'source' => 'retired', 'target' => '0', 'before' => '0', 'after' => '0', 'action' => 'retired_no_mint', 'note' => 'treasury re-anchor retired: after birth nothing is printed; remaining supply is 0', ]; } /** * Run today's emission, once per UTC day, idempotently. * * Called on ordinary page loads. The "have we already run today" test reads the * CHAIN, not economy.json — economy.json is destroyed every year, so a marker * kept there would let the empire re-mint on the first load of each new epoch. * * Splits the day's coins three ways. The KING share is the remainder, so the * three parts always sum to exactly the emission: no rounding dust is minted * into nobody's wallet, and no part can silently reach zero. * * @return array{ran:bool,day:string,minted:string,reason?:string} */ /** * The ten crop lord wallets, as written by genesis into data/crops.json. * * Trade owns the only chain, so trade is the only thing that can pay anybody - * but each crop's panel seed is minted into that crop's own vault on its own * host, so trade has no other way to learn the nine sibling addresses. Genesis * is the single moment where all ten exist in one process, so genesis writes * them down. Addresses only; a seed in this file would be the whole empire. * * Returns [] when the registry is absent, which callers must treat as "this * empire cannot name its crops yet" rather than as an error. */ /** Parse and sanitise a crops list from any source (file or chain row). */ function nst_crop_list_clean($raw): array { $out = []; if (!is_array($raw)) return $out; $seen = []; foreach ($raw as $c) { if (!is_array($c)) continue; $addr = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($c['addr'] ?? '')) ?? ''); /* A crop address is a sha256 like every other wallet. Anything else is a * malformed registry, and paying emission to a malformed address burns * it as surely as paying a pool nobody holds. */ if (strlen($addr) !== 64) continue; if (isset($seen[$addr])) continue; // never pay one wallet two shares $seen[$addr] = true; $out[] = ['tld' => (string)($c['tld'] ?? ''), 'addr' => $addr]; } return $out; } /** The crop list as anchored on the chain, or [] if never anchored. */ function nst_crop_anchor(?array $rows = null): array { $rows = $rows ?? read_chain(); $found = []; foreach ($rows as $r) { if (is_array($r) && ($r['type'] ?? '') === 'crop_registry') { $found = nst_crop_list_clean($r['crops'] ?? null); // last one wins } } return $found; } /** * ANCHOR THE REGISTRY ON THE LEDGER, ONCE. * * data/crops.json decides who receives the lord remainder of every coin ever minted, and it is * a plain file the King can rewrite at any time to ten addresses they control. * Nothing would notice. That makes the ten-way split a configuration rather than * a rule. * * So the first time the empire can name its crops, it writes that list into the * append-only chain. From then on the CHAIN is authoritative and the file is * only a cache. Later edits to crops.json change nothing and are reported as a * mismatch. * * This is trust-on-first-use, and it is worth naming as such rather than * overselling it: whoever runs genesis chooses the ten addresses. What it buys * is that the choice is made ONCE, in public, on a ledger anyone replaying the * chain can read - instead of being silently re-choosable every day forever. * A King who wants to redirect emission now has to do it where it is visible. * * Not a mint, so conservation is untouched: no output legs, no money moves. */ function nst_crop_registry_anchor(): array { $rows = read_chain(); if (nst_crop_anchor($rows)) { return ['anchored' => false, 'reason' => 'already_anchored']; } $p = $GLOBALS['DATA'] . DIRECTORY_SEPARATOR . 'crops.json'; if (!is_file($p)) return ['anchored' => false, 'reason' => 'no_registry_file']; $j = json_decode((string)@file_get_contents($p), true); $crops = nst_crop_list_clean(is_array($j) ? ($j['crops'] ?? null) : null); if (!$crops) return ['anchored' => false, 'reason' => 'registry_empty_or_malformed']; $res = nst_with_chain_lock(function (array $chain, callable $append) use ($crops) { foreach ($chain as $r) { // re-check under the lock if (is_array($r) && ($r['type'] ?? '') === 'crop_registry') { return ['anchored' => false, 'reason' => 'already_anchored']; } } $row = [ 'type' => 'crop_registry', 'ver' => 1, 'crops' => $crops, 'count' => count($crops), 'ts' => time(), 'note' => 'Crop lord wallets that receive the daily emission. Anchored once, ' . 'trust-on-first-use; the chain is authoritative from here and data/crops.json ' . 'is only a cache. Not a mint - no money moves on this row.', ]; if (!$append($row)) return ['anchored' => false, 'reason' => 'storage']; return ['anchored' => true, 'count' => count($crops)]; }); return is_array($res) ? $res : ['anchored' => false, 'reason' => 'lock']; } /** True when data/crops.json disagrees with what the chain anchored. */ function nst_crop_registry_mismatch(): bool { $anchor = nst_crop_anchor(); if (!$anchor) return false; $p = $GLOBALS['DATA'] . DIRECTORY_SEPARATOR . 'crops.json'; $j = is_file($p) ? json_decode((string)@file_get_contents($p), true) : null; $file = nst_crop_list_clean(is_array($j) ? ($j['crops'] ?? null) : null); return array_column($file, 'addr') !== array_column($anchor, 'addr'); } function nst_crop_addrs(): array { static $cache = null; if ($cache !== null) return $cache; /* THE CHAIN WINS. Once anchored, editing crops.json cannot redirect a single * coin - it only produces a visible mismatch. The file is a cache used to * bootstrap the anchor and nothing more. */ $anchor = nst_crop_anchor(); if ($anchor) { $cache = $anchor; return $cache; } $p = $GLOBALS['DATA'] . DIRECTORY_SEPARATOR . 'crops.json'; if (!is_file($p)) { $cache = []; return $cache; } $j = json_decode((string)@file_get_contents($p), true); $cache = nst_crop_list_clean(is_array($j) ? ($j['crops'] ?? null) : null); return $cache; } function nst_emission_run_daily(): array { $dayHold = gmdate('Y-m-d'); if (NST_EMISSION_PAUSED) { return ['ran' => false, 'day' => $dayHold, 'minted' => '0', 'reason' => 'paused']; } $day = gmdate('Y-m-d'); $rows = read_chain(); /* Idempotence from the ledger itself. */ foreach ($rows as $r) { if (is_array($r) && ($r['type'] ?? '') === 'emission' && ($r['day'] ?? '') === $day) { return ['ran' => false, 'day' => $day, 'minted' => '0', 'reason' => 'already_emitted_today']; } } $total = nst_emission_today($rows); if (dec_cmp($total, '0') <= 0) { return ['ran' => false, 'day' => $day, 'minted' => '0', 'reason' => 'ceiling_reached']; } $kingAddr = nst_treasury_addr_safe(); if ($kingAddr === '') { return ['ran' => false, 'day' => $day, 'minted' => '0', 'reason' => 'no_treasury_addr']; } /* Ten crops, one equal share each, King's cut off the top of every share. * * WAIT FOR THE REGISTRY RATHER THAN MISALLOCATING. * * This used to fall back to paying the whole day to the King when crops.json * was missing, on the reasoning that stalling issuance is worse than a * recoverable concentration. That reasoning was wrong because the fallback * was not rare - it fired on EVERY virgin deploy, which made it the primary * path rather than the exception. * * The cause is an ordering the fallback quietly hid: trade self-keys and runs * its first emission on the first page load, while genesis - which is what * writes the registry - runs afterwards. So day one always paid one address * and never split. Observed live on 2026-08-04: 10,000 NSU to the King, all * ten crop panels at zero. * * Skipping mints nothing and loses nothing. Emission is idempotent per UTC * day and driven by page loads, so the moment the registry lands the same * day's emission runs correctly. A day that pays the wrong wallets, by * contrast, cannot be un-minted. */ $crops = nst_crop_addrs(); if (!$crops) { return ['ran' => false, 'day' => $day, 'minted' => '0', 'reason' => 'no_crop_registry', 'note' => 'data/crops.json absent - genesis has not registered the crops yet. ' . 'Nothing is minted until the empire can name who it is paying. Run Deploy.bat.']; } $outputs = []; $perCrop = '0'; $kingTaxTotal = '0'; $n = count($crops); $perCrop = dec_div_small($total, $n); $paid = '0'; foreach ($crops as $i => $c) { $addr = (string)($c['addr'] ?? ''); if ($addr === '') continue; /* Last crop absorbs the rounding dust so the legs sum to EXACTLY the * emission. Dust that belongs to nobody is minted coins with no * owner, which conservation would then have to explain away. */ $share = ($i === $n - 1) ? dec_sub($total, $paid) : $perCrop; if (dec_cmp($share, '0') <= 0) continue; $paid = dec_add($paid, $share); $split = nst_fee_split($share); // same rule as every other lord receipt if (dec_cmp($split['lord'], '0') > 0) { $outputs[] = ['addr' => $addr, 'amount' => $split['lord']]; } if (dec_cmp($split['king'], '0') > 0) { $kingTaxTotal = dec_add($kingTaxTotal, $split['king']); } } if (dec_cmp($kingTaxTotal, '0') > 0) { $outputs[] = ['addr' => $kingAddr, 'amount' => $kingTaxTotal]; } if (!$outputs) { return ['ran' => false, 'day' => $day, 'minted' => '0', 'reason' => 'nothing_to_split']; } $res = nst_mint_guarded('emission', $outputs, [ 'day' => $day, 'reason' => 'daily_emission', 'rate_ppm' => NST_EMISSION_DAILY_RATE_PPM, 'crops_paid' => count($crops), 'per_crop_micros' => $perCrop, 'king_tax_ppm' => NST_KING_TAX_PPM, 'king_tax_micros' => $kingTaxTotal, 'split_rule' => 'equal share per crop, King tax off the top of each share', 'note' => 'daily emission: the only path by which NSU comes into existence', ]); if (empty($res['ok'])) { return ['ran' => false, 'day' => $day, 'minted' => '0', 'reason' => (string)($res['err'] ?? 'mint_refused')]; } return ['ran' => true, 'day' => $day, 'minted' => (string)$res['minted']]; } /** * Split a SERVICE FEE between the lord who earned it and the King's tax. * * This CIRCULATES money — it never creates any. Duplicating a fee into the * King's vault instead would be an infinite mint: pay a lord you control, get a * mirrored copy, repeat at zero cost. A tax makes self-dealing mildly negative * rather than infinitely profitable, which is the whole point. * * ONLY for fees paid TO the empire — ad slots, amplification, rent. NEVER for a * trade between two users. Taking a slice of two strangers settling their own * terms is a gatekeeper's cut, which the ethos refuses outright. * * @param string $feeMicros total the payer hands over * @return array{lord:string,king:string} the two legs, summing to exactly $feeMicros */ function nst_fee_split(string $feeMicros): array { if (!nst_int_micros_ok($feeMicros) || dec_cmp($feeMicros, '0') <= 0) { return ['lord' => '0', 'king' => '0']; } $king = dec_div_small(dec_mul_small($feeMicros, (int)NST_KING_TAX_PPM), 1000000); /* THE TAX MAY NEVER ROUND TO ZERO. * * Integer division floors, so at 0.01% (100 ppm) any receipt below 10000 * micros produced a King cut of exactly 0 - and revenue split into dust * slices was therefore tax-free. Small, but it is a leak with no floor: * the cost of exploiting it is only the number of receipts you are willing * to write. * * Charging a minimum of one micro closes it and makes dust-splitting * strictly WORSE than paying honestly, which is the property worth having. * A 1-micro receipt is taxed at 100% (0.000001 NSU, and the lord keeps * nothing); anything of real size is taxed at exactly 0.01%. So the * incentive runs the right way at every scale. * * dec_sub below still gives the lord the remainder, so the two legs always * reconstruct the fee exactly - dust that belongs to nobody is a * conservation violation waiting to happen. */ if (dec_cmp($king, '0') <= 0) { $king = '1'; } $lord = dec_sub($feeMicros, $king); if (dec_cmp($lord, '0') < 0) { $lord = '0'; $king = $feeMicros; } return ['lord' => $lord, 'king' => $king]; } /** Ten faces a buyer can put an ad on. Keys are crop tlds. */ function nst_ad_site_catalog(): array { return [ 'com' => ['tld' => 'com', 'name' => 'Map', 'blurb' => 'Public front door (.com)'], 'org' => ['tld' => 'org', 'name' => 'Commons', 'blurb' => 'The commons twin (.org)'], 'net' => ['tld' => 'net', 'name' => 'Network', 'blurb' => 'The network hub (.net)'], 'trade' => ['tld' => 'trade', 'name' => 'Trade', 'blurb' => 'This marketplace'], 'chat' => ['tld' => 'chat', 'name' => 'Chat', 'blurb' => 'Peer video, no signup'], 'work' => ['tld' => 'work', 'name' => 'Work', 'blurb' => 'Jobs and resumes'], 'market' => ['tld' => 'market', 'name' => 'Market', 'blurb' => 'Classifieds'], 'date' => ['tld' => 'date', 'name' => 'Date', 'blurb' => 'Personals, 18+'], 'fun' => ['tld' => 'fun', 'name' => 'Fun', 'blurb' => 'Media board'], 'info' => ['tld' => 'info', 'name' => 'Info', 'blurb' => 'Notes, local reader'], ]; } /** Paid SlowGPT is disabled until genuine Trade receipts and INFO server admission agree. * TEMP_FREE_PARALLEL on INFO remains its independent free bring-up policy. * Do not enable this gate merely because a synthetic parser fixture passes. */ function nst_slowgpt_paid_enabled(): bool { return false; } function nst_slowgpt_parallel_quote(int $parallel): ?string { static $q = [2 => '0.1', 3 => '0.2', 4 => '0.3']; return $q[$parallel] ?? null; } function nst_ad_site_norm(string $raw): string { $t = strtolower(trim($raw)); $t = preg_replace('/^nosignup\./', '', $t) ?? $t; $cat = nst_ad_site_catalog(); return isset($cat[$t]) ? $t : ''; } /** Lord wallet for a sibling crop, from the anchored registry. Empty if unnamed. */ function nst_crop_lord_addr(string $tld): string { $tld = nst_ad_site_norm($tld); if ($tld === '') return ''; foreach (nst_crop_addrs() as $c) { if (nst_ad_site_norm((string)($c['tld'] ?? '')) === $tld) { return (string)($c['addr'] ?? ''); } } return ''; } /** * Split an ad/service purchase. Circulation only — never a mint. * * King 0.01% always. Trade 0.01% only when the purchase is for another cell. * On trade itself: King + Trade-as-lord remainder (no second marketplace cut). * Legs always reconstruct $feeMicros. * * @return array{lord:string,king:string,trade:string} */ function nst_ad_purchase_split(string $feeMicros, bool $otherSite): array { if (!nst_int_micros_ok($feeMicros) || dec_cmp($feeMicros, '0') <= 0) { return ['lord' => '0', 'king' => '0', 'trade' => '0']; } $king = dec_div_small(dec_mul_small($feeMicros, (int)NST_KING_TAX_PPM), 1000000); if (dec_cmp($king, '0') <= 0) $king = '1'; $trade = '0'; if ($otherSite) { $trade = dec_div_small(dec_mul_small($feeMicros, (int)NST_TRADE_CUT_PPM), 1000000); if (dec_cmp($trade, '0') <= 0) $trade = '1'; } $taken = dec_add($king, $trade); if (dec_cmp($taken, $feeMicros) > 0) { if (dec_cmp($king, $feeMicros) >= 0) { $king = $feeMicros; $trade = '0'; } else { $trade = dec_sub($feeMicros, $king); } $taken = $feeMicros; } $lord = dec_sub($feeMicros, $taken); if (dec_cmp($lord, '0') < 0) { $lord = '0'; } return ['lord' => $lord, 'king' => $king, 'trade' => $trade]; } function nst_year_remain_ppm(): int { $ep = nst_epoch_info_read_only(); $secs = (int)($ep['secs_to_reset'] ?? 0); if ($secs <= 0) { return 0; } if ($secs >= NST_RESET_SECS) { return 1000000; } return (int)intdiv($secs * 1000000, NST_RESET_SECS); } /** Full-year price × remaining year. Floor 1 micro so a quote is never free-by-rounding. */ function nst_decay_price_micros(string $fullYearMicros): string { $fullYearMicros = dec_norm($fullYearMicros); $ppm = nst_year_remain_ppm(); if ($ppm <= 0 || dec_cmp($fullYearMicros, '0') <= 0) { return '1'; } $p = dec_div_small(dec_mul_small($fullYearMicros, $ppm), 1000000); return dec_cmp($p, '1') < 0 ? '1' : $p; } function nst_ad_shares_path(): string { return nst_data_file('ad.shares'); } function nst_ad_shares_empty(): array { $sites = []; foreach (array_keys(nst_ad_site_catalog()) as $tld) { $sites[$tld] = []; } return [ 'epoch_n' => (int)(nst_epoch_info_read_only()['epoch_n'] ?? 1), 'sites' => $sites, ]; } function nst_ad_shares_load(): array { $empty = nst_ad_shares_empty(); $path = nst_ad_shares_path(); if (!is_file($path)) { return $empty; } $j = json_decode((string)@file_get_contents($path), true); if (!is_array($j)) { return $empty; } if ((int)($j['epoch_n'] ?? 0) !== (int)$empty['epoch_n']) { return $empty; } $sites = is_array($j['sites'] ?? null) ? $j['sites'] : []; $out = $empty; foreach ($out['sites'] as $tld => $_) { $row = $sites[$tld] ?? []; if (!is_array($row)) { continue; } foreach ($row as $addr => $n) { $addr = strtolower(preg_replace('/[^a-f0-9]/', '', (string)$addr) ?? ''); $n = (int)$n; if (strlen($addr) !== 64 || $n < 1) { continue; } if ($n > NST_AD_SHARES_CAP) { $n = NST_AD_SHARES_CAP; } $out['sites'][$tld][$addr] = $n; } } return $out; } function nst_ad_shares_save(array $store): bool { $path = nst_ad_shares_path(); $ret = nst_with_file_lock($path, static function ($fh) use ($store) { rewind($fh); ftruncate($fh, 0); fwrite($fh, j($store)); fflush($fh); @chmod($path, 0600); return true; }); return $ret === true; } function nst_ad_shares_burn(): void { $path = nst_ad_shares_path(); if (is_file($path)) { @unlink($path); } } function nst_ad_shares_for_crop(string $crop, ?array $store = null): array { $crop = nst_ad_site_norm($crop); if ($crop === '') { $crop = 'trade'; } $store = $store ?? nst_ad_shares_load(); $row = $store['sites'][$crop] ?? []; return is_array($row) ? $row : []; } /** Old stake bids → implied shares (0.01 NSU = 1 share at the full-year unit). */ function nst_ad_implied_shares_from_bids(string $crop): array { $crop = nst_ad_site_norm($crop); if ($crop === '') { $crop = 'trade'; } $out = []; $unit = (int)NST_AD_SHARE_FULL_YEAR_MICROS; if ($unit < 1) { return $out; } foreach ((array)(load_economy()['bids'] ?? []) as $b) { if (!is_array($b) || empty($b['active'])) { continue; } if (ad_bid_crop($b) !== $crop) { continue; } $addr = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($b['owner'] ?? '')) ?? ''); if (strlen($addr) !== 64) { continue; } $stake = dec_norm((string)($b['stake_micros'] ?? '0')); if (dec_cmp($stake, (string)$unit) < 0) { continue; } $n = (int)dec_div_small($stake, $unit); if ($n < 1) { continue; } if ($n > NST_AD_SHARES_CAP) { $n = NST_AD_SHARES_CAP; } $out[$addr] = (int)($out[$addr] ?? 0) + $n; if ($out[$addr] > NST_AD_SHARES_CAP) { $out[$addr] = NST_AD_SHARES_CAP; } } return $out; } function nst_ad_effective_shares(string $crop): array { $out = nst_ad_implied_shares_from_bids($crop); foreach (nst_ad_shares_for_crop($crop) as $addr => $n) { $out[$addr] = (int)$n; } return $out; } function nst_ad_sold_shares(array $held): int { $sold = 0; foreach ($held as $n) { $sold += (int)$n; } if ($sold > NST_AD_SHARES_CAP) { return NST_AD_SHARES_CAP; } return $sold < 0 ? 0 : $sold; } /** Read-only operator/lord presence. Does not create operator.addr. */ function nst_ad_spend_ready(string $crop): array { $crop = nst_ad_site_norm($crop); if ($crop === '') { return ['ok' => false, 'why' => 'Unknown site']; } // Temporary purchase safeguard: keep existing ads and v2 delivery code while payment/delivery failures are repaired. return ['ok' => false, 'why' => 'New ad purchases are temporarily unavailable while payment and delivery failures are repaired.']; // ECONOMY-PATCH WAVEB2: TR-10/11 crash/race lab acceptance GREEN 2026-09-28 (receipts/tr10-tr11-crash-race.json). // Purchases use nst_ad_purchase_commit_v2 (economy-tx lock → stage → outcome → durable commit). // If spend path regresses, restore the closed early-return rather than charging without delivery. if (function_exists('nst_conservation_is_frozen') && nst_conservation_is_frozen()) { return ['ok' => false, 'why' => 'Writes frozen — quote only']; } $king = nst_treasury_addr_safe(); if ($crop === 'trade') { global $OP_ADDR_FILE, $SITE_SEED_FILE; $lord = ''; if (!empty($OP_ADDR_FILE) && is_file($OP_ADDR_FILE)) { $have = strtolower(trim((string)@file_get_contents($OP_ADDR_FILE))); if (preg_match('/^[a-f0-9]{64}$/', $have)) { $lord = $have; } } if ($lord === '' && $king === '') { return ['ok' => false, 'why' => 'No lord/king wallet to receive the spend']; } return ['ok' => true, 'why' => '']; } $lord = nst_crop_lord_addr($crop); if ($lord === '') { return ['ok' => false, 'why' => 'That site has no lord wallet on the registry yet']; } return ['ok' => true, 'why' => '']; } /** Quote only — never spends, never writes shares. */ function nst_ad_share_quote(?string $crop = null, string $addr = ''): array { $ep = nst_epoch_info_read_only(); $days = nst_ad_slot_days_left(); $nsu = nst_ad_slot_price_nsu(); $micros = parse_amt((string)$nsu); if ($micros === null) $micros = '0'; $addr = strtolower(preg_replace('/[^a-f0-9]/', '', $addr) ?? ''); if (strlen($addr) !== 64) $addr = ''; $want = $crop !== null && $crop !== '' ? nst_ad_site_norm($crop) : ''; $sites = []; foreach (nst_ad_site_catalog() as $tld => $face) { if ($want !== '' && $tld !== $want) continue; $sold = nst_ad_slot_sold_count($tld); $remain = 10000 - $sold; if ($remain < 0) $remain = 0; $ready = nst_ad_spend_ready($tld); $why = ''; if ($remain <= 0) $why = 'Board full this epoch'; elseif (empty($ready['ok'])) $why = (string)($ready['why'] ?? 'Spend path not ready'); $sites[$tld] = [ 'tld' => $tld, 'name' => $face['name'], 'sold' => $sold, 'remain' => $remain, 'you' => 0, 'slot_nsu' => $nsu, 'slot_micros' => $micros, 'slot_fmt' => fmt_amt($micros), 'days_left' => $days, 'unit_micros' => $micros, 'unit_fmt' => fmt_amt($micros), 'buyable' => $remain > 0 && !empty($ready['ok']), 'why' => $why, ]; } $eremain = nst_ad_empire_remain(); $eready = nst_ad_spend_ready('trade'); $ewhy = ''; if ($eremain <= 0) $ewhy = 'Board full this epoch'; elseif (empty($eready['ok'])) $ewhy = (string)($eready['why'] ?? 'Spend path not ready'); $sites['all'] = [ 'tld' => 'all', 'name' => 'all ten faces', 'sold' => 10000 - $eremain, 'remain' => $eremain, 'you' => 0, 'slot_nsu' => $nsu, 'slot_micros' => $micros, 'slot_fmt' => fmt_amt($micros), 'days_left' => $days, 'unit_micros' => $micros, 'unit_fmt' => fmt_amt($micros), 'buyable' => $eremain > 0 && !empty($eready['ok']), 'why' => $ewhy, ]; // WAVEB-TR07-QUOTE-ATTACH: versioned quote identity (advisory; buyable still gated). $quoteSite = $want !== '' ? $want : 'trade'; $quoteV2 = function_exists('nst_ad_quote_v2_build') ? nst_ad_quote_v2_build($quoteSite) : null; // TR-02: issuance persists the server quote. The browser still receives quote_id only. if (is_array($quoteV2) && function_exists('nst_ad_purchase_quote_persist')) { $storedQuote = nst_ad_purchase_quote_persist($quoteV2); if (empty($storedQuote['ok']) || !is_array($storedQuote['quote'] ?? null)) { $quoteV2['quote_id'] = ''; } else { $quoteV2['quote_id'] = (string)$storedQuote['quote']['quote_id']; } } $quoteOut = [ 'ok' => true, 'quote' => true, 'spend' => false, 'cap' => 10000, 'empire_remain' => $eremain, 'slot_nsu' => $nsu, 'slot_micros' => $micros, 'slot_fmt' => fmt_amt($micros), 'days_left' => $days, 'nsu_per_day' => (int)NST_RENT_NSU_PER_DAY, 'unit_micros' => $micros, 'unit_fmt' => fmt_amt($micros), 'secs_to_reset' => (int)($ep['secs_to_reset'] ?? 0), 'epoch_n' => (int)($ep['epoch_n'] ?? 1), // WAVEB-TR07 'quote_v2' => $quoteV2, 'quote_id' => is_array($quoteV2) ? (string)($quoteV2['quote_id'] ?? '') : '', 'quote_exp' => is_array($quoteV2) ? (int)($quoteV2['exp'] ?? 0) : 0, 'quote_rule_ver' => is_array($quoteV2) ? (string)($quoteV2['rule_ver'] ?? '') : '', 'decay' => 'one slot = 100 NSU × days left (not divided by 10000)', 'roll' => 'each face rolls 0–9999 and fetches nosignup.trade/{site}/{n}', 'sites' => $sites, ]; // WAVEC-TR21-HONEST-QUOTE if (function_exists('nst_ad_quote_attach_honest_copy')) { return nst_ad_quote_attach_honest_copy($quoteOut); } return $quoteOut; } function nst_ad_share_credit(string $crop, string $addr, int $n): array { $crop = nst_ad_site_norm($crop); $addr = strtolower(preg_replace('/[^a-f0-9]/', '', $addr) ?? ''); if ($crop === '' || strlen($addr) !== 64 || $n < 1) { return ['ok' => false, 'err' => 'bad share credit']; } $store = nst_ad_shares_load(); $held = nst_ad_effective_shares($crop); $sold = nst_ad_sold_shares($held); $remain = NST_AD_SHARES_CAP - $sold; if ($n > $remain) { return ['ok' => false, 'err' => 'Only ' . $remain . ' share(s) left on this site']; } $cur = (int)(($store['sites'][$crop][$addr] ?? 0)); if ($cur < 1) { $cur = (int)($held[$addr] ?? 0); } $store['sites'][$crop][$addr] = $cur + $n; if (!nst_ad_shares_save($store)) { return ['ok' => false, 'err' => 'Could not save shares']; } return ['ok' => true, 'you' => $store['sites'][$crop][$addr], 'remain' => $remain - $n]; } /** KING treasury address, or '' when the empire is not keyed yet. */ function nst_treasury_addr_safe(): string { $f = $GLOBALS['TFILE'] ?? ''; if (!$f || !is_file($f)) return ''; $sec = trim((string)@file_get_contents($f)); if ($sec === '') return ''; return addr_from_seed($sec); } /* The original re-anchor body (which minted a refill every wipe) has been * DELETED, not merely unhooked. Dead code that mints is a loaded gun: an * autonomous pass looking for a faucet would find it and wire it back up. * History for it lives in NSU-DNA/09-EXPLOIT-AUDIT.md C-1. */ /** * Generate a 12-word site-local seed (same product surface as user wallets). * Used for: (KING) treasury/master faucet at genesis; (LORD) operator panel seed on rent. * KING seed never goes to renters. LORD seed never unlocks faucet/mint. */ function nst_generate_site_seed(): string { // 12-word site seed (not BIP39). Product surface matches user wallets. // Word pool is small + high entropy bytes — site-local only. static $wl = [ 'able','acid','aged','also','aqua','arch','area','army','atom','aunt','auto','avoid', 'axis','baby','band','bank','bare','barn','base','bean','bear','belt','bike','bind', 'bird','bite','blue','boat','body','bold','bolt','bone','book','boot','born','bowl', 'brass','brave','bread','brick','brief','bring','broad','broke','brown','brush','build','bulk', 'burn','burst','bush','busy','cable','cage','cake','calm','camp','cane','cape','card', 'care','cart','case','cash','cast','cave','cell','cent','chat','chef','chin','chip', 'city','clap','clay','clip','club','coal','coat','code','coil','coin','cold','come', 'cook','cool','cope','copy','cord','core','corn','cost','cove','crab','crew','crop', 'crow','cube','cult','curb','cure','curl','dark','dart','dash','data','dawn','deal', 'dear','deck','deep','deer','desk','dial','dice','diet','dine','dirt','disc','dock', 'dome','done','door','dose','down','draw','drip','drop','drum','dual','duck','dune', 'dusk','dust','duty','each','earn','east','easy','echo','edge','edit','else','emit', 'epic','even','ever','evil','exit','face','fact','fade','fail','fair','fall','fame', 'farm','fast','fate','fear','feed','feel','fern','file','fill','film','find','fine', 'fire','firm','fish','flag','flat','flee','flip','flow','foam','foil','fold','font', 'food','fool','foot','ford','fork','form','fort','foul','four','free','frog','from', 'fuel','full','fund','fuse','gain','game','gate','gear','gene','gift','girl','give', 'glad','glow','glue','goal','goat','gold','golf','good','grab','grad','gram','gray', 'grid','grim','grin','grip','grow','gulf','guru','hail','hair','half','hall','hand', 'hang','hard','harm','harp','hate','have','hawk','haze','head','heal','heap','heat', 'heed','heel','held','help','herb','here','hero','hide','high','hill','hint','hire', 'hold','hole','home','hood','hook','hope','horn','host','hour','huge','hull','hung', 'hunt','hurt','icon','idea','idle','inch','info','into','iron','item','jade','jail', 'jazz','join','joke','jump','june','jury','just','keen','keep','kept','kick','kind', 'king','kite','knee','knew','knit','knot','know','lace','lack','lady','lake','lamp', 'land','lane','last','late','lava','lawn','lead','leaf','lean','left','lend','lens', ]; $n = count($wl); $bytes = random_bytes(12); $out = []; for ($i = 0; $i < 12; $i++) { $out[] = $wl[ord($bytes[$i]) % $n]; } return implode(' ', $out); } /** * Shared vault note writer — file body is caller-owned; never mixes KING/SITE/ADMIN secrets. * Each role builds its own $body; this only ensures dir + 0600 write. */ function nst_vault_write_note(string $filename, string $body): void { nst_vault_write_private_file($filename, $body); } /** * Vault note for KING: data/treasury.secret only. Never to renters/lords. * E3: distinct from trade panel site.seed (11-wallet: KING is the 11th, only minter). */ function nst_vault_king_seed_note(string $seed): void { $body = "NOSIGNUP.TRADE KING / MASTER FAUCET SEED (OWNER ONLY — HIDDEN VAULT)\n" . "Role: empire KING vault wallet. ONLY minter for THIS trade HQ.\n" . "File: data/treasury.secret · Unlocks faucet/mint (require_faucet_auth).\n" . "Also unlocks /controlpanel as owner override. Lords NEVER receive this.\n" . "King holds the whole " . NST_KING_FAUCET_START_NSU . " NSU cap at birth. No drip. NO RECOVERY.\n" . "11-wallet model: this is the 11th seed (hidden KING), not a site panel.\n" . "Generated: " . gmdate('c') . "\n\n" . trim($seed) . "\n"; nst_vault_write_note('KING_FAUCET_SEED.txt', $body); } /** @deprecated name kept: writes KING vault note (not panel). */ function nst_vault_site_seed_note(string $seed): void { nst_vault_king_seed_note($seed); } /** * Vault note for trade SITE PANEL wallet (one of 10 crop panels). Not a mint. */ function nst_vault_panel_seed_note(string $seed): void { $body = "NOSIGNUP.TRADE SITE PANEL WALLET SEED (THIS CROP ONLY)\n" . "Role: /controlpanel unlock + site operator wallet for trade.\n" . "File: data/site.seed · Does NOT mint. Lords never mint; KING alone mints.\n" . "After public rent pay: economy.operator_addr may become the LORD payer key.\n" . "Generated: " . gmdate('c') . "\n\n" . trim($seed) . "\n"; nst_vault_write_note('SITE_WALLET_SEED.txt', $body); } /** Read one published authority seed with the exact wallet grammar. */ function nst_seed_file_read_valid(string $path): string { if (is_link($path) || !is_file($path)) throw new RuntimeException('seed_file_invalid'); $raw = @file_get_contents($path); $seed = is_string($raw) ? norm_seed($raw) : ''; if (!preg_match('/\A[a-z]{1,24}(?: [a-z]{1,24}){11}\z/D', $seed)) { throw new RuntimeException('seed_file_invalid'); } return $seed; } /** Create a seed file once through a durable temp+rename publication. */ function nst_seed_file_create_once(string $path, callable $vaultNote): string { if (is_link($path) || (file_exists($path) && !is_file($path))) { throw new RuntimeException('seed_file_invalid'); } if (is_file($path)) { $seed = nst_seed_file_read_valid($path); @chmod($path, 0600); $vaultNote($seed); return $seed; } foreach ((array)(glob($path . '.init.*.tmp') ?: []) as $stale) { if (!preg_match('/\A' . preg_quote(basename($path), '/') . '\.init\.[0-9a-f]{24}\.tmp\z/D', basename($stale))) continue; $mtime = @filemtime($stale); if (is_file($stale) && !is_link($stale) && is_int($mtime) && $mtime < time() - 600) @unlink($stale); } $seed = nst_generate_site_seed(); $bytes = $seed . "\n"; $tmp = $path . '.init.' . bin2hex(random_bytes(12)) . '.tmp'; $fh = @fopen($tmp, 'x+b'); if (!is_resource($fh)) throw new RuntimeException('seed_file_unavailable'); $published = false; try { @chmod($tmp, 0600); if (!nst_stream_write_all($fh, $bytes) || !nst_stream_flush_durable($fh)) { throw new RuntimeException('seed_file_unavailable'); } @fclose($fh); $fh = null; if (file_exists($path) || is_link($path)) { $winner = nst_seed_file_read_valid($path); @chmod($path, 0600); $vaultNote($winner); return $winner; } if (!@rename($tmp, $path)) { /* Another process may have won between the existence check and * rename. Adopt only its fully valid published seed. */ if (is_file($path) && !is_link($path)) { $winner = nst_seed_file_read_valid($path); @chmod($path, 0600); $vaultNote($winner); return $winner; } throw new RuntimeException('seed_file_unavailable'); } $tmp = ''; @chmod($path, 0600); $saved = @file_get_contents($path); if (!is_string($saved) || !hash_equals($bytes, $saved)) { throw new RuntimeException('seed_file_invalid'); } $published = true; } finally { if (is_resource($fh)) @fclose($fh); if (!$published && $tmp !== '' && is_file($tmp) && !is_link($tmp)) @unlink($tmp); } $vaultNote($seed); return $seed; } /** * Ensure trade panel site.seed exists (distinct from KING treasury.secret on fresh genesis). * Do not invent a second key on pre-E3 installs that already genesi'd without site.seed — * only call from the fresh-genesis path. */ function ensure_panel_site_seed_only(): string { global $SITE_SEED_FILE; boot_data(); return nst_seed_file_create_once($SITE_SEED_FILE, 'nst_vault_panel_seed_note'); } /** * On fresh genesis: operator.addr = panel site.seed addr (≠ KING treasury). * File is public-ish addr only (not secret). Economy.operator_addr may diverge for rent later. * Legacy (no site.seed): falls back to treasury addr so pre-E3 installs keep working. */ function nst_ensure_operator_addr(?string $addr = null): string { global $OP_ADDR_FILE, $SITE_SEED_FILE, $TFILE; boot_data(); if ($addr === null || $addr === '') { if (is_file($OP_ADDR_FILE)) { $have = strtolower(trim((string)@file_get_contents($OP_ADDR_FILE))); if (preg_match('/^[a-f0-9]{64}$/', $have)) { return $have; } } // Prefer panel site.seed (E3 distinct panel wallet); else KING treasury (legacy conflation). if (isset($SITE_SEED_FILE) && is_file($SITE_SEED_FILE)) { $psec = trim((string)@file_get_contents($SITE_SEED_FILE)); if ($psec !== '') { $addr = addr_from_seed($psec); } } if (($addr === null || $addr === '') && is_file($TFILE)) { $sec = trim((string)@file_get_contents($TFILE)); if ($sec !== '') { $addr = addr_from_seed($sec); } } } if ($addr === null || $addr === '' || !preg_match('/^[a-f0-9]{64}$/', $addr)) { return ''; } $addr = strtolower($addr); if (!is_file($OP_ADDR_FILE) || trim((string)@file_get_contents($OP_ADDR_FILE)) === '') { file_put_contents($OP_ADDR_FILE, $addr . "\n", LOCK_EX); @chmod($OP_ADDR_FILE, 0644); } return $addr; } /** * Panel unlock addresses (E3): * - KING treasury seed (owner override; still not a lord mint path) * - trade panel site.seed (site admin door; no mint) * - economy.operator_addr (LORD after rent pay-bind) * Never ship treasury.secret to renters. * * @return list */ function panel_unlock_addrs(): array { $out = []; global $TFILE, $SITE_SEED_FILE; if (is_file($TFILE)) { $sec = trim((string)@file_get_contents($TFILE)); if ($sec !== '') { $out[] = addr_from_seed($sec); } } if (isset($SITE_SEED_FILE) && is_file($SITE_SEED_FILE)) { $psec = trim((string)@file_get_contents($SITE_SEED_FILE)); if ($psec !== '') { $pa = addr_from_seed($psec); if (!in_array($pa, $out, true)) { $out[] = $pa; } } } $e = load_economy(); $op = strtolower(trim((string)($e['operator_addr'] ?? ''))); if (preg_match('/^[a-f0-9]{64}$/', $op) && !in_array($op, $out, true)) { // rent path: LORD payer key unlocks panel without treasury secret $out[] = $op; } return $out; } /** True if seed derives KING, site panel, or economy.operator_addr (LORD). */ function panel_seed_ok(string $seed): bool { $seed = norm_seed($seed); if ($seed === '') { return false; } $addr = addr_from_seed($seed); foreach (panel_unlock_addrs() as $a) { if (hash_equals($a, $addr)) { return true; } } return false; } /** True if seed derives the treasury master faucet (KING) only — not LORD operator. */ function treasury_seed_ok(string $seed): bool { $seed = norm_seed($seed); if ($seed === '') { return false; } global $TFILE; if (!is_file($TFILE)) { return false; } $sec = trim((string)@file_get_contents($TFILE)); if ($sec === '') { return false; } return hash_equals(addr_from_seed($sec), addr_from_seed($seed)); } /** * Faucet / mint authority (KING): treasury seed ONLY. * LORD operator seed never unlocks faucet, treasury reveal, free-form pay, rent claim, * or profit extract. Leftover admin.pass.hash is unlinked; require_admin is site seed only * and MUST NOT mint — a renter must never escalate to faucet (isolation hole closed 2026-07-15). */ function require_faucet_auth(): void { $seed = (string)($_POST['seed'] ?? ''); if ($seed !== '' && treasury_seed_ok($seed)) { return; } api_out(['ok' => false, 'err' => 'faucet auth'], 401); } /** * DEAD — former market-value LORD rent GRANT compute (treasury → LORD). * Kept only so old call sites / tests that reference the symbol do not fatals until full prune. * Product rent must NOT call this for live settlement (admin_rent_claim is 410). * @param string $treasuryMicros remaining treasury balance (micros) * @param string $lastProfitMicros prior epoch profit pile at wipe (micros), or '0' * @return array{ok:bool,micros?:string,credit_nsu?:string,parts?:array,err?:string} */ function nst_lord_rent_credit_compute(string $treasuryMicros, string $lastProfitMicros = '0'): array { $treasuryMicros = dec_norm($treasuryMicros); $lastProfitMicros = dec_norm($lastProfitMicros); if (!dec_ok($treasuryMicros) || !dec_ok($lastProfitMicros)) { return ['ok' => false, 'err' => 'Amount must be a positive number (at least 0.000001 NSU)']; } // percent of remaining faucet $pctPart = '0'; if (LORD_RENT_PCT_BPS > 0 && dec_cmp($treasuryMicros, '0') > 0) { $pctPart = dec_div_small(dec_mul_small($treasuryMicros, (int)LORD_RENT_PCT_BPS), 10000); } $flatBase = parse_amt((string)(int)LORD_RENT_FLAT_BASE_NSU); if ($flatBase === null) { return ['ok' => false, 'err' => 'flat base invalid']; } $perfPart = '0'; if (LORD_RENT_FLAT_PERF_BPS > 0 && dec_cmp($lastProfitMicros, '0') > 0) { $perfPart = dec_div_small(dec_mul_small($lastProfitMicros, (int)LORD_RENT_FLAT_PERF_BPS), 10000); $cap = parse_amt((string)(int)LORD_RENT_FLAT_PERF_CAP_NSU); if ($cap !== null && dec_cmp($perfPart, $cap) > 0) { $perfPart = $cap; } } $total = dec_add(dec_add($pctPart, $flatBase), $perfPart); $minM = parse_amt((string)(int)LORD_RENT_MIN_NSU); $maxM = parse_amt((string)(int)LORD_RENT_MAX_NSU); if ($minM !== null && dec_cmp($total, $minM) < 0) { $total = $minM; } if ($maxM !== null && dec_cmp($total, $maxM) > 0) { $total = $maxM; } if (dec_cmp($total, '1') < 0) { return ['ok' => false, 'err' => 'credit too small']; } // whole NSU display (floor micros / 1e6) $creditNsu = dec_div_small($total, (int)str_pad('1', NST_DECIMALS + 1, '0')); return [ 'ok' => true, 'micros' => $total, 'credit_nsu' => $creditNsu, 'parts' => [ 'pct_bps' => LORD_RENT_PCT_BPS, 'pct_micros' => $pctPart, 'pct_fmt' => fmt_amt($pctPart), 'flat_base_nsu' => LORD_RENT_FLAT_BASE_NSU, 'flat_base_fmt' => fmt_amt($flatBase), 'flat_perf_bps' => LORD_RENT_FLAT_PERF_BPS, 'flat_perf_micros' => $perfPart, 'flat_perf_fmt' => fmt_amt($perfPart), 'last_profit_fmt' => fmt_amt($lastProfitMicros), 'treasury_fmt' => fmt_amt($treasuryMicros), 'min_nsu' => LORD_RENT_MIN_NSU, 'max_nsu' => LORD_RENT_MAX_NSU, 'legacy_flat_placeholder' => LORD_RENT_CREDIT_LEGACY, 'faucet_start_nsu_narrative' => NST_KING_FAUCET_START_NSU, ], ]; } /** @deprecated prefer nst_lord_rent_credit_compute with live treasury */ function nst_lord_rent_credit_micros(): ?string { $c = nst_lord_rent_credit_compute('0', '0'); return !empty($c['ok']) ? (string)$c['micros'] : null; } /** Ensure KING treasury.secret exists without double-genesis. New installs get 12-word KING seed. */ function ensure_genesis_secret_only(): void { global $TFILE; boot_data(); nst_seed_file_create_once($TFILE, 'nst_vault_site_seed_note'); } function fill_amounts(array $rows): array { $filled = []; foreach ($rows as $r) { if (($r['type'] ?? '') === 'fill' && !empty($r['order_id'])) { $filled[$r['order_id']] = dec_add($filled[$r['order_id']] ?? '0', $r['amount'] ?? '0'); } } return $filled; } function open_orders(array $rows): array { $live = []; $cancelled = []; $filled = fill_amounts($rows); foreach ($rows as $r) { $t = $r['type'] ?? ''; if ($t === 'order') $live[$r['id']] = $r; if ($t === 'cancel' && !empty($r['order_id'])) $cancelled[$r['order_id']] = true; } $now = time(); $out = []; foreach ($live as $id => $o) { if (!empty($cancelled[$id])) continue; if (($o['expiry'] ?? 0) < $now) continue; $rem = dec_sub($o['amount'] ?? '0', $filled[$id] ?? '0'); if (dec_cmp($rem, '1') < 0) continue; $o['remaining'] = $rem; $o['remaining_fmt'] = fmt_amt($rem); $out[] = $o; } usort($out, function ($a, $b) { $c = dec_cmp($a['price'] ?? '0', $b['price'] ?? '0'); if (($a['side'] ?? '') === 'buy') $c = -$c; // buys high first if ($c !== 0) return $c; return ($a['ts'] ?? 0) <=> ($b['ts'] ?? 0); }); return $out; } /** SA-05: true only for an exact NSU base asset (NSU/); NSUX/LTC or bare NSU is not NSU. */ function nst_pair_is_nsu_base(string $pair): bool { return (bool)preg_match('#\ANSU/(?!NSU\z)[A-Z0-9]{2,12}\z#D', strtoupper(trim($pair))); } /** NSU locked in open sell orders (exact NSU/ pairs) */ function reserved_nsu(array $rows, string $addr): string { $sum = '0'; foreach (open_orders($rows) as $o) { if (($o['addr'] ?? '') !== $addr) continue; if (($o['side'] ?? '') !== 'sell') continue; $pair = strtoupper($o['pair'] ?? ''); if (!nst_pair_is_nsu_base($pair)) continue; $sum = dec_add($sum, $o['remaining'] ?? '0'); } foreach (nst_job_grants($rows) as $g) { if ($g['sponsor'] === $addr) $sum = dec_add($sum, $g['reserved']); } return $sum; } /** * Funded-job state replayed from the chain. Rows: job_grant (sponsor reserves own coins), * job_admit (hold for one job inside a grant), job_pay (accept + pay + receipt, one row), * job_cancel (job_id: release one unpaid hold to the grant; no job_id + amount: release that much unheld grant to the sponsor; * no job_id + amount 0: close the grant and release all unheld remainder). * reserved = amount - paid - released (sponsor-side); free = reserved - held. * Invalid or out-of-order rows are skipped (writers validate under the chain lock first). */ function nst_job_grants(array $rows): array { $g = []; foreach ($rows as $r) { if (!is_array($r)) continue; $t = (string)($r['type'] ?? ''); if ($t !== 'job_grant' && $t !== 'job_admit' && $t !== 'job_pay' && $t !== 'job_cancel') continue; $id = (string)($r['grant_id'] ?? ''); if ($t === 'job_grant') { $m = (string)($r['amount'] ?? ''); if ($id === '' || isset($g[$id]) || !dec_ok($m) || dec_cmp($m, '0') <= 0) continue; $g[$id] = ['grant_id' => $id, 'sponsor' => (string)($r['from'] ?? ''), 'amount' => $m, 'paid' => '0', 'released' => '0', 'held' => '0', 'closed' => false, 'jobs' => [], 'reserved' => $m, 'free' => $m]; continue; } if (!isset($g[$id]) || (string)($r['from'] ?? '') !== $g[$id]['sponsor']) continue; $G =& $g[$id]; $job = (string)($r['job_id'] ?? ''); if ($t === 'job_admit') { $m = (string)($r['amount'] ?? ''); if ($G['closed'] || $job === '' || isset($G['jobs'][$job]) || !dec_ok($m) || dec_cmp($m, '0') <= 0) { unset($G); continue; } if (dec_cmp($m, dec_sub($G['reserved'], $G['held'])) > 0) { unset($G); continue; } $G['jobs'][$job] = ['worker' => (string)($r['to'] ?? ''), 'amount' => $m, 'state' => 'held', 'receipt' => null, 'nonce' => (string)($r['nonce'] ?? '')]; $G['held'] = dec_add($G['held'], $m); } elseif ($t === 'job_pay') { $J = $G['jobs'][$job] ?? null; if ($J === null || $J['state'] !== 'held' || (string)($r['amount'] ?? '') !== $J['amount'] || (string)($r['to'] ?? '') !== $J['worker']) { unset($G); continue; } $G['jobs'][$job]['state'] = 'paid'; $G['jobs'][$job]['receipt'] = (string)($r['receipt_id'] ?? ''); $G['held'] = dec_sub($G['held'], $J['amount']); $G['paid'] = dec_add($G['paid'], $J['amount']); } elseif ($t === 'job_cancel') { if ($job !== '') { $J = $G['jobs'][$job] ?? null; if ($J === null || $J['state'] !== 'held') { unset($G); continue; } $G['jobs'][$job]['state'] = 'cancelled'; $G['held'] = dec_sub($G['held'], $J['amount']); } elseif ($G['closed']) { unset($G); continue; } else { $m = (string)($r['amount'] ?? '0'); $free = dec_sub($G['reserved'], $G['held']); if (!dec_ok($m) || dec_cmp($m, $free) > 0) { unset($G); continue; } if (dec_cmp($m, '0') > 0) { $G['released'] = dec_add($G['released'], $m); /* partial release, grant stays open */ } else { $G['released'] = dec_add($G['released'], $free); $G['closed'] = true; } } } $G['reserved'] = dec_sub(dec_sub($G['amount'], $G['paid']), $G['released']); $G['free'] = $G['closed'] ? '0' : dec_sub($G['reserved'], $G['held']); if ($G['closed']) { /* A closed grant keeps only what unpaid holds still claim. */ $G['released'] = dec_add($G['released'], dec_sub($G['reserved'], $G['held'])); $G['reserved'] = $G['held']; } unset($G); } return $g; } function nst_job_receipt_id(string $grantId, string $jobId, string $worker, string $micros, string $digest): string { return hash('sha256', 'nsu-job-receipt-v1|' . $grantId . '|' . $jobId . '|' . $worker . '|' . $micros . '|' . $digest); } /** Public receipt view of one job_pay row (or null). Read-only; no lock needed. */ /* ---- TRADE/10: moot money gate (enforcement only; governance is INFO/10.php, decision A) ---- * Trade does NOT run a liveness clock or succession. It reads ONE envelope that INFO signs (interface: * notes/MOOT_STATE_INTERFACE.md), validates it against operator-pinned local trust config, and refuses NEW hive * work (job_admit, which is also the only path that opens a grant) unless the result is LIVE. Old obligations * (job_complete of a held job, job_cancel/release), receipts, free use and history are never gated (decision E). * Everything not provably LIVE fails closed: missing/corrupt/bad-sig/foreign/future/stale/rolled-back = UNKNOWN. */ const NST_MOOT_SECS = 604800; /* decision E boundary, re-derived by Trade from liveness_last_ts */ const NST_MOOT_SKEW = 600; const NST_MOOT_MAX_VALID = 86400; /* an envelope may promise at most 24 h */ const NST_MOOT_FIELDS = ['economy', 'mirror', 'face', 'epoch', 'state', 'liveness_last_ts', 'liveness_anchor', 'seat_map', 'succession', 'issued_ts', 'valid_until']; /** Read exactly one local moot record; reject links/non-files and cap actual bytes before JSON decoding. */ function nst_moot_read(string $path, int $cap): ?string { clearstatcache(true, $path); if (is_link($path) || !is_file($path)) return null; $raw = @file_get_contents($path, false, null, 0, $cap + 1); return is_string($raw) && strlen($raw) <= $cap ? $raw : null; } /** Operator-written local trust config (never taken from the envelope). null = missing/corrupt. */ function nst_moot_trust(): ?array { $p = nst_data_file('moot_trust.json'); if (!is_file($p)) return null; $j = json_decode(nst_moot_read($p, 16384) ?? '', true); if (!is_array($j) || ($j['schema'] ?? '') !== 'nsu.trade-moot-trust.v1') return null; foreach (['economy', 'mirror', 'info_state_spki_sha256'] as $k) if (!preg_match('/\A[0-9a-f]{64}\z/D', (string)($j[$k] ?? ''))) return null; $path = (string)($j['state_path'] ?? ''); if ($path === '') $path = nst_data_file('moot_state.json'); elseif ($path[0] !== '/' && !preg_match('/\A[A-Za-z]:[\\\\\/]/', $path)) $path = nst_data_file($path); return ['economy' => $j['economy'], 'mirror' => $j['mirror'], 'key_sha' => $j['info_state_spki_sha256'], 'path' => $path]; } /** Strict parse of the signed body: exact header, field order, LF terminator, canonical ints, lowercase hex. */ function nst_moot_parse_body(string $body): ?array { if ($body === '' || substr($body, -1) !== "\n" || strpos($body, "\r") !== false) return null; $lines = explode("\n", substr($body, 0, -1)); if (array_shift($lines) !== 'NSU-MOOT-STATE v1' || count($lines) !== count(NST_MOOT_FIELDS)) return null; $o = []; foreach (NST_MOOT_FIELDS as $i => $k) { if (strpos($lines[$i], $k . '=') !== 0) return null; $o[$k] = substr($lines[$i], strlen($k) + 1); } foreach (['economy', 'mirror', 'liveness_anchor', 'seat_map'] as $k) if (!preg_match('/\A[0-9a-f]{64}\z/D', $o[$k])) return null; if ($o['succession'] !== 'none' && !preg_match('/\A[0-9a-f]{64}\z/D', $o['succession'])) return null; foreach (['epoch', 'liveness_last_ts', 'issued_ts', 'valid_until'] as $k) { if (!preg_match('/\A(0|[1-9][0-9]{0,11})\z/D', $o[$k])) return null; $o[$k] = (int)$o[$k]; } if ($o['face'] !== 'info' || !in_array($o['state'], ['LIVE', 'MOOT', 'UNKNOWN'], true)) return null; return $o; } /** Pure evaluator. $env = raw file bytes or null; $seen = persisted floor or null (none yet); false = corrupt floor. */ function nst_moot_gate_eval(?array $trust, ?string $env, $seen, int $now): array { $u = function (string $why, array $x = []) { return ['state' => 'UNKNOWN', 'why' => $why] + $x; }; if ($trust === null) return $u('trust_config_missing_or_corrupt'); if ($seen === false) return $u('seen_floor_corrupt'); if ($env === null) return $u('state_missing'); $j = json_decode($env, true); if (!is_array($j) || array_keys($j) !== ['body', 'key', 'sig']) return $u('envelope_corrupt'); $body = base64_decode((string)$j['body'], true); $spki = base64_decode((string)$j['key'], true); $der = preg_match('/\A[0-9a-f]{16,288}\z/D', (string)$j['sig']) ? hex2bin((string)$j['sig']) : false; if ($body === false || $spki === false || $der === false) return $u('envelope_corrupt'); if (!hash_equals($trust['key_sha'], hash('sha256', $spki))) return $u('signer_not_pinned'); if (!function_exists('openssl_verify')) return $u('no_openssl'); $pub = @openssl_pkey_get_public("-----BEGIN PUBLIC KEY-----\n" . chunk_split(base64_encode($spki), 64, "\n") . "-----END PUBLIC KEY-----\n"); if ($pub === false || openssl_verify($body, $der, $pub, OPENSSL_ALGO_SHA256) !== 1) return $u('bad_signature'); $b = nst_moot_parse_body($body); if ($b === null) return $u('body_not_canonical'); if ($b['economy'] !== $trust['economy'] || $b['mirror'] !== $trust['mirror']) return $u('foreign_economy_or_mirror'); $x = ['epoch' => $b['epoch'], 'issued_ts' => $b['issued_ts'], 'liveness_last_ts' => $b['liveness_last_ts'], 'digest' => hash('sha256', $body)]; if (is_array($seen)) { if ($now < $seen['now']) return $u('clock_rollback', $x); if ($b['epoch'] < $seen['epoch'] || ($b['epoch'] === $seen['epoch'] && $b['issued_ts'] < $seen['issued_ts'])) return $u('state_rollback', $x); } if ($b['issued_ts'] > $now + NST_MOOT_SKEW || $b['liveness_last_ts'] > $now + NST_MOOT_SKEW || $b['liveness_last_ts'] > $b['issued_ts'] + NST_MOOT_SKEW) return $u('future_dated', $x); if ($b['valid_until'] <= $b['issued_ts'] || $b['valid_until'] - $b['issued_ts'] > NST_MOOT_MAX_VALID) return $u('bad_validity_window', $x); if ($now >= $b['valid_until']) return $u('stale', $x); if ($b['state'] === 'UNKNOWN') return ['state' => 'UNKNOWN', 'why' => 'info_says_unknown'] + $x; $x['age'] = $now - $b['liveness_last_ts']; if ($x['age'] >= NST_MOOT_SECS) return ['state' => 'MOOT', 'why' => 'no_liveness_604800'] + $x; if ($b['state'] !== 'LIVE') return ['state' => $b['state'], 'why' => 'info_says_' . strtolower($b['state'])] + $x; return ['state' => 'LIVE', 'why' => 'ok'] + $x; } function nst_moot_seen_read() { $p = nst_data_file('moot_seen.json'); if (!file_exists($p) && !is_link($p)) return null; $j = json_decode(nst_moot_read($p, 1024) ?? '', true); if (!is_array($j) || !is_int($j['epoch'] ?? null) || !is_int($j['issued_ts'] ?? null) || !is_int($j['now'] ?? null)) return false; return $j; } /** Called under the existing chain lock: persist the floor before a new ledger admission. */ function nst_moot_seen_write(array $floor): bool { $p = nst_data_file('moot_seen.json'); if (is_link($p) || (file_exists($p) && !is_file($p))) return false; $raw = json_encode($floor); if (!is_string($raw) || strlen($raw) > 1023) return false; $raw .= "\n"; $tmp = @tempnam(dirname($p), '.moot-seen-'); if (!is_string($tmp)) return false; try { if (realpath(dirname($tmp)) !== realpath(dirname($p))) return false; @chmod($tmp, 0600); if (@file_put_contents($tmp, $raw, LOCK_EX) !== strlen($raw)) return false; return @rename($tmp, $p); } finally { if (is_file($tmp)) @unlink($tmp); } } /** On-touch evaluation (no sweep/cron). Advances the persisted floor only from a signature-valid envelope. */ function nst_moot_gate_touch(?int $now = null): array { $now = $now ?? time(); $trust = nst_moot_trust(); $env = $trust !== null ? nst_moot_read($trust['path'], 16384) : null; if ($env === false) $env = null; $seen = nst_moot_seen_read(); $ev = nst_moot_gate_eval($trust, $env, $seen, $now); if ($seen !== false && isset($ev['epoch']) && !in_array($ev['why'], ['clock_rollback', 'state_rollback', 'future_dated', 'bad_validity_window'], true)) { $s = is_array($seen) ? $seen : ['epoch' => 0, 'issued_ts' => 0, 'now' => 0]; $n = ['epoch' => max($s['epoch'], $ev['epoch']), 'issued_ts' => $ev['epoch'] > $s['epoch'] ? $ev['issued_ts'] : max($s['issued_ts'], $ev['issued_ts']), 'now' => max($s['now'], $now)]; if ($n !== $s && !nst_moot_seen_write($n)) return ['state' => 'UNKNOWN', 'why' => 'floor_write_failed']; } return $ev; } /* ---- end TRADE/10 moot money gate ---- */ /** Read-only status hook (GET ?api=moot_gate): same evaluation, never advances the floor. */ function nst_moot_gate_touch_readonly(): array { $trust = nst_moot_trust(); $env = $trust !== null ? nst_moot_read($trust['path'], 16384) : null; return nst_moot_gate_eval($trust, $env === false ? null : $env, nst_moot_seen_read(), time()); } function nst_job_receipt_lookup(array $rows, string $grantId, string $jobId): ?array { foreach ($rows as $i => $r) { if (is_array($r) && ($r['type'] ?? '') === 'job_pay' && (string)($r['grant_id'] ?? '') === $grantId && (string)($r['job_id'] ?? '') === $jobId) { $g = nst_job_grants(array_slice($rows, 0, $i + 1)); if (($g[$grantId]['jobs'][$jobId]['receipt'] ?? null) !== (string)($r['receipt_id'] ?? '')) continue; return ['receipt_id' => (string)$r['receipt_id'], 'grant_id' => $grantId, 'job_id' => $jobId, 'from' => (string)$r['from'], 'to' => (string)$r['to'], 'amount' => (string)$r['amount'], 'amount_fmt' => fmt_amt((string)$r['amount']), 'result_digest' => (string)($r['result_digest'] ?? ''), 'ts' => (int)($r['ts'] ?? 0), 'chain_index' => (int)$i, 'row_sha256' => hash('sha256', j($r))]; } } return null; } function available_nsu(array $bal, array $rows, string $addr): string { $have = $bal[$addr] ?? '0'; $res = reserved_nsu($rows, $addr); if (dec_cmp($have, $res) <= 0) return '0'; $av = dec_sub($have, $res); return dec_cmp($av, '0') < 0 ? '0' : $av; } function rep_scores(array $rows): array { // latest edge per (rater,rated) $edges = []; foreach ($rows as $r) { if (($r['type'] ?? '') !== 'reputation') continue; $k = ($r['rater'] ?? '') . '>' . ($r['rated'] ?? ''); $prev = $edges[$k] ?? null; if (!$prev || ($r['ts'] ?? 0) >= ($prev['ts'] ?? 0)) $edges[$k] = $r; } $score = []; foreach ($edges as $e) { $rated = $e['rated'] ?? ''; $s = (int)($e['score'] ?? 0); if ($rated === '' || $s < -10 || $s > 10) continue; $score[$rated] = ($score[$rated] ?? 0) + $s; } return $score; } /* ---- meta / epochs / modifiers / admin (data/*.txt inaccessible via .htaccess) ---- */ /* META-RACE: missing vs unreadable. A torn or corrupt meta.json is never * treated as empty: one brief retry, then 'unreadable' and callers skip writes. */ function nst_meta_read(): array { global $META; if (!is_file($META)) return ['status' => 'missing', 'meta' => []]; for ($try = 0; $try < 2; $try++) { if ($try > 0) usleep(30000); $raw = @file_get_contents($META); if (is_string($raw) && $raw !== '') { $j = json_decode($raw, true); if (is_array($j)) return ['status' => 'ok', 'meta' => $j]; } } return ['status' => 'unreadable', 'meta' => []]; } function load_meta(): array { $r = nst_meta_read(); $GLOBALS['NST_META_UNREADABLE'] = ($r['status'] === 'unreadable'); return $r['meta']; } function save_meta(array $m): void { global $META; /* The last load_meta() on this request saw unreadable meta: never overwrite it. */ if (!empty($GLOBALS['NST_META_UNREADABLE'])) return; $raw = j($m); /* Atomic replace: readers see the old or the new file, never a truncated one. */ $tmp = dirname($META) . DIRECTORY_SEPARATOR . '.meta.json.' . getmypid() . '.' . bin2hex(random_bytes(6)) . '.tmp'; if (@file_put_contents($tmp, $raw, LOCK_EX) === strlen($raw)) { $perm = @fileperms($META); if ($perm !== false) @chmod($tmp, $perm & 0777); if (@rename($tmp, $META)) return; } @unlink($tmp); file_put_contents($META, $raw, LOCK_EX); } /** Site-local economy: profit pile + ad bid stake (not empire-wide). */ function load_economy_result(): array { // WAVEA-TR04-TYPED-READS global $ECON; $def = [ 'profit_micros' => '0', 'ad_bid_micros' => '0', 'donate_k' => NST_DONATE_K, 'value_note' => '', 'bids' => [], 'donations' => [], ]; if (!is_file($ECON)) { $init = nst_data_file('economy.init.json'); if (is_file($init)) { return ['status' => 'missing-established', 'payload' => null, 'err' => 'economy missing after prior init']; } return ['status' => 'missing-new-install', 'payload' => null, 'err' => 'economy missing']; } $raw = @file_get_contents($ECON); if ($raw === false) { return ['status' => 'unreadable', 'payload' => null, 'err' => 'economy unreadable']; } if (trim((string)$raw) === '') { return ['status' => 'corrupt', 'payload' => null, 'err' => 'economy empty file']; } $j = json_decode((string)$raw, true); if (!is_array($j) || nst_is_list_compat($j)) { return ['status' => 'corrupt', 'payload' => null, 'err' => 'economy not object']; } return ['status' => 'valid-current', 'payload' => array_merge($def, $j), 'err' => '']; } function load_economy(): array { $r = load_economy_result(); if (($r['status'] ?? '') === 'valid-current' && is_array($r['payload'] ?? null)) { return $r['payload']; } $def = [ 'profit_micros' => '0', 'ad_bid_micros' => '0', 'donate_k' => NST_DONATE_K, 'value_note' => '', 'bids' => [], 'donations' => [], '_read_status' => (string)($r['status'] ?? 'unknown'), '_read_err' => (string)($r['err'] ?? ''), '_inventory_unavailable' => true, ]; return $def; } function save_economy(array $e): void { global $ECON; file_put_contents($ECON, j($e), LOCK_EX); } function ads_dir(): string { global $ADS; if (!is_dir($ADS)) @mkdir($ADS, 0755, true); $ht = $ADS . DIRECTORY_SEPARATOR . '.htaccess'; // creatives served only via ?api=ad_img - deny direct if under web (belt) if (!is_file($ht)) @file_put_contents($ht, "Require all denied\nDeny from all\n"); return $ADS; } /* LOCKED AD MODEL 2026-08-27 — 10000 slots/site; images only on trade disk. */ function nst_ad_slot_sites(): array { return array_keys(nst_ad_site_catalog()); } function nst_ad_slot_norm_n($raw): ?int { if (is_int($raw)) { $n = $raw; } else { $s = trim((string)$raw); if ($s === '' || !preg_match('/^\d{1,4}$/', $s)) return null; $n = (int)$s; } if ($n < 0 || $n > 9999) return null; return $n; } function nst_ad_slot_pad(int $n): string { return str_pad((string)$n, 4, '0', STR_PAD_LEFT); } function nst_ad_slot_shard(int $n): string { return substr(nst_ad_slot_pad($n), 0, 2); } function nst_ad_public_url(string $site, int $n): string { return nsu_trade_origin() . '/' . $site . '/' . nst_ad_slot_pad($n); } function nst_ad_slot_dir(string $site, int $n, bool $create = false): string { $root = ads_dir() . DIRECTORY_SEPARATOR . $site . DIRECTORY_SEPARATOR . nst_ad_slot_shard($n); if ($create && !is_dir($root)) @mkdir($root, 0755, true); return $root; } function nst_ad_slot_candidates(string $site, int $n): array { $pad = nst_ad_slot_pad($n); $dir = nst_ad_slot_dir($site, $n, false); $root = ads_dir(); $exts = ['png','jpg','jpeg','webp','gif']; $out = []; foreach ($exts as $ext) { $out[] = $dir . DIRECTORY_SEPARATOR . $n . '.' . $ext; $out[] = $dir . DIRECTORY_SEPARATOR . $pad . '.' . $ext; $out[] = $root . DIRECTORY_SEPARATOR . $site . DIRECTORY_SEPARATOR . $n . '.' . $ext; $out[] = $root . DIRECTORY_SEPARATOR . $site . DIRECTORY_SEPARATOR . $pad . '.' . $ext; $out[] = $root . DIRECTORY_SEPARATOR . $n . '.' . $ext; $out[] = $root . DIRECTORY_SEPARATOR . $pad . '.' . $ext; } return $out; } function nst_ad_slot_find(string $site, int $n): ?string { foreach (nst_ad_slot_candidates($site, $n) as $p) { if (is_file($p) && filesize($p) > 8) return $p; } return null; } function nst_ad_placeholder_path(): string { return ads_dir() . DIRECTORY_SEPARATOR . '_placeholder.png'; } function nst_ad_placeholder_bin(): string { static $bin = null; if ($bin !== null) return $bin; $b64 = nst_ad_placeholder_b64(); $bin = base64_decode($b64, true); return is_string($bin) ? $bin : ''; } function nst_ad_placeholder_ensure(): string { $p = nst_ad_placeholder_path(); $bin = nst_ad_placeholder_bin(); $need = false; if ($bin === '') { return $p; } if (!is_file($p) || (int)filesize($p) !== strlen($bin)) { $need = true; } else { $have = (string)@file_get_contents($p); if (!hash_equals(hash('sha256', $have), hash('sha256', $bin))) { $need = true; } } if ($need) { @file_put_contents($p, $bin, LOCK_EX); @chmod($p, 0644); } return $p; } function nst_ad_index_path(): string { return nst_data_file('ad.slots.json'); } function nst_ad_index_empty(): array { $sites = []; foreach (nst_ad_slot_sites() as $tld) $sites[$tld] = []; return ['epoch_n' => (int)(nst_epoch_info_read_only()['epoch_n'] ?? 1), 'sites' => $sites]; } function nst_ad_index_load_result(): array { // WAVEA-TR04-TYPED-READS $empty = nst_ad_index_empty(); $path = nst_ad_index_path(); if (!is_file($path)) { $init = nst_data_file('ad.slots.init.json'); if (is_file($init)) { return ['status' => 'missing-established', 'payload' => null, 'err' => 'ad index missing after prior init']; } return ['status' => 'missing-new-install', 'payload' => null, 'err' => 'ad index missing']; } $raw = @file_get_contents($path); if ($raw === false) { return ['status' => 'unreadable', 'payload' => null, 'err' => 'ad index unreadable']; } if (trim((string)$raw) === '') { return ['status' => 'corrupt', 'payload' => null, 'err' => 'ad index empty file']; } $j = json_decode((string)$raw, true); if (!is_array($j)) { return ['status' => 'corrupt', 'payload' => null, 'err' => 'ad index not json object/array']; } if (nst_is_list_compat($j) || !array_key_exists('epoch_n', $j) || !array_key_exists('sites', $j) || !is_array($j['sites'])) { return ['status' => 'corrupt', 'payload' => null, 'err' => 'ad index wrong shape']; } $epoch = (int)($j['epoch_n'] ?? 0); $cur = (int)$empty['epoch_n']; if ($epoch !== $cur) { return ['status' => 'valid-old-epoch', 'payload' => $j, 'err' => 'ad index epoch mismatch', 'epoch_n' => $epoch, 'current_epoch_n' => $cur]; } $out = $empty; foreach ($out['sites'] as $tld => $_) { $row = $j['sites'][$tld] ?? []; if (!is_array($row)) continue; foreach ($row as $k => $meta) { $n = nst_ad_slot_norm_n($k); if ($n === null) continue; $out['sites'][$tld][(string)$n] = is_array($meta) ? $meta : ['ext' => 'png']; } } return ['status' => 'valid-current', 'payload' => $out, 'err' => '']; } function nst_ad_index_load(): array { // Compatibility wrapper: NEVER treat damaged/old/missing as free inventory. // TR-RESET: a prior-epoch owner is still that owner. valid-old-epoch returns // those rows. It is not replaced with an empty current epoch. $r = nst_ad_index_load_result(); $status = (string)($r['status'] ?? ''); if ($status === 'valid-current' && is_array($r['payload'] ?? null)) { return $r['payload']; } if ($status === 'valid-old-epoch' && is_array($r['payload'] ?? null)) { $j = $r['payload']; $epoch = (int)($j['epoch_n'] ?? 0); $sites = []; foreach (nst_ad_slot_sites() as $tld) $sites[$tld] = []; $srcSites = $j['sites'] ?? []; if (is_array($srcSites)) { foreach ($sites as $tld => $_) { $row = $srcSites[$tld] ?? []; if (!is_array($row)) continue; foreach ($row as $k => $meta) { $n = nst_ad_slot_norm_n($k); if ($n === null) continue; $sites[$tld][(string)$n] = is_array($meta) ? $meta : ['ext' => 'png']; } } } return ['epoch_n' => ($epoch > 0 ? $epoch : 1), 'sites' => $sites]; } // Sentinel empty sites but mark via synthetic meta key for call sites that only check sold counts. // Public advertisers must use nst_ad_index_load_result() status; payload null => unavailable. $empty = nst_ad_index_empty(); $empty['_read_status'] = $status !== '' ? $status : 'unknown'; $empty['_read_err'] = (string)($r['err'] ?? ''); $empty['_inventory_unavailable'] = true; return $empty; } function nst_ad_index_save(array $idx): bool { // WAVEC-TR19-INDEX-ATOMIC: temp+verify+rename when helper present. if (function_exists('nst_ad_index_publish_atomic')) { if (!isset($idx['schema'])) $idx['schema'] = function_exists('nst_ad_index_schema_ver') ? nst_ad_index_schema_ver() : 'nsu-ad-index-v2'; if (empty($idx['watermark'])) $idx['watermark'] = hash('sha256', j($idx)); $r = nst_ad_index_publish_atomic($idx); return !empty($r['ok']); } $path = nst_ad_index_path(); $ret = nst_with_file_lock($path, static function ($fh) use ($idx, $path) { rewind($fh); ftruncate($fh, 0); fwrite($fh, j($idx)); fflush($fh); @chmod($path, 0600); return true; }); return $ret === true; } function nst_ad_slot_sold_map(string $site): array { $site = nst_ad_site_norm($site); $idx = nst_ad_index_load(); $map = []; foreach (($idx['sites'][$site] ?? []) as $k => $meta) { $n = nst_ad_slot_norm_n($k); if ($n !== null) $map[$n] = true; } $root = ads_dir() . DIRECTORY_SEPARATOR . $site; if (is_dir($root)) { $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS)); foreach ($it as $f) { if (!$f->isFile()) continue; $name = $f->getBasename(); if (!preg_match('/^(\d{1,4})\.(png|jpe?g|webp|gif)$/i', $name, $m)) continue; $n = nst_ad_slot_norm_n($m[1]); if ($n !== null) $map[$n] = true; } } return $map; } function nst_ad_slot_sold_count(string $site): int { $n = count(nst_ad_slot_sold_map($site)); return $n > 10000 ? 10000 : $n; } function nst_ad_slot_assign(string $site): ?int { $sold = nst_ad_slot_sold_map($site); $free = []; for ($i = 0; $i < 10000; $i++) { if (!isset($sold[$i])) $free[] = $i; } if (!$free) return null; return $free[random_int(0, count($free) - 1)]; } /** Union of sold roll numbers across all ten faces. */ function nst_ad_empire_sold_union(): array { $sold = []; foreach (array_keys(nst_ad_site_catalog()) as $tld) { foreach (nst_ad_slot_sold_map($tld) as $n => $_) { $sold[(int)$n] = true; } } return $sold; } function nst_ad_empire_remain(): int { $n = 10000 - count(nst_ad_empire_sold_union()); return $n < 0 ? 0 : $n; } /** One roll number free on every face, so one buy can propagate. */ function nst_ad_slot_assign_empire(): ?int { $sold = nst_ad_empire_sold_union(); $free = []; for ($i = 0; $i < 10000; $i++) { if (!isset($sold[$i])) $free[] = $i; } if (!$free) return null; return $free[random_int(0, count($free) - 1)]; } function nst_ad_write_empire(int $n, string $bin, string $ext, string $owner): ?string { // TR-EMPIRE: none of the faces are written when any face is owned by a // different wallet. That check runs before the write loop, so a taken face // cannot leave earlier free faces with this owner. When every face is free // or already this owner, the existing loop still commits each face. $actor = strtolower(preg_replace('/[^a-f0-9]/', '', $owner) ?? ''); if (strlen($actor) !== 64) return null; $sites = array_keys(nst_ad_site_catalog()); foreach ($sites as $tld) { $existing = nst_ad_index_owner_of($tld, $n); if ($existing !== '' && !hash_equals($existing, $actor)) return null; } $last = null; foreach ($sites as $tld) { $saved = nst_ad_write_slot($tld, $n, $bin, $ext, $actor); if ($saved === null) return null; nst_ad_mark_sold($tld, $n, $ext, $actor); $last = $saved; } return $last; } function nst_ad_slot_days_left(): int { $ep = nst_epoch_info_read_only(); $secs = (int)($ep['secs_to_reset'] ?? 0); return (int)max(1, (int)ceil($secs / 86400)); } function nst_ad_slot_price_nsu(): int { return (int)NST_RENT_NSU_PER_DAY * nst_ad_slot_days_left(); } function nst_ad_parse_public_path(): ?array { $cands = []; foreach (['PATH_INFO', 'ORIG_PATH_INFO'] as $k) $cands[] = (string)($_SERVER[$k] ?? ''); $uri = (string)($_SERVER['REQUEST_URI'] ?? ''); $path = (string)(@parse_url($uri, PHP_URL_PATH) ?? ''); $cands[] = $path; $script = str_replace('\\', '/', (string)($_SERVER['SCRIPT_NAME'] ?? '')); if ($script !== '' && $path !== '' && str_starts_with(str_replace('\\', '/', $path), $script)) { $cands[] = substr(str_replace('\\', '/', $path), strlen($script)); } $sites = implode('|', nst_ad_slot_sites()); foreach ($cands as $raw) { $raw = str_replace('\\', '/', (string)$raw); if ($raw === '') continue; if (preg_match('#(?:^|/)(' . $sites . ')/(\d{1,4})/?$#', $raw, $m)) { $n = nst_ad_slot_norm_n($m[2]); if ($n === null) continue; return ['site' => $m[1], 'n' => $n]; } } return null; } /* NS_HOUSE_PER_FACE_DIMS 2026-09-26 — per-face house WxH; GD + cache ads/_house/{site}/{pad}.png; portrait width-first; trade/info 160x960 tall-cover 2026-09-26 */ function nst_ad_house_dims(string $site): array { static $map = [ 'trade' => ['w' => 160, 'h' => 960], 'info' => ['w' => 160, 'h' => 960], 'com' => ['w' => 512, 'h' => 512], 'org' => ['w' => 512, 'h' => 512], 'net' => ['w' => 512, 'h' => 512], 'fun' => ['w' => 512, 'h' => 512], 'market' => ['w' => 360, 'h' => 300], 'date' => ['w' => 500, 'h' => 200], 'work' => ['w' => 640, 'h' => 256], 'chat' => ['w' => 640, 'h' => 360], ]; $site = strtolower(trim($site)); return $map[$site] ?? ['w' => 640, 'h' => 256]; } function nst_ad_house_font(string $which = 'bold'): string { $cands = $which === 'bold' ? [ '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', '/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf', ] : [ '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf', ]; foreach ($cands as $p) { if (is_file($p)) return $p; } return ''; } function nst_ad_house_cache_path(string $site, int $n): string { $dir = ads_dir() . DIRECTORY_SEPARATOR . '_house' . DIRECTORY_SEPARATOR . $site; if (!is_dir($dir)) @mkdir($dir, 0755, true); return $dir . DIRECTORY_SEPARATOR . nst_ad_slot_pad($n) . '.png'; } /** Per-face house creative: "Buy this ad slot / N/10000 / on Nosignup.trade". Portrait (W 9999) $n = 9999; $dims = nst_ad_house_dims($site); $W = (int)$dims['w']; $H = (int)$dims['h']; if ($W < 16) $W = 16; if ($H < 16) $H = 16; $im = imagecreatetruecolor($W, $H); if ($im === false) return ''; $bg = imagecolorallocate($im, 12, 16, 22); $border = imagecolorallocate($im, 55, 65, 80); $white = imagecolorallocate($im, 235, 240, 245); $teal = imagecolorallocate($im, 64, 220, 200); $muted = imagecolorallocate($im, 200, 210, 220); imagefilledrectangle($im, 0, 0, $W - 1, $H - 1, $bg); $bpad = ($W < 80 || $H < 80) ? 2 : 4; imagerectangle($im, $bpad, $bpad, $W - $bpad - 1, $H - $bpad - 1, $border); if ($bpad >= 4) { imagerectangle($im, 5, 5, $W - 6, $H - 6, $border); imagerectangle($im, 6, 6, $W - 7, $H - 7, $border); } $pad = nst_ad_slot_pad($n) . '/10000'; $bold = nst_ad_house_font('bold'); $reg = nst_ad_house_font('reg'); /* Portrait trade/info (160x960): width-first so copy stays readable in skyscraper; landscape stays H/256. */ if ($W < $H) { $scale = $W / 160.0; } else { $scale = $H / 256.0; } if ($scale < 0.2) $scale = 0.2; if ($bold === '' || $reg === '') { $lines = ['Buy this ad slot', $pad, 'on Nosignup.trade']; $cols = [$white, $teal, $muted]; $ystep = max(10, (int)round(28 * $scale)); $y = (int)max(4, ($H - $ystep * 3) / 2); foreach ($lines as $i => $t) { $tw = imagefontwidth(5) * strlen($t); imagestring($im, 5, (int)(($W - $tw) / 2), $y, $t, $cols[$i]); $y += $ystep; } } else { $measure = static function (float $scale, string $bold, string $reg, string $pad, int $W) { $rows = [ ['Buy this ad slot', $bold, 42.0 * $scale], [$pad, $bold, 56.0 * $scale], ['on Nosignup.trade', $reg, 32.0 * $scale], ]; $gap = max(2, (int)round(14 * $scale)); $boxes = []; $total_h = 0; $max_w = 0; foreach ($rows as $row) { [$t, $f, $sz] = $row; if ($sz < 6.0) $sz = 6.0; $bbox = imagettfbbox($sz, 0, $f, $t); if ($bbox === false) return null; $w = (int)abs($bbox[2] - $bbox[0]); $h = (int)abs($bbox[7] - $bbox[1]); $boxes[] = [$t, $f, $sz, $w, $h, $bbox]; $total_h += $h; if ($w > $max_w) $max_w = $w; } $total_h += $gap * (count($rows) - 1); return [$boxes, $total_h, $gap, $max_w]; }; $colors = [$white, $teal, $muted]; $m = $measure($scale, $bold, $reg, $pad, $W); if ($m === null) { imagedestroy($im); return ''; } [$boxes, $total_h, $gap, $max_w] = $m; $inner = max(8, $W - 16); if ($max_w > $inner && $max_w > 0) { $scale *= $inner / $max_w; if ($scale < 0.15) $scale = 0.15; $m = $measure($scale, $bold, $reg, $pad, $W); if ($m === null) { imagedestroy($im); return ''; } [$boxes, $total_h, $gap, $max_w] = $m; } if ($total_h > $H - 8 && $total_h > 0) { $scale *= ($H - 8) / $total_h; if ($scale < 0.15) $scale = 0.15; $m = $measure($scale, $bold, $reg, $pad, $W); if ($m === null) { imagedestroy($im); return ''; } [$boxes, $total_h, $gap, $max_w] = $m; } $y = (int)(($H - $total_h) / 2); foreach ($boxes as $i => $b) { [$t, $f, $sz, $w, $h, $bbox] = $b; $col = $colors[$i]; $x = (int)(($W - $w) / 2) - (int)min($bbox[0], $bbox[6]); $baseline = $y - (int)$bbox[7]; imagettftext($im, $sz, 0, $x, $baseline, $col, $f, $t); $y += $h + $gap; } } ob_start(); imagepng($im, null, 6); $bin = ob_get_clean(); imagedestroy($im); return is_string($bin) ? $bin : ''; } function nst_ad_png_ihdr_wh(string $binOrPath, bool $isPath = false): ?array { $hdr = ''; if ($isPath) { $fh = @fopen($binOrPath, 'rb'); if (!$fh) return null; $hdr = (string)fread($fh, 24); fclose($fh); } else { $hdr = substr($binOrPath, 0, 24); } if (strlen($hdr) < 24 || substr($hdr, 0, 8) !== "\x89PNG\r\n\x1a\n") return null; $u = unpack('Nwidth/Nheight', substr($hdr, 16, 8)); if (!is_array($u)) return null; return [(int)$u['width'], (int)$u['height']]; } function nst_ad_house_ensure(string $site, int $n): string { /* Generated house cache only (ads/_house). Paid slot files, the ad index, reports, and ledgers are not touched. */ $p = nst_ad_house_cache_path($site, $n); $dims = nst_ad_house_dims($site); $wantW = (int)$dims['w']; $wantH = (int)$dims['h']; $matches = static function (string $path) use ($wantW, $wantH): bool { if (!is_file($path) || (int)@filesize($path) <= 8) return false; $wh = nst_ad_png_ihdr_wh($path, true); return is_array($wh) && $wh[0] === $wantW && $wh[1] === $wantH; }; if ($matches($p)) return $p; $dir = dirname($p); if (!is_dir($dir)) @mkdir($dir, 0755, true); $lockPath = $dir . DIRECTORY_SEPARATOR . '.house-ensure.lock'; $lf = @fopen($lockPath, 'c+b'); if (!$lf) { return $matches($p) ? $p : nst_ad_placeholder_ensure(); } $got = false; $deadline = microtime(true) + 2.0; do { $got = flock($lf, LOCK_EX | LOCK_NB); if ($got) break; usleep(20000); } while (microtime(true) < $deadline); if (!$got) { fclose($lf); return $matches($p) ? $p : nst_ad_placeholder_ensure(); } try { if ($matches($p)) return $p; $bin = nst_ad_house_png_bin($site, $n); $wh = ($bin !== '') ? nst_ad_png_ihdr_wh($bin, false) : null; if ($bin === '' || !is_array($wh) || $wh[0] !== $wantW || $wh[1] !== $wantH) { return nst_ad_placeholder_ensure(); } $tmp = $dir . DIRECTORY_SEPARATOR . '.' . basename($p) . '.' . getmypid() . '.tmp'; if (@file_put_contents($tmp, $bin, LOCK_EX) === false) { @unlink($tmp); return nst_ad_placeholder_ensure(); } @chmod($tmp, 0644); if (!@rename($tmp, $p)) { @unlink($tmp); return nst_ad_placeholder_ensure(); } @chmod($p, 0644); return $p; } finally { flock($lf, LOCK_UN); fclose($lf); } } function nst_ad_serve_file(string $path, string $ext): void { $map = [ 'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'webp' => 'image/webp', 'gif' => 'image/gif', ]; $ext = strtolower($ext); header('Content-Type: ' . ($map[$ext] ?? 'application/octet-stream')); header('Cache-Control: public, max-age=60'); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET'); header('X-Content-Type-Options: nosniff'); header('Cross-Origin-Resource-Policy: cross-origin'); header('Referrer-Policy: no-referrer'); readfile($path); exit; } function nst_ad_serve_slot(string $site, int $n): void { /* NS_HOUSE_PER_FACE_DIMS 2026-09-26 */ /* WAVEC-TR25-SERVE-PROJECTION: prefer verified index; no cross-site fallback. */ $site = nst_ad_site_norm($site); if ($site === '') { http_response_code(404); echo "Not found\n"; exit; } if (function_exists('nst_ad_serve_slot_from_projection')) { $proj = nst_ad_serve_slot_from_projection($site, $n); if (empty($proj['ok'])) { http_response_code(503); header('Content-Type: text/plain; charset=UTF-8'); header('Cache-Control: no-store'); echo "Unavailable\n"; exit; } $kind = (string)($proj['kind'] ?? ''); if (($kind === 'owned' || $kind === 'takedown') && !empty($proj['path'])) { if (($kind === 'owned' || $kind === 'takedown') && function_exists('nst_ad_report_showing_inc')) { // Owned creative + post-takedown placeholder views ++ My Ads digit. // Unsold pure house (no owner) stays uncounted — falls through below. nst_ad_report_showing_inc( $site, $n, (int)($proj['epoch'] ?? 0), (string)($proj['content_sha256'] ?? '') ); } header('ETag: "' . (string)($proj['content_sha256'] ?? '') . '"'); header('X-NSU-Ad-Gen: ' . (string)($proj['cache_gen'] ?? '')); if ($kind === 'takedown') header('X-NSU-Ad-State: takedown'); nst_ad_serve_file((string)$proj['path'], (string)($proj['ext'] ?? 'png')); } // kind=house → fall through to house ensure } else { $found = nst_ad_slot_find($site, $n); if ($found) { nst_ad_serve_file($found, strtolower(pathinfo($found, PATHINFO_EXTENSION))); } } // Unsold / house: per-slot house PNG with real N/10000 (GD + optional disk cache). // Stable rendition token (not a per-request query). Cache-Control stays public, max-age=60. $ph = nst_ad_house_ensure($site, $n); if (is_file($ph)) { $hd = nst_ad_house_dims($site); header('X-NSU-House-Rendition: ' . (int)$hd['w'] . 'x' . (int)$hd['h']); nst_ad_serve_file($ph, 'png'); } http_response_code(404); echo "Not found\n"; exit; } function nst_ad_try_public_serve(): void { if (PHP_SAPI === 'cli') return; $api = (string)($_GET['api'] ?? $_POST['api'] ?? ''); if ($api === 'ad_slot') { $site = nst_ad_site_norm((string)($_GET['site'] ?? $_GET['crop'] ?? '')); $n = nst_ad_slot_norm_n($_GET['n'] ?? $_GET['slot'] ?? ''); if ($site === '' || $n === null) { http_response_code(404); header('Content-Type: text/plain; charset=UTF-8'); echo "Not found\n"; exit; } nst_ad_serve_slot($site, $n); } if ($api !== '') return; $hit = nst_ad_parse_public_path(); if ($hit) nst_ad_serve_slot($hit['site'], $hit['n']); } function nst_ad_read_upload_bin(): ?string { // WAVEB2-TR23-UPLOAD-BOUND: reject oversized bodies before full decode/work. $max = (int)NST_AD_MAX_PNG; $b64Ceiling = (int)ceil($max * 4 / 3) + 128; // base64 + data-URL prefix slack if (!empty($_FILES['image']['tmp_name']) && is_uploaded_file((string)$_FILES['image']['tmp_name'])) { $sz = (int)($_FILES['image']['size'] ?? 0); if ($sz > $max) return null; $bin = @file_get_contents((string)$_FILES['image']['tmp_name']); if (!is_string($bin) || $bin === '') return null; if (strlen($bin) > $max) return null; return $bin; } $b64 = (string)($_POST['image_base64'] ?? $_POST['png_base64'] ?? ''); if ($b64 === '') return null; if (strlen($b64) > $b64Ceiling) return null; if (preg_match('#^data:image/[a-zA-Z0-9.+-]+;base64,#', $b64)) { $b64 = substr($b64, strpos($b64, ',') + 1); } if (strlen($b64) > $b64Ceiling) return null; $bin = base64_decode($b64, true); if ($bin === false || $bin === '') return null; if (strlen($bin) > $max) return null; return $bin; } function nst_brain_ad_image(string $bin): array { $len = strlen($bin); if ($len < 12) return ['brain' => 'ad_image', 'ok' => false, 'err' => 'image too small', 'score' => 1.0, 'notes' => ['tiny'], 'flag' => true]; if ($len > NST_AD_MAX_PNG) return ['brain' => 'ad_image', 'ok' => false, 'err' => 'image too big (max ' . NST_AD_MAX_PNG . 'b)', 'score' => 1.0, 'notes' => ['bytes'], 'flag' => true, 'bytes' => $len]; $ext = ''; if (substr($bin, 0, 8) === "\x89PNG\r\n\x1a\n") $ext = 'png'; elseif (substr($bin, 0, 3) === "\xFF\xD8\xFF") $ext = 'jpg'; elseif (substr($bin, 0, 6) === 'GIF87a' || substr($bin, 0, 6) === 'GIF89a') $ext = 'gif'; elseif (substr($bin, 0, 4) === 'RIFF' && substr($bin, 8, 4) === 'WEBP') $ext = 'webp'; else return ['brain' => 'ad_image', 'ok' => false, 'err' => 'Use png, jpg, or gif', 'score' => 1.0, 'notes' => ['magic'], 'flag' => true]; if ($ext === 'png') { $p = nst_brain_ad_png($bin); $p['ext'] = 'png'; return $p; } $w = 0; $h = 0; if (function_exists('getimagesizefromstring')) { $info = @getimagesizefromstring($bin); if (is_array($info)) { $w = (int)($info[0] ?? 0); $h = (int)($info[1] ?? 0); } } // WAVEA-TR03-IMAGE-ADMISSION: reject missing/zero geometry before money. if ($w <= 0 || $h <= 0) { return ['brain' => 'ad_image', 'ok' => false, 'err' => 'image geometry unavailable or zero', 'ext' => $ext, 'w' => $w, 'h' => $h, 'score' => 1.0, 'notes' => ['geometry'], 'flag' => true, 'bytes' => $len]; } if ($w < NST_AD_MIN_W || $h < NST_AD_MIN_H) { return ['brain' => 'ad_image', 'ok' => false, 'err' => 'min ' . NST_AD_MIN_W . 'x' . NST_AD_MIN_H, 'ext' => $ext, 'w' => $w, 'h' => $h, 'score' => 1.0, 'notes' => ['min'], 'flag' => true]; } if ($w > NST_AD_MAX_W || $h > NST_AD_MAX_H) { return ['brain' => 'ad_image', 'ok' => false, 'err' => 'longest side max ' . NST_AD_MAX_W . 'px', 'ext' => $ext, 'w' => $w, 'h' => $h, 'score' => 1.0, 'notes' => ['max'], 'flag' => true]; } return ['brain' => 'ad_image', 'ok' => true, 'ext' => $ext, 'w' => $w, 'h' => $h, 'bytes' => $len, 'score' => 0.0, 'notes' => [], 'flag' => false]; } function nst_ad_write_slot(string $site, int $n, string $bin, string $ext, string $owner = ''): ?string { $ext = strtolower($ext); if ($ext === 'jpeg') $ext = 'jpg'; if (!in_array($ext, ['png','jpg','webp','gif'], true)) $ext = 'png'; $site = nst_ad_site_norm($site); $actor = strtolower(preg_replace('/[^a-f0-9]/', '', $owner) ?? ''); // A different owner's crop already occupies this site slot. Fail before any unlink or write. if ($site === '' || strlen($actor) !== 64) return null; $existing = nst_ad_index_owner_of($site, $n); if ($existing !== '' && !hash_equals($existing, $actor)) return null; $dir = nst_ad_slot_dir($site, $n, true); $path = $dir . DIRECTORY_SEPARATOR . $n . '.' . $ext; $siteRoot = ads_dir() . DIRECTORY_SEPARATOR . $site . DIRECTORY_SEPARATOR; foreach (nst_ad_slot_candidates($site, $n) as $old) { if ($old !== $path && is_file($old) && str_starts_with($old, $siteRoot)) @unlink($old); } if (file_put_contents($path, $bin, LOCK_EX) === false) return null; @chmod($path, 0644); return $path; } function nst_ad_mark_sold(string $site, int $n, string $ext, string $owner, string $creativeSha = ''): bool { // WAVEE-TR24-MARK-SOLD-SHA: optional public creative hash in index meta. $actor = strtolower(preg_replace('/[^a-f0-9]/', '', $owner) ?? ''); if (strlen($actor) !== 64) return false; $path = nst_ad_index_path(); $ret = nst_with_file_lock($path, static function ($fh) use ($site, $n, $ext, $actor, $creativeSha) { $raw = stream_get_contents($fh); $idx = is_string($raw) && $raw !== '' ? json_decode($raw, true) : null; if (!is_array($idx)) $idx = nst_ad_index_empty(); // TR-RESET: do not replace a prior epoch with an empty current epoch // before the owner check. A different owner fails with the file unwritten. if (!isset($idx['sites']) || !is_array($idx['sites'])) $idx['sites'] = []; if (!isset($idx['sites'][$site]) || !is_array($idx['sites'][$site])) $idx['sites'][$site] = []; $prior = $idx['sites'][$site][(string)$n] ?? null; if (is_array($prior)) { $own = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($prior['owner'] ?? '')) ?? ''); if (strlen($own) === 64 && !hash_equals($own, $actor)) return false; } // Same owner may replace their own creative. A different owner does not. $row = ['ext' => $ext, 'owner' => $actor, 'ts' => time(), 'takedown' => false]; $sha = strtolower(preg_replace('/[^a-f0-9]/', '', $creativeSha) ?? ''); if (preg_match('/^[a-f0-9]{64}$/', $sha)) $row['creative_sha256'] = $sha; $idx['sites'][$site][(string)$n] = $row; rewind($fh); ftruncate($fh, 0); fwrite($fh, j($idx)); fflush($fh); return true; }); return $ret === true; } function nst_ad_slots_for_owner(string $addr): array { $addr = strtolower(preg_replace('/[^a-f0-9]/', '', $addr) ?? ''); if (strlen($addr) !== 64) return []; $idx = nst_ad_index_load(); $days = nst_ad_slot_days_left(); $epoch = function_exists('nst_econ_current_epoch_n') ? nst_econ_current_epoch_n() : (int)($idx['epoch_n'] ?? 1); $store = function_exists('nst_ad_report_store_read_unlocked') ? nst_ad_report_store_read_unlocked() : ['buckets' => []]; $out = []; foreach (($idx['sites'] ?? []) as $site => $slots) { if (!is_array($slots)) continue; $site = nst_ad_site_norm((string)$site); if ($site === '') continue; foreach ($slots as $k => $meta) { if (!is_array($meta)) continue; $own = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($meta['owner'] ?? '')) ?? ''); if ($own !== $addr) continue; $n = nst_ad_slot_norm_n($k); if ($n === null) continue; $ext = strtolower((string)($meta['ext'] ?? 'png')); if ($ext === 'jpeg') $ext = 'jpg'; $sha = function_exists('nst_ad_report_resolve_creative_sha') ? nst_ad_report_resolve_creative_sha($site, $n, $meta) : ''; $peek = function_exists('nst_ad_report_bucket_peek') ? nst_ad_report_bucket_peek($site, $n, (int)$epoch, $sha, $store) : ['showings' => 0, 'takedown' => false, 'reports' => 0]; $td = !empty($meta['takedown']) || !empty($peek['takedown']); $out[] = [ 'site' => $site, 'slot' => $n, 'slot_pad' => nst_ad_slot_pad($n), 'days_left' => $days, 'ext' => $ext, 'public_url' => nst_ad_public_url($site, $n), 'thumb' => '?api=ad_slot&site=' . rawurlencode($site) . '&n=' . $n, 'showings' => (int)($peek['showings'] ?? 0), 'impressions' => (int)($peek['showings'] ?? 0), 'takedown' => $td, 'status' => $td ? 'takedown' : 'ok', ]; } } $byN = []; foreach ($out as $row) { $byN[(int)$row['slot']][] = $row; } $flat = []; foreach ($byN as $n => $rows) { if (count($rows) >= 10) { $r = $rows[0]; $sum = 0; $anyTd = false; foreach ($rows as $rr) { $sum += (int)($rr['showings'] ?? 0); if (!empty($rr['takedown'])) $anyTd = true; } $r['site'] = 'all'; $r['public_url'] = nst_ad_public_url('trade', (int)$n); $r['thumb'] = '?api=ad_slot&site=trade&n=' . (int)$n; $r['showings'] = $sum; $r['impressions'] = $sum; $r['takedown'] = $anyTd; $r['status'] = $anyTd ? 'takedown' : 'ok'; $flat[] = $r; } else { foreach ($rows as $r) $flat[] = $r; } } return $flat; } function nst_ad_index_owner_of(string $site, int $n): string { $site = nst_ad_site_norm($site); if ($site === '') return ''; $idx = nst_ad_index_load(); $meta = $idx['sites'][$site][(string)$n] ?? null; if (!is_array($meta)) return ''; $own = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($meta['owner'] ?? '')) ?? ''); return strlen($own) === 64 ? $own : ''; } function nst_ad_rewrite_owner(string $site, int $n, string $from, string $to): array { $site = nst_ad_site_norm($site); $from = strtolower(preg_replace('/[^a-f0-9]/', '', $from) ?? ''); $to = strtolower(preg_replace('/[^a-f0-9]/', '', $to) ?? ''); if ($site === '' || $n < 0 || $n > 9999 || strlen($from) !== 64 || strlen($to) !== 64) { return ['ok' => false, 'err' => 'bad give']; } if ($from === $to) return ['ok' => false, 'err' => 'Cannot give a slot to yourself']; $path = nst_ad_index_path(); $ret = nst_with_file_lock($path, static function ($fh) use ($site, $n, $from, $to) { $raw = stream_get_contents($fh); $idx = is_string($raw) && $raw !== '' ? json_decode($raw, true) : null; if (!is_array($idx)) $idx = nst_ad_index_empty(); $empty = nst_ad_index_empty(); if ((int)($idx['epoch_n'] ?? 0) !== (int)$empty['epoch_n']) $idx = $empty; if (!isset($idx['sites'][$site]) || !is_array($idx['sites'][$site])) $idx['sites'][$site] = []; $meta = $idx['sites'][$site][(string)$n] ?? null; if (!is_array($meta)) return ['ok' => false, 'err' => 'That slot is not live']; $own = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($meta['owner'] ?? '')) ?? ''); if ($own !== $from) return ['ok' => false, 'err' => 'Not your slot']; $idx['sites'][$site][(string)$n]['owner'] = $to; $idx['sites'][$site][(string)$n]['ts'] = time(); rewind($fh); ftruncate($fh, 0); fwrite($fh, j($idx)); fflush($fh); return ['ok' => true, 'site' => $site, 'slot' => $n, 'owner' => $to]; }); return is_array($ret) ? $ret : ['ok' => false, 'err' => 'Could not save']; } function nst_ad_slots_wipe(): void { $root = ads_dir(); foreach (nst_ad_slot_sites() as $site) { $dir = $root . DIRECTORY_SEPARATOR . $site; if (!is_dir($dir)) continue; $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST); foreach ($it as $f) { $p = $f->getPathname(); if ($f->isFile()) @unlink($p); elseif ($f->isDir()) @rmdir($p); } } $idx = nst_ad_index_path(); if (is_file($idx)) @unlink($idx); nst_ad_placeholder_ensure(); } function nst_ad_slot_selftest(): void { $ok = 0; $fail = 0; $chk = static function (string $label, bool $cond) use (&$ok, &$fail): void { if ($cond) { $ok++; echo " PASS $label\n"; } else { $fail++; echo " FAIL $label\n"; } }; echo "== AD SLOT SELFTEST ==\n"; $chk('norm 0', nst_ad_slot_norm_n('0') === 0); $chk('norm 9999', nst_ad_slot_norm_n('9999') === 9999); $chk('norm 0042', nst_ad_slot_norm_n('0042') === 42); $chk('norm 10000 reject', nst_ad_slot_norm_n('10000') === null); $chk('pad 42', nst_ad_slot_pad(42) === '0042'); $chk('shard 42 is 00', nst_ad_slot_shard(42) === '00'); $chk('shard 1234 is 12', nst_ad_slot_shard(1234) === '12'); $chk('price not divided by 10000', nst_ad_slot_price_nsu() === (int)NST_RENT_NSU_PER_DAY * nst_ad_slot_days_left()); $chk('price is 100 * days', nst_ad_slot_price_nsu() === 100 * nst_ad_slot_days_left()); $_SERVER['REQUEST_URI'] = '/com/0042'; $_SERVER['PATH_INFO'] = ''; $_SERVER['SCRIPT_NAME'] = '/index.php'; $hit = nst_ad_parse_public_path(); $chk('parse /com/0042', is_array($hit) && $hit['site'] === 'com' && $hit['n'] === 42); $_SERVER['PATH_INFO'] = '/market/7'; $_SERVER['REQUEST_URI'] = '/index.php/market/7'; $hit = nst_ad_parse_public_path(); $chk('parse PATH_INFO /market/7', is_array($hit) && $hit['site'] === 'market' && $hit['n'] === 7); $_SERVER['REQUEST_URI'] = '/setup'; $_SERVER['PATH_INFO'] = ''; $chk('parse /setup is not a slot', nst_ad_parse_public_path() === null); $png = nst_ad_placeholder_bin(); $chk('placeholder is PNG', is_string($png) && str_starts_with($png, "\x89PNG")); $ihdr = (strlen($png) >= 24) ? unpack('Nwidth/Nheight', substr($png, 16, 8)) : []; $chk('placeholder 640x256', (int)($ihdr['width'] ?? 0) === 640 && (int)($ihdr['height'] ?? 0) === 256); $brain = nst_brain_ad_image($png); $chk('placeholder validates', !empty($brain['ok'])); $chk('jpeg magic reject tiny', empty(nst_brain_ad_image('not-an-image')['ok'])); /* NS_HOUSE_PER_FACE_DIMS 2026-09-26 */ $sites = nst_ad_slot_sites(); $chk('ten slot sites', count($sites) === 10); foreach (['com','org','net','trade','chat','work','market','date','fun','info'] as $tld) { $chk('site '.$tld, in_array($tld, $sites, true)); } $wantDims = [ 'trade' => [160, 960], 'info' => [160, 960], 'com' => [512, 512], 'org' => [512, 512], 'net' => [512, 512], 'fun' => [512, 512], 'market' => [360, 300], 'date' => [500, 200], 'work' => [640, 256], 'chat' => [640, 360], ]; foreach ($wantDims as $tld => $wh) { $d = nst_ad_house_dims($tld); $chk('dims '.$tld.' '.$wh[0].'x'.$wh[1], (int)$d['w'] === $wh[0] && (int)$d['h'] === $wh[1]); } if (function_exists('imagecreatetruecolor')) { foreach ($wantDims as $tld => $wh) { $hbin = nst_ad_house_png_bin($tld, ($tld === 'date') ? 420 : 1); $chk('house '.$tld.' is PNG', is_string($hbin) && str_starts_with($hbin, "\x89PNG")); $hihdr = (strlen($hbin) >= 24) ? unpack('Nwidth/Nheight', substr($hbin, 16, 8)) : []; $chk('house '.$tld.' '.$wh[0].'x'.$wh[1], (int)($hihdr['width'] ?? 0) === $wh[0] && (int)($hihdr['height'] ?? 0) === $wh[1]); } $hbrain = nst_brain_ad_image(nst_ad_house_png_bin('work', 0)); $chk('house png validates', !empty($hbrain['ok'])); } else { $chk('house png gd unavailable skip', true); $chk('house png dims skip', true); $chk('house png validates skip', true); } echo "ad-slot selftest $ok pass / $fail fail\n"; if ($fail > 0) exit(1); } /* NS_HOUSE_AD_PLACEHOLDER 2026-09-26 */ /* NS_HOUSE_PER_FACE_DIMS 2026-09-26 — fallback sample 0000/10000 work 640×256 */ function nst_ad_placeholder_b64(): string { return 'iVBORw0KGgoAAAANSUhEUgAAAoAAAAEACAIAAACs91AxAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAg' . 'AElEQVR4nO3dd1wT5/8A8EsIEEjYELayFQQUt9SFKE4cVRFH3RtrK61WW63aft1WrVpHa7VKXTgQ' . 'q6jgREXc4kT23jNsCOH3R9qYH1yeXBZ3ST7vl3+Eu+fuPuZJ7pO7ewaNbczBAAAAANC+6GQHAAAA' . 'AGgiSMAAAAAACSABAwAAACRgiP7RxceXrDgAAAAAtffuZZzwNVwBAwAAACSABAwAAACQgIG7VPQa' . 'GQAAAADywH3CC1fAAAAAAAkgAQMAAAAkgAQMAAAAkAASMAAAAEACSMAAAAAACSABAwAAACSABAwA' . 'AACQABIwAAAAQAJIwAAAAAAJIAEDAAAAJIAEDAAAAJAAEjAAAABAAkjAAAAAAAkgAQMAAAAkgAQM' . 'AAAAkAASMAAAAEACSMAAAAAACSABAwAAACSABAwAAACQABIwAAAAQAJIwAAAAAAJIAEDAAAAJIAE' . 'DAAAAJAAEjAAAABAAkjAAAAAAAkgAQMAAAAkgAQMAAAAkIBBdgAAKNJgv8HHjh0Tt3b37t17f92r' . 'TscFrcyfP/+HtT+IW/vdqu/Cw8PbMx5FgQ+YWoIrYAAAAIAEGnEFfOfuHQcHB+LleTwej8erra3l' . 'crnFxcW5ubkpySlv3759+vRpbW2t0sIEAACgQTQiAUuLwWAwGAwmk2lqaurg4NCrVy/Bch6PFxcX' . 'F342PCoqqqWlhdwgAQAAqDRIwFJgMBgDBw4cOHBgclLyypUrExISyI5IHQQFBW3bvk3cWtV9aAeA' . 'xoIvNUHwDFgWrm6uZ86eCQgIIDsQAAAAqgoSsIyYTOZvB37z8vIiOxAAAAAqCRKw7BgMxrbt22g0' . 'GtmBAAAAUD3wDFgu7u7ugwYPunvnLtmBAJLdvXPX0cGR7CgAAKoEEjAWdTUqJCSk1UJDQ8Munl2m' . 'TZs2ZswY9ObDA4ZDAgYAACAtSMD4uFzuo7hHj+IeJbxKQAysg2FY33592y0qAAAAagMSsARHjhyZ' . 'NHlSp06dxBWwsLBoz3iAAtFoNL8hfv7+/t5e3nb2dmw2u7q6uqSkJDU19c6dO9E3osvLy9s5JBMT' . 'Ez8/v87unV1dXTt06MBms1kslp6eXl1dXU1NDZfLzcnJycnJSU9Lf/369fv379t5ZBgOh+Pp5enh' . '4WFnZ2dra2tjY8Nms/X09PT09Ph8fkNDQ0VFRVFhUVp6WuKHxPj4+Pfv38t/UC0trYDhAYJqsrK2' . 'ErwbhYWFiYmJ92PvR0VFVVdXy38URaF4DQppa2v7+vr26NHD09PTzs7OgmPBZDJ1dHTq6uq4XG5+' . 'fn5qauqb129iY2MzMzNJiVATQAKWLDY2FpGAWSyWrq5uQ0MD7trtO7ZPnjxZ3Lbjx41HdyZGbz5s' . '2LCU5BThn1ZWVvcf3Gcw8Ov0ypUrXy77EnEsUYsWL1q9erW4tQvmL7h58ybBXbXVz7ffqVOnCBbe' . 'tn0bokPhzh07f/vtN9nCmDRp0tcrvra1tRVdaGxsbGxs7OLiMnz48HXr1h08cPD3339vamqSuDf5' . 'h+rt59tv6dKlffv2xa1BFovFYrE4HI6Li4twIZ/PT05OfvDgwYMHD548fqKkUzmHwxk4cOCAgQN8' . 'fX3Nzc0RJXV0dAwMDOzt7Xv07CFYkpubGx4e/texv7hcrmxHnzRp0rcrv7W0tBRdaGBgYGBg4OLi' . 'MmbMmB/X/3js2LHf9v9WX18v2yEUhbI12Iqbm9ucOXMCxwayWCxxcVpbW3fv3l1w8vnw4UPYibDz' . '588jvggU+VKrHGgFLVl5WXtfBsmmoKDgZozYvBgQEGBkZERwV6NGjRK3qqSk5M6dO1IHRyVmZmZh' . 'f4ft2LmjVfZthcVifbvy27C/w5hMplLj0dfXP3T40KlTp/r37y/u9xMuOp3eqVOnefPmHTt2bMPG' . 'DUoK758r/+zYuWPs2LHo7IvL1tZ2xYoVsfdjJ02aJO22TCbz0OFDO3buaJV9W2GxWMuWLbsaddXN' . 'zU3aQygKxWtQyNDQcPuO7deuXwueGoybfXG5u7tv3rL51u1bQ/yHKDU8DQQJWDITUxPE2qqqKnGX' . 'v+3v+PHj4lbp6OhMnDiRyE7s7Oy8vb3FrY2IiGhubpYlOGpwdHSMuBTRv39/guX79Olz9NhR5cXD' . 'ZrNPnjo5fPhw5R2CdEZGRjt27tiwYQPxTfT19aV6W5ycnM5fON+9R3dZ4pOPqtSgl5dXdEz05MmT' . '6XRZTvv29vZ//vnnhg0boOOlAkEClgx9sn7z+k27RSJRfHx8clKyuLXBwcFEdjJy1EjE2vPnzksd' . 'FmV06NDh9JnT9vb2Um3Vr1+/mbNmKimktevWduvWTUk7p5RZs2d9/fXXBAvv37+/e3fpsqmBgcHR' . 'o0fdOrX3dbBK1GA/335nw8+i7yUQMWv2rIOHDkIOVhRIwBLMnjPb3d0dUeD69esy75zP58u8rTiI' . 'i2BXN1cfHx+Je0D0vHr75m1SUpKMkVHAxIkTZTsHrVq1Sl9fX+HxeHh4IJ7xq58vl3/ZtWtXicWm' . 'Tp3qN8RPhv0bGRm18/upEjXo5ub2xx9/6OnpKWRvw4cP37x5s0J2BaARFj5DQ0MPD48pwVPGjx+P' . 'KFZYWEi1UcUvXLiwctVKcY97p06b+vLlS8Tm6PvPVPvPthsWixU4NvDsmbOK3e24cePE3Q+sqqo6' . 'fvz4nTt30tPSuVwunU43MTExNjZ2dXX19PTs2q1rr169pHrcqBBVVVUPHzx8/fr169ev8/LyuFyu' . 'oHWVvr6+g4ODt7d34NhA4exhbdHp9O9Wfzdt6jTEITgczprv1yg+dOWgfg3q6ur+duA34k98iQie' . 'GhwXF/fPP/8ocJ+aCRIwNmr0qPTR6TJsyOfzV367Up4HwPwWxV8B19fXX7xwcc7cObhrR40atWH9' . 'BkRjS8T958bGxsjISAWEqJomTJig8ATs54d/nVdTUzNh/ITU1FThkubm5qKioqKioqSkpKtXr2IY' . 'xmaz/f39R44a6e/v3w7n8bt374aHh9+MuYnbFLaysjIhISEhISEsLGzkqJG7du0S13KtX79+bm5u' . 'iPsoX339lYGBASISHo8XdiIsIiIiPT2dx+PZ2dsNDxi+YOEC4m0MFYj6Nbhg4QLRdtdt8fn88LPh' . 'ERERycnJ1dXVlpaWAwYMmL9gvpOTE2KrtevW3rp1C+ZHlxMkYBnV19eHhobev39fnp208JUyqfDx' . '48dnzZ6F+8OcxWKNGzfu9OnT4rZF3H++GXNT5s4koh7FPRIdtZGUmcsaGxuPHz8eGRmZmZHJ4/E8' . 'ungsXLgQ3Y6ma9euNBpNsfNA29nb4S6PvhEteu7GVV1dHRkZGRkZaWVlNX3GdIaWsr7LL1++PPLH' . 'kWfPnhEsfy3qmh5T75ddv4grEDg28Jed+Gv19fXHjRuH2HlVVdUXM74Q7byXkpySkpxy7ty502dO' . 'o3OGMlC8BvX19RcvXowoUFNTM3fO3CdPngiX5OTknD59+uLFi7t27Ro1WmxvCA6HM2vWrIMHDwr+' . 'pMKXWhXBM2Cp8fn8mJiYUSNHXYu6RnYs+DIzMx88eCBubfBUsU2xbGxsEPefz50/J29k1FBSUhI4' . 'JnDzps3v3r6rrq6ur69/8fzF4kWL//zzT8RWTCbT1dVVgWHo6+uLeyxXVV1FfD8FBQW/7Pxl2zax' . '5zs5LV60mHj2Fbh48WJ2dra4tYhWjRMmTEDfLA1dEYrbdb6oqGj2rNnt3BWY+jU4afIkie+naPYV' . 'amho+Prrr9++eYvY9ouZX8gbn8aDBCy1urq6/Px82Zryt6KMRlgCiKZY3t7enTt3xl01esxocVsV' . 'Fhbeu3tPAZGRjcfjzZ0zF/cW6Lat28rKyhDbOrs4KzASxEfI399fGW2+2lNcXJy4VR4eHuKa0fr7' . '+6P2+TAOMQJMdnb2ieMniEcoP+rXIHoo+7iHcdHR0eLWNjU1bdq0CbG5tbU1kUadAAESsNRYLNbM' . 'mTOv37j+448/amlpybMr5SXg27duZ2VliVsrrj8S4usacTFCsXdfyXLmzJk3b/B7jjU1NcU/ikds' . 'a2hoqMBIqqurGxsbcVfZ2tpeunRp/ITxpDzXVIiiwiJxq3R0dBwcHHBXde2GaiMt8b5lO9/YpHgN' . 'MpnMHj16IAr8ffJv9B7i4+MzMjIQBQYMHCBDYEAIErCMGAzGnLlzTp46qaurS3Ys+MJOhIlbNX7C' . 'eG1t7VYL0fefz59X4e6/ok7+fRKxFv3czoCNahwkg5ycHHGrXN1cd+/e/Srh1YOHD46fOL523dqp' . 'U6f27t1bVVIyunEibmewjh07mpqaIraSOARbamoq4nenMlC5Br29vRHX6Hw+/85tyUPaxcTEINZ2' . '60r1DtAUB42w5NKnT59du3a1nc2QIGW0ghYKDw8P/SYU9xmVkZHRqFGjWjVpRtx/fvXqlcQWJSoh' . 'Nzc3MTERUaCsHHULWp+l4JuKcXFxEtsN2dra2traDhw4ULgkNzf3+fPnDx88vHPnTnFxsWJDQtPX' . '1+/Vq5eXt5erq6udnZ2FhYWhoaGenh6DwZDqoQzu6HLiLosF8vPzibQB/PjxY4cOHYhHIicq16Cj' . 'E2qC6oyMDCKPzBM/oL4v6EMAiSAB488HrKura2Zm5uXtNXHixGHDhiE2HzV6VEBkAOJRClm4XG7k' . 'pUhxTa6mTpvaKgEjxn8OP6smTRYlTs5TW9Ou3SoiIiJmzJgh7VaCE/rYsWN5PN792Pu///57fDzq' . 'zrlCDBo06IsvvhgwcICOjo78e8O9b2RsbIzYhOClLfqWqcJRuQatrawRazMzCM1xlJGZgVhrZWUl' . 'VUigFbgFja+hoSEvL+/G9RsLFyxctHCRuCc9AqHfhLZbYFJBNMXq06dPx44dhX/a2NiIG06vvr5e' . 'bXrc5+XmoQs0NqEqWuFePH9B5DagOAwGw2+I3+kzp3/d+6vyZozgcDhnzp756/hf/kP9FZJ9MQzT' . '0cbZD/oRe3UVoTkH2/knFJVrkG3ARqwl2E4b/bYzmUw528FoOEjAkkVHR+/btw9RoFOnTl08u8iw' . 'ZzpNwvvf9kmtVBITE58+fSpurWhTLMT95+joaErNtyoPiSedZl57zzOxYsWKtLQ0OXcyduzYAwcO' . 'KGOEXjc3t0uRl/r06aPwPbeFvsNfV19HZCcEiykQZWsQffYgOIJQXZ2E95OyjWBUAiRgQsJOhKFb' . 'LPfpLcsZSuLwN/K3+jn+l9iL4M8nfi78wiPuP6v07AutSJzZV3nt0sWprKycNHHS9Wuyjygu4DfE' . 'b9bsWQoJSYjJZB7584i1NepOpgKhL171mISGMiZYTIEoW4PoTzvBxClxBGnqzAWniiABE1JZWYl+' . 'BNXZHb9nLZrEC1wra3kfsVy/fr2wsBB3FYfDETzeRtx/zs/Pl3O0LyBReXn5kiVLpgZPjYmJkecX' . 'wJIlSxR7PzD0m1BpZ46SB7qNFcEWcApvKEcENWsQffeY4I979H3s+vp6lZ6clHTQCIso9KfZ1ATV' . 'fUIc9MlCS0tL/qGXmpubT506tWLFCty1wcHB0dHRiMvfCxcuyBkAICg+Pj4+Pt7IyKh///4DBg7w' . '9PR0dnaW6rkgh8Px8fGRdtQqcbS0tCTO8xMTExN5KfLt27eFhYWtmtSGhIR8u/JbqY5YUVGBWItu' . 'Iy1tMWWgWg3mF+Qj1nZ06IhYK+TQ0QGxtqCgQKqQQCuQgIkyNEK1ENHWwb+WRT9BMTczR6wdNHiQ' . 'Qtq8nPz7ZEhICO6uBg0exOFwEA+A1en+s0qorKy8evWqYLB+Go3m4ODg6OTo5Ojk7Ozs5e3l7u6O' . '7u3Tp08fRZ2++/bti2iWzOfzly5deuP6DXEFZHg0iG7AbG1tbWhoKLEnUqdOnaQ9rmJRpwbT01Bz' . 'zDg4ODCZTIk9kdD39tCHABJBAibExMTEzg5/1HWB0pJS3OXo8wX6Ahc9axtxpaWl169fHzt2bNtV' . 'dDo99JtQcfefnz59mplJqK+CPHg8HmKtJrexbGlpSU9PT09Pv43dFizhcDjr169HDZFvyVHU0V3d' . 'UB/O8LPhiOyLyfT0JDMzs6ysDDEWh5+fH3o+LkdHx/bsBCwRuTX4+vVrPp8vLt/T6XS/IX4SB7RH' . 'd8J8lfAKdzl8qQmCZ8CEzJqFP7mQkLje9OgELG4uMwzDhvgP8R+KGhdXKoj+SFOmTBG3qn0uf2tq' . 'axBr0UMjaZqioqLly5cjmiOgu9JKxcLCArE29n4sevO+ffvKcNCEVzgTLQgFBQWhN5dYgHTtWYP1' . '9fXPnz9HFJg2TcJP/N69e6Nv6d+PxW8gAl9qgiABSzZy1MiQZRLGunr7Fn/aEPQtGk8vz5mzZrZd' . '3rNnz7179xKPUKIXz1+gJzZpq66u7sqVKwqMQRz0b5RBgwe1QwwqpLm5+dUr/MsOTKFNUtH3kNH9' . 'tYb4D5Gt9datW7cQa30/8x3sN1jcWjs7O4W3A1eGdqtBDMPQX+H+/fsPHTpU3FotLa1169YhNs/P' . 'z3/58iXuKvhSEwQJGJ+urq6Njc3IUSOPHDly4MABdH+hxsZGcaPUikvMQhs3btyyZYuHh4eurq6h' . 'oWH3Ht03bd50NvwsehIxGSAugnFdv3a9fWbbzskWO5ouhmG9evXavmO7l5cXFeaWUbg1a9Ys+3KZ' . 'iQnOuIwIiH5BChzXsLysHLEWMQ2OkZHRhg0bZDtoRERETQ3q4mnPnj1eXl5tl3M4nGN/HZPYZ0bh' . 'qFyDGIZdvHBRwvv5656ePXu2Xa6trb3n1z2eXp6IbRGjzWvyl1oq8AwYGzV6VPpouZoS3L51W9xQ' . 'FQUFBbm5uba2tojNg6cGI+boVZTIyMgf1v5A/AZXu00sk52dXVlZiRihfvLkybjNcSMuRoSGUnQM' . 'MoIsLCwWLloYEhJyLera9evXY2NjJTaKGT5iOGKKm+TkZEXFlpaOGlxi+ozp58+fbztCOIfDOXrs' . 'qMydl2prayMjIxG3Ro2MjM5fOB8WFhZ5KTI9Pb2pqcnOzi4gIGDhooUKvHlLHJVrEMOw6urq33//' . 'XVwnCAzDWCzW2fCz4WfDL1y4kJycXFtba2FhMWDggPnz57u4uCD2XFJSgvhNr8lfaqlAApZXXV0d' . 'etbMiIiIZcuWtVs84jQ1NZ05fWbxksVECmdnZ7fD8MJC8fHxw4cPb7fDUQ2TyZzw+YQJn0+or69/' . '9uzZ2zdvExMTk5OTKyoquFxuTU0Ng8GwsLDo7N45MDBw7Nix4pojNDY23owRO12utO7H3ufxeOLu' . '/RgYGFy4eOHgwYPRN6KzsrK0tbUdHBxGjBgxd95cAwO5Ro/Zt3dfYGAgYic6Ojrz5s2bN2+ePEdR' . 'LGrWoMDhQ4cDAwMR2ZROp8twDfDTxp/Qd8g0/EtNECRgef3808+IKckwDDt/7vzSpUulmitGScLC' . 'whYuWkgkkosXLrZDPEKnT52G7yqGYUwms3///v3795dt83/++UeBg4ZWV1dHRUXhNp4XMDIyWr16' . '9erVqxV1RIGCgoId23f89PNPit1t+6BUDQo0NDR8uezL8xfOK/Cp1pnTZySODw9faiLIzwqqi8/n' . 'r169+vTp0+himZmZx44dk2H/Hz9+vHUT1SZFWnl5eehGLgJ8Pr+dZ/+9d+/eo0eP2vOI6qesrGzT' . '/1B3YmSwY/sOIjPWtVVZWRlxMULm44aFhcXGSmhljauqqurcuXMyH5dcyqhBgcTExAULFkgc1Zmg' . 'GzdufP/99xKLwZeaCEjAMkpLS5s+bfrZM2eJFN65Y2dKSopU+3/37t30adPR09PKADE0tNDjx4/R' . '1/TK8NXyr9p5Ijl1UlFRMXfO3PJyVLMpGeTk5ISGhko7tmJNTc3CBQvlnEB6yeIlr1+/lva48+fN' . 'T/qYJM9xyaKkGhR6FPdoavBUcaPSEnfixIkli5e0tLQQKQxfaokgAUuHx+PFPYxb/uXyof5DiT8l' . 'ra+vD54S/PHjR4LlL1++HDQ5qLQUf3APeTx8+FDiTwFSRr8qLi4ePWr02bNn0TM/grZevHjx+YTP' . 'ExJQPWhldi3qWuiKUOIXT7m5uVODpz558kTO49bW1k4Nnkr8DlBWVlbQ5CD5j0sKpdagUEJCQsCw' . 'gHPnzsk2WnV2dvaC+QvW/7ieYPbF4EtNADwDFovH4zU0NHC53NKS0pycnJTUlDev38THx0scDA9X' . 'aWnppImTln+1fNasWYgBJlNSUnZs3xEdHS1H4BKEhYVt3LhR3NqampqoqCjlHR2htrZ29Xert23d' . 'NmbMmK7dunp4eJiZmRkYGLR/35J2s3bt2sv/XPb19fX19e3UqZPE2bFE8Xi858+fHz9+XOJgRnKK' . 'jIx8+/bt2nVrBw8ejChWX19/+tTpPXv2yPYFaau2tnb+/PlBQUGh34RaWloijhsWFrZn95726TXX' . 'ikrUoBCXy121ctWRP47MnTd3zJgxBJ8Kf/jw4eTfJ8PDwyVOJtaWBn6ppUJjG38a+ayLj6/gxbuX' . 'cSTFo/6srKwChgcMHjTY0cnRxMSExWJVV1dnZWW9fPEyJibm4cOHyg5AX18//nG8uFam586dW7Vy' . 'lbJjAG3p6uq6ubl5eno6Ojna29nb2NqYmZmxWCwmk8lgMOrq6mpra6uqqrKzspOTkz8kfrh3957y' . '7lji6ty5c8DwgL59+3bo0MHU1FRbW7umpqagoODjx4/xj+KjoqIqKyuVcVwtLa0RI0b4+/t7eXtZ' . 'W1vr6enV1dUVFhYmfUyKjY29evWqolK+nKhfg6K0tbV9fX179urZxaOLnZ0dx5Kjp6fHYDAEVx35' . '+fmpqakJrxJiY2PbYTxaTYCbXiEBa6Lwc+G9evXCXTV50mRFjQUPAABAADe9wjNgjWNkZCRuGKOs' . 'rCzIvgAA0D4gAWucoClB4p5UnTp1qp2DAQAAjQUJWLPY29uHhOBPLFFTU3P6lIQ+zQAAABQFWkFr' . 'BF1dXTs7u0GDBi1ZukTcAK1nzpyhSGMWAADQBJCA1VZQUNC27dsIFq6oqNj7qyInQAQAAIAGt6AB' . 'hmHYpv9tgstfAABoT5CAAXbu3Ll2HvwZAAAAJGBNd+nSpTWr15AdBQAAaBx4Bqy5Kioqtm/bLnE2' . 'JwAAAMoACViD8Hi86urqwsLCt2/f3rt77/r16zIM7goAAEAhIAGrrfDw8PDwcLKjAAAAgA+eAQMA' . 'AAAkgAQMAAAAkAASMAAAAEACSMAAAAAACSABAwAAACSABAwAAACQABIwAAAAQAJIwAAAAAAJIAED' . 'AAAAJIAEDAAAAJAAEjAAAABAAkjAAAAAAAlgMobWdDvYGPTwYrk769pZ69pYahmw6ExdjE7j1zfw' . 'a+sbC4rrs/PrktOrnr+tTUyFwCA2QBCVKwJiU6fAVAiNbcwR/tHFx1fw4t3LOJLiIY2OlYVZoL/5' . '6CG6dlYEN2kqryyLvl8SGVOXlK6BgUFs8vO+elTH0rzt8g9zV9W8TpRqVzrWHHY3DwMfD7ZPFz1H' . 'eyKbFJ69kr3jd6mOIgMqVwTEpk6BURxueoUEjGmbm9osDDYfO4zG0JJtDxX3HuceCKtLzdKQwDCI' . 'TRH0Ozt7/L277fKmsoqEgJlE9qDn4sD28TDw6cL28dCxMJM2AGUnYCpXBMSmToGpBEjAOMwC/Tt8' . 's0CLrS/nflp4zflHw/P+PIs189U7MAxiUxCbxdNt5k9pu7w44kbmpt9wN6HpaLO6uP17pevtLud/' . 'U6kJmMoVAbGpU2CqAje9aukwWcI/ONb/3rkqLshuz8jIoUV33Pi1zYJguo62/Duj0ekGPbwMe3ct' . 'vxvf0tConoFBbApl/+1CbVPjtsvzDp1syM7H3cQuZKbDui8Ne3dl2tvI/9+seZfEjXsu505wULki' . 'IDZ1Ckyl4KZXDW0FTWfquu3baDbKT7G7ZXd1dz+2Q5sj9c1AIcoGhkFsCqVjzdF36dh2eXNtHfdx' . 'gsIP126oXBEQmwwoG5h60MgETKM5bf3OsHdXZeyb2dHW7beftNgsyUXbomxgGMQma2xiGA/ui7uc' . 'G/+yhcdT7LHaD5UrAmKTAWUDUxeamIA7rFxo3L+n8vav52jvsusHjEaTdkPKBoZBbLLGJo6JH34C' . 'Lr8Tr8CjtDMqVwTEpmZnJPWgcQnYaEAvTtBoZR/FoLun9dzJUm1C2cAwiE3W2MTRMmSzvd3bLm/h' . 'NVfef6qoo7QzKlcExKZmZyS1oVkDcdBZeg7rviRYuLGwpPBUZGXci4bcgpbmZh2OuUFPL07QaJa7' . 'C5HNbRZMrbgbT7DBPWUDg9hkjg3BeEAv3I4cVS/eNlfXyL//9kflioDYZIiNsoGpGc1qBW27ZIZR' . 'Xx8iJctuPkxauq765TteBRdr5mMtLc3VNXVJ6SURN1paWgx7ekncA41O17WzKrt2V6UDg9hkjg3B' . 'ZuFU3LEyCk9G1rxLRmxo2KcbuyvOpbNsFNgKmsoVAbHJEBtlA1NduOlVgxKwNsfMadO3NC3JXcgr' . 'H71I/XazuLYw1S/e0nR1DLp5SNwP09666tX7xtxCFQ0MYpM5NgSajnbHH0Lo2jg3nzK3HGyurkVs' . 'i0jADflFlQ+eFp+Pyt1/XN/NUcfKQmIkikrAVK4IiE2G2CgbmErT9G5InClj6NqS+7HxGxozNu7F' . 'WloQZXIPhNVn5hI5qNWM8aobGMQmc2wIhn19tPSYbZfXfkxrLCgmvp8WPr82Ob0o/Gra9zsSRsx+' . 'Ezg/fd2u4gvX61KzWvjtOsQBlSsCYpMhNsoGpn40JQHTdLQtxgcQKVl8KbqppExCoWZ+/l/niezN' . 'sK+Prp21KgYGsckcG5qJmA5I5Xclt3/mNzZVvXibfzQ86csNL/2mvp/6Vdb2w2XR9yX/B5WGyhUB' . 'sckQG2UDU0uakoCNPuvJMDIgUrIkMoZIsfKYB8119RKL0eh0s1GDVTEwiE3m2NCMBvTCXV5BoANS' . '3qGTHxd+n3vgb+6jF/yaOnnCUBQqVwTEJkNslA1MLWlKAjYZ+hmRYo3FpQTn69yS5dkAACAASURB' . 'VODXN1Q9fS3/oSkbGJECAhAbcexuHtomRm2XN+QV1qVkyLxbElG5IiA2GQ5N2cDUkmYkYBrN+DNC' . '3cmrXrwjvtfqV++JFNNz6iD21gplA8MgNrEkxIYkbgCsCgL3n6mIyhUBsYmhqmckdaQRCVi/kxPB' . 'STxq36M6gbRS8yGFYEm2D347QMoGhkFsSIjY0IwH9cZdXnHvsWw7JBeVKwJiQ1DFM5Ja0ogEzCbQ' . 'Dl6gLk2K/lf16TlyBkDZwNCrWoHYCGI62jPtbdou51VWSXU9QR1UrgiIDUEVz0hqSSMSMMuD0IAs' . 'GIaJmwYOV1NJGZHGBRiGsbq44S+namAYxIaEiA1B7P3n+0/RfTkoi8oVAbEhqOIZSS1pRAJm4o06' . 'hKuxqESqPRPs/sG0x3+wQdnAMIgNCREbgrgJGFT1ATC1KwJiQ1DFM5Ja0owE3NGWSLHm6tqWxiap' . '9txUTOhTRdfV0bG1VKHAMIgNCRGbONrmpvqdndsu5zc0cuNfSrUr6qByRUBsCKp4RlJL6p+AtQzZ' . 'Wvp6REryuFXS7ry5iujQ+bo2rT9VlA0Mg9gIwI0NwdivL42O83Xjxr/k1zdItSuKoHJFQGwSqdYZ' . 'SV2pfwLWNjMhWJJXKfWnivgmbXt/UjYwDGIjADc2BOOBatX+GaN2RUBsEqnWGUldaUACNiVanTJc' . 'iBDfhGFq3GoJZQPDIDYCcGMTh87SM8CbFqaFz6+IfUJ8P5RC5YqA2CRSrTOSulL/BKxlSGhYNUzZ' . 'nypDdqsllA0Mg9gIwI1NHOPPeuKObl+d8IFXwSW+H0qhckVAbBKp1hlJXal/Asad9w1XSxP+pFqo' . 'TcTMw9UWTVen1RLKBoZBbATgxiaOug2AhWEYtSsCYpNItc5I6kr9E7AU1Sl9X8wWPtFN2n64KRsY' . 'BrERQPxshWnRjXx74K4hMgMSZVG5IiA2iVTrjKSuNCABE5hWWkCWWVQJb0JjtPm4UzUwDGIjADc2' . 'XIa9uuIO71ebkqnSM5BTuSIgNolU64ykrtQ/Abc0NxMsidtLRALCm7S9A0PZwDCIjQDit9TE3X+u' . 'jFXV9s8CVK4IiE0i1TojqSsNSMANjUSLEv4BKESj0wiW5Ld5akLZwDCIjQDc2HCJm4ChnMAEwFRG' . '5YqA2CRSrTOSulL/BEy8OmV49kDTIfrUpO2Hm7KBYRAbAQTPVqwurjoWZm2XNxaW1BKeIoaaqFwR' . 'EJtEqnVGUlfqn4CbCQ/aQmfqSrtz4pvwuNWtllA2MAxiIwA3traMB/XBXa66428IUbkiIDaJVOuM' . 'pK7UPwE3lVUSLEnXZ0q7cyk+VWUVrZZQNjAMYiMAN7a2xD0AVun2zwJUrgiITSLVOiOpK/VPwLxy' . 'op8qLTZL2p0T7zPe1CYMygaGQWwE4MbWio6tpZ5Th7bLeVXVVc/fSBcZ9VC5IiA2iVTrjKSuNCAB' . 'V3Cba+uIlGQYER0IRoZNGvJadzihbGAYxEYAbmytmAzxxV1e+eAZ1ix9Rw6KoXJFQGwSqdYZSV2p' . 'fwLGMKw+M5dIMS19PbqedLdWtC1MiRTjNzQ25hWpUGAYxIaEiE2U2AkYVHb851aoXBEQG4IqnpHU' . 'kmYk4IwcgiV1LM2l2jPBT1V9dj7u2DGUDQyD2JAQsQkxjA3ZXd3bLuc3NVU+eCZVVJRF5YqA2BBU' . '8YykljQiAde8J9rfQ7eDDfHd6lhZ4I6w31bt+2Tc5ZQNDIPYkBCxCRkP7I07XkHV09f8unriUVEZ' . 'lSsCYkNQxTOSWtKIBFz96j3BknoOdsR3y3S0lzMAygaGXtUKxIZLbPtnFR9/QxSVKwJiQ1DFM5Ja' . '0ogEXJuY2lxdS6Qky9ON+G5ZHi4ES1a9eIe7nLKBYRAbEiI2AZqOtmGfbm2Xt/D5FSo+AqUoKlcE' . 'xIagimcktaQRCRhraal4SOipG7ubB/G9GvTAmWK9rbq0rIacfBULDIPYxJIQG4ZhGGbk24OON7dM' . 'zbskXqkadXOkckVAbGKo6hlJHWlGAsawittxRIppmxqzurgSKUln6bF9CH0Ey28/UsXAMIhNDImx' . 'YYgJgO+pSftnISpXBMSGS3XPSOpHYxLw/ae8SkIDrZmPG0akmNkoPyLNClr4/NKoO6oYGMQmc2wY' . 'jWY8oBfumgrVHwCrFSpXBMQmQ2yUDUwtaUoCbmlsKomMIVLSLNBfx8oCXYamo20183Mie+M+ftWQ' . 'laeKgUFsMsdm0L0L7rAD9Zm5xLt5qAoqVwTEJkNslA1MLWlKAsYwrCj8Kr+pSWIxura2w4avMS3U' . 'O2P/9Vxdaw6RgxaejFTdwCA22WITNwGDGoz/jIuyFQGxyRYbZQNTP1LPKqW6GguKi89FWU4bJ7Gk' . 'YU8v563fpf+4G6e/Jo1mu2wmJ2g0kSNynyRw41+qbmAQm2yxiX0ALHcHJP3Ozh5/75ZzJ0KWU8ZY' . 'ThkjsdizXuPQYyNQtiIgNtlio2xg6kdLh/lpWG2O9b+9tYoLskmKR7lq3iSZjxumRWAQNT1He7PA' . 'IRid3lxVIxgfVcfKwmRIP4cNX5mKGeC3lRZec+rKLQRn9qBsYBCbtLHpuThYz5ncdnlTSXn2riNE' . 'DoSgbW5q8fkIOXcirbw/zkgsQ8GKgNjkiY2ygaku3PRKYxt/uj/Qxeff9+vdS0IN4VSR8eC+Lju/' . 'b4cD5R4+lU/gzCVE2cAwiA3DMMKxWS8Itl00re3y4ogbmZt+kzMGxV4BEyTxCliAahUhCmLD1OuM' . 'pIpw06sGPQMWqLgbX3zxurKPUvXyXf6Rs1JtQtnAMIhNmtjETsCgpg+AhahWEaIgNjU7I6kNjUvA' . 'GIZlbjtU+eiF8vZfn5mb8s0mGYYUp2xgGMRGLDZtjhnLHWfQn+baOu7jBCWERi3UqYi2IDY1OyOp' . 'B01MwFgzP3XVVu4zpcyIXp+dlxTyYzO3WpaNKRsYBrERis1ETPOryrgXLTyeQuOiJMpUBA6ITQaU' . 'DUxdaFYjLKEWHq/seqyuvbW+S0cF7rb6bVLSkrVNJeXqFxgGsRFgGzJT19aq7fL8o+fqUjLlj4ey' . 'jbCEKFIRuCA2dQpM5eCmVw1NwBiGYXx+xe24xqJSg55eBKfKQmjhNRf8dT79x10KmGmOsoFBbEha' . 'bFbH7xa3nYKwhdec+b/9LY2SO1ZKRP0EjGHkVwTEpuDYKBuYSoEEjKM2MbX06h0tfT19V0fc2VuJ' . 'qHjwLG319rIbsQp8mEHZwCA2cUyG+JoO/azt8qpnb0oibsgWSSuqkYAxDIMPidrFRtnAVAV0Q0LR' . 'sbIwHzvUbMwQXRtLgpvwKqvKbsQWX4quS0rXwMAgtlactn6Hm4Czth8uCr8qww7bonI3JHHgQ6Jm' . 'sVE2MIrDTa+QgFtjOtgZ9PDU7+ysa2+ja8PRYunT9Zk0Gp1f39BcU9tYVNKQnV+bnFH17E1tYmp7' . '/o6jbGAQG4ZhNAaj2+2TWvp6bVcljJrTVFQq339CHcCHRM1io2xg1ISbXjVoKEqC6jNyqDliPmUD' . 'wyA2DDPs0xU3+9Z8SIHsKwAfEtlQNjbKBqZCNLIbEgCKJm4CBrUffwMAIDNIwAAogPFASMAAAOlA' . 'AgZAXizPTtrmJm2XN+QU1KVmtX88AACVAAkYAHkZ+4mZfzD2cTtHAgBQIZCAAZCXuAkYyuWeABgA' . 'oMagFTQA8no3OYTsEAAAqgeugAEAAAASQAIGAAAASAAJGAAAACABJGAAAACABJCAAQAAABJAAgYA' . 'AABIAAkYAAAAIAEkYAAAAIAEkIABAAAAEkACBgAAAEgACRgAAAAgAYwFDYAU/rl+U09fH8OwyoqK' . 'iWNHkR0OUH9Tpk1fsPjfwcZ3bt18PeoKufEABYIrYAAAAIAEcAUM5LX34O8eXTxFlyxdMDfpY6LE' . 'DTs6OPx54pTgdV1tbeCIoUqJD6gpGo0Wc++h4HVBfv6MKRPJjQcAacEVMFC8BYuXkh0CAABQHSRg' . 'oHg+PXp279GL7CgAAIDS4BY0UIr5i5cuXTCH7CgUD+6TAwAUBa6AgVK4deo0yM+f7CgAAIC6IAED' . 'RXrzOkH4eu6ChXQtLRKDAQAAKoNb0FRhYmo6YODgPv187eztjU1MtRmM8orykqKi58+ePnp4Pzkp' . 'SYZ92nfoGDBiZPeevSwsOAaGhlVcbn5e7pPH8VcvR1ZUlCv8v4Bh2OWICxYWHCtrawzDbO3sR40Z' . 'eyUyQlE7V9RbZGxsMmRYgHfXbk4uLoYGhnp6enV1ddXV1dXVVVVcblZWZmpy0svnz/Pz8+SPuUev' . '3v7DAjq7e5iZW2AYVlpSnJOdfedWzIP7sU2NjfLsuf0rV1qeXt5Dh4/o7O5hZm7O0meVlZVlZqTf' . 'vX0r9u7thoYGsqP7f1zd3Ab5De3m42NiZmZqaqatrX37Zszmn9YL1urq6jo4OTs7Ozu7unXo0NHQ' . 'yJhtwGazDXR0dKq43MrKivfv3r149vR+7F1+c7O0h+7WvYf/sAB3D08LDgf77xNy9/ath/fvyfwu' . 'eXh69fP9rKtPdzNzcyMj4+bm5srKiuzMzGdPH9+9fau8rEy23QKFo7GNOcI/uvj4Cl68exlHUjya' . 'SFdXd9oXsyZPmaqjqyuuzKOHDw7u35uXm4O71tjY5Pzlq4LXHxM/hCycp6+v/9U3K/2HDcctX1db' . 'e3D/r1FX/pE/eOz/d0PasHYNk8lcvfbfM1dZaekXwZPEnUeId0OS/y0SYDAYs+ctnDwlWIsh+afn' . '6GF+bSMnPhCHtbXNyjU/eHfzwV1bVlq6a8fW+LiHEodZUHjlDvLzX7fxZ8Hrv48f++vPPxD/CwzD' . 'Zs9bMGPWv4/zf16/7t6dWxIjtLSy+n7dhi5e3rg7LCws+GXrlhfPn6KPi3A07FSHjg5ESqanpS6Y' . '/QUiVHMLi+++X+fTo2erDUUT8M9btvf7rL/EYxUWFhw5dODOrZtEAsMwzNzCInTV6t59+uGuLS8v' . '27V966OHD6QaiMPTy3vh0mWtugWKqq+vjzgffvzoER6PRzBOoBC46RVuQZPM0Mho174D02fORqQW' . 'DMP6fdb/4JFjXl27EdmnsbHJ/sNHxJ2gMQzT09cPXbVm3OeTpA6XgJvRN1JTUgSvTc3MJgYFy7lD' . 'Rb1F2jo6W3bsCp4+g0j2lZODo9Nvf/wpLvtiGGZqZvbT5m3DR0o9lha5lUuEja3d/sNHxGVfDMMs' . 'La22/rJ7yNCA9owKl7W1zf7DR9pmXwzD6HSatHuztLT6Yf1Pc+YvJH5ocdkXwzATE9ONm7aOGDWG' . 'eACTg6ft2ncAkX0xDGMymVNnzNy+61cWm018z0BJ4BY0mbR1dHbtPeDg6Cj4k8/n37h29UbU1cyM' . 'jPqGeisr6wGDBk+aEmxoaIRhGIvF2rZz94rlIR8/vEfsk8HQ/t/W7YLrg/v37t64djXx/XtuFdfI' . 'yKibT/cvZs8VXjosWhLy/OnjnOxshf+//vz94ObtvwheT5k2459LEVVVXNl2pcC3aNac+cJTbV1t' . '7ZXLkXEP72dnZnKruFpaWmw2u6ODo0cXz4GD/Vxc3WSLVsDY2GT7rl8FIQmOdeHc2bu3b+Xn5Wpp' . 'adnZd/AfFjB2wkRtbe3Qlasf3I8lvmcqVC6ajo7Oxk1bTExMMQx7+fzZxfPhiR/eV3G5Zubmvfr0' . 'C5o6zcbGFsMwOp3+3fdrK8rL5bkOlj/Un7ZsMze3wDDs0cMHUVcuC0K1sOC4d/Hs6ODQqnxdbe2j' . 'uIevXjxLTvpYXFTMreJqMxhm5hZe3t6B4yZ09ugiKDZ95uyc7OyYG9cQh2ax2Tv37hccGvvvqvTO' . 'rZjcnBwtLS37jg4BI0aOCRynxWCs+HYVwU/InPkLp8+cLfzzyeNHN6KiPrx7W1ZWytDWtrGx6T9w' . '8MSgYBaLhWGYdzefnzdv++arZS0tLQTfLqAMkIDJtDjkS2Fqqa+vX7t65asXz4Vrs7MyT4UdvxF1' . 'desvux2dnDEM09HVXf3Dj4vnzUI8HHJ2ccEwrLGhYdNP6x+KfHXLy8ru3LoZ9+D+L7/uF5wsdHR1' . 'g6fP3Ll1k8L/X0/iH71+9VJw/cdisWbMmn1w/17ZdqWot4ipp/f5pMmC11xu5VdLF2dnZQrX8pub' . 'y8vKysvKXr14firsuKub25wFi2QLGMOwZV+vMDUzE7wuyM9f+fWXoo+Tkz4mJn1MjL5+bceevYaG' . 'RoP8hhDfMxUqF01QCxiGHf3j8Kmw48LlhQUFVyIjYq5H/fjTpj79fDEM02IwQletnjdzmgxPOud+' . 'MQ2TeyQsQajNPN62zf+7fTNauDw/P6/V4/+iosK9u3Zei7rS6rF9Q3NzXm5OXm7OjWtRk4OnLVq6' . 'TLA8ZPnXcQ/v11RXizv04pAvLS2tBK+Li4pWhX4l+mn8+OH9xw/vb8VEb9mxi8ViEfmE9O7bT5h9' . 'udzKLT9tfPokXriWx+OlpaampaZGXbm8ZccuwX/cu5vPjFlzwv46KnHnQHngFjRpOjo4jJvw6ZSx' . '5ecNoqlFqLS0ZM3K0MqKCsGf9h06TJoyVeLOd2zd9BDvh3NDQ8OOrZv4fL7gz0GD/bR1dGSJXpI/' . 'Dh0Qvg4cN4HDsZRhJwp8i3y69xDewQ4/fUr0fNdWclLS9yu/ka0JTCd3j8FD/n2Y3djQsGZlKG5j' . 'rtSU5PU/rBFWhFRIr1yJrv5zWTT7CjU0NPz04w/paamCP62srYOmTm/f0Fo7dGCfaPbFtW/3L5cv' . 'XUQ3mjt35lT46ZOC12wDg+EjR4sr6eziOnJ0oOB1U1PT2tUrcT+N79++2bTxRwnRYxiGYdo6OivX' . '/CCyw1Wi2VdUSXHxmpWhXG6l4M9JU6bq6+sTOQRQEkjApBkv8pTu0cMHuKdUgZLi4uNHjwj/HDN2' . 'PI2GekD1/NkTREuQzIyMt/91FtLT13d16yRF0IR9eP/uQew9wWsdXd3Z8xfIsBMFvkWChtkCich7' . '+HIaM3ac8PXF8+cQmf5Nwqu7t1s3aJKICpWLVl1VdUTk51crDQ0Nh37bJ/xz1Jix6A+zUmWkp1+6' . 'cF5Rezvx19HqqirB64ARI8UVE/2E/HMpIjUlWVzJJ/GP4h7cl3jc4SNHC+75YxgWcT78/ds3iMIl' . 'xcVnT/0teM1isUb891MAkAISMDloNNoQkXY0F86dRZePunJZ+N224HC690QN9Hg54iJ6b69evhC+' . 'dnJ2kRCrrP78/VDzfy0thwaMaPtQDU2xb5GuLlP4mq205ic0Gm2wyPAjlyMuoMtfuij12Z8ilYtw' . '62Y0+pH/86dPsrOyBK8tOJyuPt3bJS4cUVcuK/AhaH1d3fNn/z7SdnR0wr39QKPR/PyHCf+U+AG4' . 'eD5c4nGFv1P5fP65s6cllr929VM76h7IMwlQNkjA5HB0chK0hsAwrLKiIkHkpImLx+M9inso/LOL' . 'p5e4knw+/+XzZ+i9ibbNMTU1lRyuTLKzMqP/a4pCp9PnL5JuhgbFvkXlZaXC1+MmTFTSVZeTs4ve' . 'f/f0UpKTiooK0eXfv31TXi5Fp0zqVC7C/Xt3JZZ5EPupDOLDrGxP4hXc3zIjPU3wQovBcHFxbVvA' . '0cmJbWAgeJ2eloruNYdhWMLLF8I7xriMjU2EjSSSEj8Q6ePLrawU/gAi8c0HGCRgsnR29xC+Tk76' . 'SORnuOiN084eHuKKlZaU1NbWondVXV0lfC1Mcsrw15E/Gv97ktrvs/4e0nzbFfsWvXn9Wvjap0fP' . 'X37dL1UwBIne8k1O+khkk7TUFOL7p1TlipOU+EFiGdE3p1Nnd2WGI1Z9fX1ujoT8Jy0u99Olv7Ah' . 'nii3Tp/+sx8TJU/Z2dLSkpos9h41hmGe3p+6e6WmphKKEsNKS0sEL9gGBkw9PYJbAYWDVtDkMPuv' . 'BwKGYVmZqAZBQpkZ6Z82N7MQV0z0/CtOo0hzEh1lttMpLS25eP5c8PQZgj8XLQn5KmQxwW0V+xbl' . '5eY8efxI2O3Su5vP3gOH8/Jy4x8+fPH86ZvXCYg2q8RxLD+1NcvJziKySXZmZo+evQnun1KVi6u8' . 'vEziTwTs/1eo6JvWnirKy6W9/2xnb99/4GBXt04dHRyNjI309PSZTKa4wrhPOmT4hGRlZeL2VP4v' . 'pA7C1yNHjxnxX89yGh3n4oqOt9DQwLC+ro5IJEDhIAGTQ/TLSeSsimFYlciPa0NDQ3HFGuUb4FDh' . 'Tp88MWbsOMFtty5e3n19P4sXuVGMoPC36JetW/Ye+l3Y/QPDMBsb288nB30+OYjP56elprx8/vzu' . 'nVvobtZoolec1VWEMnpNTQ3x/VOtctsi+L8WrVA220Bp4aBI9c67urktWfYVYmSVtph6OA2MRf+z' . '1cR+8wlbNuAS/ZzT6XQML8Wi6enDFTBp4BY0OUS/nA0N9UQ2ES2mpzp3jWqqq0+fPCH8c95ColfA' . 'Cn+LSktLQhbMu3fndtsN6XS6i6vb5OCpvx0+cujPv3r17kswyFZEYyaYLKk2KrKciNZU/af/NVkf' . 'Zh6viWDJIUMD9h86IlX2xcSMpaUrcsXcUE/wU436hOiT8ZQBKApcAZOjvu7TbTrRBroIosXqVOqW' . '0cXz5yZMChKM++Po5Dxs+Ej0OEECyniLKirKf16/9sQxhxGjAvv064c7nrCLq9uWnbvOnPz7yGGx' . 'fWnEET2lErz9q4scX1PlEP1fMz/9ryn+Ye7k7rF67Y/Cm7fl5WW3Y6LfvE7Iy80pLiqub6gX7R88' . 'dvzny0O/RexN4Z+QxoZPRz98YP+5M6eI7BNQBCRgcojefSJ4C070lqxoWw/qa2psPHH0SOiqNYI/' . 'Z89fcOdWjMSx4JX3FmVmZBw+sO/wgX0mpqbeXX26+fh0697TvkMH0TLB02dkZqQT+aHw/2P+dLeQ' . '4KUJKe2kpKIlzZyShGtK9E4soecLZFm6bLkw+969fXPb5v8hRuSQeDv3/917NyDUHU7YahpXZWWF' . '8LVoZ3egEuAWNDmKi4qErzt07Ehkk44OjsLXpaXFio9Jma5dvZKVmSF4bWlpNX6i5KkC2uEtKi8r' . 'u3fn1q+7ds6ZETx7+pSL58Kbmj7dlpw1dz6Rg4oqKZY6ZtFGNO2jufnTTx8iyVWqbtOmZmZEBlcS' . 'fXOKCiV01iKRuYWFcFaJ4qIidPbFMMzYWEK/L9H/rH0HByIxdOiA+iCJfuRI6fYN5AEJmByiHWZc' . '3ToR6ZYq2q/mw7t3SglLaVpaWo7+cVj457QvZkk8TbfzW5STnX1g3571P6wWLrGytraxtZNqJ6Id' . 'S5xdCM3o4OyK01tUqWprPt3bJ3KZbm1jK9X+3Qh0KxLtr/WRQLclsojGGffwvsQpnDt17owukPTx' . '039WYmEMw2g0GvoT8vrVK+FrD48u0KdItUACJkdGepqwcaORsbHEwYAYDIbvZwOEf757gxptjpoe' . 'xN57/+6t4LWhodGUaTPQ5Ul5i57EP0r6+KmLqoWF2O5euNJSU+r+64Tj1qmTxBGwPTy9hIMItpuq' . 'qk8DO9hK+oXBYDDckdPbtTVg0GCJZfoP/FTmHXLoRISWlhbhwNc06WcPJMLIyFj4WvSWDC5DIyPR' . 'zuu40tPShK2vHZ2cJf7C6+rTXTitFq78/LyC/HzBay0GI2C42CEwAQVBAibNLZEh4CdOnoIuPGJ0' . 'oPBRUHFR0csXEoZDoibRGRomBgUjejMLkPIWlZZ8unfdJOWk5S0tLXdF5qsPHD8BXX7chM+l2r9C' . 'ZGRkCIcI7ezuQUfehfYfFiDtU2r/oQHouWZ79OotfOJeXFQkcYwzBGGbJiaxZnrSEm3Kjuj7J/D5' . 'pCD0lNUYhrW0tNy5FSP8c7ykaZs/nxQkKUYsUmTE0ynTZ8BFsAqBBEyayxEXhL/f+33Wv99n/cWV' . 'NDMznzPv02QGkREXVHQWzzcJr548fiR4zWQyv5gzF12elLdIOKEen8/Pz8uVdvOofy4LX0+cPMXO' . '3l5cSU8vb9FhgdtNU2Njyn8TALANDAYN9hNX0tDIaN7CJdLun21gsHDJMnFrtXV0Fod8KfxTztGY' . 'yyvKBS8MDA2VMfVTQUG+8HXvvr6Ikq5ubhJv6gj8cylC+Dpw/AQnZ2dxJXv37efbf4C4tUKXIy4I' . 'pwKztLT69rs1RMIAVAAJmDSZGRmig/V/v25DV7yOhiamppt37DIy/vdWWHZWlsRpCajsyKEDwpzq' . '5d0VXViBb9Gyr1bMnrdA4j3h2fMWCJuSvnv7hsjIuq18eP/u7u1/ZyvS0dXdvGOXpZVV22JOzs4b' . 'N23FHZmoHURf/9S6e1HIl7gRmpmZ79i9D3c8RYlGB46d9sWstst1dXV/3Pg/4U+cwsIC4RR+bfYw' . '7mZsnPAfg4HfXyMt5d+BPOl0es9eRAcUI+7Du7fCoZgdHB1nzpmHW8zD02vbrl+1tbWJ7DM1Jfl6' . '1FXBa21t7U3bfsH9lebh6fXD+p+I7LChoWHb5p+FX6vBQ4b+tHmbgYGE6/Wu3XzW/7zZq2s3IocA' . 'SgLdkMh0+OBvXX26C85Hevr6O/bsu3b1yo1rVzMz0hsbGy0tLfsPHBw0dZrwIVB9ff2WnzdIbAlC' . 'ZWmpqXduxfiLTHOEpqi3yMTUbPzEyTNmzUlJTnr8KC456WNGelp5eXltTQ1DW9vCgtPZ3WPM2HHC' . 'wRb4fL7orHlS2b9nt3dXH0HqsrGxPfLX3+fOno69eyc/L5dGp9vb2w8ZOnz8xEna2trNPN6D+7FE' . 'ZlxXrBvXrgZNnSYYFMzc3OLA70f/PnEs7sH94uJipq6unX2HAYMGj/98kp6+fk1NzdPHj4QzHEuU' . 'npZKo9EdHB3nLljk073HxfPhH969q66uMjU169Wn75TpM2z+a9LVzOPt2bFdznFInsTH9R84SPB6' . '+YpvmUzmq5cvZBhjUpyWlpbw06fmL/r3NsDMOfM6dXa/eD488cP72poaA0PDTp3chw4f7uc/jE6n' . 'N/N4d+/cIvLZPvzbPp8ePQTvvwWHc+jP4xfCz9y7cys3J0dLS8u+o0PAqWLycQAABeZJREFUiJFj' . 'AsdpMRgEPyFP4h8d/eOwME7f/gP+Dr9w/eo/jx89Sk9Prays1NLSYumzOJaWTs4uXby8+vT1FXw+' . 'IyVN2AWUChIwmZoaG0O/DNn6y27BePR0On104NjRgWNxC1dXVa1dvTLpo+QB3Cnu6B+HBw4eQvBy' . 'QeFvkYurm4urhPbJzTzejq2bZB6TsqKifFXoV7v2/Sb4WaCnrz9zzry2F098Pn/3zu1GxkbC02tz' . 'c7NsR5RWfV3d9k0/b9mxS/DM0sjYOGT5ipDlK1oXq6//cc0qxEDEbTU2Nm7auP7XA4dMTEx9evQU' . 'ty2fz9+5bYu4eeOJuxUTPe2L2YKbFhYcTttLxvS01AWzv5DnEOFnTvXo2Uv4H+nTz7dPP5x70c08' . '3rbN/2Oz2UQScFUVd+XXy/fsPyjIgkwmc/rM2dNnzm5VrO0nBOHMybDSkpIV364S1CmLxZoYFDwx' . 'KFjihoBEcAuaZFVV3NAvl5488Vcj8lIg7sH9pQvnvX3zGlFGVRQWFIg+BpOond+ij4kflocsvhl9' . 'Q56dZKSnLVu04PWrl+IKlJeXrf9h9fWoK6Kjd9XWSjE6sZwSXr387tsViJa9qSkpy5csSBD/XxAn' . 'Lzdn2aL578RXRGFhwZqVodIOcoKroaHhx+9X5Un/qJ44fnPz9999K7xpjCsvL3dV6Fe3RdoMSiR4' . 'l549eSyuQHl52cZ131+PuiKuQFsxN64tmjf77u2bwtvRgOLgCph8DQ0Nx478funi+QEDB/fp52tn' . 'b29sYqrNYFRWVhYVFrx88fzh/XvJSUlkh6lIf584NnzUaOLNa+V/i7Zu+unMyRNOzi4Ojs4dHR2M' . 'jU0NDQ3ZbLaenl5jY2N1dXVOdtbHxMTYu7cVdY8hLzcndHlIr959hwwL6OzuYW5ujmFYSUlJTnbW' . 'nVs3H96/J7j7+v/GhEIOu69wbxJezZwWNHzk6H6+nzk5uxgZGTU2NpaUlCQnfbwZff3Fs6cy38gt' . 'LCj4KmSxp5f30OEj3D08zc3N9fT0ysrKsrIy7t2+dff2LQWOgJ2Wmjpv5vRBg/36+n7m5Oxiamqm' . 'p6enJeaZsWyaGht3bt108dyZUWPGenX1sbS01GUyq7jcsrLSlOTke7dvPX/+lC/93YuiosLV367o' . '3qPXkGEBHl26mJlb0Gm0tp8QqWRnZf5vw4/W1od69/P16d6jQ8eORkbGbDZb8CGvqampra0pKixI' . 'S01NT0tNS00pLCiQ9hBAgWhsY47wjy4+/95aefdSwfNUAwBwbdu1Rzgd4fSgz1XxhGhsbHL+8r8X' . 'iB8TP4QsxG+pBIAmw02vcAsaANIwGIzO7l0ErysrKlQx+wIAZAYJGADSiA5z8TrhFbowAEDNQAIG' . 'gBythrkQHcEDAKAJIAEDoHirvl/72YCBiALW1ja79h4QDnORnpYqf58cAIBqgVbQACheRwfHgBGj' . 'CvLzb8XceJ3wKiMtrbKygkanGxkZubp18u0/YGjACGFP6MaGhi0/byA1XgAACSABA6AsVtbW02fO' . 'no4sU1db+/OGdWmpqe0UEwCAMiABA0CaZ08eH9z/a2ZGBtmBAABIAAkYAMVbszK0m093dw9PRycn' . 'E1MzFoslGPSjrq6OW8XNy8l5nfAqPu4BXPgCoMlgIA4AAABAuWAgDgAAAIAqIAEDAAAAJIAEDAAA' . 'AJAAEjAAAABAAkjAAAAAAAkgAQMAAAAkgAQMAAAAkAASMAAAAEACSMAAAAAACSABAwAAACSABAwA' . 'AACQABIwAAAAQAJIwAAAAAAJIAEDAAAAJIAEDAAAAJAAEjAAAABAAkjAAAAAAAkgAQMAAAAkgAQM' . 'AAAAkAASMAAAAEACSMAAAAAACSABAwAAACSABAwAAACQABIwAAAAQAJIwAAAAAAJIAEDAAAAJIAE' . 'DAAAAJAAEjAAAABAAgbu0i4+vu0cBwAAAKBR4AoYAAAAIAEkYAAAAIAENLYxh+wYAAAAAI0DV8AA' . 'AAAACSABAwAAACSABAwAAACQ4P8Are+g/sSPHccAAAAASUVORK5CYII=' ; } function nst_ad_png_path(string $id, bool $create = false): string { $id = preg_replace('/[^a-f0-9]/', '', strtolower($id)) ?? ''; $aa = nst_addr_shard2($id); $root = ads_dir(); $dir = $root . DIRECTORY_SEPARATOR . $aa; if ($create && !is_dir($dir)) @mkdir($dir, 0755, true); $p = $dir . DIRECTORY_SEPARATOR . $id . '.png'; if (is_file($p)) return $p; $flat = $root . DIRECTORY_SEPARATOR . $id . '.png'; if (!$create && is_file($flat)) return $flat; return $p; } function nst_ad_png_wipe(): void { nst_ad_slots_wipe(); $root = ads_dir(); foreach (glob($root . DIRECTORY_SEPARATOR . '*.png') ?: [] as $f) { if (basename($f) === '_placeholder.png') continue; @unlink($f); } foreach (glob($root . DIRECTORY_SEPARATOR . '[a-f0-9][a-f0-9]' . DIRECTORY_SEPARATOR . '*.png') ?: [] as $f) @unlink($f); } /** * Yearly reset wipe (product intent = Chinese New Year epoch boundary). * User NSU wallet balances and existing wallet-access records survive (2026-09-29). * Superstructure is burned so a leaked/brute-forced panel password can only * steal for the remainder of the epoch - not forever. * * CNY / epoch inferences (operator 2026-07-15, honesty 2026-08-15): * - Rent is an epoch lease: LORD operator seed + rented flag die at wipe. * - Re-anchor is retired (no-op). After birth nothing is printed. * - User balances survive (no seizure); open orders / ads / profit piles do not. * - Password product path (if any residual) is re-keyed; seed-panel is product door. * - Existing device-wallet records, HMAC and lock survive to preserve access. * Legacy compatibility rows, if present, are retained unchanged; no new seeds are stored. * * Wiped: profit pile, ad stakes/bids, ad creatives, donate credit log, rent lease. * Not wiped: transfer ledger (balances), device-wallet records / lock / HMAC. * Explicit device clear and signed bind replacement retain their existing behavior. * Real-chain vault seeds stay offline * (operator must move external funds; site does not auto-sweep chains). * Admin panel password: AUTOMATICALLY rotated on wipe so leaked passwords die * at year boundary; new cleartext lands in vault/ only. */ function economy_on_epoch_roll(int $fromN, int $toN): void { $prev = load_economy(); $oldProfit = (string)($prev['profit_micros'] ?? '0'); $oldAd = (string)($prev['ad_bid_micros'] ?? '0'); $oldBids = is_array($prev['bids'] ?? null) ? count($prev['bids']) : 0; $oldLordCredit = (string)($prev['lord_credit_micros'] ?? '0'); // burn site-local superstructure; end renter lease; stash last-year performance $e = [ 'profit_micros' => '0', 'ad_bid_micros' => '0', 'donate_k' => (string)($prev['donate_k'] ?? NST_DONATE_K), 'bids' => [], 'donations' => [], 'epoch_n' => $toN, 'rolled_from' => $fromN, 'roll_ts' => time(), 'wipe' => 'balances_and_wallet_access_survive', 'wipe_calendar' => 'chinese_new_year_epoch', // CNY: renter lease ends — new year needs new purchase + new LORD seed 'rented' => false, 'operator_addr' => '', 'rented_at' => 0, 'lord_credit_micros' => '0', // performance inputs for next rent grant formula 'last_epoch_profit_micros' => $oldProfit, 'last_epoch_ad_bid_micros' => $oldAd, 'last_epoch_lord_credit_micros' => $oldLordCredit, 'last_epoch_n' => $fromN, ]; save_economy($e); // clear public operator.addr file so panel falls back to KING treasury until re-rent global $OP_ADDR_FILE; if (isset($OP_ADDR_FILE) && is_file($OP_ADDR_FILE)) { @file_put_contents($OP_ADDR_FILE, '', LOCK_EX); } nst_ad_shares_burn(); // TR-RESET: presentation expire keeps slot owners and committed outcomes. // It no longer archives those files or publishes an empty index. // Open-order cancellation below is unchanged and was not run in the lab. if (function_exists('nst_epoch_roll_presentation_guarded')) { nst_epoch_roll_presentation_guarded($fromN, $toN); } else { // delete ad PNGs under data/ads/{aa}/ and legacy flat nst_ad_png_wipe(); } // cancel open orders so book does not survive as superstructure $ts = time(); $nCancel = 0; foreach (open_orders(read_chain()) as $o) { $oid = (string)($o['id'] ?? ''); $addr = (string)($o['addr'] ?? ''); if ($oid === '' || $addr === '') continue; append_row([ 'type' => 'cancel', 'ver' => 1, 'order_id' => $oid, 'addr' => $addr, 'ts' => $ts, 'sig' => 'year_wipe', 'auth' => 'epoch_roll', 'note' => 'yearly wipe - open orders do not survive', ]); $nCancel++; } // permanent early modifiers do not survive the yearly service reset global $MODS_FILE; if (is_file($MODS_FILE)) { @file_put_contents($MODS_FILE, '{}', LOCK_EX); } // Wallet Inbox v0: sealed notices burn with superstructure (balances survive). nst_inbox_burn_all(); // leftover panel password dies (product door is site seed) $rotated = false; try { admin_pass_burn(); $rotated = true; @file_put_contents( vault_dir() . DIRECTORY_SEPARATOR . 'YEAR_WIPE.txt', "Year wipe epoch $fromN -> $toN at " . gmdate('c') . "\n" . "Leftover panel password unlinked (admin.pass.hash / admin.pass.txt / ADMIN_PASSWORD.txt).\n" . "Panel door is site seed only.\n" . "User NSU balances and existing wallet-access records survived.\n" . "Device-wallet records and HMAC retained; browser storage expiry remains independent.\n" . "Re-anchor retired; after birth nothing is printed. No drip.\n", LOCK_EX ); } catch (Throwable $ex) { $rotated = false; } // Wallet continuity: do not unlink/re-key identity records or their stable lock. // This retains existing legacy compatibility rows too; explicit device cleanup is unchanged. $deviceWalletBurned = false; // Re-anchor is retired (no-op). Do not mint to make old comments true. Users keep coins. $anchor = ['action' => 'skipped', 'pop' => 0, 'source' => '', 'target' => '0', 'before' => '0', 'after' => '0']; try { $anchor = nst_treasury_reanchor_to_population('year_wipe_epoch_' . $fromN . '_to_' . $toN); } catch (Throwable $ex) { $anchor['action'] = 'error'; } // Book was wiped empty — re-seed loose discovery spread (no live orders to protect). $boot = ['ok' => false]; try { $boot = nst_bootstrap_spread_if_empty(); } catch (Throwable $ex) { $boot = ['ok' => false, 'err' => 'bootstrap_exception']; } append_row([ 'type' => 'economy_year_wipe', 'ver' => 2, 'ts' => $ts, 'from_epoch' => $fromN, 'to_epoch' => $toN, 'wiped_profit_fmt' => fmt_amt($oldProfit), 'wiped_ad_bid_fmt' => fmt_amt($oldAd), 'wiped_bids' => $oldBids, 'orders_cancelled' => $nCancel, 'admin_rotated' => $rotated, 'population' => $anchor['pop'] ?? 0, 'population_source' => $anchor['source'] ?? '', 'treasury_target_fmt' => isset($anchor['target']) ? fmt_amt((string)$anchor['target']) : null, 'treasury_reanchor' => $anchor['action'] ?? null, 'treasury_after_fmt' => isset($anchor['after']) ? fmt_amt((string)$anchor['after']) : null, 'bootstrap_spread' => !empty($boot['ok']) && empty($boot['skipped']), 'bootstrap_note' => $boot['reason'] ?? ($boot['amount_fmt'] ?? null), 'device_wallet_burned' => $deviceWalletBurned, 'device_wallet_access_preserved' => true, 'siphon_note' => 'profit/ad superstructure burns; wallet balances and existing access records survive; re-anchor retired; renters never mint; after birth nothing is printed', 'note' => 'yearly reset: wallet balances and existing access records survive; service superstructure wiped; admin re-keyed; re-anchor retired; bootstrap spread if book empty', ]); @file_put_contents( vault_dir() . DIRECTORY_SEPARATOR . 'YEAR_WIPE.txt', "Year wipe epoch $fromN -> $toN at " . gmdate('c') . "\n" . "Leftover panel password unlinked; panel door is site seed only.\n" . "Old panel passwords are dead.\n" . "User NSU balances and existing wallet-access records survived.\n" . "Device-wallet access records removed by annual reset: " . ($deviceWalletBurned ? 'yes' : 'no') . "\n" . "Rent siphon: profit/ad piles wiped. Re-anchor retired. After birth nothing is printed (renters never mint).\n" . "CNY epoch: LORD rent lease ended; operator_addr cleared; next renter gets new seed + new grant.\n" . "Last-year profit stashed for next LORD rent flat-performance term: " . fmt_amt($oldProfit) . " NSU.\n" . "Treasury re-anchor call (retired no-op): action=" . ($anchor['action'] ?? '?') . " pop=" . ($anchor['pop'] ?? '?') . " source=" . ($anchor['source'] ?? '?') . " target_fmt=" . (isset($anchor['target']) ? fmt_amt((string)$anchor['target']) : '?') . " after_fmt=" . (isset($anchor['after']) ? fmt_amt((string)$anchor['after']) : '?') . "\n" . "No faucet re-mint. Live constitution is king-cap-at-birth + no drip.\n" . "Rent grant = % remaining faucet + flat(base+last year perf).\n", FILE_APPEND ); } /** * House / empire discovery ads (text). Compartment-local board only. * Recruits sister nosignup.* faces + rare aligned no-signup free tools. * Not a shared ad exchange - each site's pick is independent. */ function house_ad_roster(): array { return [ ['id' => 'house_chat', 'href' => 'https://nosignup.chat/', 'label' => 'nosignup.chat - free peer chat, no account', 'weight' => 3], ['id' => 'house_work', 'href' => 'https://nosignup.work/', 'label' => 'nosignup.work - jobs & resumes, no signup', 'weight' => 2], ['id' => 'house_market', 'href' => 'https://nosignup.market/', 'label' => 'nosignup.market - classifieds, no signup', 'weight' => 2], ['id' => 'house_date', 'href' => 'https://nosignup.date/', 'label' => 'nosignup.date - personals map, no signup', 'weight' => 2], ['id' => 'house_fun', 'href' => 'https://nosignup.fun/', 'label' => 'nosignup.fun - media organism, no signup', 'weight' => 2], ['id' => 'house_info', 'href' => 'https://nosignup.info/', 'label' => 'nosignup.info - free modules, no signup', 'weight' => 2], ['id' => 'house_com', 'href' => 'https://nosignup.com/', 'label' => 'nosignup.com - free forever pinwheel map', 'weight' => 2], ['id' => 'house_org', 'href' => 'https://nosignup.org/', 'label' => 'nosignup.org - free forever MIRROR pinwheel', 'weight' => 2], ['id' => 'house_net', 'href' => 'https://nosignup.net/', 'label' => 'nosignup.net - free map · MAP pinwheel', 'weight' => 2], ['id' => 'house_trade', 'href' => 'https://nosignup.trade/', 'label' => 'nosignup.trade - disposable NSU wallet, no signup', 'weight' => 3], // aligned free no-signup directory (external; spirit match, not a dependency) ['id' => 'house_fns', 'href' => 'https://freenosignup.com/', 'label' => 'freenosignup.com - free tools, no account (aligned)', 'weight' => 1], ]; } function house_ad_pick(): array { /* NS_HOUSE_AD_PLACEHOLDER 2026-09-26 */ /* NS_HOUSE_PER_FACE_DIMS 2026-09-26 */ /* ONE house creative family. Unsold board space is not a sister-site carousel. * Click goes to nosignup.trade. Per-slot art shows Buy this ad slot / N/10000. Promo mailbox stays ops-only. */ return [ 'id' => 'house_trade_placeholder', 'active' => true, 'house' => true, 'stake_micros' => '0', 'stake_fmt' => 'house', 'href' => 'https://nosignup.trade/', 'label' => 'Buy this ad slot · on Nosignup.trade', 'creative_url' => '', 'creative_file' => '', ]; } /* -------- Promo ad credit: mail-front-door KING spend (NOT remint, NOT public faucet) -------- */ function nst_promo_email_norm(string $email): string { $email = strtolower(trim($email)); $email = preg_replace('/\s+/', '', $email) ?? ''; return $email; } function nst_promo_email_ok(string $email): bool { $email = nst_promo_email_norm($email); if ($email === '' || strlen($email) > 190) return false; return (bool)preg_match('/^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,24}$/', $email); } function nst_promo_claims_path(): string { return nst_data_file('promo.ad.claims.json'); } function nst_promo_identity_path(): string { return nst_data_file('promo.identity.json'); } function nst_promo_bot_path(): string { return nst_data_file('promo.bot'); } function nst_promo_mailbox_path(): string { return nst_data_file('promo.mailbox.json'); } function nst_promo_maildir_root(): string { $d = nst_data_file('promo-maildir'); foreach (['cur', 'new', 'tmp'] as $sub) { $p = $d . DIRECTORY_SEPARATOR . $sub; if (!is_dir($p)) @mkdir($p, 0755, true); } $ht = $d . DIRECTORY_SEPARATOR . '.htaccess'; if (!is_file($ht)) @file_put_contents($ht, "Require all denied\nDeny from all\n", LOCK_EX); return $d; } function nst_promo_rand_pass(int $n = 24): string { $alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@$%^*-_'; $out = ''; $max = strlen($alphabet) - 1; for ($i = 0; $i < $n; $i++) { $out .= $alphabet[random_int(0, $max)]; } return $out; } function nst_promo_bot_ensure(): string { $p = nst_promo_bot_path(); $dir = dirname($p); if (!is_dir($dir)) @mkdir($dir, 0755, true); if (!is_file($p) || (int)filesize($p) < 32) { @file_put_contents($p, bin2hex(random_bytes(32)) . "\n", LOCK_EX); @chmod($p, 0600); } $k = strtolower(trim((string)@file_get_contents($p))); $k = preg_replace('/[^a-f0-9]/', '', $k) ?? ''; return strlen($k) >= 32 ? $k : ''; } function nst_promo_mailbox_ensure(): array { $p = nst_promo_mailbox_path(); $dir = dirname($p); if (!is_dir($dir)) @mkdir($dir, 0755, true); $j = []; if (is_file($p)) { $j = json_decode((string)@file_get_contents($p), true); if (!is_array($j)) $j = []; } $changed = false; if (trim((string)($j['username'] ?? '')) === '') { $j['username'] = 'ads'; $changed = true; } if (strlen((string)($j['password'] ?? '')) < 20) { $j['password'] = nst_promo_rand_pass(24); $changed = true; } $j['address'] = NST_PROMO_MAILBOX; $j['maildir'] = nst_promo_maildir_root(); if (empty($j['created_ts'])) { $j['created_ts'] = time(); $changed = true; } if ($changed || !is_file($p)) { @file_put_contents($p, json_encode($j, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n", LOCK_EX); @chmod($p, 0600); } return $j; } function nst_promo_claims_load(): array { $p = nst_promo_claims_path(); if (!is_file($p)) return ['claims' => []]; $j = json_decode((string)@file_get_contents($p), true); if (!is_array($j) || !isset($j['claims']) || !is_array($j['claims'])) return ['claims' => []]; return $j; } function nst_promo_identity_get(string $addr): string { $addr = strtolower(preg_replace('/[^a-f0-9]/', '', $addr) ?? ''); if (strlen($addr) !== 64) return ''; $p = nst_promo_identity_path(); if (!is_file($p)) return ''; $j = json_decode((string)@file_get_contents($p), true); if (!is_array($j)) return ''; $row = $j['by_addr'][$addr] ?? null; if (!is_array($row)) return ''; $em = nst_promo_email_norm((string)($row['email'] ?? '')); return nst_promo_email_ok($em) ? $em : ''; } function nst_promo_identity_put(string $addr, string $email): void { $addr = strtolower(preg_replace('/[^a-f0-9]/', '', $addr) ?? ''); $email = nst_promo_email_norm($email); if (strlen($addr) !== 64 || !nst_promo_email_ok($email)) return; $p = nst_promo_identity_path(); nst_with_file_lock($p, static function ($fh) use ($addr, $email): void { rewind($fh); $raw = stream_get_contents($fh); $j = is_string($raw) ? json_decode($raw, true) : []; if (!is_array($j)) $j = []; if (!isset($j['by_addr']) || !is_array($j['by_addr'])) $j['by_addr'] = []; $j['by_addr'][$addr] = ['email' => $email, 'ts' => time()]; $dump = json_encode($j, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; rewind($fh); ftruncate($fh, 0); fwrite($fh, $dump); fflush($fh); @chmod(nst_promo_identity_path(), 0600); }); } /** * KING spend of already-minted coins (treasury → visitor). Once per email, FCFS 100. * Never mints. Never logs the treasury seed. */ function nst_promo_ad_claim(string $email, string $toRaw): array { if (function_exists('nst_conservation_is_frozen') && nst_conservation_is_frozen()) { return ['ok' => false, 'err' => 'Trading is paused while the ledger is checked']; } $email = nst_promo_email_norm($email); if (!nst_promo_email_ok($email)) { return ['ok' => false, 'err' => 'Need a plausible email']; } $to = nst_resolve_receipt($toRaw); if (strlen($to) !== 64) { return ['ok' => false, 'err' => 'Need a real NSU ID (ticket or 64-hex)']; } $pay = NST_PROMO_AD_CREDIT_MICROS; $sec = treasury_secret(); $from = addr_from_seed($sec); $out = nst_with_chain_lock(static function (array $chain, callable $append) use ($email, $to, $pay, $from, $sec): array { $store = nst_promo_claims_load(); $claims = is_array($store['claims'] ?? null) ? $store['claims'] : []; foreach ($claims as $c) { if (!is_array($c)) continue; if (nst_promo_email_norm((string)($c['email'] ?? '')) === $email) { return ['ok' => false, 'err' => 'already credited']; } } if (count($claims) >= (int)NST_PROMO_AD_MAX_CLAIMS) { return ['ok' => false, 'err' => 'promo closed']; } $bal = balances($chain); $have = available_nsu($bal, $chain, $from); if (dec_cmp($have, $pay) < 0) { return ['ok' => false, 'err' => 'treasury empty']; } $ts = time(); $nonce = bin2hex(random_bytes(8)); $memo = 'promo:ad-credit'; $tbody = implode('|', ['transfer', '1', $from, $to, $pay, $nonce, (string)$ts, $memo]); $row = [ 'type' => 'transfer', 'ver' => 1, 'from' => $from, 'to' => $to, 'amount' => $pay, 'amount_fmt' => fmt_amt($pay), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => mac_sign($sec, $tbody), 'kind' => 'promo_ad_credit', ]; $sec = ''; if (!$append($row)) { return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment']; } $claims[] = ['email' => $email, 'addr' => $to, 'ts' => $ts]; $dump = json_encode(['claims' => $claims], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; @file_put_contents(nst_promo_claims_path(), $dump, LOCK_EX); @chmod(nst_promo_claims_path(), 0600); nst_promo_identity_put($to, $email); return [ 'ok' => true, 'credited_fmt' => fmt_amt($pay), 'addr' => $to, 'claims_used' => count($claims), 'claims_cap' => (int)NST_PROMO_AD_MAX_CLAIMS, ]; }); if ($out === null) { return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment']; } return $out; } function nst_promo_extract_email_from_header(string $from): string { $from = trim($from); if (preg_match('/<([A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,24})>/i', $from, $m)) { return nst_promo_email_norm($m[1]); } if (preg_match('/([A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,24})/i', $from, $m)) { return nst_promo_email_norm($m[1]); } return ''; } function nst_promo_extract_id_from_text(string $text): string { $text = str_replace("\r", "\n", $text); if (preg_match('/nsu\s*id\s*[:#]?\s*([0-9]{1,18}|[a-f0-9]{64})/i', $text, $m)) { $got = nst_resolve_receipt($m[1]); if ($got !== '') return $got; } if (preg_match('/\b([a-f0-9]{64})\b/i', $text, $m)) { $got = nst_resolve_receipt($m[1]); if ($got !== '') return $got; } if (preg_match('/(?:^|[^\d])([1-9][0-9]{0,17})(?:[^\d]|$)/', $text, $m)) { $got = nst_resolve_receipt($m[1]); if ($got !== '') return $got; } return ''; } function nst_promo_parse_rfc822(string $raw): array { $raw = str_replace("\r\n", "\n", $raw); $head = $raw; $body = ''; $split = preg_split("/\n\n/", $raw, 2); if (is_array($split) && count($split) === 2) { $head = $split[0]; $body = $split[1]; } $from = ''; $subj = ''; foreach (explode("\n", $head) as $ln) { if (stripos($ln, 'From:') === 0) $from = trim(substr($ln, 5)); if (stripos($ln, 'Subject:') === 0) $subj = trim(substr($ln, 8)); } return [ 'from' => $from, 'subject' => $subj, 'body' => $body, 'email' => nst_promo_extract_email_from_header($from), 'to_raw' => nst_promo_extract_id_from_text($subj . "\n" . $body), ]; } function nst_promo_maildir_ingest(int $limit = 8): array { nst_promo_mailbox_ensure(); nst_promo_bot_ensure(); $root = nst_promo_maildir_root(); $new = $root . DIRECTORY_SEPARATOR . 'new'; $cur = $root . DIRECTORY_SEPARATOR . 'cur'; $files = []; foreach (['new', 'cur'] as $box) { $dir = $root . DIRECTORY_SEPARATOR . $box; if (!is_dir($dir)) continue; foreach ((array)@scandir($dir) as $fn) { if ($fn === '.' || $fn === '..') continue; $p = $dir . DIRECTORY_SEPARATOR . $fn; if (!is_file($p)) continue; if (str_ends_with($fn, '.seen')) continue; $files[] = $p; } } usort($files, static function (string $a, string $b): int { return (int)@filemtime($a) <=> (int)@filemtime($b); }); $out = ['ok' => true, 'scanned' => 0, 'credited' => 0, 'skipped' => 0, 'items' => []]; $n = 0; foreach ($files as $p) { if ($n >= $limit) break; $seen = $p . '.seen'; if (is_file($seen)) continue; $n++; $out['scanned']++; $raw = (string)@file_get_contents($p); $msg = nst_promo_parse_rfc822($raw); $item = [ 'file' => basename($p), 'from' => (string)($msg['from'] ?? ''), 'subject' => (string)($msg['subject'] ?? ''), 'email' => (string)($msg['email'] ?? ''), 'result' => '', ]; if (!nst_promo_email_ok((string)$msg['email'])) { $item['result'] = 'no from-email'; $out['skipped']++; @file_put_contents($seen, $item['result'] . "\n", LOCK_EX); $out['items'][] = $item; continue; } if ((string)$msg['to_raw'] === '') { $item['result'] = 'no NSU ID'; $out['skipped']++; @file_put_contents($seen, $item['result'] . "\n", LOCK_EX); $out['items'][] = $item; continue; } $claim = nst_promo_ad_claim((string)$msg['email'], (string)$msg['to_raw']); $item['result'] = !empty($claim['ok']) ? 'credited' : (string)($claim['err'] ?? 'fail'); if (!empty($claim['ok'])) $out['credited']++; else $out['skipped']++; @file_put_contents($seen, $item['result'] . "\n", LOCK_EX); $base = basename($p); $dest = $cur . DIRECTORY_SEPARATOR . $base; if (strpos($p, DIRECTORY_SEPARATOR . 'new' . DIRECTORY_SEPARATOR) !== false && !is_file($dest)) { @rename($p, $dest); if (is_file($seen) && $seen !== $dest . '.seen') { @rename($seen, $dest . '.seen'); } } $out['items'][] = $item; } $store = nst_promo_claims_load(); $out['claims_used'] = count($store['claims'] ?? []); $out['claims_cap'] = (int)NST_PROMO_AD_MAX_CLAIMS; $out['address'] = NST_PROMO_MAILBOX; return $out; } function nst_promo_maildir_ingest_lazy(): array { try { return nst_promo_maildir_ingest(8); } catch (Throwable $e) { return ['ok' => false, 'err' => 'ingest failed', 'items' => []]; } } function ad_span_int(string $span): int { $span = dec_norm($span); if (dec_cmp($span, '1') < 0) return 0; if (dec_cmp($span, (string)PHP_INT_MAX) > 0) return PHP_INT_MAX; return (int)$span; } function ad_bid_crop(array $b): string { $c = nst_ad_site_norm((string)($b['crop'] ?? $b['site'] ?? 'trade')); return $c !== '' ? $c : 'trade'; } function ad_paid_stake_total(?array $e = null, ?string $crop = null): string { if ($e === null) $e = load_economy(); $want = ($crop !== null && $crop !== '') ? nst_ad_site_norm($crop) : null; if ($want === '') $want = null; $total = '0'; foreach ($e['bids'] ?? [] as $b) { if (!is_array($b) || empty($b['active'])) continue; $has = !empty($b['creative_url']) || !empty($b['creative_file']) || !empty($b['label']); if (!$has) continue; if ($want !== null && ad_bid_crop($b) !== $want) continue; $total = dec_add($total, (string)($b['stake_micros'] ?? '0')); } return $total; } function ad_chance_denom(string $paidTotal): string { return (dec_cmp($paidTotal, NST_AD_BOARD_FULL_MICROS) >= 0) ? $paidTotal : NST_AD_BOARD_FULL_MICROS; } function ad_chance_pct(string $part, string $paidTotal): float { $denom = ad_chance_denom($paidTotal); if (dec_cmp($denom, '0') <= 0 || dec_cmp($part, '0') <= 0) return 0.0; return round(((float)$part / (float)$denom) * 100, 2); } function ad_unsold_pct(string $paidTotal): float { if (dec_cmp($paidTotal, NST_AD_BOARD_FULL_MICROS) >= 0) return 0.0; return ad_chance_pct(dec_sub(NST_AD_BOARD_FULL_MICROS, $paidTotal), $paidTotal); } /** Weighted random active bid that has a creative (site-local board). */ function ad_pick_weighted(?string $crop = null): ?array { $e = load_economy(); $want = nst_ad_site_norm((string)($crop ?? '')); if ($want === '') $want = 'trade'; $held = nst_ad_effective_shares($want); $byOwner = []; foreach ($e['bids'] ?? [] as $b) { if (!is_array($b) || empty($b['active'])) continue; if (ad_bid_crop($b) !== $want) continue; $owner = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($b['owner'] ?? '')) ?? ''); if (strlen($owner) === 64) $byOwner[$owner] = $b; } $r = random_int(1, NST_AD_SHARES_CAP); $acc = 0; foreach ($held as $who => $n) { $n = (int)$n; if ($n < 1) continue; $acc += $n; if ($r <= $acc) { if (isset($byOwner[$who])) { $bid = $byOwner[$who]; $bid['shares'] = $n; return $bid; } $tid = nst_ticket_lookup_ticket($who); return [ 'id' => hash('sha256', 'share|' . $who . '|' . $want), 'active' => true, 'house' => false, 'owner' => $who, 'stake_micros' => '0', 'stake_fmt' => $n . '/' . (string)NST_AD_SHARES_CAP, 'href' => '', 'label' => $tid ? ('#' . $tid) : 'share', 'creative_url' => '', 'creative_file' => '', 'shares' => $n, ]; } if ($acc >= NST_AD_SHARES_CAP) break; } return house_ad_pick(); } /** Integer half of a micros string (floor). */ function dec_half(string $a): string { $a = dec_norm($a); if (function_exists('bcdiv')) return dec_norm(bcdiv($a, '2', 0)); $half = ''; $carry = 0; for ($i = 0; $i < strlen($a); $i++) { $n = $carry * 10 + (int)$a[$i]; $half .= (string)intdiv($n, 2); $carry = $n % 2; } return dec_norm($half); } /** Floor-divide decimal string by small positive int (e.g. treasury/1000 = 0.1%). */ function dec_div_small(string $a, int $d): string { $a = dec_norm($a); if ($d <= 1) return $a; if (function_exists('bcdiv')) return dec_norm(bcdiv($a, (string)$d, 0)); $n = ''; $rem = 0; for ($i = 0; $i < strlen($a); $i++) { $cur = $rem * 10 + (int)$a[$i]; $n .= (string)intdiv($cur, $d); $rem = $cur % $d; } return dec_norm($n); } /** * Bootstrap loose-spread order book when there are ZERO open orders. * Temp discovery only: anchor 1.0 NOTE/NSU → BUY @ 0.5, SELL @ 1.5 (micros). * Amount ≈ 0.1% of treasury each side so market makers can tighten. * kind=bootstrap_spread. Does NOT cancel live orders — only posts if book empty. * Discovery quotes only: fill API rejects kind=bootstrap_spread (not free NSU). * * MASTER FAUCET: treasury holds the whole 100 million NSU cap at birth. * That wallet IS the empire faucet wallet for THIS site. Renters must NOT get * treasury.secret / site wallet seed (panel unlock material for owner only). * * @return array{ok:bool,skipped?:bool,reason?:string,err?:string,n?:int,posted?:int,amount_fmt?:string} */ function nst_bootstrap_spread_if_empty(): array { global $TFILE; boot_data(); $chain = read_chain(); $hasG = false; foreach ($chain as $r) { if (($r['type'] ?? '') === 'genesis') { $hasG = true; break; } } if (!$hasG) { return ['ok' => false, 'skipped' => true, 'reason' => 'no_genesis']; } $open = open_orders($chain); if (count($open) > 0) { return ['ok' => true, 'skipped' => true, 'reason' => 'open_orders', 'n' => count($open)]; } if (!is_file($TFILE)) { return ['ok' => false, 'err' => 'no_treasury_secret']; } $sec = trim((string)@file_get_contents($TFILE)); if ($sec === '') { return ['ok' => false, 'err' => 'empty_treasury_secret']; } $addr = addr_from_seed($sec); $bal = balances($chain); $have = (string)($bal[$addr] ?? '0'); $amt = dec_div_small($have, NST_BOOTSTRAP_DENOM); if (dec_cmp($amt, '1') < 0) { return ['ok' => false, 'err' => 'treasury_too_small', 'have_fmt' => fmt_amt($have)]; } // sell side locks NSU; with empty book all liquid is available if (dec_cmp($have, $amt) < 0) { return ['ok' => false, 'err' => 'Not enough free NSU for this action', 'have_fmt' => fmt_amt($have)]; } $buyPx = NST_BOOTSTRAP_BUY_PX; $sellPx = NST_BOOTSTRAP_SELL_PX; $anchor = NST_BOOTSTRAP_ANCHOR_PX; $ts = time(); $expiry = $ts + 30 * 86400; $pair = 'NSU/NOTE'; $posted = 0; foreach ([['buy', $buyPx], ['sell', $sellPx]] as [$side, $px]) { $id = hash('sha256', $addr . '|bootstrap|' . $side . '|' . $ts . '|' . $amt . '|' . $px . '|' . bin2hex(random_bytes(4))); $body = implode('|', ['order', '1', $id, $addr, $side, $pair, $amt, $px, (string)$expiry, (string)$ts]); $row = [ 'type' => 'order', 'ver' => 1, 'id' => $id, 'addr' => $addr, 'side' => $side, 'pair' => $pair, 'amount' => $amt, 'amount_fmt' => fmt_amt($amt), 'price' => $px, 'price_fmt' => fmt_amt($px), 'expiry' => $expiry, 'ts' => $ts, 'sig' => mac_sign($sec, $body), 'kind' => 'bootstrap_spread', 'anchor_px' => $anchor, 'note' => 'loose discovery spread around temp anchor 1.0 NOTE/NSU; makers should tighten', ]; if (!append_row($row)) { return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment', 'posted' => $posted]; } $posted++; } append_row([ 'type' => 'bootstrap_spread', 'ver' => 1, 'ts' => $ts, 'buy_px' => $buyPx, 'sell_px' => $sellPx, 'anchor_px' => $anchor, 'amount' => $amt, 'amount_fmt' => fmt_amt($amt), 'treasury_addr' => $addr, 'note' => 'seeded empty book: BUY@0.5 SELL@1.5 around 1.0; ~0.1% treasury each side; P still book-converge', ]); return [ 'ok' => true, 'posted' => $posted, 'amount_fmt' => fmt_amt($amt), 'buy_px_fmt' => fmt_amt($buyPx), 'sell_px_fmt' => fmt_amt($sellPx), 'anchor_px_fmt' => fmt_amt($anchor), ]; } /** * GENESIS ONLY: king ASK + BID spread on landing pair NSU/LTC. * Not bootstrap_spread. Not page-load. Not SitesOnly. Not year-wipe. * Geometric price rungs (~24). Zipf sizes: fat at the cheap/low end. * Asks list available king treasury (buys do not lock NSU). Bids ~1 LTC notional. * Does not remint. Does not wipe or cancel existing book (v1 lots stay). * Seed/addr MUST be pre-read — never call treasury_secret()/ensure_genesis here. * * @return array{ok:bool,posted?:int,bids_posted?:int,skipped?:int,nsu_total?:string,prices_fmt?:array,err?:string,why?:string} */ function nst_genesis_muldiv(string $a, string $b, string $den): string { $a = dec_norm($a); $b = dec_norm($b); $den = dec_norm($den); if ($den === '0' || $a === '0' || $b === '0') return '0'; if (function_exists('bcmul') && function_exists('bcdiv')) { return dec_norm(bcdiv(bcmul($a, $b, 0), $den, 0)); } $prod = '0'; $n = strlen($b); for ($i = 0; $i < $n; $i++) { $prod = dec_add(dec_mul_small($prod, 10), dec_mul_small($a, (int)$b[$i])); } if (function_exists('bcdiv')) return dec_norm(bcdiv($prod, $den, 0)); if (dec_cmp($prod, $den) < 0) return '0'; $q = ''; $acc = '0'; $len = strlen($prod); for ($i = 0; $i < $len; $i++) { $acc = dec_norm($acc . $prod[$i]); $d = 0; while (dec_cmp($acc, $den) >= 0) { $acc = dec_sub($acc, $den); $d++; if ($d > 9) break; } $q .= (string)$d; } return dec_norm($q); } function nst_genesis_nsu_from_ltc(string $ltcMicros, string $px): string { $ltc = dec_norm($ltcMicros); $px = dec_norm($px); if ($px === '0' || $ltc === '0') return '0'; return nst_genesis_muldiv($ltc, '1000000', $px); } /** Inclusive geometric rungs lo…hi as integer micros (parse_amt/fmt). First=lo, last=hi. */ function nst_genesis_geometric_prices(string $loFmt, string $hiFmt, int $rungs): array { $lo = parse_amt($loFmt); $hi = parse_amt($hiFmt); if ($lo === null || $hi === null || dec_cmp($lo, '1') < 0 || dec_cmp($hi, $lo) < 0) return []; $n = $rungs; if ($n < 2) $n = 2; if ($n > 64) $n = 64; $out = []; $loF = (float)$lo; $hiF = (float)$hi; for ($k = 0; $k < $n; $k++) { if ($k === 0) { $px = $lo; } elseif ($k === $n - 1) { $px = $hi; } else { $raw = (int)round($loF * pow($hiF / $loF, $k / ($n - 1))); if ($raw < 1) $raw = 1; $parsed = parse_amt(fmt_amt((string)$raw)); $px = $parsed !== null ? $parsed : dec_norm((string)$raw); } $px = dec_norm($px); if ($out && dec_cmp($px, $out[count($out) - 1]) <= 0) { $px = dec_add($out[count($out) - 1], '1'); } if (dec_cmp($px, $hi) > 0) $px = dec_norm($hi); if ($out && $px === $out[count($out) - 1]) continue; $out[] = $px; } if (!$out) return []; $out[0] = dec_norm($lo); if ($out[count($out) - 1] !== dec_norm($hi)) { if (dec_cmp($out[count($out) - 1], $hi) < 0) $out[] = dec_norm($hi); else $out[count($out) - 1] = dec_norm($hi); } return $out; } function nst_genesis_landing_ask_prices(): array { return nst_genesis_geometric_prices( NST_GENESIS_ASK_PX_LO_FMT, NST_GENESIS_ASK_PX_HI_FMT, (int)NST_GENESIS_LADDER_RUNGS ); } function nst_genesis_landing_bid_prices(): array { return nst_genesis_geometric_prices( NST_GENESIS_BID_PX_LO_FMT, NST_GENESIS_BID_PX_HI_FMT, (int)NST_GENESIS_LADDER_RUNGS ); } /** Zipf amounts: rank 1 = first (cheapest/lowest). amount_k ∝ 1/k. Last rung eats dust. */ function nst_genesis_zipf_amounts(int $n, string $totalMicros, string $minMicros): array { if ($n < 1) return []; $total = dec_norm($totalMicros); $min = dec_norm($minMicros); if (dec_cmp($min, '1') < 0) $min = '1'; if (dec_cmp($total, $min) < 0) return []; $need = dec_mul_small($min, $n); if (dec_cmp($total, $need) < 0) { $can = 0; $left = $total; while ($can < $n && dec_cmp($left, $min) >= 0) { $left = dec_sub($left, $min); $can++; } $n = $can; if ($n < 1) return []; } $scale = '1000000'; $weights = []; $wsum = '0'; for ($k = 1; $k <= $n; $k++) { $w = dec_div_small($scale, $k); if (dec_cmp($w, '1') < 0) $w = '1'; $weights[] = $w; $wsum = dec_add($wsum, $w); } $amts = []; $used = '0'; for ($i = 0; $i < $n; $i++) { if ($i === $n - 1) { $a = dec_sub($total, $used); } else { $a = nst_genesis_muldiv($total, $weights[$i], $wsum); } if (dec_cmp($a, $min) < 0) $a = $min; $amts[] = $a; $used = dec_add($used, $a); } if (dec_cmp($used, $total) > 0) { $over = dec_sub($used, $total); for ($i = 0; $i < $n && dec_cmp($over, '0') > 0; $i++) { $room = dec_sub($amts[$i], $min); if (dec_cmp($room, '0') <= 0) continue; $take = dec_cmp($room, $over) <= 0 ? $room : $over; $amts[$i] = dec_sub($amts[$i], $take); $over = dec_sub($over, $take); } } elseif (dec_cmp($used, $total) < 0) { $amts[$n - 1] = dec_add($amts[$n - 1], dec_sub($total, $used)); } return $amts; } /** Zipf bid amounts so ∑(amount_i × price_i) ≈ target LTC micros. Last rung eats dust. */ function nst_genesis_zipf_bid_amounts(array $prices, string $ltcNotionalMicros, string $minNsu): array { $n = count($prices); if ($n < 1) return []; $target = dec_norm($ltcNotionalMicros); $min = dec_norm($minNsu); if (dec_cmp($min, '1') < 0) $min = '1'; if (dec_cmp($target, '1') < 0) return []; $scale = '1000000000'; $raws = []; $nsum = '0'; for ($k = 1; $k <= $n; $k++) { $raw = dec_div_small($scale, $k); if (dec_cmp($raw, '1') < 0) $raw = '1'; $raws[] = $raw; $nsum = dec_add($nsum, nst_ltc_total_micros($raw, (string)$prices[$k - 1])); } if (dec_cmp($nsum, '1') < 0) return []; $amts = []; $usedN = '0'; for ($i = 0; $i < $n; $i++) { $px = dec_norm((string)$prices[$i]); if ($i === $n - 1) { $remain = dec_sub($target, $usedN); if (dec_cmp($remain, '0') < 0) $remain = '0'; $a = nst_genesis_nsu_from_ltc($remain, $px); } else { $a = nst_genesis_muldiv($raws[$i], $target, $nsum); } if (dec_cmp($a, $min) < 0) $a = $min; $amts[] = $a; $usedN = dec_add($usedN, nst_ltc_total_micros($a, $px)); } return $amts; } function nst_genesis_seed_landing_asks(string $tsec, string $taddr): array { $tsec = (string)$tsec; $taddr = strtolower(trim($taddr)); if ($tsec === '' || strlen($taddr) !== 64) { return ['ok' => false, 'err' => 'no treasury material', 'posted' => 0, 'bids_posted' => 0, 'why' => 'no king seed/addr']; } $min = parse_amt(NST_ORDER_MIN_NSU) ?? '1'; $askPrices = nst_genesis_landing_ask_prices(); $bidPrices = nst_genesis_landing_bid_prices(); if (!$askPrices || !$bidPrices) { return ['ok' => false, 'err' => 'empty geometric ladder', 'posted' => 0, 'bids_posted' => 0]; } $bidNotional = parse_amt(NST_GENESIS_BID_LTC_NOTIONAL); if ($bidNotional === null || dec_cmp($bidNotional, '1') < 0) { return ['ok' => false, 'err' => 'bid notional constants', 'posted' => 0, 'bids_posted' => 0]; } $pair = NST_PUBLIC_PAIR; $ts = time(); $expiry = $ts + NST_RESET_SECS; $out = nst_with_chain_lock(function (array $chain, callable $append) use ( $tsec, $taddr, $min, $askPrices, $bidPrices, $bidNotional, $pair, $ts, $expiry ) { $bal = balances($chain); $have = (string)($bal[$taddr] ?? '0'); // reserved_nsu locks SELL only — buy orders do not lock NSU. List 100% available on asks. $av = available_nsu($bal, $chain, $taddr); $wantN = count($askPrices); $askAmts = []; $usedPx = []; if (dec_cmp($av, $min) >= 0) { $askAmts = nst_genesis_zipf_amounts($wantN, $av, $min); $usedPx = array_slice($askPrices, 0, count($askAmts)); } $bidAmts = nst_genesis_zipf_bid_amounts($bidPrices, $bidNotional, $min); $haveIds = []; foreach ($chain as $r) { if (($r['type'] ?? '') === 'order' && isset($r['id'])) { $haveIds[(string)$r['id']] = true; } } $posted = 0; $skipped = 0; $bidsPosted = 0; $bidsSkipped = 0; $orders = []; $nsuTotal = '0'; $why = ''; if (count($askAmts) < $wantN) { $why = 'shrunk to available king treasury: ' . fmt_amt($av) . ' NSU free; Zipf asks; emission paused / no remint. wanted ' . $wantN . ' asks, posting ' . count($askAmts) . '.'; } foreach ($usedPx as $i => $px) { $px = dec_norm($px); $amt = dec_norm((string)($askAmts[$i] ?? '0')); if (dec_cmp($px, '1') < 0 || dec_cmp($amt, $min) < 0) continue; $id = hash('sha256', $taddr . '|genesis_landing_ask_v2|' . $px . '|' . $amt); if (!empty($haveIds[$id])) { $skipped++; continue; } $body = implode('|', ['order', '1', $id, $taddr, 'sell', $pair, $amt, $px, (string)$expiry, (string)$ts]); $row = [ 'type' => 'order', 'ver' => 1, 'id' => $id, 'addr' => $taddr, 'side' => 'sell', 'pair' => $pair, 'amount' => $amt, 'amount_fmt' => fmt_amt($amt), 'price' => $px, 'price_fmt' => fmt_amt($px), 'expiry' => $expiry, 'ts' => $ts, 'sig' => mac_sign($tsec, $body), 'kind' => 'genesis_landing_ask', 'note' => 'GENESIS ONLY king ASK Zipf: ' . fmt_amt($amt) . ' NSU @ ' . fmt_amt($px) . ' LTC/NSU. Fillable. No remint.', ]; if (!$append($row)) { return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment', 'posted' => $posted, 'bids_posted' => $bidsPosted, 'why' => $why]; } $haveIds[$id] = true; $posted++; $orders[] = $row; $nsuTotal = dec_add($nsuTotal, $amt); } $bidLtc = '0'; foreach ($bidPrices as $i => $px) { $px = dec_norm($px); $amt = dec_norm((string)($bidAmts[$i] ?? '0')); if (dec_cmp($px, '1') < 0 || dec_cmp($amt, $min) < 0) continue; $id = hash('sha256', $taddr . '|genesis_landing_bid_v2|' . $px . '|' . $amt); if (!empty($haveIds[$id])) { $bidsSkipped++; continue; } $body = implode('|', ['order', '1', $id, $taddr, 'buy', $pair, $amt, $px, (string)$expiry, (string)$ts]); $row = [ 'type' => 'order', 'ver' => 1, 'id' => $id, 'addr' => $taddr, 'side' => 'buy', 'pair' => $pair, 'amount' => $amt, 'amount_fmt' => fmt_amt($amt), 'price' => $px, 'price_fmt' => fmt_amt($px), 'expiry' => $expiry, 'ts' => $ts, 'sig' => mac_sign($tsec, $body), 'kind' => 'genesis_landing_bid', 'note' => 'GENESIS ONLY king BID Zipf: ' . fmt_amt($amt) . ' NSU @ ' . fmt_amt($px) . ' LTC/NSU. ~1 LTC book. No remint.', ]; if (!$append($row)) { return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment', 'posted' => $posted, 'bids_posted' => $bidsPosted, 'why' => $why]; } $haveIds[$id] = true; $bidsPosted++; $orders[] = $row; $bidLtc = dec_add($bidLtc, nst_ltc_total_micros($amt, $px)); } if ($why === '') { $why = 'posted ' . $posted . ' king Zipf asks (' . fmt_amt($nsuTotal) . ' NSU locked) and ' . $bidsPosted . ' king Zipf bids (~' . fmt_amt($bidLtc) . ' LTC). No remint. No live cancel.'; } return [ 'ok' => true, 'posted' => $posted, 'bids_posted' => $bidsPosted, 'skipped' => $skipped, 'bids_skipped' => $bidsSkipped, 'wanted' => $wantN, 'nsu_total' => $nsuTotal, 'nsu_total_fmt' => fmt_amt($nsuTotal), 'bid_ltc' => $bidLtc, 'bid_ltc_fmt' => fmt_amt($bidLtc), 'pair' => $pair, 'px_lo_fmt' => NST_GENESIS_ASK_PX_LO_FMT, 'px_hi_fmt' => NST_GENESIS_ASK_PX_HI_FMT, 'bid_px_lo_fmt' => NST_GENESIS_BID_PX_LO_FMT, 'bid_px_hi_fmt' => NST_GENESIS_BID_PX_HI_FMT, 'prices_fmt' => array_map('fmt_amt', $usedPx), 'bid_prices_fmt' => array_map('fmt_amt', $bidPrices), 'treasury_fmt' => fmt_amt($have), 'available_fmt' => fmt_amt($av), 'why' => $why, 'note' => 'King asks list available treasury Zipf; King bids ~1 LTC Zipf; no remint; no live cancel.', ]; }); if (!is_array($out)) { return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment', 'posted' => 0, 'bids_posted' => 0]; } return $out; } function nst_money_law_selftest(): void { $ok = 0; $fail = 0; $chk = static function (string $label, bool $cond) use (&$ok, &$fail): void { if ($cond) { $ok++; echo ' PASS ' . $label . "\n"; } else { $fail++; echo ' FAIL ' . $label . "\n"; } }; echo "== MONEY LAW SELFTEST ==\n"; $chk('cap is 100M NSU micros', NST_MAX_SUPPLY === '100000000000000'); $chk('birth amount equals cap', NST_GENESIS_TREASURY === NST_MAX_SUPPLY); $chk('emission paused', NST_EMISSION_PAUSED === true); $em = nst_emission_run_daily(); $chk('daily emission is a no-op while paused', empty($em['ran']) && (($em['reason'] ?? '') === 'paused') && (($em['minted'] ?? '') === '0')); $ra = nst_treasury_reanchor_to_population('selftest_dry'); $chk('reanchor stays retired no mint', ($ra['action'] ?? '') === 'retired_no_mint'); $asks = nst_genesis_landing_ask_prices(); $min = parse_amt(NST_ORDER_MIN_NSU) ?? '1'; $need = dec_mul_small($min, count($asks) > 0 ? count($asks) : 24); $chk('zipf ask ladder at min size is under the birth pile', dec_cmp(NST_GENESIS_TREASURY, $need) >= 0); $zipf = nst_genesis_zipf_amounts(count($asks), NST_GENESIS_TREASURY, $min); $zsum = '0'; foreach ($zipf as $z) $zsum = dec_add($zsum, $z); $chk('full-treasury Zipf asks sum to birth pile', $zsum === dec_norm(NST_GENESIS_TREASURY)); echo 'money-law selftest ' . $ok . ' pass / ' . $fail . " fail\n"; if ($fail > 0) exit(1); } function nst_genesis_ask_selftest(): void { $ok = 0; $fail = 0; $chk = static function (string $label, bool $cond) use (&$ok, &$fail): void { if ($cond) { $ok++; echo ' PASS ' . $label . "\n"; } else { $fail++; echo ' FAIL ' . $label . "\n"; } }; echo "== GENESIS ASK SELFTEST ==\n"; $pxs = nst_genesis_landing_ask_prices(); $nAsk = count($pxs); $chk('~24 ask rungs', $nAsk >= 22 && $nAsk <= 30); $chk('first 0.0001', isset($pxs[0]) && $pxs[0] === parse_amt('0.0001')); $lastAsk = $nAsk > 0 ? $pxs[$nAsk - 1] : '0'; $chk('last ~0.1', dec_cmp($lastAsk, parse_amt('0.09') ?? '0') >= 0 && dec_cmp($lastAsk, parse_amt('0.11') ?? '1') <= 0); $bids = nst_genesis_landing_bid_prices(); $nBid = count($bids); $chk('~24 bid rungs', $nBid >= 22 && $nBid <= 30); $chk('first bid 0.00001', isset($bids[0]) && $bids[0] === parse_amt('0.00001')); $lastBid = $nBid > 0 ? $bids[$nBid - 1] : '0'; $chk('last bid 0.0001', $lastBid === parse_amt('0.0001')); $min = parse_amt(NST_ORDER_MIN_NSU) ?? '1'; $askAmts = nst_genesis_zipf_amounts($nAsk, parse_amt('1000000') ?? '1000000000000', $min); $chk('Zipf ask[0] > ask[last]', $askAmts && dec_cmp($askAmts[0], $askAmts[count($askAmts) - 1]) > 0); foreach ($askAmts as $a) { if (dec_cmp($a, $min) < 0) { $chk('every ask amount >= min', false); break; } } if ($askAmts) $chk('every ask amount >= min', true); $bidNotional = parse_amt(NST_GENESIS_BID_LTC_NOTIONAL) ?? '1000000'; $bidAmts = nst_genesis_zipf_bid_amounts($bids, $bidNotional, $min); $gotN = '0'; foreach ($bids as $i => $px) { $amt = $bidAmts[$i] ?? '0'; $gotN = dec_add($gotN, nst_ltc_total_micros($amt, $px)); } $loN = parse_amt('0.95') ?? '950000'; $hiN = parse_amt('1.05') ?? '1050000'; $chk('bid notional ~1 LTC', dec_cmp($gotN, $loN) >= 0 && dec_cmp($gotN, $hiN) <= 0); $chk('Zipf bid[0] > bid[last]', $bidAmts && dec_cmp($bidAmts[0], $bidAmts[count($bidAmts) - 1]) > 0); $chk('public pair NSU/LTC', NST_PUBLIC_PAIR === 'NSU/LTC'); $chk('no remint constants (emission paused)', NST_EMISSION_PAUSED === true); echo 'genesis-ask selftest ' . $ok . ' pass / ' . $fail . " fail\n"; if ($fail > 0) exit(1); } function nst_tx_gate_selftest(): void { $ok = 0; $fail = 0; $chk = static function (string $label, bool $cond) use (&$ok, &$fail): void { if ($cond) { $ok++; echo ' PASS ' . $label . "\n"; } else { $fail++; echo ' FAIL ' . $label . "\n"; } }; echo "== TX GATE SELFTEST ==\n"; $chk('plain GET book not gated', nst_tx_api_is_gated('book', 'GET') === false); $chk('GET state not gated', nst_tx_api_is_gated('state', 'GET') === false); $chk('GET ad_pick not gated', nst_tx_api_is_gated('ad_pick', 'GET') === false); $chk('GET ad_share_quote not gated', nst_tx_api_is_gated('ad_share_quote', 'GET') === false); $chk('POST transfer gated', nst_tx_api_is_gated('transfer', 'POST') === true); $chk('POST order gated', nst_tx_api_is_gated('order', 'POST') === true); $chk('POST fill gated', nst_tx_api_is_gated('fill', 'POST') === true); $chk('POST cancel gated', nst_tx_api_is_gated('cancel', 'POST') === true); $chk('POST ad_stake gated', nst_tx_api_is_gated('ad_stake', 'POST') === true); $chk('POST ad_swap gated', nst_tx_api_is_gated('ad_swap', 'POST') === true); $chk('POST ad_give gated', nst_tx_api_is_gated('ad_give', 'POST') === true); $chk('POST ad_mine not gated', nst_tx_api_is_gated('ad_mine', 'POST') === false); $chk('POST vanity_apply gated', nst_tx_api_is_gated('vanity_apply', 'POST') === true); $chk('POST ltc_buy_ticket gated', nst_tx_api_is_gated('ltc_buy_ticket', 'POST') === true); $chk('POST device_wallet gated', nst_tx_api_is_gated('device_wallet', 'POST') === true); $chk('POST id gated', nst_tx_api_is_gated('id', 'POST') === true); $msg = nst_tx_too_fast_msg(''); $chk('no-number copy', $msg === "Apologies, You're going too fast"); $chk('id example is not hardcoded 107', strpos($msg, '107') === false); $k = nst_tx_gate_key('aa' . str_repeat('ab', 31)); $chk('addr key shape', (bool)preg_match('/^a_[a-f0-9]{64}$/', $k)); $k2 = nst_tx_gate_key(''); $chk('ip key shape', (bool)preg_match('/^i_[a-f0-9]{64}$/', $k2)); echo 'tx-gate selftest ' . $ok . ' pass / ' . $fail . ' fail' . "\n"; if ($fail > 0) exit(1); } /** * Converged market price P (NOTE micros per 1 NSU). Never a fixed sticker. * both sides → mid; else best bid; else best ask; else null (wait for market). */ function book_mid_micros(array $chain): ?string { $orders = open_orders($chain); $bids = array_values(array_filter($orders, fn($o) => ($o['side'] ?? '') === 'buy')); $asks = array_values(array_filter($orders, fn($o) => ($o['side'] ?? '') === 'sell')); usort($bids, fn($a, $b) => dec_cmp((string)($b['price'] ?? '0'), (string)($a['price'] ?? '0'))); usort($asks, fn($a, $b) => dec_cmp((string)($a['price'] ?? '0'), (string)($b['price'] ?? '0'))); if ($bids && $asks) { $sum = dec_add($bids[0]['price'] ?? '0', $asks[0]['price'] ?? '0'); return dec_half($sum); } if ($bids) return dec_norm((string)($bids[0]['price'] ?? '0')); if ($asks) return dec_norm((string)($asks[0]['price'] ?? '0')); return null; } /** * L3-RR293-LANDMARK nst_public_book — single state-assembly book slice (bids/asks/mid). * Shared by api=state and absolute-smallest api=book (copy book only, no full dump). * @return array{bids:list,asks:list,mid:?string,totals:array{bids:int,asks:int},omitted:array{bids:int,asks:int},truncated:bool,side_limit:int} */ function nst_public_book(array $chain): array { $orders = open_orders($chain); $bids = array_values(array_filter($orders, fn($o) => ($o['side'] ?? '') === 'buy')); $asks = array_values(array_filter($orders, fn($o) => ($o['side'] ?? '') === 'sell')); // Side-local sort (open_orders mixed comparator is not best-bid/best-ask safe). usort($bids, fn($a, $b) => dec_cmp((string)($b['price'] ?? '0'), (string)($a['price'] ?? '0'))); usort($asks, fn($a, $b) => dec_cmp((string)($a['price'] ?? '0'), (string)($b['price'] ?? '0'))); // Single source of P: same helper as SSR / buyback (never fixed sticker). $convergeP = book_mid_micros($chain); $mid = $convergeP !== null ? fmt_amt($convergeP) : null; $bidTotal = count($bids); $askTotal = count($asks); $bidRows = array_slice($bids, 0, NST_PUBLIC_BOOK_SIDE_MAX); $askRows = array_slice($asks, 0, NST_PUBLIC_BOOK_SIDE_MAX); $bidOmitted = max(0, $bidTotal - count($bidRows)); $askOmitted = max(0, $askTotal - count($askRows)); $recentFills = nst_public_recent_fills($chain); return [ 'bids' => $bidRows, 'asks' => $askRows, 'mid' => $mid, 'totals' => ['bids' => $bidTotal, 'asks' => $askTotal], 'omitted' => ['bids' => $bidOmitted, 'asks' => $askOmitted], 'truncated' => ($bidOmitted + $askOmitted) > 0, 'side_limit' => NST_PUBLIC_BOOK_SIDE_MAX, // Compact and complete: no current pair disappears merely because its // detailed row ranks below a bounded global publication slice. 'pairs' => nst_public_pair_summaries($bids, $asks, $recentFills), ]; } /** Complete compact pair universe plus exact best quotes/counts from current open rows. */ function nst_public_pair_summaries(array $bids, array $asks, array $recentFills): array { $pairs = []; $init = static function (string $pair) use (&$pairs): void { if (isset($pairs[$pair])) return; $pairs[$pair] = [ 'pair' => $pair, 'current' => false, 'fresh' => 0, 'bid_count' => 0, 'ask_count' => 0, 'fillable_ask_count' => 0, 'fill_count' => 0, 'best_bid' => null, 'best_ask' => null, 'last_fill' => null, ]; }; $quote = static function (array $row): array { $keep = ['type', 'id', 'addr', 'side', 'pair', 'amount', 'amount_fmt', 'remaining', 'remaining_fmt', 'price', 'price_fmt', 'expiry', 'ts', 'kind']; return array_intersect_key($row, array_fill_keys($keep, true)); }; foreach ($bids as $row) { $pair = strtoupper(trim((string)($row['pair'] ?? ''))); if ($pair === '') continue; $init($pair); $pairs[$pair]['current'] = true; $pairs[$pair]['bid_count']++; $pairs[$pair]['fresh'] = max((int)$pairs[$pair]['fresh'], (int)($row['ts'] ?? 0)); if ($pairs[$pair]['best_bid'] === null) $pairs[$pair]['best_bid'] = $quote($row); } foreach ($asks as $row) { $pair = strtoupper(trim((string)($row['pair'] ?? ''))); if ($pair === '') continue; $init($pair); $pairs[$pair]['current'] = true; $pairs[$pair]['ask_count']++; if (($row['kind'] ?? '') !== 'bootstrap_spread') $pairs[$pair]['fillable_ask_count']++; $pairs[$pair]['fresh'] = max((int)$pairs[$pair]['fresh'], (int)($row['ts'] ?? 0)); if ($pairs[$pair]['best_ask'] === null) $pairs[$pair]['best_ask'] = $quote($row); } foreach ($recentFills as $row) { $pair = strtoupper(trim((string)($row['pair'] ?? ''))); if ($pair === '' || ($row['type'] ?? '') !== 'fill') continue; $init($pair); $pairs[$pair]['fill_count']++; $pairs[$pair]['fresh'] = max((int)$pairs[$pair]['fresh'], (int)($row['ts'] ?? 0)); $pairs[$pair]['last_fill'] = $quote($row); } $out = array_values($pairs); usort($out, static function (array $a, array $b): int { if ((bool)$a['current'] !== (bool)$b['current']) return !empty($a['current']) ? -1 : 1; if ((int)$a['fresh'] !== (int)$b['fresh']) return (int)$b['fresh'] <=> (int)$a['fresh']; return strcmp((string)$a['pair'], (string)$b['pair']); }); return $out; } /** Recent exact-pair price history. Open/cancelled orders never manufacture charts. */ function nst_public_recent_fills(array $chain): array { $fills = []; foreach ($chain as $row) { if (($row['type'] ?? '') !== 'fill') continue; $pair = strtoupper(trim((string)($row['pair'] ?? ''))); if ($pair === '') continue; $fills[] = $row; } return array_slice($fills, -NST_PUBLIC_RECENT_FILL_MAX); } /** Buyback floor price = P/2 from market convergence. */ function nst_buyback_price_micros(array $chain): ?string { $p = book_mid_micros($chain); if ($p === null || dec_cmp($p, '2') < 0) return null; $half = dec_half($p); return dec_cmp($half, '1') < 0 ? '1' : $half; } /** * Arm treasury BUY order at converged P/2 for nsuAmount micros. * Stable floor: market sets P; we only bid half. No fixed list price. * Fail-open if no mid yet (returns err, does not block gift). */ function nst_arm_buyback_bids(string $nsuAmountMicros, string $reason): array { $nsuAmountMicros = dec_norm($nsuAmountMicros); if (dec_cmp($nsuAmountMicros, '1') < 0) return ['ok' => false, 'err' => 'Amount must be a positive number (at least 0.000001 NSU)']; $chain = read_chain(); $px = nst_buyback_price_micros($chain); if ($px === null) return ['ok' => false, 'err' => 'no_market_converge', 'note' => 'wait for book; P is never fixed']; $sec = treasury_secret(); $addr = addr_from_seed($sec); $ts = time(); $expiry = $ts + 30 * 86400; $pair = 'NSU/NOTE'; $id = hash('sha256', $addr . '|buyback|' . $ts . '|' . $nsuAmountMicros . '|' . $px . '|' . bin2hex(random_bytes(4))); $body = implode('|', ['order', '1', $id, $addr, 'buy', $pair, $nsuAmountMicros, $px, (string)$expiry, (string)$ts]); $row = [ 'type' => 'order', 'ver' => 1, 'id' => $id, 'addr' => $addr, 'side' => 'buy', 'pair' => $pair, 'amount' => $nsuAmountMicros, 'amount_fmt' => fmt_amt($nsuAmountMicros), 'price' => $px, 'price_fmt' => fmt_amt($px), 'expiry' => $expiry, 'ts' => $ts, 'sig' => mac_sign($sec, $body), 'kind' => 'buyback_floor', 'reason' => substr($reason, 0, 80), 'note' => 'stable floor: BUY at market_P/2 (P from book convergence)', ]; if (!append_row($row)) return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment']; return [ 'ok' => true, 'order' => $row, 'converge_p_fmt' => fmt_amt(book_mid_micros(read_chain()) ?? '0'), 'buyback_px_fmt' => fmt_amt($px), ]; } /** * OPERATOR_GO §3c: treasury BUY at half the sell price, same pair/amount. * Idempotent id = sha256(sellId|treasury_half_bid). Signed with treasury seed. * Quote-side NOTE settles off-chain (settle_ref belief) — same rail as any book BID. * Does not mint; open BID alone does not move balances (conservation held). * * @return array{ok:bool,order?:array,skipped?:bool,reason?:string,err?:string} */ function nst_treasury_half_bid_from_sell(string $sellId, string $pair, string $amount, string $sellPrice, int $expiry, int $ts, array $chain, callable $append, string $tsec, string $taddr): array { $sellId = preg_replace('/[^a-f0-9]/', '', strtolower($sellId)) ?? ''; if (strlen($sellId) !== 64) return ['ok' => false, 'err' => 'bad sell id']; $amount = dec_norm($amount); $sellPrice = dec_norm($sellPrice); if (dec_cmp($amount, '1') < 0 || dec_cmp($sellPrice, '1') < 0) return ['ok' => false, 'err' => 'amount/price']; $halfPx = dec_half($sellPrice); if (dec_cmp($halfPx, '1') < 0) $halfPx = '1'; $pair = strtoupper(trim($pair)); if ($pair === '') $pair = NST_PUBLIC_PAIR; $bidId = hash('sha256', $sellId . '|treasury_half_bid'); foreach ($chain as $r) { if (($r['type'] ?? '') === 'order' && ($r['id'] ?? '') === $bidId) { return ['ok' => true, 'skipped' => true, 'reason' => 'already', 'order' => $r]; } } // Seed/addr MUST be pre-read outside nst_with_chain_lock — never call treasury_secret()/ensure_genesis under LOCK_EX (deadlock). $sec = $tsec; $addr = strtolower($taddr); if ($sec === '' || strlen($addr) !== 64) return ['ok' => false, 'err' => 'no treasury material']; $body = implode('|', ['order', '1', $bidId, $addr, 'buy', $pair, $amount, $halfPx, (string)$expiry, (string)$ts]); $row = [ 'type' => 'order', 'ver' => 1, 'id' => $bidId, 'addr' => $addr, 'side' => 'buy', 'pair' => $pair, 'amount' => $amount, 'amount_fmt' => fmt_amt($amount), 'price' => $halfPx, 'price_fmt' => fmt_amt($halfPx), 'expiry' => $expiry, 'ts' => $ts, 'sig' => mac_sign($sec, $body), 'kind' => 'treasury_half_bid', 'paired_sell_id' => $sellId, 'note' => 'OPERATOR_GO §3c: half-price treasury BID floor paired to treasury SELL', ]; if (!$append($row)) return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment']; return ['ok' => true, 'order' => $row]; } /** * OPERATOR_GO §4 E2a: KING places a small REAL fillable SELL-ask ladder. * Not bootstrap_spread (those stay discovery-only / non-fillable). * Each SELL is an ordinary book order signed by treasury → C2 half-BID arms. * Deterministic ids (sha256 taddr|treasury_ladder_v1|price) → re-call is idempotent. * Seed/addr MUST be pre-read outside nst_with_chain_lock (same deadlock law as C2). * * @return array{ok:bool,posted?:int,skipped?:int,orders?:array,half_bids?:array,err?:string,amount_fmt?:string} */ function nst_treasury_ask_ladder(string $tsec, string $taddr, ?string $amountMicros = null): array { $tsec = (string)$tsec; $taddr = strtolower(trim($taddr)); if ($tsec === '' || strlen($taddr) !== 64) { return ['ok' => false, 'err' => 'no treasury material']; } $amt = $amountMicros !== null && $amountMicros !== '' ? dec_norm($amountMicros) : parse_amt(NST_LADDER_AMT_NSU); if ($amt === null || dec_cmp($amt, '1') < 0) { return ['ok' => false, 'err' => 'Amount must be a positive number (at least 0.000001 NSU)']; } $amtMax = parse_amt(NST_LADDER_AMT_MAX_NSU); if ($amtMax !== null && dec_cmp($amt, $amtMax) > 0) { return ['ok' => false, 'err' => 'amount exceeds ladder cap', 'max_fmt' => fmt_amt($amtMax)]; } $prices = [NST_LADDER_PX_LO, NST_LADDER_PX_MID, NST_LADDER_PX_HI]; $pair = 'NSU/NOTE'; $ts = time(); $expiry = $ts + 30 * 86400; $out = nst_with_chain_lock(function (array $chain, callable $append) use ( $tsec, $taddr, $amt, $prices, $pair, $ts, $expiry ) { $bal = balances($chain); $have = (string)($bal[$taddr] ?? '0'); $av = available_nsu($bal, $chain, $taddr); $need = '0'; foreach ($prices as $px) { $need = dec_add($need, $amt); } if (dec_cmp($av, $need) < 0) { return [ 'ok' => false, 'err' => 'Not enough free NSU — cancel open sells or buy more on the book', 'available_fmt' => fmt_amt($av), 'need_fmt' => fmt_amt($need), 'treasury_fmt' => fmt_amt($have), ]; } $nOpen = 0; foreach (open_orders($chain) as $o) { if (($o['addr'] ?? '') === $taddr) { $nOpen++; } } // Each new sell + half-BID needs 2 slots; leave headroom under 20. $slotsNeeded = count($prices) * 2; if ($nOpen + $slotsNeeded > 20) { return ['ok' => false, 'err' => 'too many open orders', 'open' => $nOpen]; } $posted = 0; $skipped = 0; $orders = []; $halfBids = []; foreach ($prices as $px) { $px = dec_norm($px); if (dec_cmp($px, '1') < 0) { continue; } $id = hash('sha256', $taddr . '|treasury_ladder_v1|' . $px); $exists = false; foreach ($chain as $r) { if (($r['type'] ?? '') === 'order' && ($r['id'] ?? '') === $id) { $exists = true; break; } } if ($exists) { $skipped++; continue; } $body = implode('|', ['order', '1', $id, $taddr, 'sell', $pair, $amt, $px, (string)$expiry, (string)$ts]); $row = [ 'type' => 'order', 'ver' => 1, 'id' => $id, 'addr' => $taddr, 'side' => 'sell', 'pair' => $pair, 'amount' => $amt, 'amount_fmt' => fmt_amt($amt), 'price' => $px, 'price_fmt' => fmt_amt($px), 'expiry' => $expiry, 'ts' => $ts, 'sig' => mac_sign($tsec, $body), 'kind' => 'treasury_ladder', 'note' => 'OPERATOR_GO §4: KING real fillable SELL ladder (buy path); not bootstrap discovery', ]; if (!$append($row)) { return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment', 'posted' => $posted, 'orders' => $orders]; } $hb = nst_treasury_half_bid_from_sell( $id, $pair, $amt, $px, $expiry, $ts, $chain, $append, $tsec, $taddr ); if (empty($hb['ok'])) { return [ 'ok' => false, 'err' => 'treasury half-bid failed after ladder sell', 'detail' => $hb['err'] ?? 'unknown', 'posted' => $posted, 'order' => $row, ]; } $posted++; $orders[] = $row; if (!empty($hb['order'])) { $halfBids[] = $hb['order']; } // Keep open-count honest for later rungs in same lock. $nOpen += 2; } return [ 'ok' => true, 'posted' => $posted, 'skipped' => $skipped, 'orders' => $orders, 'half_bids' => $halfBids, 'amount_fmt' => fmt_amt($amt), 'prices_fmt' => array_map('fmt_amt', $prices), 'note' => 'Real fillable treasury asks on book; visitors TAKE ORDER / ?api=fill. NOTE settles off-chain (settle_ref).', ]; }); if ($out === null) { return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment']; } return $out; } /** * Donate → NSU gift (mid×k or direct), then 50% profit pile / 50% ad-bid stake. * Faucet free gas does NOT use this path. */ /* ===== LTC LIGHT WALLET + NSU/LTC BOOK (receive + ticket + king console) ===== */ /** * NSU LTC light-wallet engine — receive path + honest outbound gap. * BIP84 native bech32 (ltc1). Stock PHP 7.4. No explorer API, no composer, no custodial API. * Watch talks to Litecoin peers (headers + scrypt work + merkleblock/block proof). Fail-open. * Outbound: visitor pastes ltc1 when selling into the king half-bid; address is stored * on the payout ticket / king pay list. BIP84 spend+sign is an honest remaining gap * (no UTXO outpoints, no BIP143, no low-S ECDSA). Do not pretend LTC was sent. * HD seed: 12 product words. BIP32 master = HMAC-SHA512("Bitcoin seed", utf8(words)). * Not BIP39 PBKDF2. Path m/84'/2'/0'/0/i. * Priv→pub uses OpenSSL secp256k1 via PKCS#8 without a public-key field * (OpenSSL recomputes the point). In-file hex add is only for BIP32 IL+k mod n. */ const NST_LTC_CURVE_N = 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'; const NST_LTC_BECH32_CS = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; function nst_hx_norm(string $h): string { $h = strtolower(preg_replace('/[^0-9a-f]/', '', $h) ?? ''); $h = ltrim($h, '0'); return $h === '' ? '0' : $h; } function nst_hx_pad(string $h, int $n = 64): string { $h = nst_hx_norm($h); if (strlen($h) > $n) return substr($h, -$n); return str_pad($h, $n, '0', STR_PAD_LEFT); } function nst_hx_cmp(string $a, string $b): int { $a = nst_hx_norm($a); $b = nst_hx_norm($b); if (strlen($a) !== strlen($b)) return strlen($a) < strlen($b) ? -1 : 1; return $a <=> $b; } function nst_hx_add(string $a, string $b): string { $a = nst_hx_norm($a); $b = nst_hx_norm($b); $n = max(strlen($a), strlen($b)); $a = str_pad($a, $n, '0', STR_PAD_LEFT); $b = str_pad($b, $n, '0', STR_PAD_LEFT); $c = 0; $r = ''; for ($i = $n - 1; $i >= 0; $i--) { $s = hexdec($a[$i]) + hexdec($b[$i]) + $c; $r = dechex($s & 15) . $r; $c = $s >> 4; } if ($c) $r = dechex($c) . $r; return nst_hx_norm($r); } function nst_hx_sub(string $a, string $b): string { if (nst_hx_cmp($a, $b) < 0) return '0'; $a = nst_hx_norm($a); $b = nst_hx_norm($b); $n = max(strlen($a), strlen($b)); $a = str_pad($a, $n, '0', STR_PAD_LEFT); $b = str_pad($b, $n, '0', STR_PAD_LEFT); $c = 0; $r = ''; for ($i = $n - 1; $i >= 0; $i--) { $d = hexdec($a[$i]) - hexdec($b[$i]) - $c; if ($d < 0) { $d += 16; $c = 1; } else { $c = 0; } $r = dechex($d) . $r; } return nst_hx_norm($r); } function nst_hx_modadd(string $a, string $b, string $m): string { $s = nst_hx_add($a, $b); while (nst_hx_cmp($s, $m) >= 0) $s = nst_hx_sub($s, $m); return $s; } /** OpenSSL secp256k1: PKCS#8 without public key so the library computes the point. */ function nst_ec_openssl_pub(string $privHex): ?array { $priv = @hex2bin(nst_hx_pad($privHex, 64)); if ($priv === false || strlen($priv) !== 32) return null; $alg = hex2bin('301006072a8648ce3d020106052b8104000a'); $inner = hex2bin('30250201010420') . $priv; $body = "\x02\x01\x00" . $alg . "\x04" . chr(strlen($inner)) . $inner; $der = "\x30" . chr(strlen($body)) . $body; $pem = "-----BEGIN PRIVATE KEY-----\n" . chunk_split(base64_encode($der), 64, "\n") . "-----END PRIVATE KEY-----\n"; $key = @openssl_pkey_get_private($pem); if ($key === false) return null; $det = openssl_pkey_get_details($key); if (!is_array($det) || empty($det['ec']['x']) || empty($det['ec']['y'])) return null; $x = nst_hx_pad(bin2hex($det['ec']['x']), 64); $y = nst_hx_pad(bin2hex($det['ec']['y']), 64); return ['x' => $x, 'y' => $y]; } function nst_ec_compressed_pub(string $privHex): string { $p = nst_ec_openssl_pub($privHex); if ($p === null) return ''; $odd = hexdec(substr($p['y'], -1)) & 1; return ($odd ? '03' : '02') . $p['x']; } function nst_hash160(string $bin): string { return hash('ripemd160', hash('sha256', $bin, true), true); } function nst_bech32_polymod(array $values): int { $gen = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; $chk = 1; foreach ($values as $v) { $b = $chk >> 25; $chk = (($chk & 0x1ffffff) << 5) ^ $v; for ($i = 0; $i < 5; $i++) { if (($b >> $i) & 1) $chk ^= $gen[$i]; } } return $chk; } function nst_bech32_hrp_expand(string $hrp): array { $ret = []; $n = strlen($hrp); for ($i = 0; $i < $n; $i++) $ret[] = ord($hrp[$i]) >> 5; $ret[] = 0; for ($i = 0; $i < $n; $i++) $ret[] = ord($hrp[$i]) & 31; return $ret; } function nst_bech32_convertbits(array $data, int $from, int $to, bool $pad): ?array { $acc = 0; $bits = 0; $ret = []; $maxv = (1 << $to) - 1; foreach ($data as $v) { if ($v < 0 || ($v >> $from) !== 0) return null; $acc = ($acc << $from) | $v; $bits += $from; while ($bits >= $to) { $bits -= $to; $ret[] = ($acc >> $bits) & $maxv; } } if ($pad) { if ($bits) $ret[] = ($acc << ($to - $bits)) & $maxv; } elseif ($bits >= $from || (($acc << ($to - $bits)) & $maxv)) { return null; } return $ret; } function nst_bech32_encode(string $hrp, array $data): string { $values = array_merge(nst_bech32_hrp_expand($hrp), $data, [0, 0, 0, 0, 0, 0]); $mod = nst_bech32_polymod($values) ^ 1; $ret = $hrp . '1'; foreach ($data as $d) $ret .= NST_LTC_BECH32_CS[$d]; for ($i = 0; $i < 6; $i++) $ret .= NST_LTC_BECH32_CS[($mod >> (5 * (5 - $i))) & 31]; return $ret; } function nst_bech32_decode(string $addr): ?array { $addr = strtolower(trim($addr)); $pos = strrpos($addr, '1'); if ($pos === false || $pos < 1) return null; $hrp = substr($addr, 0, $pos); $data = substr($addr, $pos + 1); $values = []; $n = strlen($data); for ($i = 0; $i < $n; $i++) { $p = strpos(NST_LTC_BECH32_CS, $data[$i]); if ($p === false) return null; $values[] = $p; } if (nst_bech32_polymod(array_merge(nst_bech32_hrp_expand($hrp), $values)) !== 1) return null; $witver = $values[0]; $prog5 = array_slice($values, 1, -6); $prog = nst_bech32_convertbits($prog5, 5, 8, false); if ($prog === null) return null; $bin = ''; foreach ($prog as $b) $bin .= chr($b); return ['hrp' => $hrp, 'witver' => $witver, 'prog' => $bin]; } function nst_ltc_p2wpkh(string $compressedPubHex): string { $pub = @hex2bin($compressedPubHex); if ($pub === false || strlen($pub) !== 33) return ''; $h160 = nst_hash160($pub); $prog = []; for ($i = 0; $i < 20; $i++) $prog[] = ord($h160[$i]); $conv = nst_bech32_convertbits($prog, 8, 5, true); if ($conv === null) return ''; return nst_bech32_encode('ltc', array_merge([0], $conv)); } function nst_ltc_script_p2wpkh(string $addr): string { $dec = nst_bech32_decode($addr); if ($dec === null || $dec['hrp'] !== 'ltc' || $dec['witver'] !== 0 || strlen($dec['prog']) !== 20) return ''; return "\x00\x14" . $dec['prog']; } function nst_ltc_scripthash(string $addr): string { $spk = nst_ltc_script_p2wpkh($addr); if ($spk === '') return ''; return bin2hex(strrev(hash('sha256', $spk, true))); } function nst_bip32_master(string $seedBytes): array { $I = hash_hmac('sha512', $seedBytes, 'Bitcoin seed', true); return ['k' => bin2hex(substr($I, 0, 32)), 'c' => bin2hex(substr($I, 32, 32))]; } function nst_bip32_ckd_priv(string $k, string $c, int $index): array { $hardened = ($index >= 0x80000000); $serI = pack('N', $index); if ($hardened) { $data = "\x00" . hex2bin(nst_hx_pad($k, 64)) . $serI; } else { $pub = nst_ec_compressed_pub($k); if ($pub === '') return ['k' => '', 'c' => '']; $data = hex2bin($pub) . $serI; } $I = hash_hmac('sha512', $data, hex2bin($c), true); $il = bin2hex(substr($I, 0, 32)); $ir = bin2hex(substr($I, 32, 32)); $ki = nst_hx_modadd($il, $k, NST_LTC_CURVE_N); return ['k' => nst_hx_pad($ki, 64), 'c' => $ir]; } function nst_bip32_derive_path(string $seedBytes, array $path): array { $node = nst_bip32_master($seedBytes); foreach ($path as $i) { $node = nst_bip32_ckd_priv($node['k'], $node['c'], $i); if ($node['k'] === '') return $node; } return $node; } function nst_ltc_derive_recv(string $seedBytes, int $index): array { if ($index < 0 || $index > 0x7fffffff) { return ['ok' => false, 'err' => 'bad index']; } $path = [84 + 0x80000000, 2 + 0x80000000, 0 + 0x80000000, 0, $index]; $node = nst_bip32_derive_path($seedBytes, $path); if ($node['k'] === '') return ['ok' => false, 'err' => 'derive']; $pub = nst_ec_compressed_pub($node['k']); $addr = nst_ltc_p2wpkh($pub); return [ 'ok' => $addr !== '', 'index' => $index, 'path' => "m/84'/2'/0'/0/" . $index, 'addr' => $addr, 'pub' => $pub, 'scheme' => 'bip84', ]; } /** LTC book / ticket / king-console helpers. Relies on 4.php money primitives. */ function nst_public_pair(): string { return NST_PUBLIC_PAIR; } function nst_public_pair_label(): string { return NST_PUBLIC_PAIR_LABEL; } function nst_host_is_trade(): bool { $h = strtolower(preg_replace('/:\d+$/', '', (string)($_SERVER['HTTP_HOST'] ?? '')) ?? ''); if ($h === '') return true; if (preg_match('/(?:^|\.)nosignup\.([a-z]{2,10})$/', $h, $m)) { return $m[1] === 'trade'; } return true; } function nst_ltc_net_allowed(): bool { global $NST_SOURCE_LOCAL_RUNTIME; if ((string)getenv('NST_LTC_ALLOW_NET') === '1') return true; if ((string)getenv('NST_LTC_NO_NET') === '1') return false; if (!empty($NST_SOURCE_LOCAL_RUNTIME)) return false; if (PHP_SAPI === 'cli') return false; return true; } function nst_ltc_secret_path(): string { return nst_data_file('ltc.secret'); } function nst_ltc_wallet_path(): string { return nst_data_file('ltc-wallet.json'); } function nst_ltc_secret_read(): string { $p = nst_ltc_secret_path(); if (!is_file($p) || is_link($p)) return ''; return norm_seed((string)@file_get_contents($p)); } function nst_ltc_secret_write(string $words): bool { $p = nst_ltc_secret_path(); $dir = dirname($p); if (!is_dir($dir)) @mkdir($dir, 0755, true); $ok = @file_put_contents($p, $words . "\n", LOCK_EX) !== false; if ($ok) @chmod($p, 0600); return $ok; } function nst_ltc_state_empty(): array { return [ 'ver' => 1, 'scheme' => 'bip84', 'path' => "m/84'/2'/0'/0", 'next_index' => 0, 'master_addr' => '', 'created_ts' => 0, 'tickets' => [], 'pay_list' => [], 'watched_sat' => '0', 'watch_note' => '', ]; } function nst_ltc_state_valid(array $st): bool { if (($st['ver'] ?? null) !== 1 || !isset($st['tickets'], $st['pay_list']) || !is_array($st['tickets']) || !is_array($st['pay_list']) || !isset($st['next_index']) || !is_int($st['next_index']) || $st['next_index'] < 0 || $st['next_index'] > 0x80000000) return false; $indexes = []; foreach ($st['tickets'] as $id => $ticket) { if (!is_array($ticket) || (string)($ticket['id'] ?? '') !== (string)$id) return false; if (($ticket['kind'] ?? '') === 'half_bid_payout') continue; if (!isset($ticket['index']) || !is_int($ticket['index']) || $ticket['index'] < 0 || $ticket['index'] >= $st['next_index']) return false; if (isset($indexes[$ticket['index']])) return false; $indexes[$ticket['index']] = true; } foreach ($st['pay_list'] as $p) if (!is_array($p)) return false; return true; } function nst_ltc_with_state(callable $fn) { $path = nst_ltc_wallet_path(); $dir = dirname($path); $lock = null; $fh = null; $temp = null; try { if (!is_dir($dir) && !@mkdir($dir, 0755, true)) throw new RuntimeException('ltc_state_unavailable'); $lockPath = $path . '.lock'; if (is_link($lockPath) || is_link($path) || (file_exists($path) && !is_file($path))) throw new RuntimeException('ltc_state_unavailable'); $lock = @fopen($lockPath, 'c+b'); if (!is_resource($lock) || !@flock($lock, LOCK_EX)) throw new RuntimeException('ltc_state_unavailable'); if (is_link($path) || (file_exists($path) && !is_file($path))) throw new RuntimeException('ltc_state_unavailable'); $exists = is_file($path); $raw = $exists ? @file_get_contents($path) : ''; if (!is_string($raw)) throw new RuntimeException('ltc_state_unavailable'); if ($raw === '') { // Old readers could create an empty file before a wallet existed. // A wallet secret without its derivation index must never restart at zero. if (file_exists(nst_ltc_secret_path()) || is_link(nst_ltc_secret_path())) throw new RuntimeException('ltc_state_missing'); $st = nst_ltc_state_empty(); } else { $st = json_decode($raw, true); if (!is_array($st) || !nst_ltc_state_valid($st)) throw new RuntimeException('ltc_state_invalid'); } $out = $fn($st); if (!is_array($out) || empty($out['_dirty'])) return $out; unset($out['_dirty']); $ticketKeep = $out['_ticket'] ?? null; $createdKeep = $out['_created'] ?? null; unset($out['_ticket'], $out['_created'], $out['seed'], $out['xprv'], $out['xpub'], $out['k']); if (!nst_ltc_state_valid($out)) throw new RuntimeException('ltc_state_invalid'); $encoded = json_encode($out, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if (!is_string($encoded)) throw new RuntimeException('ltc_state_encode'); $bytes = $encoded . "\n"; if (!hash_equals($raw, $bytes)) { $temp = $path . '.tmp.' . bin2hex(random_bytes(12)); $fh = @fopen($temp, 'xb'); if (!is_resource($fh) || !@chmod($temp, 0600)) throw new RuntimeException('ltc_state_unavailable'); if (!nst_stream_write_all($fh, $bytes) || !nst_stream_flush_durable($fh)) throw new RuntimeException('ltc_state_unavailable'); @fclose($fh); $fh = null; $written = @file_get_contents($temp); if (!is_string($written) || !hash_equals($bytes, $written)) throw new RuntimeException('ltc_state_unavailable'); clearstatcache(true, $path); $current = is_file($path) ? @file_get_contents($path) : ''; if (is_link($path) || !is_string($current) || $exists !== is_file($path) || !hash_equals($raw, $current)) throw new RuntimeException('ltc_state_changed'); if (!@rename($temp, $path)) throw new RuntimeException('ltc_state_unavailable'); $temp = null; $saved = @file_get_contents($path); if (!is_string($saved) || !hash_equals($bytes, $saved)) throw new RuntimeException('ltc_state_unavailable'); } if (is_array($ticketKeep)) $out['_ticket'] = $ticketKeep; if (is_array($createdKeep)) $out['_created'] = $createdKeep; return $out; } catch (Throwable $e) { $allowed = ['ltc_state_unavailable', 'ltc_state_missing', 'ltc_state_invalid', 'ltc_state_encode', 'ltc_state_changed']; $err = in_array($e->getMessage(), $allowed, true) ? $e->getMessage() : 'ltc_state_unavailable'; return ['ok' => false, 'err' => $err]; } finally { if (is_resource($fh)) @fclose($fh); if (is_string($temp) && is_file($temp) && !is_link($temp)) @unlink($temp); if (is_resource($lock)) { @flock($lock, LOCK_UN); @fclose($lock); } } } function nst_ltc_total_micros(string $amt, string $px): string { $amt = dec_norm($amt); $px = dec_norm($px); if (!dec_ok($amt) || !dec_ok($px) || $amt === '0' || $px === '0') return '0'; $prod = '0'; $n = strlen($px); for ($i = 0; $i < $n; $i++) { $prod = dec_add(dec_mul_small($prod, 10), dec_mul_small($amt, (int)$px[$i])); } if (strlen($prod) <= 6) return '0'; return dec_norm(substr($prod, 0, -6)); } function nst_ltc_sat_from_micros(string $micros): string { return dec_mul_small(dec_norm($micros), 100); } function nst_ltc_ticket_public(array $t): array { $keep = ['id','addr','order_id','buyer','amount','amount_fmt','price','price_fmt', 'ltc_exact','ltc_exact_fmt','status','txid','confirms','created_ts','expires_ts','paid_ts','index','path', 'kind','payout_addr','outbound_sign','outbound_note','sent']; $out = []; foreach ($keep as $k) { if (array_key_exists($k, $t)) $out[$k] = $t[$k]; } unset($out['seed'], $out['xprv'], $out['k']); return $out; } function nst_ltc_open_reserved_on_order(array $st, string $oid, string $exceptId = ''): string { $sum = '0'; foreach ($st['tickets'] as $t) { if (!is_array($t) || ($t['kind'] ?? '') === 'half_bid_payout' || (string)($t['order_id'] ?? '') !== $oid) continue; if ($exceptId !== '' && (string)($t['id'] ?? '') === $exceptId) continue; // Time passing alone cannot prove a receive address was never paid. // Legacy expired/seen records retain their reservation until reconciled. if (in_array((string)($t['status'] ?? ''), ['paid', 'cancelled'], true)) continue; $sum = dec_add($sum, dec_norm((string)($t['amount'] ?? '0'))); } return $sum; } function nst_ltc_reserved_ltc_micros(array $chain): string { $sum = '0'; foreach (open_orders($chain) as $o) { if (strtoupper((string)($o['pair'] ?? '')) !== NST_PUBLIC_PAIR) continue; if ((string)($o['side'] ?? '') !== 'buy') continue; if ((string)($o['kind'] ?? '') !== 'treasury_half_bid') continue; $sum = dec_add($sum, nst_ltc_total_micros((string)($o['remaining'] ?? $o['amount'] ?? '0'), (string)($o['price'] ?? '0'))); } return $sum; } function nst_ltc_create_wallet(): array { $out = nst_ltc_with_state(function (array $st) { if (file_exists(nst_ltc_secret_path()) || is_link(nst_ltc_secret_path())) return ['ok' => false, 'err' => 'wallet already exists']; if (!empty($st['created_ts']) || $st['next_index'] !== 0 || $st['tickets'] !== [] || $st['pay_list'] !== []) return ['ok' => false, 'err' => 'ltc_state_missing_secret']; $words = nst_generate_site_seed(); $recv = nst_ltc_derive_recv($words, 0); if (empty($recv['ok']) || (string)($recv['addr'] ?? '') === '') return ['ok' => false, 'err' => 'Could not derive a receive address']; if (!nst_ltc_secret_write($words)) return ['ok' => false, 'err' => 'could not write ltc.secret']; $st['master_addr'] = (string)$recv['addr']; $st['created_ts'] = time(); $st['_dirty'] = true; $st['_created'] = ['seed_once' => $words, 'master_addr' => (string)$recv['addr']]; return $st; }); if (!is_array($out) || isset($out['err']) || !is_array($out['_created'] ?? null)) return is_array($out) && isset($out['err']) ? $out : ['ok' => false, 'err' => 'ltc_state_unavailable']; return [ 'ok' => true, 'created' => true, 'seed_once' => $out['_created']['seed_once'], 'master_addr' => $out['_created']['master_addr'], 'scheme' => 'bip84', 'path' => "m/84'/2'/0'/0/i", 'note' => 'Write these 12 words down now. They are shown once here. Restore = paste them. BIP32 master = HMAC-SHA512("Bitcoin seed", utf8(words)); not BIP39 PBKDF2.', ]; } function nst_ltc_status_king(bool $reveal): array { $words = nst_ltc_secret_read(); $missing = ($words === ''); $chain = function_exists('read_chain') ? read_chain() : []; $reserved = nst_ltc_reserved_ltc_micros($chain); $st = nst_ltc_with_state(function (array $s) { return $s; }); if (!is_array($st) || isset($st['err'])) return is_array($st) ? $st : ['ok' => false, 'err' => 'ltc_state_unavailable']; $master = (string)($st['master_addr'] ?? ''); if ($master === '' && $words !== '') { $d = nst_ltc_derive_recv($words, 0); $master = (string)($d['addr'] ?? ''); } $tickets = []; foreach ($st['tickets'] as $t) { if (is_array($t)) $tickets[] = nst_ltc_ticket_public($t); } $watched = dec_norm((string)($st['watched_sat'] ?? '0')); $watchedMicros = strlen($watched) <= 2 ? '0' : dec_norm(substr($watched, 0, -2)); $free = dec_cmp($watchedMicros, $reserved) >= 0 ? dec_sub($watchedMicros, $reserved) : '0'; $out = [ 'ok' => true, 'present' => !$missing, 'scheme' => 'bip84', 'path' => "m/84'/2'/0'/0/i", 'master_addr' => $master, 'next_index' => (int)($st['next_index'] ?? 0), 'reserved_ltc_fmt' => fmt_amt($reserved), 'watched_ltc_fmt' => fmt_amt($watchedMicros), 'free_ltc_fmt' => fmt_amt($free), 'watch_note' => (string)($st['watch_note'] ?? ''), 'tickets' => $tickets, 'pay_list' => $st['pay_list'], 'outbound_sign' => 'honest_gap', 'outbound_sent' => false, 'remaining_gap' => nst_ltc_outbound_gap_reasons(), 'pair' => NST_PUBLIC_PAIR, 'lot_nsu' => NST_LTC_LOT_NSU, 'lot_price' => NST_LTC_LOT_PX_FMT, ]; if ($reveal && !$missing) { $out['seed'] = $words; $out['seed_note'] = 'KING only. Never paste into chat. Visitors never see this.'; } return $out; } function nst_ltc_auto_settlement_readiness(): array { // This is deliberately not an environment switch. Reopening requires a // separately qualified chain observer, lifecycle and operator decision. return ['ok' => false, 'err' => 'ltc_observer_unqualified', 'available' => false, 'note' => 'New LTC deposits and automatic NSU release are unavailable while chain verification is being qualified. Existing tickets and payout obligations remain available for reconciliation.']; } function nst_ltc_ticket_response(array $tick, bool $idempotent = false): array { $ready = nst_ltc_auto_settlement_readiness(); return ['ok' => true, 'idempotent' => $idempotent, 'ticket' => nst_ltc_ticket_public($tick), 'admission_available' => !empty($ready['ok']), 'reconciliation_note' => (string)($ready['note'] ?? ''), 'pay' => ['asset' => 'LTC', 'address' => (string)($tick['addr'] ?? ''), 'amount_fmt' => (string)($tick['ltc_exact_fmt'] ?? ''), 'amount_nsu_fmt' => (string)($tick['amount_fmt'] ?? ''), 'pair' => NST_PUBLIC_PAIR, 'available' => !empty($ready['ok']), 'note' => !empty($ready['ok']) ? 'Use this same ticket for this purchase. NSU releases only after verified confirmation.' : 'Existing ticket reference only. Do not send a new LTC payment while automatic settlement is unavailable.']]; } function nst_ltc_ticket_open(string $buyer, string $oid, string $amountMicros, string $nonce = '', int $intentTs = 0): array { if (preg_match('/\A[0-9a-f]{64}\z/D', $buyer) !== 1 || !dec_ok($amountMicros) || preg_match('/\A[a-zA-Z0-9_-]{8,128}\z/D', $nonce) !== 1) return ['ok' => false, 'err' => 'Need a valid signed LTC ticket intent']; $amountMicros = dec_norm($amountMicros); $intentId = hash('sha256', 'ltc_ticket_v2|' . $buyer . '|' . $nonce); $snapshot = nst_ltc_with_state(function (array $s) { return $s; }); if (!is_array($snapshot) || isset($snapshot['err'])) return is_array($snapshot) ? $snapshot : ['ok' => false, 'err' => 'ltc_state_unavailable']; if (isset($snapshot['tickets'][$intentId])) { $prior = $snapshot['tickets'][$intentId]; if (($prior['buyer'] ?? '') !== $buyer || ($prior['order_id'] ?? '') !== $oid || ($prior['amount'] ?? '') !== $amountMicros) return ['ok' => false, 'err' => 'LTC ticket intent already used with different details']; return nst_ltc_ticket_response($prior, true); } if ($intentTs < 1 || abs(time() - $intentTs) > 600) return ['ok' => false, 'err' => 'Clock skew: no new LTC ticket opened']; $oid = preg_replace('/[^a-f0-9]/', '', strtolower($oid)) ?? ''; if (strlen($oid) !== 64) return ['ok' => false, 'err' => 'Bad order id']; $min = parse_amt(NST_ORDER_MIN_NSU); if ($min === null || dec_cmp($amountMicros, $min) < 0) { return ['ok' => false, 'err' => 'Order amount must be at least 0.000001 NSU (one micro)']; } $chain = read_chain(); $found = null; foreach (open_orders($chain) as $o) { if (($o['id'] ?? '') === $oid) { $found = $o; break; } } if (!$found) return ['ok' => false, 'err' => 'That ask is not open']; if ((string)($found['side'] ?? '') !== 'sell') return ['ok' => false, 'err' => 'Click a sell (ask) to buy NSU']; if (strtoupper((string)($found['pair'] ?? '')) !== NST_PUBLIC_PAIR) return ['ok' => false, 'err' => 'Wrong pair']; if (($found['kind'] ?? '') === 'bootstrap_spread') return ['ok' => false, 'err' => 'Discovery quote — not fillable']; $rem = dec_norm((string)($found['remaining'] ?? '0')); if (dec_cmp($amountMicros, $rem) > 0) { return ['ok' => false, 'err' => 'That ask no longer has enough left', 'remaining_fmt' => fmt_amt($rem)]; } $king = strtolower(treasury_addr()); if (!hash_equals($king, strtolower((string)($found['addr'] ?? '')))) return ['ok' => false, 'err' => 'Only KING asks support LTC receive tickets; other makers settle manually']; $ready = nst_ltc_auto_settlement_readiness(); if (empty($ready['ok'])) return $ready; $words = nst_ltc_secret_read(); if ($words === '') return ['ok' => false, 'err' => 'LTC receive is not open yet']; $px = dec_norm((string)($found['price'] ?? '0')); $ltc = nst_ltc_total_micros($amountMicros, $px); if (dec_cmp($ltc, '1') < 0) return ['ok' => false, 'err' => 'LTC total too small']; $out = nst_ltc_with_state(function (array $st) use ($words, $buyer, $oid, $amountMicros, $px, $ltc, $rem, $intentId, $nonce) { if (isset($st['tickets'][$intentId])) { $prior = $st['tickets'][$intentId]; if (($prior['buyer'] ?? '') !== $buyer || ($prior['order_id'] ?? '') !== $oid || ($prior['amount'] ?? '') !== $amountMicros) return ['ok' => false, 'err' => 'LTC ticket intent already used with different details']; return ['ok' => true, '_ticket' => $prior, 'idempotent' => true]; } $reserved = nst_ltc_open_reserved_on_order($st, $oid); $avail = dec_sub($rem, $reserved); if (dec_cmp($avail, $amountMicros) < 0) { return ['ok' => false, 'err' => 'That chunk is already reserved by an open ticket', 'available_fmt' => fmt_amt($avail)]; } $idx = (int)($st['next_index'] ?? 0); $der = nst_ltc_derive_recv($words, $idx); if (empty($der['ok']) || (string)($der['addr'] ?? '') === '') { return ['ok' => false, 'err' => 'Could not derive a receive address']; } $id = $intentId; $ts = time(); $tick = [ 'id' => $id, 'intent_nonce' => $nonce, 'index' => $idx, 'path' => (string)$der['path'], 'addr' => (string)$der['addr'], 'order_id' => $oid, 'buyer' => $buyer, 'amount' => $amountMicros, 'amount_fmt' => fmt_amt($amountMicros), 'price' => $px, 'price_fmt' => fmt_amt($px), 'ltc_exact' => $ltc, 'ltc_exact_fmt' => fmt_amt($ltc), 'status' => 'open', 'txid' => '', 'confirms' => 0, 'created_ts' => $ts, 'expires_ts' => $ts + NST_LTC_TICKET_TTL, 'paid_ts' => 0, ]; $st['tickets'][$id] = $tick; $st['next_index'] = $idx + 1; if ((string)($st['master_addr'] ?? '') === '') $st['master_addr'] = (string)$der['addr']; $st['_dirty'] = true; $st['_ticket'] = $tick; return $st; }); if (!is_array($out) || empty($out['ok']) && empty($out['_ticket'])) { if (is_array($out) && isset($out['err'])) return $out; return ['ok' => false, 'err' => 'Could not open ticket']; } if (isset($out['err'])) return $out; $tick = $out['_ticket'] ?? null; if (!is_array($tick)) return ['ok' => false, 'err' => 'Could not open ticket']; return nst_ltc_ticket_response($tick, !empty($out['idempotent'])); } function nst_ltc_ticket_lookup(string $id): array { $id = preg_replace('/[^a-f0-9]/', '', strtolower($id)) ?? ''; $st = nst_ltc_with_state(function (array $s) { return $s; }); if (!is_array($st) || isset($st['err'])) return is_array($st) ? $st : ['ok' => false, 'err' => 'ltc_state_unavailable']; foreach ($st['tickets'] as $t) { if (!is_array($t)) continue; if ((string)($t['id'] ?? '') === $id || (string)($t['addr'] ?? '') === $id) { return ['ok' => true, 'ticket' => nst_ltc_ticket_public($t), 'settlement' => nst_ltc_auto_settlement_readiness()]; } } return ['ok' => false, 'err' => 'no ticket']; } $nstLtcP2pFile = __DIR__ . DIRECTORY_SEPARATOR . 'ltc-p2p.php'; if (!in_array((string)($_GET['api'] ?? ''), ['wallet_summary','wallet_receipt','wallet_register','wallet_send'], true) && is_file($nstLtcP2pFile) && is_readable($nstLtcP2pFile)) { require_once $nstLtcP2pFile; } function nst_ltc_watch_open_tickets(): array { $st = nst_ltc_with_state(function (array $s) { return $s; }); if (!is_array($st) || isset($st['err'])) return is_array($st) ? $st : ['ok' => false, 'err' => 'ltc_state_unavailable']; $ready = nst_ltc_auto_settlement_readiness(); if (empty($ready['ok'])) return $ready + ['checked' => 0, 'settled' => 0, 'engine' => 'p2p', 'net' => false, 'records_preserved' => true]; $now = time(); $checked = 0; $settled = 0; $notes = []; $open = []; foreach ($st['tickets'] as $t) { if (!is_array($t)) continue; $status = (string)($t['status'] ?? ''); if ((string)($t['kind'] ?? '') === 'half_bid_payout') continue; if ($status === 'paid' || $status === 'cancelled') continue; $open[] = $t; } $watch = ['ok' => true, 'proofs' => [], 'notes' => [], 'fail_open' => true, 'engine' => 'p2p', 'net' => nst_ltc_net_allowed()]; if (function_exists('nst_ltc_p2p_watch')) { $watch = nst_ltc_p2p_watch($open); if (!is_array($watch)) { $watch = ['ok' => true, 'proofs' => [], 'notes' => ['p2p_blink'], 'fail_open' => true, 'engine' => 'p2p']; } } else { $watch['notes'][] = 'p2p_missing'; } $proofs = (isset($watch['proofs']) && is_array($watch['proofs'])) ? $watch['proofs'] : []; foreach ($open as $t) { $id = (string)($t['id'] ?? ''); $checked++; $pr = $proofs[$id] ?? null; if (!is_array($pr) || empty($pr['ok'])) { continue; } $confSat = (string)($pr['sat'] ?? '0'); $need = nst_ltc_sat_from_micros((string)($t['ltc_exact'] ?? '0')); $confirms = (int)($pr['confirms'] ?? 0); $observed = nst_ltc_with_state(function (array $s) use ($t, $confSat, $need, $confirms, $pr) { $tid = (string)($t['id'] ?? ''); if ($tid === '' || !isset($s['tickets'][$tid])) return $s; $s['tickets'][$tid]['confirms'] = $confirms; $s['tickets'][$tid]['seen_sat'] = $confSat; if ((string)($pr['txid'] ?? '') !== '') $s['tickets'][$tid]['txid'] = (string)$pr['txid']; $s['tickets'][$tid]['proof'] = 'p2p'; if (dec_cmp($confSat, '0') > 0 && ($s['tickets'][$tid]['status'] ?? '') === 'open') { $s['tickets'][$tid]['status'] = 'seen'; } $sum = '0'; foreach ($s['tickets'] as $tt) { if (!is_array($tt)) continue; $sum = dec_add($sum, dec_norm((string)($tt['seen_sat'] ?? '0'))); } $s['watched_sat'] = $sum; $s['watch_note'] = 'p2p'; $s['_dirty'] = true; return $s; }); if (!is_array($observed) || isset($observed['err'])) { $notes[] = 'observation_not_saved'; continue; } $needConf = NST_LTC_CONFIRMS; if (dec_cmp($confSat, $need) >= 0 && $confirms >= $needConf && (string)($t['status'] ?? '') !== 'paid') { $r = nst_ltc_settle_ticket($id); if (!empty($r['ok'])) $settled++; else $notes[] = (string)($r['err'] ?? 'settle'); } } $notes = array_merge($notes, is_array($watch['notes'] ?? null) ? $watch['notes'] : []); return [ 'ok' => true, 'checked' => $checked, 'settled' => $settled, 'fail_open' => true, 'notes' => array_values(array_unique($notes)), 'net' => nst_ltc_net_allowed(), 'engine' => 'p2p', 'tip' => (string)($watch['tip'] ?? ''), 'height' => (int)($watch['height'] ?? 0), 'peers' => (int)($watch['peers'] ?? 0), ]; } function nst_ltc_settle_ticket(string $ticketId): array { $ticketId = preg_replace('/[^a-f0-9]/', '', strtolower($ticketId)) ?? ''; if (strlen($ticketId) !== 64) return ['ok' => false, 'err' => 'bad ticket']; $st = nst_ltc_with_state(function (array $s) { return $s; }); if (!is_array($st) || isset($st['err'])) return is_array($st) ? $st : ['ok' => false, 'err' => 'ltc_state_unavailable']; $t = $st['tickets'][$ticketId] ?? null; if (!is_array($t)) return ['ok' => false, 'err' => 'no ticket']; $knownFillId = hash('sha256', $ticketId . '|ltc_fill'); $alreadyCommitted = false; foreach (read_chain() as $r) if (($r['type'] ?? '') === 'fill' && ($r['fill_id'] ?? '') === $knownFillId) { $alreadyCommitted = true; break; } if (!$alreadyCommitted) { $ready = nst_ltc_auto_settlement_readiness(); if (empty($ready['ok'])) return $ready + ['nsu_committed' => false]; } $oid = (string)($t['order_id'] ?? ''); $buyer = (string)($t['buyer'] ?? ''); $qty = dec_norm((string)($t['amount'] ?? '0')); $tsec = treasury_secret(); $taddr = strtolower(addr_from_seed($tsec)); $ts = time(); $fillId = hash('sha256', $ticketId . '|ltc_fill'); $nonce = substr(hash('sha256', $ticketId . '|ltc_nonce'), 0, 16); $settleRef = 'ltc:' . (string)($t['addr'] ?? ''); $fillOut = nst_with_chain_lock(function (array $chain, callable $append, callable $appendBatch) use ( $oid, $buyer, $qty, $taddr, $tsec, $ts, $fillId, $nonce, $settleRef, $t ) { foreach ($chain as $prior) { if (($prior['type'] ?? '') === 'fill' && ($prior['fill_id'] ?? '') === $fillId) { if (($prior['order_id'] ?? '') !== $oid || ($prior['taker'] ?? '') !== $buyer || ($prior['maker'] ?? '') !== $taddr || ($prior['amount'] ?? '') !== $qty || ($prior['side'] ?? '') !== 'sell' || ($prior['pair'] ?? '') !== NST_PUBLIC_PAIR) return ['ok' => false, 'err' => 'committed_ticket_conflict']; $priorTx = null; foreach ($chain as $row) if (($row['type'] ?? '') === 'transfer' && ($row['fill_id'] ?? '') === $fillId && ($row['order_id'] ?? '') === $oid && ($row['from'] ?? '') === $taddr && ($row['to'] ?? '') === $buyer && ($row['amount'] ?? '') === $qty) { $priorTx = $row; break; } if ($priorTx === null) return ['ok' => false, 'err' => 'committed_ticket_transfer_missing']; $hb = nst_treasury_half_bid_from_sell($fillId, NST_PUBLIC_PAIR, $qty, (string)$prior['price'], (int)$prior['ts'] + 30 * 86400, (int)$prior['ts'], $chain, $append, $tsec, $taddr); if (empty($hb['ok'])) return ['ok' => false, 'err' => 'half_bid_recovery_pending', 'nsu_committed' => true]; return ['ok' => true, 'idempotent' => true, 'fill' => $prior, 'transfer' => $priorTx, 'half_bid' => $hb['order'] ?? null, 'half_ok' => true]; } } $found = null; foreach (open_orders($chain) as $o) { if (($o['id'] ?? '') === $oid) { $found = $o; break; } } if (!$found) return ['ok' => false, 'err' => 'order not open']; if (strtolower((string)($found['addr'] ?? '')) !== $taddr) return ['ok' => false, 'err' => 'not king ask']; if ((string)($found['side'] ?? '') !== 'sell') return ['ok' => false, 'err' => 'not a sell']; if (strtoupper((string)($found['pair'] ?? '')) !== NST_PUBLIC_PAIR) return ['ok' => false, 'err' => 'pair']; $rem = dec_norm((string)($found['remaining'] ?? '0')); if (dec_cmp($qty, $rem) > 0) return ['ok' => false, 'err' => 'remaining too small']; $bal = balances($chain); if (dec_cmp((string)($bal[$taddr] ?? '0'), $qty) < 0) return ['ok' => false, 'err' => 'KING lacks NSU; no ticket release committed']; $px = dec_norm((string)($found['price'] ?? '0')); if ($px !== (string)($t['price'] ?? '')) return ['ok' => false, 'err' => 'ticket_price_conflict']; $memo = 'fill:' . substr($oid, 0, 12); $tbody = implode('|', ['transfer', '1', $taddr, $buyer, $qty, $nonce, (string)$ts, $memo]); $tx = [ 'type' => 'transfer', 'ver' => 1, 'from' => $taddr, 'to' => $buyer, 'amount' => $qty, 'amount_fmt' => fmt_amt($qty), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => mac_sign($tsec, $tbody), 'auth' => 'king_ltc_release', 'order_id' => $oid, 'fill_id' => $fillId, 'kind' => 'ltc_ticket_fill', ]; $fbody = implode('|', ['fill', '2', $fillId, $oid, $taddr, $buyer, $qty, $settleRef, (string)$ts]); $fill = [ 'type' => 'fill', 'ver' => 2, 'fill_id' => $fillId, 'order_id' => $oid, 'maker' => $taddr, 'taker' => $buyer, 'side' => 'sell', 'pair' => NST_PUBLIC_PAIR, 'amount' => $qty, 'amount_fmt' => fmt_amt($qty), 'price' => $px, 'price_fmt' => fmt_amt($px), 'settle_ref' => $settleRef, 'ts' => $ts, 'taker_sig' => mac_sign($tsec, $fbody), 'sig_mode' => 'server_mac', 'kind' => 'ltc_ticket_fill', ]; $batch = [$tx, $fill]; $stage = function (array $row) use (&$batch): bool { $batch[] = $row; return true; }; $hb = nst_treasury_half_bid_from_sell( $fillId, NST_PUBLIC_PAIR, $qty, $px, $ts + 30 * 86400, $ts, $chain, $stage, $tsec, $taddr ); if (empty($hb['ok'])) return ['ok' => false, 'err' => 'half_bid_not_staged']; if (!$appendBatch($batch)) return ['ok' => false, 'err' => 'Could not complete that trade fill — try again in a moment']; return ['ok' => true, 'fill' => $fill, 'transfer' => $tx, 'half_bid' => $hb['order'] ?? null, 'half_ok' => true]; }); if (!is_array($fillOut) || empty($fillOut['ok'])) { return is_array($fillOut) ? $fillOut : ['ok' => false, 'err' => 'settle lock']; } $saved = nst_ltc_with_state(function (array $s) use ($ticketId, $fillId, $t) { if (!isset($s['tickets'][$ticketId]) || ($s['tickets'][$ticketId]['buyer'] ?? '') !== ($t['buyer'] ?? '') || ($s['tickets'][$ticketId]['order_id'] ?? '') !== ($t['order_id'] ?? '') || ($s['tickets'][$ticketId]['amount'] ?? '') !== ($t['amount'] ?? '')) return ['ok' => false, 'err' => 'ticket_changed_during_settlement']; if (isset($s['tickets'][$ticketId])) { $s['tickets'][$ticketId]['status'] = 'paid'; if (empty($s['tickets'][$ticketId]['paid_ts'])) $s['tickets'][$ticketId]['paid_ts'] = time(); $s['tickets'][$ticketId]['fill_id'] = $fillId; $s['tickets'][$ticketId]['confirms'] = NST_LTC_CONFIRMS; } $listed = false; foreach ($s['pay_list'] as $p) if (($p['ticket_id'] ?? '') === $ticketId && ($p['fill_id'] ?? '') === $fillId) { $listed = true; break; } if (!$listed) $s['pay_list'][] = [ 'kind' => 'half_bid_payout', 'ticket_id' => $ticketId, 'fill_id' => $fillId, 'half_bid_id' => hash('sha256', $fillId . '|treasury_half_bid'), 'order_id' => hash('sha256', $fillId . '|treasury_half_bid'), 'nsu_fmt' => (string)($t['amount_fmt'] ?? ''), 'ltc_fmt' => fmt_amt(nst_ltc_total_micros((string)($t['amount'] ?? '0'), dec_half((string)($t['price'] ?? '0')))), 'payout_addr' => '', 'status' => 'awaiting_seller_addr', 'outbound_sign' => 'honest_gap', 'sent' => false, 'note' => 'Half-bid armed. Seller pastes ltc1 on fill. Outbound BIP84 sign not shipped — king pays by hand. Not sent.', ]; $s['_dirty'] = true; return $s; }); if (!is_array($saved) || isset($saved['err'])) return ['ok' => false, 'err' => 'ticket_status_recovery_pending', 'nsu_committed' => true, 'fill' => $fillOut['fill'] ?? null]; try { if (is_array($fillOut['fill'] ?? null) && function_exists('nst_inbox_notify_fill')) { nst_inbox_notify_fill($fillOut['fill']); } if (is_array($fillOut['half_bid'] ?? null) && function_exists('nst_inbox_notify_half_bid')) { nst_inbox_notify_half_bid($fillOut['fill'] ?? null, $fillOut['half_bid']); } } catch (Throwable $e) { } return $fillOut; } function nst_king_ltc_lot_post(): array { $tsec = treasury_secret(); $taddr = strtolower(addr_from_seed($tsec)); $amt = parse_amt(NST_LTC_LOT_NSU); $px = parse_amt(NST_LTC_LOT_PX_FMT); if ($amt === null || $px === null) return ['ok' => false, 'err' => 'lot constants']; $ts = time(); $expiry = $ts + 30 * 86400; $pair = NST_PUBLIC_PAIR; $out = nst_with_chain_lock(function (array $chain, callable $append) use ($tsec, $taddr, $amt, $px, $ts, $expiry, $pair) { $av = available_nsu(balances($chain), $chain, $taddr); if (dec_cmp($av, $amt) < 0) { return ['ok' => false, 'err' => 'Not enough free NSU — cancel open sells or buy more on the book', 'available_fmt' => fmt_amt($av)]; } $nOpen = 0; foreach (open_orders($chain) as $o) { if (($o['addr'] ?? '') === $taddr) $nOpen++; } if ($nOpen >= 20) return ['ok' => false, 'err' => 'too many open orders']; $id = hash('sha256', $taddr . '|king_ltc_lot|' . $amt . '|' . $px . '|' . $ts . '|' . bin2hex(random_bytes(4))); $body = implode('|', ['order', '1', $id, $taddr, 'sell', $pair, $amt, $px, (string)$expiry, (string)$ts]); $row = [ 'type' => 'order', 'ver' => 1, 'id' => $id, 'addr' => $taddr, 'side' => 'sell', 'pair' => $pair, 'amount' => $amt, 'amount_fmt' => fmt_amt($amt), 'price' => $px, 'price_fmt' => fmt_amt($px), 'expiry' => $expiry, 'ts' => $ts, 'sig' => mac_sign($tsec, $body), 'kind' => 'king_ltc_lot', 'note' => '1000 NSU for 1 LTC (0.001 LTC per NSU). Half-bid arms on fill, same size, half price.', ]; if (!$append($row)) return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment']; return ['ok' => true, 'order' => $row]; }); return is_array($out) ? $out : ['ok' => false, 'err' => 'Could not save to the server — try again in a moment']; } function nst_king_ltc_lot_cancel(string $oid): array { $oid = preg_replace('/[^a-f0-9]/', '', strtolower($oid)) ?? ''; if (strlen($oid) !== 64) return ['ok' => false, 'err' => 'Bad order id']; $tsec = treasury_secret(); $taddr = strtolower(addr_from_seed($tsec)); $ts = time(); $out = nst_with_chain_lock(function (array $chain, callable $append) use ($oid, $tsec, $taddr, $ts) { $found = null; foreach (open_orders($chain) as $o) { if (($o['id'] ?? '') === $oid) { $found = $o; break; } } if (!$found) return ['ok' => false, 'err' => 'That order is not open']; if (strtolower((string)($found['addr'] ?? '')) !== $taddr) return ['ok' => false, 'err' => 'not king lot']; $body = implode('|', ['cancel', '1', $oid, $taddr, (string)$ts]); $row = [ 'type' => 'cancel', 'ver' => 1, 'order_id' => $oid, 'addr' => $taddr, 'ts' => $ts, 'sig' => mac_sign($tsec, $body), 'sig_mode' => 'server_mac', ]; if (!$append($row)) return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment']; return ['ok' => true, 'cancel' => $row]; }); return is_array($out) ? $out : ['ok' => false, 'err' => 'Could not save to the server — try again in a moment']; } function nst_ltc_require_king(): void { if (!nst_host_is_trade()) { api_out(['ok' => false, 'err' => 'not found'], 404); } require_faucet_auth(); } /** Visitor paste: native BIP84 ltc1 (P2WPKH). Rejects cashaddr / legacy / empty. */ function nst_ltc_payout_addr_ok(string $addr): bool { $addr = strtolower(trim($addr)); if ($addr === '' || strpos($addr, 'ltc1') !== 0) return false; return nst_ltc_script_p2wpkh($addr) !== ''; } /** * Remaining gap for king outbound LTC. Prefer this over a invented BIP84 spender * that can lose coins. Receive HD + P2P watch exist; spend+sign does not. */ function nst_ltc_outbound_gap_reasons(): array { return [ 'no_utxo_outpoints' => 'P2P watch records ticket sat sums, not spendable txid:vout', 'no_bip143' => 'P2WPKH BIP143 sighash is not implemented', 'no_ecdsa_low_s' => 'OpenSSL is used for pubkey only, not Bitcoin/LTC low-S signatures', 'no_change_path' => "HD derive is receive m/84'/2'/0'/0/i only; no change chain", 'no_fee_policy' => 'no mempool/fee estimator; inventing one can burn or stall LTC', 'broadcast_ready' => 'ltc-p2p.php can push a raw tx message; nothing here builds one', ]; } /** * Store visitor ltc1 on the payout ticket + king pay list. * Never marks sent. Never claims a broadcast happened. */ function nst_ltc_record_half_bid_payout(string $fillId, string $oid, string $seller, string $qty, string $px, string $payoutAddr): array { $payoutAddr = strtolower(trim($payoutAddr)); foreach ([$fillId, $oid, $seller] as $hex) if (preg_match('/\A[0-9a-f]{64}\z/D', $hex) !== 1) return ['ok' => false, 'err' => 'bad payout identity', 'sent' => false]; if (!nst_ltc_payout_addr_ok($payoutAddr) || !dec_ok($qty) || !dec_ok($px) || dec_cmp($qty, '1') < 0 || dec_cmp($px, '1') < 0) return ['ok' => false, 'err' => 'bad payout details', 'sent' => false]; $qty = dec_norm($qty); $px = dec_norm($px); $ltc = nst_ltc_total_micros($qty, $px); $tid = hash('sha256', $fillId . '|payout'); $out = nst_ltc_with_state(function (array $s) use ($fillId, $oid, $seller, $qty, $px, $payoutAddr, $ltc, $tid) { $existing = $s['tickets'][$tid] ?? null; $matches = []; foreach ($s['pay_list'] as $i => $p) { // The order can have many sellers. Only this committed fill identifies an obligation. if (($p['kind'] ?? '') === 'half_bid_payout' && ($p['fill_id'] ?? '') === $fillId) $matches[$i] = $p; } if (count($matches) > 1) return ['ok' => false, 'err' => 'payout_legacy_ambiguous']; foreach ($matches as $p) { if (($p['ticket_id'] ?? '') !== $tid || ($p['order_id'] ?? '') !== $oid || ($p['seller'] ?? '') !== $seller || ($p['payout_addr'] ?? '') !== $payoutAddr || ($p['nsu_fmt'] ?? '') !== fmt_amt($qty) || ($p['ltc_fmt'] ?? '') !== fmt_amt($ltc)) return ['ok' => false, 'err' => 'payout_identity_conflict']; } if ($existing !== null) { if (!is_array($existing) || ($existing['kind'] ?? '') !== 'half_bid_payout' || ($existing['fill_id'] ?? '') !== $fillId || ($existing['order_id'] ?? '') !== $oid || ($existing['buyer'] ?? '') !== $seller || ($existing['payout_addr'] ?? '') !== $payoutAddr || ($existing['amount'] ?? '') !== $qty || ($existing['price'] ?? '') !== $px || ($existing['ltc_exact'] ?? '') !== $ltc) return ['ok' => false, 'err' => 'payout_identity_conflict']; if ($matches) return ['ok' => true, '_ticket' => $existing, 'idempotent' => true]; } $prior = $existing ?? ($matches ? reset($matches) : []); $sent = !empty($prior['sent']); $status = (string)($prior['status'] ?? 'unpaid'); $note = 'Visitor LTC address recorded for this fill. Outbound signing is unavailable; this action sends no LTC.'; $tick = $existing ?? [ 'id' => $tid, 'kind' => 'half_bid_payout', 'payout_addr' => $payoutAddr, 'order_id' => $oid, 'fill_id' => $fillId, 'buyer' => $seller, 'amount' => $qty, 'amount_fmt' => fmt_amt($qty), 'price' => $px, 'price_fmt' => fmt_amt($px), 'ltc_exact' => $ltc, 'ltc_exact_fmt' => fmt_amt($ltc), 'status' => $status, 'outbound_sign' => 'honest_gap', 'outbound_note' => $note, 'sent' => $sent, 'txid' => (string)($prior['txid'] ?? ''), 'confirms' => (int)($prior['confirms'] ?? 0), 'created_ts' => time(), 'expires_ts' => 0, 'paid_ts' => (int)($prior['paid_ts'] ?? 0), ]; if (!$matches) $s['pay_list'][] = [ 'kind' => 'half_bid_payout', 'ticket_id' => $tid, 'fill_id' => $fillId, 'half_bid_id' => $oid, 'order_id' => $oid, 'seller' => $seller, 'nsu_fmt' => fmt_amt($qty), 'ltc_fmt' => fmt_amt($ltc), 'payout_addr' => $payoutAddr, 'status' => $status, 'outbound_sign' => 'honest_gap', 'sent' => $sent, 'note' => $note, ]; $s['tickets'][$tid] = $tick; $s['_ticket'] = $tick; $s['_dirty'] = true; return $s; }); if (!is_array($out) || isset($out['err']) || !is_array($out['_ticket'] ?? null)) return ['ok' => false, 'err' => is_array($out) ? (string)($out['err'] ?? 'payout_store_failed') : 'payout_store_failed', 'sent' => false]; return ['ok' => true, 'sent' => !empty($out['_ticket']['sent']), 'outbound_sign' => 'honest_gap', 'payout_addr' => $payoutAddr, 'ticket' => nst_ltc_ticket_public($out['_ticket']), 'idempotent' => !empty($out['idempotent']), 'note' => 'Payout obligation recorded for this fill. No LTC sent by this action.']; } /** * Server job hook: would build/sign/broadcast via ltc-p2p.php tx message. * This pass returns the honest gap. Does not invent a spender. Does not mark sent. */ function nst_ltc_payout_proof_body(string $fillId, string $oid, string $seller, string $payoutAddr, int $ts): string { return implode('|', ['ltc_payout', '1', $fillId, $oid, $seller, $payoutAddr, (string)$ts]); } function nst_ltc_reconcile_payout_fill(array $fill): array { // Recovery uses only the recipient committed after its separate signed proof. // An old fill without that field cannot adopt a retry's new address. if (($fill['payout_proof_ver'] ?? 0) !== 1 || ($fill['side'] ?? '') !== 'buy' || ($fill['pair'] ?? '') !== NST_PUBLIC_PAIR || !isset($fill['payout_ltc'])) { return ['payout_recorded' => false, 'outbound_sent' => false, 'payout_error' => 'legacy_payout_manual_reconciliation', 'outbound_note' => 'NSU fill is committed. Its legacy payout needs manual reconciliation; no new recipient adopted and no LTC sent.']; } $rec = nst_ltc_record_half_bid_payout((string)$fill['fill_id'], (string)$fill['order_id'], (string)$fill['taker'], (string)$fill['amount'], (string)$fill['price'], (string)$fill['payout_ltc']); $out = ['payout_recorded' => !empty($rec['ok']), 'payout_addr' => (string)$fill['payout_ltc'], 'outbound_sign' => 'honest_gap', 'outbound_sent' => !empty($rec['sent']), 'outbound_note' => !empty($rec['ok']) ? (string)$rec['note'] : 'NSU fill is committed; payout record needs recovery. Retry this same fill. No LTC sent.']; if (empty($rec['ok'])) $out['payout_error'] = (string)($rec['err'] ?? 'payout_store_failed'); return $out; } function nst_ltc_outbound_try_pay(): array { $gap = nst_ltc_outbound_gap_reasons(); return [ 'ok' => false, 'sent' => false, 'outbound_sign' => 'honest_gap', 'remaining_gap' => $gap, 'note' => 'King pays this half-bid by hand / SSH from data/ltc.secret. Pay list shows the visitor ltc1. This job does not sign or broadcast and does not mark sent.', ]; } /* ===== END LTC LIGHT WALLET ===== */ /** Operator idempotency key (admin money routes). REQUIRED POST request_id: one per operator action, reused on its retries. */ function nst_op_request_id(): string { $rid = trim((string)($_POST['request_id'] ?? '')); if ($rid === '') api_out(['ok' => false, 'err' => 'request_id required (one per action; reuse it when retrying)'], 400); if (!preg_match('/^[A-Za-z0-9_-]{1,80}$/D', $rid)) api_out(['ok' => false, 'err' => 'Bad request_id (1-80 of A-Z a-z 0-9 _ -)'], 400); return $rid; } /** Deterministic treasury nonce for a keyed operator request; route-scoped so keys never collide across routes. */ function nst_op_request_nonce(string $route, string $rid): string { return 'rq-' . substr(hash('sha256', $route . '|' . $rid), 0, 32); } function economy_credit_donate(string $toAddr, string $nsuMicros, string $txid, string $asset, string $extAmt, string $requestId = ''): array { $nsuMicros = dec_norm($nsuMicros); if (dec_cmp($nsuMicros, '1') < 0) return ['ok' => false, 'err' => 'gift too small']; $sec = treasury_secret(); $from = addr_from_seed($sec); // half ad stake / half profit pile; then arm BUYBACK at market P/2 for the ad half // (stable floor: P converges from book — never a fixed sticker price) $half = dec_half($nsuMicros); $bidPart = $half; $profitPart = dec_sub($nsuMicros, $bidPart); $ts = time(); $nonce = $requestId !== '' ? nst_op_request_nonce('admin_donate_credit', $requestId) : bin2hex(random_bytes(8)); $memo = 'donate:gift:' . substr($txid !== '' ? $txid : 'honor', 0, 40); $tbody = implode('|', ['transfer', '1', $from, $toAddr, $nsuMicros, $nonce, (string)$ts, $memo]); $row = [ 'type' => 'transfer', 'ver' => 1, 'from' => $from, 'to' => $toAddr, 'amount' => $nsuMicros, 'amount_fmt' => fmt_amt($nsuMicros), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => mac_sign($sec, $tbody), 'kind' => 'donate_gift', 'txid' => substr($txid, 0, 128), 'asset' => substr($asset, 0, 16), ]; $splitRow = [ 'type' => 'economy_split', 'ver' => 1, 'ts' => $ts, 'gift_fmt' => fmt_amt($nsuMicros), 'profit_fmt' => fmt_amt($profitPart), 'ad_bid_fmt' => fmt_amt($bidPart), 'txid' => substr($txid, 0, 128), 'note' => '50% profit pile / 50% site ad bid (donate path); buyback arms at market P/2', ]; $creditOut = nst_with_chain_lock(function (array $chain, callable $append) use ( $from, $toAddr, $nsuMicros, $nonce, $row, $splitRow, $bidPart, $profitPart, $ts, $txid, $asset, $extAmt, $requestId ) { /* Operator idempotency: key = request_id, else the donate txid. Identical -> original tx, no row; different -> 409. */ if ($requestId !== '' || $txid !== '') { foreach ($chain as $r) { if (($r['type'] ?? '') !== 'transfer' || ($r['from'] ?? '') !== $from || ($r['kind'] ?? '') !== 'donate_gift') continue; if (!(($requestId !== '' && ($r['nonce'] ?? '') === $nonce) || ($txid !== '' && (string)($r['txid'] ?? '') === substr($txid, 0, 128)))) continue; if (($r['to'] ?? '') === $toAddr && ($r['amount'] ?? '') === $nsuMicros && (string)($r['asset'] ?? '') === substr($asset, 0, 16) && (string)($r['txid'] ?? '') === substr($txid, 0, 128)) { return ['ok' => true, 'idempotent' => true, 'replay' => true, 'tx' => $r]; } return ['ok' => false, 'http' => 409, 'err' => 'That donate reference was already credited with different terms']; } } $bal = balances($chain); $have = $bal[$from] ?? '0'; if (dec_cmp($have, $nsuMicros) < 0) { return ['ok' => false, 'err' => 'Treasury does not have enough free NSU for that — try a smaller amount', 'have_fmt' => fmt_amt($have)]; } foreach ($chain as $r) { if (($r['type'] ?? '') === 'transfer' && ($r['from'] ?? '') === $from && ($r['nonce'] ?? '') === $nonce) { return ['ok' => false, 'err' => 'That transfer was already sent (nonce replay) — use a new nonce']; } } if (!$append($row)) return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment']; $e = load_economy(); $e['profit_micros'] = dec_add((string)($e['profit_micros'] ?? '0'), $profitPart); $e['ad_bid_micros'] = dec_add((string)($e['ad_bid_micros'] ?? '0'), $bidPart); $bid = [ 'id' => hash('sha256', $toAddr . '|' . $ts . '|' . $bidPart . '|' . $nonce), 'stake_micros' => $bidPart, 'stake_fmt' => fmt_amt($bidPart), 'ts' => $ts, 'from_donate' => true, 'txid' => substr($txid, 0, 128), 'active' => true, ]; if (!isset($e['bids']) || !is_array($e['bids'])) $e['bids'] = []; $e['bids'][] = $bid; if (count($e['bids']) > 200) $e['bids'] = array_slice($e['bids'], -200); if (!isset($e['donations']) || !is_array($e['donations'])) $e['donations'] = []; $e['donations'][] = [ 'ts' => $ts, 'to' => $toAddr, 'gift_micros' => $nsuMicros, 'gift_fmt' => fmt_amt($nsuMicros), 'profit' => $profitPart, 'ad_bid' => $bidPart, 'txid' => substr($txid, 0, 128), 'asset' => substr($asset, 0, 16), 'ext' => substr($extAmt, 0, 40), ]; if (count($e['donations']) > 200) $e['donations'] = array_slice($e['donations'], -200); save_economy($e); if (!$append($splitRow)) { return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment']; } return [ 'ok' => true, 'tx' => $row, 'gift_fmt' => fmt_amt($nsuMicros), 'profit_fmt' => fmt_amt($profitPart), 'ad_bid_fmt' => fmt_amt($bidPart), 'economy' => [ 'profit_fmt' => fmt_amt((string)$e['profit_micros']), 'ad_bid_fmt' => fmt_amt((string)$e['ad_bid_micros']), ], ]; }); if ($creditOut === null) return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment']; if (empty($creditOut['ok'])) return $creditOut; if (!empty($creditOut['replay'])) return $creditOut; /* already credited: no second buyback arm */ // Lock floor: treasury BUY at converged P/2 for the ad-half quantity (fail-open if no book yet) $bb = nst_arm_buyback_bids($bidPart, 'donate_buyback'); if (!empty($bb['ok'])) { append_row([ 'type' => 'buyback_arm', 'ver' => 1, 'ts' => time(), 'amount_fmt' => fmt_amt($bidPart), 'buyback_px_fmt' => $bb['buyback_px_fmt'] ?? null, 'order_id' => $bb['order']['id'] ?? null, 'note' => 'market-converged P/2 floor bid (not fixed list price)', ]); } return [ 'ok' => true, 'tx' => $creditOut['tx'] ?? $row, 'gift_fmt' => $creditOut['gift_fmt'] ?? fmt_amt($nsuMicros), 'profit_fmt' => $creditOut['profit_fmt'] ?? fmt_amt($profitPart), 'ad_bid_fmt' => $creditOut['ad_bid_fmt'] ?? fmt_amt($bidPart), 'buyback' => $bb, 'price_policy' => 'market_converge_P_then_bid_P_over_2', 'economy' => $creditOut['economy'] ?? null, ]; } /** Convert external amount + book mid → NSU micros. mid = quote per 1 NSU (in micros of quote). */ function donate_nsu_from_external(string $extHuman, ?string $midMicros, string $k): ?string { $ext = parse_amt($extHuman); if ($ext === null || dec_cmp($ext, '1') < 0) return null; // If mid present: nsu = ext / mid * k (all integer micros) // ext and mid same decimal basis (6). nsu_micros = ext * 1e6 / mid * k ≈ ext/mid when k=1 // Actually both are already micros: nsu = floor(ext * k / mid) when mid is price in quote-micros per NSU unit? // price_fmt "2" means 2 NOTE per NSU → price micros = 2000000. amount 1 NSU = 1000000 micros. // gift NSU micros = ext_micros * 1e6 / mid_micros ... use: nsu = ext / mid * 10^0 in unit space. // unit: nsu_units = ext_units / mid_units * k → micros: nsu_m = ext_m * k / mid_m * 10^6 / 10^6 = ext*k/mid if ($midMicros !== null && dec_cmp($midMicros, '1') > 0) { if (function_exists('bcmul') && function_exists('bcdiv')) { $num = bcmul($ext, $k, 0); return dec_norm(bcdiv($num, $midMicros, 0)); } // fallback float for small values only $n = (float)$ext * (float)$k / (float)$midMicros; if ($n < 1) return null; return dec_norm((string)(int)floor($n)); } // no mid: nsu = ext * k if (function_exists('bcmul')) return dec_norm(bcmul($ext, $k, 0)); return dec_mul_small($ext, max(1, (int)$k)); } function ensure_meta_epochs(): array { $m = load_meta(); if (!empty($GLOBALS['NST_META_UNREADABLE'])) { /* Unreadable meta: no stamp, roll, save or emission on this request. */ $ro = nst_epoch_info_read_only(); return ['epoch_start' => (int)$ro['epoch_start'], 'epoch_n' => (int)$ro['epoch_n'], 'meta_unreadable' => true]; } $now = time(); if (empty($m['epoch_start'])) { $m['epoch_start'] = $now; $m['epoch_n'] = 1; } // advance yearly reset windows until current; roll economy stakes forward while (($m['epoch_start'] + NST_RESET_SECS) <= $now) { $fromN = (int)($m['epoch_n'] ?? 1); $m['epoch_start'] = (int)$m['epoch_start'] + NST_RESET_SECS; $m['epoch_n'] = $fromN + 1; economy_on_epoch_roll($fromN, (int)$m['epoch_n']); } if (empty($m['created'])) $m['created'] = $now; $m['version'] = NST_VERSION; save_meta($m); /* Anchor the crop registry BEFORE the first emission can spend against it, * so the ten wallets that receive issuance are on the ledger rather than * only in a file the King can rewrite. Idempotent - it writes once, ever. */ if (function_exists('nst_crop_registry_anchor')) { @nst_crop_registry_anchor(); } /* Emission rides the same page-load path as the epoch roll. It is * idempotent per UTC day and decides from the CHAIN, so extra calls * cost a replay and mint nothing. Deliberately AFTER save_meta so a * mint failure can never leave the epoch half-rolled. */ if (function_exists('nst_emission_run_daily')) { @nst_emission_run_daily(); } return $m; } function epoch_info(): array { $m = ensure_meta_epochs(); $start = (int)$m['epoch_start']; $end = $start + NST_RESET_SECS; $winEnd = $start + NST_EARLY_WINDOW_SECS; $now = time(); return [ 'epoch_n' => (int)($m['epoch_n'] ?? 1), 'epoch_start' => $start, 'epoch_end' => $end, 'early_window_end' => $winEnd, 'in_early_window' => $now >= $start && $now < $winEnd, 'secs_to_window_end' => max(0, $winEnd - $now), 'secs_to_reset' => max(0, $end - $now), ]; } /** Read-only epoch snapshot for the pre-ENTER HTML shell. Never rolls or emits. */ function nst_epoch_info_read_only(): array { $m = load_meta(); $now = time(); $start = (int)($m['epoch_start'] ?? $m['created'] ?? $now); $number = max(1, (int)($m['epoch_n'] ?? 1)); if ($start < 1) $start = $now; if ($start + NST_RESET_SECS <= $now) { $elapsed = intdiv(max(0, $now - $start), NST_RESET_SECS); $start += $elapsed * NST_RESET_SECS; $number += $elapsed; } $end = $start + NST_RESET_SECS; $winEnd = $start + NST_EARLY_WINDOW_SECS; return [ 'epoch_n' => $number, 'epoch_start' => $start, 'epoch_end' => $end, 'early_window_end' => $winEnd, 'in_early_window' => $now >= $start && $now < $winEnd, 'secs_to_window_end' => max(0, $winEnd - $now), 'secs_to_reset' => max(0, $end - $now), 'read_only' => true, ]; } /** Rentable crop ids (empire ten). Quotes only — settle/bind is a later residual. */ function nst_rent_site_ids(): array { return ['com', 'org', 'net', 'trade', 'chat', 'work', 'market', 'date', 'fun', 'info']; } /** Path to durable rent quote map (data/rent_quotes.json). */ function nst_rent_quotes_path(): string { global $RENT_QUOTES_FILE; return $RENT_QUOTES_FILE; } function nst_rent_quotes_read(): array { $path = nst_rent_quotes_path(); if (!is_file($path)) { return ['quotes' => [], 'updated' => 0]; } $j = json_decode((string)@file_get_contents($path), true); if (!is_array($j)) { return ['quotes' => [], 'updated' => 0]; } if (!isset($j['quotes']) || !is_array($j['quotes'])) { $j['quotes'] = []; } return $j; } /** Newest non-expired open quote for a site. Read-only — no flock, no create. */ function nst_rent_quote_open_for_site(string $site): ?array { $site = strtolower(trim($site)); $now = time(); $best = null; $bestCreated = -1; foreach (nst_rent_quotes_read()['quotes'] as $row) { if (!is_array($row)) { continue; } if ((string)($row['status'] ?? '') !== 'open') { continue; } if (strtolower((string)($row['site'] ?? '')) !== $site) { continue; } $exp = (int)($row['expires'] ?? 0); if ($exp > 0 && $exp < $now) { continue; } $created = (int)($row['created'] ?? 0); if ($created >= $bestCreated) { $best = $row; $bestCreated = $created; } } return $best; } function nst_rent_quote_view(array $row, string $treasuryAddr): array { $now = time(); $exp = (int)($row['expires'] ?? 0); $amount = (string)($row['amount'] ?? '0'); return [ 'ok' => true, 'quoted' => true, 'site' => (string)($row['site'] ?? ''), 'leased' => false, 'quote_id' => (string)($row['quote_id'] ?? ''), 'pay_to' => (string)($row['pay_to'] ?? $treasuryAddr), 'amount' => $amount, 'amount_fmt' => fmt_amt($amount), 'amount_nsu' => (string)($row['amount_nsu'] ?? ''), 'nsu_per_day' => (int)($row['nsu_per_day'] ?? NST_RENT_NSU_PER_DAY), 'days_left' => (int)($row['days_left'] ?? 0), 'memo' => (string)($row['memo'] ?? ''), 'expires' => $exp, 'expires_in_secs' => max(0, $exp - $now), 'epoch_n' => (int)($row['epoch_n'] ?? 0), 'epoch_end' => (int)($row['epoch_end'] ?? 0), 'durable' => true, 'settle' => false, 'law' => 'Quote durable. Pay treasury with exact memo+amount; transfer POST settles.', ]; } /** * Exclusive RMW on rent_quotes.json via flock. * Callback: fn(array $state): array — return new full state to write (or same). * State shape: ['quotes' => [quote_id => row, ...], 'updated' => ts] * @return mixed callback result, or null on lock/open failure */ function nst_with_rent_quotes_lock(callable $fn) { boot_data(); $path = nst_rent_quotes_path(); return nst_with_file_lock($path, function ($fh) use ($fn) { rewind($fh); $raw = stream_get_contents($fh); $state = ['quotes' => [], 'updated' => 0]; if (is_string($raw) && trim($raw) !== '') { $j = json_decode($raw, true); if (is_array($j)) { $state = array_merge($state, $j); if (!isset($state['quotes']) || !is_array($state['quotes'])) { $state['quotes'] = []; } } } $ret = $fn($state); // If callback returned a state-shaped array with quotes, persist it. if (is_array($ret) && isset($ret['quotes']) && is_array($ret['quotes'])) { $ret['updated'] = time(); $toWrite = $ret; unset($toWrite['_last']); // never persist helper side-channel $blob = j($toWrite); ftruncate($fh, 0); rewind($fh); if (fwrite($fh, $blob) === false) { return null; } fflush($fh); } return $ret; }); } /** Drop expired open quotes; keep paid ~7d for first-paid-wins / audit; thrift cap. */ function nst_rent_quotes_prune(array $state, int $now): array { $quotes = $state['quotes'] ?? []; if (!is_array($quotes)) { $quotes = []; } $kept = []; $paidKeepSecs = 7 * 86400; foreach ($quotes as $id => $row) { if (!is_array($row)) { continue; } $st = (string)($row['status'] ?? 'open'); $qid = (string)($row['quote_id'] ?? $id); if ($qid === '') { continue; } if ($st === 'paid') { $paidTs = (int)($row['paid_ts'] ?? 0); if ($paidTs > 0 && ($now - $paidTs) > $paidKeepSecs) { continue; } $kept[$qid] = $row; continue; } if ($st !== 'open') { continue; } $exp = (int)($row['expires'] ?? 0); if ($exp > 0 && $exp < $now) { continue; } $kept[$qid] = $row; } // Cap: prefer paid + newest open; hard ceiling 250 rows if (count($kept) > 250) { uasort($kept, static function ($a, $b) { $sa = (string)($a['status'] ?? ''); $sb = (string)($b['status'] ?? ''); if ($sa === 'paid' && $sb !== 'paid') { return -1; } if ($sb === 'paid' && $sa !== 'paid') { return 1; } return ((int)($b['created'] ?? 0)) <=> ((int)($a['created'] ?? 0)); }); $kept = array_slice($kept, 0, 250, true); } $state['quotes'] = $kept; return $state; } /** * Persist one open quote. Returns stored row or null. * @param array $row must include quote_id, site, pay_to, amount, memo, expires, … */ function nst_rent_quote_store(array $row): ?array { $qid = (string)($row['quote_id'] ?? ''); if ($qid === '' || !preg_match('/^[a-f0-9]{16,64}$/', $qid)) { return null; } $now = time(); $out = nst_with_rent_quotes_lock(function (array $state) use ($row, $qid, $now) { $state = nst_rent_quotes_prune($state, $now); $row['status'] = 'open'; $row['created'] = (int)($row['created'] ?? $now); $state['quotes'][$qid] = $row; // Return full state so lock helper writes; also stash row for return via side channel $state['_last'] = $row; return $state; }); if (!is_array($out) || empty($out['_last']) || !is_array($out['_last'])) { return null; } return $out['_last']; } /** Lookup quote by id (open non-expired, or paid). For settle residual / verify. */ function nst_rent_quote_get(string $quoteId): ?array { $quoteId = strtolower(preg_replace('/[^a-f0-9]/', '', $quoteId) ?? ''); if ($quoteId === '') { return null; } $now = time(); $found = null; nst_with_rent_quotes_lock(function (array $state) use ($quoteId, $now, &$found) { $state = nst_rent_quotes_prune($state, $now); $row = $state['quotes'][$quoteId] ?? null; if (is_array($row)) { $st = (string)($row['status'] ?? ''); if ($st === 'paid') { $found = $row; } elseif ($st === 'open') { $exp = (int)($row['expires'] ?? 0); if ($exp <= 0 || $exp >= $now) { $found = $row; } } } return $state; // write pruned state }); return $found; } /** Parse memo rent:{site}:{quote_id}. */ function nst_rent_parse_memo(string $memo): ?array { $memo = trim($memo); if (!preg_match('/^rent:([a-z]+):([a-f0-9]{16,64})$/i', $memo, $m)) { return null; } return ['site' => strtolower($m[1]), 'quote_id' => strtolower($m[2])]; } /** Settle failure reasons that should auto-refund the payer (P3d). */ function nst_rent_refundable_reason(string $reason): bool { return in_array($reason, [ 'already_paid', 'amount_mismatch', 'quote_missing', 'quote_expired', 'quote_not_open', 'site_mismatch', 'pay_to_mismatch', 'already_leased', 'bad_site', ], true); } /** * P3d: refund a failed rent pay from treasury → original payer (conserved). * Idempotent per inbound transfer nonce (memo rent:refund:{qid}:{nonce}). * @param array $failedTx original inbound transfer to treasury * @param array $settle result of nst_rent_try_settle_transfer * @return array{ok:bool,refunded?:bool,reason?:string,tx?:array,err?:string} */ function nst_rent_auto_refund(array $failedTx, array $settle): array { if (!empty($settle['settled'])) { return ['ok' => true, 'refunded' => false, 'reason' => 'settled_no_refund']; } $why = (string)($settle['reason'] ?? ''); if (!nst_rent_refundable_reason($why)) { return ['ok' => true, 'refunded' => false, 'reason' => 'not_refundable:' . $why]; } if (function_exists('nst_conservation_is_frozen') && nst_conservation_is_frozen()) { return ['ok' => false, 'refunded' => false, 'err' => 'writes frozen']; } $payer = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($failedTx['from'] ?? '')) ?? ''); $amount = dec_norm((string)($failedTx['amount'] ?? '0')); $inNonce = substr((string)($failedTx['nonce'] ?? ''), 0, 64); $parsed = nst_rent_parse_memo((string)($failedTx['memo'] ?? '')); $qid = $parsed['quote_id'] ?? 'unknown'; if (strlen($payer) !== 64 || !dec_ok($amount) || dec_cmp($amount, '1') < 0 || $inNonce === '') { return ['ok' => false, 'refunded' => false, 'err' => 'bad_failed_tx']; } $sec = treasury_secret(); $from = strtolower(addr_from_seed($sec)); $taddr = strtolower(treasury_addr()); if ($from !== $taddr) { return ['ok' => false, 'refunded' => false, 'err' => 'treasury_mismatch']; } // Idempotent refund nonce derived from inbound nonce (stable). $refundNonce = substr(hash('sha256', 'rent-refund-v1|' . $inNonce), 0, 32); $refundMemo = 'rent:refund:' . $qid . ':' . $inNonce; $ts = time(); $body = implode('|', ['transfer', '1', $from, $payer, $amount, $refundNonce, (string)$ts, $refundMemo]); $row = [ 'type' => 'transfer', 'ver' => 1, 'from' => $from, 'to' => $payer, 'amount' => $amount, 'amount_fmt' => fmt_amt($amount), 'nonce' => $refundNonce, 'ts' => $ts, 'memo' => $refundMemo, 'sig' => mac_sign($sec, $body), 'sig_mode' => 'server_mac', 'kind' => 'rent_refund', 'rent_refund_of_nonce' => $inNonce, 'rent_refund_reason' => $why, 'quote_id' => $qid, ]; $out = nst_with_chain_lock(function (array $chain, callable $append) use ($from, $payer, $amount, $refundNonce, $inNonce, $row) { foreach ($chain as $r) { if (($r['type'] ?? '') !== 'transfer') { continue; } // already refunded this inbound pay if ((string)($r['rent_refund_of_nonce'] ?? '') === $inNonce) { return ['ok' => true, 'refunded' => false, 'reason' => 'already_refunded', 'tx' => $r]; } if ((string)($r['nonce'] ?? '') === $refundNonce && strtolower((string)($r['from'] ?? '')) === $from) { return ['ok' => true, 'refunded' => false, 'reason' => 'already_refunded', 'tx' => $r]; } } $bal = balances($chain); $have = $bal[$from] ?? '0'; if (dec_cmp($have, $amount) < 0) { return [ 'ok' => false, 'refunded' => false, 'err' => 'treasury insufficient for refund', 'have_fmt' => fmt_amt($have), 'need_fmt' => fmt_amt($amount), ]; } if (!$append($row)) { return ['ok' => false, 'refunded' => false, 'err' => 'Could not save to the server — try again in a moment']; } return [ 'ok' => true, 'refunded' => true, 'tx' => $row, 'treasury_bal_fmt' => fmt_amt(dec_sub($have, $amount)), 'payer_credit_fmt' => fmt_amt($amount), ]; }); if ($out === null) { return ['ok' => false, 'refunded' => false, 'err' => 'Could not save to the server — try again in a moment']; } $out['reason'] = $why; $out['law'] = 'auto-refund failed rent pay · conserved · idempotent per inbound nonce'; return $out; } /** * P3c: try to settle a rent payment from an on-chain transfer row. * first-paid-wins · memo+amount+pay_to match · bind operator_addr = payer (no server seed). * P3d: caller may auto-refund when reason is refundable. * @param array $tx transfer row (from,to,amount,memo,nonce,ts,…) * @return array{settled:bool,reason?:string,site?:string,payer?:string,quote_id?:string,lease?:array} */ function nst_rent_try_settle_transfer(array $tx): array { $parsed = nst_rent_parse_memo((string)($tx['memo'] ?? '')); if ($parsed === null) { return ['settled' => false, 'reason' => 'not_rent_memo']; } $site = $parsed['site']; $qid = $parsed['quote_id']; if (!in_array($site, nst_rent_site_ids(), true)) { return ['settled' => false, 'reason' => 'bad_site']; } $to = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($tx['to'] ?? '')) ?? ''); $from = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($tx['from'] ?? '')) ?? ''); $amount = dec_norm((string)($tx['amount'] ?? '0')); if (strlen($to) !== 64 || strlen($from) !== 64 || !dec_ok($amount) || dec_cmp($amount, '1') < 0) { return ['settled' => false, 'reason' => 'bad_tx_fields']; } $taddr = strtolower(treasury_addr()); if ($to !== $taddr) { return ['settled' => false, 'reason' => 'not_to_treasury']; } $result = ['settled' => false, 'reason' => 'lock_failed']; $lockRet = nst_with_rent_quotes_lock(function (array $state) use ($site, $qid, $from, $to, $amount, $taddr, $tx, &$result) { $now = time(); $state = nst_rent_quotes_prune($state, $now); $row = $state['quotes'][$qid] ?? null; if (!is_array($row)) { $result = ['settled' => false, 'reason' => 'quote_missing']; return $state; } $st = (string)($row['status'] ?? 'open'); if ($st === 'paid') { $result = [ 'settled' => false, 'reason' => 'already_paid', 'winner' => (string)($row['payer'] ?? ''), 'quote_id' => $qid, ]; return $state; } if ($st !== 'open') { $result = ['settled' => false, 'reason' => 'quote_not_open', 'status' => $st]; return $state; } $exp = (int)($row['expires'] ?? 0); if ($exp > 0 && $exp < $now) { $result = ['settled' => false, 'reason' => 'quote_expired']; return $state; } if (strtolower((string)($row['site'] ?? '')) !== $site) { $result = ['settled' => false, 'reason' => 'site_mismatch']; return $state; } if (strtolower((string)($row['pay_to'] ?? '')) !== $taddr) { $result = ['settled' => false, 'reason' => 'pay_to_mismatch']; return $state; } if (dec_cmp(dec_norm((string)($row['amount'] ?? '0')), $amount) !== 0) { $result = ['settled' => false, 'reason' => 'amount_mismatch', 'expected' => (string)($row['amount'] ?? '0'), 'got' => $amount]; return $state; } // trade crop: refuse if already leased to someone else $e = load_economy(); if ($site === 'trade') { $opHave = strtolower(trim((string)($e['operator_addr'] ?? ''))); $rented = !empty($e['rented']) && $opHave !== '' && $opHave !== $taddr; if ($rented) { $result = ['settled' => false, 'reason' => 'already_leased', 'operator_addr' => $opHave]; return $state; } } $row['status'] = 'paid'; $row['payer'] = $from; $row['paid_ts'] = $now; $row['paid_tx'] = [ 'from' => $from, 'to' => $to, 'amount' => $amount, 'nonce' => (string)($tx['nonce'] ?? ''), 'ts' => (int)($tx['ts'] ?? $now), 'memo' => (string)($tx['memo'] ?? ''), ]; $state['quotes'][$qid] = $row; $lease = [ 'operator_addr' => $from, 'rented_at' => $now, 'quote_id' => $qid, 'site' => $site, 'epoch_n' => (int)($row['epoch_n'] ?? 0), 'amount' => $amount, 'amount_fmt' => fmt_amt($amount), ]; if ($site === 'trade') { $e['rented'] = true; $e['operator_addr'] = $from; $e['rented_at'] = $now; $e['rent_quote_id'] = $qid; $e['rent_payer'] = $from; } if (!isset($e['leases']) || !is_array($e['leases'])) { $e['leases'] = []; } $e['leases'][$site] = $lease; save_economy($e); if ($site === 'trade') { global $OP_ADDR_FILE; boot_data(); file_put_contents($OP_ADDR_FILE, $from . "\n", LOCK_EX); @chmod($OP_ADDR_FILE, 0644); } $result = [ 'settled' => true, 'site' => $site, 'payer' => $from, 'quote_id' => $qid, 'lease' => $lease, 'law' => 'first-paid-wins · operator_addr = payer key · no server LORD seed', ]; return $state; }); if ($lockRet === null) { return ['settled' => false, 'reason' => 'lock_failed']; } return $result; } /** * Scan chain for a transfer that pays a given open quote (or any open quote if id empty). * Idempotent: already-paid quotes report already_paid. */ function nst_rent_settle_scan(?string $quoteId = null): array { $quoteId = $quoteId !== null ? strtolower(preg_replace('/[^a-f0-9]/', '', $quoteId) ?? '') : ''; $chain = read_chain(); $taddr = strtolower(treasury_addr()); $attempts = []; $settled = null; foreach (array_reverse($chain) as $r) { if (($r['type'] ?? '') !== 'transfer') { continue; } if (strtolower((string)($r['to'] ?? '')) !== $taddr) { continue; } $parsed = nst_rent_parse_memo((string)($r['memo'] ?? '')); if ($parsed === null) { continue; } if ($quoteId !== '' && $parsed['quote_id'] !== $quoteId) { continue; } $out = nst_rent_try_settle_transfer($r); $refund = null; if (empty($out['settled']) && nst_rent_refundable_reason((string)($out['reason'] ?? ''))) { $refund = nst_rent_auto_refund($r, $out); } // Fail-open server_notice after money/rent residual commits (scan path). try { if (!empty($out['settled'])) { nst_inbox_notify_rent_settle($out); } if (is_array($refund) && !empty($refund['refunded'])) { nst_inbox_notify_rent_refund($refund); } } catch (Throwable $e) { // never abort settle/refund residual } $attempts[] = [ 'quote_id' => $parsed['quote_id'], 'from' => (string)($r['from'] ?? ''), 'result' => $out, 'refund' => $refund, ]; if (!empty($out['settled'])) { $settled = $out; break; } if (count($attempts) >= 40) { break; } } return [ 'ok' => true, 'settled' => $settled !== null, 'lease' => $settled['lease'] ?? null, 'settle' => $settled, 'attempts' => $attempts, 'law' => 'scan · settle first-paid-wins · auto-refund failed rent pays (P3d)', ]; } /** * Public rent quote (P3a+P3b). No chain write, no seed, no operator bind. * amount = NST_RENT_NSU_PER_DAY * days_left (days_left = ceil(secs_to_reset/86400), min 1). * P3b: persists open quote under flock for later settle match. * @return array JSON-ready */ function nst_rent_quote_public(string $site, string $treasuryAddr, bool $mint = false): array { $site = strtolower(trim($site)); if ($site === '' || $site === 'this' || $site === 'me') { $site = 'trade'; } $allowed = nst_rent_site_ids(); if (!in_array($site, $allowed, true)) { return [ 'ok' => false, 'http' => 400, 'err' => 'bad site', 'sites' => $allowed, ]; } $ep = epoch_info(); $secs = (int)($ep['secs_to_reset'] ?? 0); $daysLeft = (int)max(1, (int)ceil($secs / 86400)); $amountNsu = (int)NST_RENT_NSU_PER_DAY * $daysLeft; $amountMicros = parse_amt((string)$amountNsu); if ($amountMicros === null || dec_cmp($amountMicros, '1') < 0) { return ['ok' => false, 'http' => 500, 'err' => 'rent amount invalid']; } // This crop's economy only tracks trade lease today; other sites quote as free until multi-lease store. $e = load_economy(); $op = strtolower(trim((string)($e['operator_addr'] ?? ''))); $t = strtolower($treasuryAddr); $rentedHere = !empty($e['rented']) && $op !== '' && $op !== $t; if ($site === 'trade' && $rentedHere) { return [ 'ok' => true, 'quoted' => false, 'site' => $site, 'leased' => true, 'leased_until' => (int)($ep['epoch_end'] ?? 0), 'operator_addr' => $op, 'rented_at' => (int)($e['rented_at'] ?? 0), 'err' => 'already leased', 'law' => 'Occupied crop: no new quote until epoch wipe (or future multi-lease free).', ]; } $now = time(); $ttl = (int)NST_RENT_QUOTE_TTL_SECS; if ($ttl < 60) { $ttl = 1800; } $existing = nst_rent_quote_open_for_site($site); if (is_array($existing)) { return nst_rent_quote_view($existing, $treasuryAddr); } if (!$mint) { return [ 'ok' => true, 'quoted' => false, 'site' => $site, 'leased' => false, 'err' => 'no open quote', 'law' => 'GET reads the open quote. POST mints one if none.', ]; } $quoteId = bin2hex(random_bytes(12)); $memo = 'rent:' . $site . ':' . $quoteId; $expires = $now + $ttl; $storeRow = [ 'quote_id' => $quoteId, 'site' => $site, 'pay_to' => $treasuryAddr, 'amount' => $amountMicros, 'amount_nsu' => (string)$amountNsu, 'nsu_per_day' => (int)NST_RENT_NSU_PER_DAY, 'days_left' => $daysLeft, 'memo' => $memo, 'expires' => $expires, 'created' => $now, 'epoch_n' => (int)($ep['epoch_n'] ?? 0), 'epoch_end' => (int)($ep['epoch_end'] ?? 0), 'status' => 'open', ]; $stored = nst_rent_quote_store($storeRow); if ($stored === null) { return [ 'ok' => false, 'http' => 500, 'err' => 'quote store failed', 'durable' => false, 'settle' => false, ]; } return [ 'ok' => true, 'quoted' => true, 'site' => $site, 'leased' => false, 'quote_id' => $quoteId, 'pay_to' => $treasuryAddr, 'amount' => $amountMicros, 'amount_fmt' => fmt_amt($amountMicros), 'amount_nsu' => (string)$amountNsu, 'nsu_per_day' => (int)NST_RENT_NSU_PER_DAY, 'days_left' => $daysLeft, 'memo' => $memo, 'expires' => $expires, 'expires_in_secs' => $ttl, 'epoch_n' => (int)($ep['epoch_n'] ?? 0), 'epoch_end' => (int)($ep['epoch_end'] ?? 0), 'durable' => true, 'settle' => false, 'law' => 'Quote durable. Pay treasury with exact memo+amount; transfer POST settles.', ]; } function load_mods(): array { global $MODS_FILE; if (!is_file($MODS_FILE)) return []; $j = json_decode((string)file_get_contents($MODS_FILE), true); return is_array($j) ? $j : []; } function save_mods(array $mods): void { global $MODS_FILE; file_put_contents($MODS_FILE, j($mods), LOCK_EX); } /** * RETIRED: THIS WAS A PER-HEAD PAYOUT WITH UNLIMITED HEADS. * * Any address could claim a PERMANENT 0.1 NSU/day draw on the treasury during a * 30-day window, proving nothing. Addresses are sha256 of a seed, so they cost * nothing and there is no limit on how many you make: 5,000 generated wallets * captured the King's entire daily income (5% of emission, ~500 NSU) forever, * for the price of 5,000 HTTP posts. It drained rather than minted, so the 100M * ceiling held - but the treasury went to zero and stayed there. * * The charter already forbids exactly this, in as many words: * * "NEVER PAY PER HEAD. [...] Paying per visitor makes issuance = participants * x amount and the attacker chooses the participant count - one residential * IPv6 /64 is 18 quintillion addresses. An IP is not a scarce resource." * * The rule was written for UBI and the same arithmetic applies here; this * predates the rule and was never revisited. It also predates the economy it * spends from: it was designed when genesis handed the King 24,000,000 NSU, and * under zero-genesis the King holds 5% of a day's emission and cannot fund it. * * EXISTING grants are still honoured by lazy_accrue() - retiring a perk is not a * licence to confiscate one already given. Only new grants stop. * * If early-adopter rewards are wanted again, a LORD funds them from their own * crop stream against something scarce. The protocol does not pay per head. */ function grant_early_modifier(string $addr): array { $mods = load_mods(); if (!empty($mods[$addr]['permanent'])) { return ['granted' => false, 'reason' => 'already', 'mod' => $mods[$addr]]; } return ['granted' => false, 'reason' => 'per_head_payout_retired', 'note' => 'Permanent per-address income is retired: addresses are free, so it was an ' . 'unlimited claim on the treasury. See the NEVER PAY PER HEAD clause in the charter.']; } /** Lazy catch-up: pay floor(days elapsed)*0.1 from treasury if permanent modifier */ function lazy_accrue(string $addr): array { $modsPeek = load_mods(); if (empty($modsPeek[$addr]['permanent'])) { return ['ok' => true, 'paid_fmt' => '0', 'reason' => 'no_modifier']; } $nowPeek = time(); $lastPeek = (int)($modsPeek[$addr]['last_accrual_ts'] ?? $nowPeek); if ($lastPeek > $nowPeek) $lastPeek = $nowPeek; if (intdiv($nowPeek - $lastPeek, 86400) < 1) { return ['ok' => true, 'paid_fmt' => '0', 'reason' => 'too_soon', 'next_in' => 86400 - (($nowPeek - $lastPeek) % 86400)]; } $sec = treasury_secret(); $from = addr_from_seed($sec); // Chain RMW + re-load mods under lock so concurrent unlocks cannot double-pay days. $out = nst_with_chain_lock(function (array $chain, callable $append) use ($addr, $from, $sec) { $mods = load_mods(); if (empty($mods[$addr]['permanent'])) { return ['ok' => true, 'paid_fmt' => '0', 'reason' => 'no_modifier']; } $now = time(); $last = (int)($mods[$addr]['last_accrual_ts'] ?? $now); if ($last > $now) $last = $now; $days = intdiv($now - $last, 86400); if ($days < 1) { return ['ok' => true, 'paid_fmt' => '0', 'reason' => 'too_soon', 'next_in' => 86400 - (($now - $last) % 86400)]; } $pay = dec_mul_small(NST_MOD_DAILY_MICROS, $days); $bal = balances($chain); $have = $bal[$from] ?? '0'; if (dec_cmp($have, $pay) < 0) { // whole days treasury can afford only (lazy ketchup, no partial-day mint) $daysCan = 0; $payCan = '0'; for ($d = 1; $d <= $days; $d++) { $try = dec_mul_small(NST_MOD_DAILY_MICROS, $d); if (dec_cmp($try, $have) > 0) break; $daysCan = $d; $payCan = $try; } if ($daysCan < 1) { return ['ok' => false, 'err' => 'treasury empty', 'paid_fmt' => '0']; } $days = $daysCan; $pay = $payCan; } $ts = $now; $nonce = bin2hex(random_bytes(8)); $memo = 'modifier:daily:' . $days . 'd'; $tbody = implode('|', ['transfer', '1', $from, $addr, $pay, $nonce, (string)$ts, $memo]); $row = [ 'type' => 'transfer', 'ver' => 1, 'from' => $from, 'to' => $addr, 'amount' => $pay, 'amount_fmt' => fmt_amt($pay), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => mac_sign($sec, $tbody), 'kind' => 'modifier_accrual', ]; if (!$append($row)) { return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment']; } // advance last_accrual by whole days only (while still holding chain lock) $mods[$addr]['last_accrual_ts'] = $last + ($days * 86400); save_mods($mods); return ['ok' => true, 'paid_fmt' => fmt_amt($pay), 'days' => $days, 'tx' => $row]; }); if ($out === null) { return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment', 'paid_fmt' => '0']; } return $out; } function vault_dir(): string { global $VAULT, $VAULT_BASE; /* Production has one configured sibling vault and fails closed if it is * unavailable. Falling back under the document root turns a permissions * problem into a seed disclosure on nginx/static-server configurations. * Isolated CLI/Desktop fixtures deliberately configure VAULT_BASE=$ROOT; * that explicit test mode is not a production fallback. */ try { $dir = nst_device_wallet_safe_dir((string)$VAULT, (string)$VAULT_BASE, true); nst_device_wallet_private_marker($dir); if (is_writable($dir)) return $dir; } catch (Throwable $e) { // Fail below without manufacturing a second, public location. } throw new RuntimeException('vault_unavailable'); } /** Best-effort human vault copy with the same no-link/atomic-write gates as assignments. */ function nst_vault_write_private_file(string $filename, string $body): bool { if ($filename === '' || basename($filename) !== $filename || str_contains($filename, "\0")) return false; try { $dir = vault_dir(); $path = $dir . DIRECTORY_SEPARATOR . $filename; nst_device_wallet_atomic_replace($path, $body); $fh = nst_device_wallet_open_regular($path, 'rb'); $saved = @stream_get_contents($fh, strlen($body) + 1); @fclose($fh); return is_string($saved) && hash_equals($body, $saved); } catch (Throwable $e) { return false; } } /** Write human-only vault material. PHP auth uses HASH only (ethos: vault not hot-path). */ function vault_write_operator(string $plainPass, array $walletSeeds = []): void { $readme = "NOSIGNUP.TRADE VAULT - ROOT/OPERATOR ONLY\n" . "Not served by nginx (keep outside html/ or deny).\n" . "Panel door is site seed (SITE_WALLET_SEED.txt + data/site.seed). Ctrl+Alt+Shift+N is seed-only.\n" . "/controlpanel unlocks with panel site.seed; KING mint is treasury.secret + KING_FAUCET_SEED.txt.\n" . "Leftover admin password files are unlinked on boot/wipe. No password product path.\n" . "Optional WALLET_SEEDS.txt only if operator seeds were passed at write time.\n" . "Site wallet seed does not rotate on yearly wipe.\n" . "Generated: " . gmdate('c') . "\n"; nst_vault_write_private_file('README.txt', $readme); admin_pass_burn(); if ($walletSeeds) { $w = ''; foreach ($walletSeeds as $k => $v) $w .= strtoupper((string)$k) . "=" . trim((string)$v) . "\n"; nst_vault_write_private_file('WALLET_SEEDS.txt', $w); } } function admin_pass_burn(): void { global $ADMIN_HASH_FILE, $ADMIN_PASS_FILE; foreach ([$ADMIN_HASH_FILE, $ADMIN_PASS_FILE] as $p) { if (!is_string($p) || $p === '') continue; try { $info = nst_device_wallet_safe_file($p, true); if (!empty($info['exists']) && !empty($info['real'])) { @unlink($info['real']); } } catch (Throwable $e) { if (is_file($p) && !is_link($p)) @unlink($p); } } try { $vp = vault_dir() . DIRECTORY_SEPARATOR . 'ADMIN_PASSWORD.txt'; $info = nst_device_wallet_safe_file($vp, true); if (!empty($info['exists']) && !empty($info['real'])) { @unlink($info['real']); } } catch (Throwable $e) { // vault unavailable — do not invent a public leftover path } } function require_admin(): void { // Charter: admin secrets belong in POST bodies, never URLs (no GET seed/pass). admin_pass_burn(); $seed = (string)($_POST['seed'] ?? ''); if ($seed !== '' && panel_seed_ok($seed)) { return; } api_out(['ok' => false, 'err' => 'admin auth'], 401); } /** * Genesis: credit the king the entire 100 million NSU cap (MASTER FAUCET for this site). * One path: nst_mint_guarded('genesis', …) IS the genesis row. No double-mint. * After birth nothing is printed. Landing asks run AFTER the king has the cap. * E3: also birth a DISTINCT trade panel site.seed; operator.addr := panel addr (≠ treasury). * KING seed: data/treasury.secret + vault KING_FAUCET_SEED.txt (only minter). * Panel seed: data/site.seed + vault SITE_WALLET_SEED.txt (admin door; no mint). * Pre-E3 installs (had genesis, no site.seed): stay on treasury=panel conflation fallback. * Renters must NOT receive treasury.secret. Population fields metadata only. */ function ensure_genesis(): void { global $TFILE, $META, $CHAIN, $DATA; boot_data(); /* Serialize the whole first-birth sequence, not only its final append. * Otherwise concurrent first requests can mint several genesis identities, * overwrite one another's secret files, and each append a genesis row. */ $init = @fopen($DATA . DIRECTORY_SEPARATOR . 'genesis.lock', 'c+b'); if (!is_resource($init) || !@flock($init, LOCK_EX)) { if (is_resource($init)) @fclose($init); throw new RuntimeException('genesis_unavailable'); } try { if (!file_exists($CHAIN) || (is_file($CHAIN) && @filesize($CHAIN) === 0)) { nst_genesis_pending_begin(); nst_chain_create_for_genesis(); } $hadGenesis = false; $genesisRow = null; foreach (read_chain() as $r) { if (($r['type'] ?? '') === 'genesis') { $hadGenesis = true; $genesisRow = $r; break; } } if ($hadGenesis) { nst_genesis_pending_finish(); /* Reconcile only missing, derivable sidecars. A crash after the durable * genesis append must not leave meta/operator initialization permanently * half-born. Never invent site.seed on a legacy empire. */ $g = is_array($genesisRow) ? $genesisRow : []; $gTreasury = strtolower((string)($g['treasury_addr'] ?? '')); $savedTreasurySeed = is_file($TFILE) ? norm_seed((string)@file_get_contents($TFILE)) : ''; if (!preg_match('/\A[a-f0-9]{64}\z/D', $gTreasury) || !preg_match('/\A[a-z]{1,24}(?: [a-z]{1,24}){11}\z/D', $savedTreasurySeed) || !hash_equals($gTreasury, addr_from_seed($savedTreasurySeed)) ) { throw new RuntimeException('genesis_identity_mismatch'); } $panelAddr = strtolower((string)($g['panel_addr'] ?? '')); if (!preg_match('/\A[a-f0-9]{64}\z/D', $panelAddr)) $panelAddr = ''; if (is_file($GLOBALS['SITE_SEED_FILE'])) { $savedPanelSeed = norm_seed((string)@file_get_contents($GLOBALS['SITE_SEED_FILE'])); if (!preg_match('/\A[a-z]{1,24}(?: [a-z]{1,24}){11}\z/D', $savedPanelSeed) || $panelAddr === '' || !hash_equals($panelAddr, addr_from_seed($savedPanelSeed)) ) { throw new RuntimeException('genesis_identity_mismatch'); } } elseif ((int)($g['ver'] ?? 0) >= 2 && $panelAddr !== '') { /* A current genesis committed a distinct panel key. Its absence is * authority loss, not permission to fall back to the KING key. */ throw new RuntimeException('genesis_identity_mismatch'); } nst_ensure_operator_addr($panelAddr !== '' ? $panelAddr : null); $m = load_meta(); $metaReadable = empty($GLOBALS['NST_META_UNREADABLE']); $beforeMeta = j($m); if (empty($m['version'])) $m['version'] = NST_VERSION; if (empty($m['treasury']) && !empty($g['treasury_addr'])) $m['treasury'] = (string)$g['treasury_addr']; if (empty($m['panel_addr']) && $panelAddr !== '') $m['panel_addr'] = $panelAddr; if (empty($m['created']) && !empty($g['ts'])) $m['created'] = (int)$g['ts']; if (!isset($m['population']) && isset($g['population'])) $m['population'] = (int)$g['population']; if (empty($m['population_source']) && isset($g['population_source'])) $m['population_source'] = (string)$g['population_source']; if (!isset($m['population_ts']) && !empty($g['ts'])) $m['population_ts'] = (int)$g['ts']; if (!isset($m['treasury_target_micros']) && isset($g['treasury_at_birth'])) $m['treasury_target_micros'] = (string)$g['treasury_at_birth']; if (empty($m['soft_max_supply_micros'])) $m['soft_max_supply_micros'] = NST_MAX_SUPPLY; $needsEpoch = empty($m['epoch_start']); /* Merge into readable meta only; unreadable meta is never rebuilt or stamped. */ if ($metaReadable && !hash_equals($beforeMeta, j($m))) save_meta($m); if ($metaReadable && $needsEpoch) ensure_meta_epochs(); nst_bootstrap_spread_if_empty(); return; } ensure_genesis_secret_only(); $sec = trim((string)file_get_contents($TFILE)); $taddr = addr_from_seed($sec); // Distinct panel wallet (one of 10 crop panels); never holds genesis mint. $panelSeed = ensure_panel_site_seed_only(); $panelAddr = $panelSeed !== '' ? addr_from_seed($panelSeed) : $taddr; $ts = time(); $popInfo = nst_world_population(); $pop = (int)$popInfo['pop']; /* BIRTH CREDITS THE KING THROUGH nst_mint_guarded('genesis', …). * The guarded mint IS the genesis row (one path, no double-mint, no * hardcoded outputs that bypass the choke point). Full cap at birth; * remaining supply after that is 0. No drip. Landing asks run AFTER * this so the lots can fund from the king balance. * * Yearly re-anchor stays retired (09-EXPLOIT-AUDIT.md C-1). admin_pay * is still a transfer, not a mint. Utterly fair except the king starts * with 100 million. */ $treasuryAmt = NST_GENESIS_TREASURY; if (!dec_ok($treasuryAmt) || dec_cmp($treasuryAmt, NST_MAX_SUPPLY) !== 0) { $treasuryAmt = NST_MAX_SUPPLY; } $body = 'genesis|2|' . $treasuryAmt . '|' . $taddr . '|' . $pop . '|' . $ts; $mint = nst_mint_guarded('genesis', [ ['addr' => $taddr, 'amount' => $treasuryAmt], ], [ 'ts' => $ts, 'max_supply' => NST_MAX_SUPPLY, // hard ceiling on all issuance, for all time 'treasury_at_birth' => $treasuryAmt, // what the KING held at genesis - a balance, not a limit 'treasury_addr' => $taddr, 'panel_addr' => $panelAddr, 'population' => $pop, 'population_source' => (string)$popInfo['source'], 'optimum_micros_per_human' => NST_OPTIMUM_MICROS_PER_HUMAN, // legacy metadata only; not supply law // MASTER FAUCET: KING address holds the whole cap for THIS site only. // Panel site.seed is distinct (no mint). Renters must not receive treasury.secret. 'note' => 'NSU genesis: king holds the whole 100 million cap at birth. After that nothing is printed. No drip. Public faucet stays 410. Buy on the book. No yearly re-anchor.', 'sig' => mac_sign($sec, $body), ]); if (empty($mint['ok'])) throw new RuntimeException('genesis_unavailable'); nst_genesis_pending_finish(); $m = load_meta(); $m['version'] = NST_VERSION; $m['treasury'] = $taddr; $m['panel_addr'] = $panelAddr; $m['created'] = $m['created'] ?? $ts; $m['population'] = $pop; $m['population_source'] = (string)$popInfo['source']; $m['population_ts'] = (int)$popInfo['ts']; $m['treasury_target_micros'] = $treasuryAmt; $m['soft_max_supply_micros'] = NST_MAX_SUPPLY; // ceiling, not the treasury balance if (empty($m['epoch_start'])) { $m['epoch_start'] = $ts; $m['epoch_n'] = 1; } save_meta($m); // E3: site panel wallet is operator at birth; KING treasury is separate minter. nst_ensure_operator_addr($panelAddr); nst_vault_king_seed_note($sec); ensure_meta_epochs(); // GENESIS ONLY: fillable king ASKs on NSU/LTC. No bids. No remint. Not page-load. nst_genesis_seed_landing_asks($sec, $taddr); } finally { @flock($init, LOCK_UN); @fclose($init); } } function treasury_secret(): string { ensure_genesis(); global $TFILE; return trim((string)file_get_contents($TFILE)); } function treasury_addr(): string { return addr_from_seed(treasury_secret()); } function api_out(array $x, int $c = 200): void { if (!empty($GLOBALS['NST_SHARED_WALLET_REQUEST'])) $x=nst_shared_wallet_filter_response($x); http_response_code($c); header('Content-Type: application/json; charset=UTF-8'); header('Cache-Control: no-store'); echo j($x); exit; } /** Public, structurally strict ledger diagnostic; linkage failure is reported, not hidden by boot. */ function nst_audit_api_out(array $chain): void { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET'); $total = count($chain); $verify = nst_verify_chain($chain); $rep = nst_conservation_replay($chain); if (isset($_GET['from'])) { $from = max(0, (int)$_GET['from']); $limit = max(1, min(2000, (int)($_GET['limit'] ?? 1000))); api_out([ 'ok' => true, 'n' => $total, 'from' => $from, 'limit' => $limit, 'next' => ($from + $limit < $total) ? ($from + $limit) : null, 'rows' => array_slice($chain, $from, $limit), 'head' => nst_chain_head($chain), 'height' => $total, 'verify' => $verify, ]); } $n = max(1, min(500, (int)($_GET['n'] ?? 100))); api_out([ 'ok' => true, 'n' => $total, 'head' => nst_chain_head($chain), 'height' => $total, 'verify' => $verify, 'tail' => array_slice($chain, -$n), 'conservation' => [ 'ok' => !empty($rep['ok']), 'frozen' => nst_conservation_is_frozen(), 'sum_fmt' => fmt_amt((string)$rep['observed_sum']), 'expected_fmt' => fmt_amt((string)$rep['expected_issued']), 'checked_ts' => time(), 'int_ok' => !empty($rep['int_ok']), 'detail' => $rep['detail'] ?? null, ], ]); } function require_seed(): array { $seed = nst_device_wallet_normalize_seed_value($_POST['seed'] ?? null); // One root-authority format everywhere: exactly twelve lowercase alphabetic words. if ($seed === null) api_out(['ok' => false, 'err' => 'Need your exact 12-word seed'], 401); return [$seed, addr_from_seed($seed)]; } /* -------- Public ticket mill + wallet bind (same integer as chat) -------- * LEFT IN PLACE — economy+identity telomere IDENTITY slot points here. * Not moved: mill + bind sit with top-level ?api=ticket handlers (telomere is functions-only). */ function nst_ticket_norm(string $t): string { $t = trim($t); return preg_match('/^[1-9][0-9]{0,17}$/', $t) ? $t : ''; } function nst_ticket_read_counter(string $path): int { if (!is_file($path) || !is_readable($path)) return 0; $raw = trim((string)@file_get_contents($path)); return preg_match('/^[0-9]{1,18}$/', $raw) ? (int)$raw : 0; } function nst_ticket_seed_counter(string $path, int $max): bool { $dir = dirname($path); if (!is_dir($dir)) { if (!@mkdir($dir, 0775, true) && !is_dir($dir)) return false; } if (!is_writable($dir)) return false; $fh = @fopen($path, 'c+'); if (!is_resource($fh)) return false; if (!@flock($fh, LOCK_EX)) { @fclose($fh); return false; } $raw = stream_get_contents($fh); $now = (is_string($raw) && preg_match('/^[0-9]{1,18}$/', trim($raw))) ? (int)trim($raw) : 0; $write = max($now, $max); if ($write !== $now || $raw === '' || $raw === false) { rewind($fh); ftruncate($fh, 0); fwrite($fh, (string)$write); fflush($fh); } @flock($fh, LOCK_UN); @fclose($fh); @chmod($path, 0600); return true; } /** * One mill both faces call. Live html at /var/www/nosignup.{chat,trade}/html * uses /var/www/nosignup.shared/id.counter (else the sibling crop's file). * Desktop/NSU stays on local data/id.counter (already shared). Never lowers * an existing counter — old chat numbers stay valid. */ function nst_ticket_mill_path(): string { $localDir = dirname(nst_data_file('id.counter')); $local = $localDir . DIRECTORY_SEPARATOR . 'id.counter'; $siteRoot = dirname(__DIR__); $www = dirname($siteRoot); $site = strtolower(basename($siteRoot)); if (!preg_match('/^nosignup\.(chat|trade)$/', $site)) { return $local; } $sharedDir = $www . DIRECTORY_SEPARATOR . 'nosignup.shared'; $shared = $sharedDir . DIRECTORY_SEPARATOR . 'id.counter'; $chatC = $www . DIRECTORY_SEPARATOR . 'nosignup.chat' . DIRECTORY_SEPARATOR . 'html' . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'id.counter'; $tradeC = $www . DIRECTORY_SEPARATOR . 'nosignup.trade' . DIRECTORY_SEPARATOR . 'html' . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'id.counter'; $max = max( nst_ticket_read_counter($local), nst_ticket_read_counter($shared), nst_ticket_read_counter($chatC), nst_ticket_read_counter($tradeC) ); /* Do not create /var/www/nosignup.shared. Sibling vault is optional; one public_html is enough. */ if (is_dir($sharedDir) && nst_ticket_seed_counter($shared, $max)) return $shared; if (nst_ticket_seed_counter($chatC, $max)) return $chatC; if (nst_ticket_seed_counter($tradeC, $max)) return $tradeC; nst_ticket_seed_counter($local, $max); return $local; } function nst_mint_next_id(): ?string { $path = nst_ticket_mill_path(); $fh = @fopen($path, 'c+'); if (!is_resource($fh)) return null; $lockExtra = null; if (!@flock($fh, LOCK_EX)) { /* flock failed: local lock file, then skip mill (fail-closed; never remint) */ $lockPath = dirname($path) . DIRECTORY_SEPARATOR . 'id.counter.lock'; $lockExtra = @fopen($lockPath, 'c+'); if (!is_resource($lockExtra) || !@flock($lockExtra, LOCK_EX)) { if (is_resource($lockExtra)) @fclose($lockExtra); @fclose($fh); return null; } } $raw = stream_get_contents($fh); $cur = (is_string($raw) && preg_match('/^[0-9]{1,18}$/', trim($raw))) ? trim($raw) : '0'; $next = (string)((int)$cur + 1); if ($next === '0' || strlen($next) > 18) { @flock($fh, LOCK_UN); @fclose($fh); if (is_resource($lockExtra)) { @flock($lockExtra, LOCK_UN); @fclose($lockExtra); } return null; } rewind($fh); ftruncate($fh, 0); fwrite($fh, $next); fflush($fh); @flock($fh, LOCK_UN); @fclose($fh); if (is_resource($lockExtra)) { @flock($lockExtra, LOCK_UN); @fclose($lockExtra); } @chmod($path, 0600); return $next; } function nst_ticket_bind_file(): string { return nst_data_file('ticket.bind'); } function nst_ticket_bind_load(string $raw): array { $t2a = []; $a2t = []; foreach (preg_split('/\R/', $raw) as $line) { $line = trim($line); if ($line === '' || ($line[0] ?? '') === '#') continue; $p = explode("\t", $line); if (count($p) < 2) continue; $tid = nst_ticket_norm($p[0]); $a = strtolower(preg_replace('/[^a-f0-9]/', '', $p[1]) ?? ''); if ($tid === '' || strlen($a) !== 64) continue; if (isset($t2a[$tid]) || isset($a2t[$a])) continue; $t2a[$tid] = $a; $a2t[$a] = $tid; } return ['t2a' => $t2a, 'a2t' => $a2t]; } function nst_ticket_bind_dump(array $map): string { $out = ''; foreach ($map['t2a'] as $tid => $a) { $out .= $tid . "\t" . $a . "\n"; } return $out; } function nst_ticket_bind_with(callable $fn) { $path = nst_ticket_bind_file(); $dir = dirname($path); if (!is_dir($dir) && !@mkdir($dir, 0755, true)) throw new RuntimeException('bind_unavailable'); // Stable lock survives atomic replacement of the public mapping file. $lockPath = $path . '.lock'; if (is_link($lockPath) || is_link($path) || (file_exists($path) && !is_file($path))) throw new RuntimeException('bind_unavailable'); $lock = @fopen($lockPath, 'c+b'); if (!is_resource($lock)) throw new RuntimeException('bind_unavailable'); if (!@flock($lock, LOCK_EX)) {@fclose($lock);throw new RuntimeException('bind_unavailable');} $temp = null; $fh = null; try { if (is_link($path) || (file_exists($path) && !is_file($path))) throw new RuntimeException('bind_unavailable'); $raw = is_file($path) ? @file_get_contents($path) : ''; if (!is_string($raw)) throw new RuntimeException('bind_unavailable'); $map = nst_ticket_bind_load($raw); $ret = $fn($map); $dump = nst_ticket_bind_dump($map); if ($dump !== $raw) { $temp = $path . '.tmp.' . bin2hex(random_bytes(12)); $fh = @fopen($temp, 'xb'); if (!is_resource($fh) || !@chmod($temp, 0600)) throw new RuntimeException('bind_unavailable'); if (!nst_stream_write_all($fh, $dump) || !nst_stream_flush_durable($fh)) throw new RuntimeException('bind_unavailable'); @fclose($fh);$fh=null; $written=@file_get_contents($temp); if (!is_string($written) || !hash_equals($dump,$written)) throw new RuntimeException('bind_unavailable'); // Refuse to replace an unexpected external edit, even under our own lock. $current=is_file($path) ? @file_get_contents($path) : ''; if (is_link($path) || !is_string($current) || !hash_equals($raw,$current)) throw new RuntimeException('bind_changed'); if (!@rename($temp,$path)) throw new RuntimeException('bind_unavailable'); $temp=null; $saved=@file_get_contents($path); if (!is_string($saved) || !hash_equals($dump,$saved)) throw new RuntimeException('bind_unavailable'); } return $ret; } finally { if (is_resource($fh)) @fclose($fh); if (is_string($temp) && is_file($temp) && !is_link($temp)) @unlink($temp); @flock($lock, LOCK_UN);@fclose($lock); } } function nst_ticket_lookup_addr(string $ticket): ?string { $ticket = nst_ticket_norm($ticket); if ($ticket === '') return null; $path = nst_ticket_bind_file(); if (!is_file($path)) return null; $map = nst_ticket_bind_load((string)@file_get_contents($path)); return $map['t2a'][$ticket] ?? null; } function nst_ticket_lookup_ticket(string $addr): ?string { $addr = strtolower(preg_replace('/[^a-f0-9]/', '', $addr) ?? ''); if (strlen($addr) !== 64) return null; $path = nst_ticket_bind_file(); if (!is_file($path)) return null; $map = nst_ticket_bind_load((string)@file_get_contents($path)); return $map['a2t'][$addr] ?? null; } /** Bind addr to prefer ticket, or mill one. Never remints an already-bound wallet. */ function nst_ticket_ensure_for_addr(string $addr, string $prefer = ''): string { $addr = strtolower(preg_replace('/[^a-f0-9]/', '', $addr) ?? ''); if (strlen($addr) !== 64) throw new RuntimeException('bad_addr'); $prefer = nst_ticket_norm($prefer); return nst_ticket_bind_with(function (array &$map) use ($addr, $prefer): string { if (isset($map['a2t'][$addr])) { return $map['a2t'][$addr]; } if ($prefer !== '' && !isset($map['t2a'][$prefer])) { $map['t2a'][$prefer] = $addr; $map['a2t'][$addr] = $prefer; return $prefer; } $next = nst_mint_next_id(); if ($next === null || $next === '') throw new RuntimeException('mill_busy'); $guard = 0; while (isset($map['t2a'][$next]) && $guard < 8) { $next = nst_mint_next_id(); if ($next === null) throw new RuntimeException('mill_busy'); $guard++; } if (isset($map['t2a'][$next])) throw new RuntimeException('mill_busy'); $map['t2a'][$next] = $addr; $map['a2t'][$addr] = $next; return $next; }); } /** Integer ticket or legacy 64-hex → ledger addr. Empty if unknown/unbound. */ function nst_resolve_receipt(string $raw): string { $raw = trim($raw); $tid = nst_ticket_norm($raw); if ($tid !== '') { return (string)(nst_ticket_lookup_addr($tid) ?? ''); } $hex = strtolower(preg_replace('/[^a-f0-9]/', '', $raw) ?? ''); return strlen($hex) === 64 ? $hex : ''; } function nst_ticket_json(array $payload, int $status = 200): void { http_response_code($status); header('Content-Type: application/json; charset=UTF-8'); header('Cache-Control: private, no-store, max-age=0, must-revalidate'); header('Pragma: no-cache'); header('X-Content-Type-Options: nosniff'); echo j($payload); exit; } function nst_id_cors_sisters(): void { $origin = trim((string)($_SERVER['HTTP_ORIGIN'] ?? '')); if ($origin !== '' && preg_match('~\Ahttps://nosignup\.(com|org|net|trade|chat|work|market|date|fun|info)\z~', $origin)) { header('Access-Control-Allow-Origin: ' . $origin); header('Vary: Origin'); header('Access-Control-Allow-Methods: POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type'); } } function nst_id_mill_handle(): void { nst_id_cors_sisters(); $method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? '')); if ($method === 'OPTIONS') { http_response_code(204); exit; } if ($method !== 'POST') { nst_ticket_json(['ok' => false, 'err' => 'POST only'], 405); } nst_tx_gate_enforce(nst_tx_gate_peek_addr()); $id = nst_mint_next_id(); if ($id === null || $id === '') { nst_ticket_json(['ok' => false, 'err' => 'ID mill busy'], 503); } nst_ticket_json(['ok' => true, 'id' => $id]); } function nst_ticket_resolve_handle(): void { $raw = (string)($_POST['id'] ?? $_GET['id'] ?? ''); if ($raw === '') { $ctype = (string)($_SERVER['CONTENT_TYPE'] ?? ''); if (stripos($ctype, 'application/json') !== false) { $body = json_decode((string)@file_get_contents('php://input'), true); if (is_array($body)) $raw = (string)($body['id'] ?? $body['ticket'] ?? ''); } } $tid = nst_ticket_norm($raw); if ($tid === '') { nst_ticket_json(['ok' => false, 'err' => 'Need a chat/wallet number'], 400); } $addr = nst_ticket_lookup_addr($tid); if ($addr === null || $addr === '') { nst_ticket_json(['ok' => false, 'err' => 'That number has no wallet yet'], 404); } nst_ticket_json(['ok' => true, 'ticket' => $tid, 'addr' => $addr]); } /** * Vanity sink groundwork (ethos 9 perk). Trade only. * taken = bound in ticket.bind; free = absent (holes OK, no hole list, no phonebook). * Price decays with wait; old number unbound later; lose seed lose coins; * PHP is not a recovery desk. MUST NOT move coins. MUST NOT write bind yet. */ function nst_vanity_quote(string $raw): array { $tid = nst_ticket_norm($raw); if ($tid === '') { return ['ok' => false, 'err' => 'Need an integer ticket', 'spend' => false]; } if (nst_ticket_lookup_addr($tid) !== null) { return ['ok' => false, 'err' => 'bound', 'taken' => true, 'ticket' => $tid, 'spend' => false]; } $price = nst_decay_price_micros(NST_VANITY_FULL_YEAR_MICROS); $ep = nst_epoch_info_read_only(); return [ 'ok' => true, 'quote' => true, 'ticket' => $tid, 'taken' => false, 'price_micros' => $price, 'price_fmt' => fmt_amt($price), 'secs_to_reset' => (int)($ep['secs_to_reset'] ?? 0), 'spend' => false, 'apply' => '501', 'note' => 'Quote only. Apply does not spend or bind yet.', ]; } function nst_vanity_apply(string $raw): array { /* Stay 501: no safe vanity spend+bind path yet. MUST NOT move coins or write bind. */ $q = nst_vanity_quote($raw); $q['ok'] = false; $q['apply'] = '501'; $q['spend'] = false; if (empty($q['err'])) { $q['err'] = 'Apply is not live — spend+bind is not safe yet'; } return $q; } function nst_vanity_raw_id(): string { $raw = (string)($_POST['id'] ?? $_GET['id'] ?? ''); if ($raw === '') { $ctype = (string)($_SERVER['CONTENT_TYPE'] ?? ''); if (stripos($ctype, 'application/json') !== false) { $body = json_decode((string)@file_get_contents('php://input'), true); if (is_array($body)) $raw = (string)($body['id'] ?? $body['ticket'] ?? ''); } } return $raw; } function nst_vanity_quote_handle(): void { $q = nst_vanity_quote(nst_vanity_raw_id()); $st = 200; if (empty($q['ok'])) { $st = (($q['err'] ?? '') === 'bound') ? 409 : 400; } nst_ticket_json($q, $st); } function nst_vanity_apply_handle(): void { nst_tx_gate_enforce(nst_tx_gate_peek_addr()); nst_ticket_json(nst_vanity_apply(nst_vanity_raw_id()), 501); } function nst_ad_share_quote_handle(): void { $crop = nst_ad_site_norm((string)($_GET['crop'] ?? $_GET['site'] ?? '')); $raw = (string)($_GET['from'] ?? $_POST['from'] ?? ''); $addr = nst_resolve_receipt($raw); if ($addr === '') { $hex = strtolower(preg_replace('/[^a-f0-9]/', '', $raw) ?? ''); $addr = strlen($hex) === 64 ? $hex : ''; } nst_ticket_json(nst_ad_share_quote($crop === '' ? null : $crop, $addr), 200); } /* -------- Public ID presence gossip (telemetry only; NEVER wallets/chain) -------- * Advertises the public ticket + last-seen. Lazy-deletes presence files after * ~30 days of INACTIVITY. Must never unlink ticket.bind, id.counter, * device_wallet_assignments, or chain.jsonl. IP is not a lookup key. */ function nst_id_presence_root(): string { $dir = nst_data_file('id-presence'); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } $ht = $dir . DIRECTORY_SEPARATOR . '.htaccess'; if (!is_file($ht)) { @file_put_contents($ht, "Require all denied\nDeny from all\n", LOCK_EX); } return $dir; } function nst_id_presence_path(string $ticket): string { $tid = nst_ticket_norm($ticket); if ($tid === '') return ''; $aa = substr(hash('sha256', 'nosignup.id-presence.shard.v1|' . $tid), 0, 2); $dir = nst_id_presence_root() . DIRECTORY_SEPARATOR . $aa; if (!is_dir($dir)) { @mkdir($dir, 0755, true); } return $dir . DIRECTORY_SEPARATOR . $tid . '.txt'; } function nst_id_presence_touch(string $ticket): void { $path = nst_id_presence_path($ticket); if ($path === '') return; $now = (string)time(); @file_put_contents($path, $ticket . "\t" . $now . "\n", LOCK_EX); @chmod($path, 0600); /* Request-local only: the following nst_id_presence_gc_lazy() visits this shard and no other, * starting at an offset derived from the bytes just written (no random, no extra write). */ $GLOBALS['NST_ID_PRESENCE_TOUCHED_DIR'] = dirname($path); $GLOBALS['NST_ID_PRESENCE_TOUCHED_SEED'] = hash('sha256', $ticket . "\t" . $now); } /** Last-seen time of one presence file, or 0 when missing/expired; an expired file is unlinked. Used only by the on-touch gc. */ function nst_id_presence_file_ts(string $p): int { if (!is_file($p)) return 0; $raw = (string)@file_get_contents($p, false, null, 0, 64); $ts = 0; if (preg_match('/\\t([0-9]{9,12})\\s*$/', $raw, $m)) $ts = (int)$m[1]; elseif (is_file($p)) $ts = (int)@filemtime($p); if ($ts > 0 && $ts >= time() - (int)NST_ID_PRESENCE_TTL_SECS) return $ts; if ($ts > 0) @unlink($p); return 0; } /** On-touch cleanup of the shard touched by this request only: reads at most 64 entries and checks at most 24 * presence files, rotating the start by a hash of the touch so repeated touches cover the listing deterministically. */ function nst_id_presence_gc_lazy(): void { $dir = (string)($GLOBALS['NST_ID_PRESENCE_TOUCHED_DIR'] ?? ''); $seed = (string)($GLOBALS['NST_ID_PRESENCE_TOUCHED_SEED'] ?? ''); unset($GLOBALS['NST_ID_PRESENCE_TOUCHED_DIR'], $GLOBALS['NST_ID_PRESENCE_TOUCHED_SEED']); if ($dir === '' || !preg_match('/[\\\\\/][0-9a-f]{2}$/', $dir) || !is_dir($dir)) return; $dh = @opendir($dir); if ($dh === false) return; $names = []; $seen = 0; while ($seen < 64 && ($fn = readdir($dh)) !== false) { $seen++; if (preg_match('/^[1-9][0-9]{0,17}\\.txt$/', $fn)) $names[] = $fn; } closedir($dh); $k = count($names); if ($k === 0) return; $o = preg_match('/^[0-9a-f]{7}/', $seed) ? hexdec(substr($seed, 0, 7)) % $k : 0; for ($i = 0; $i < 24 && $i < $k; $i++) { nst_id_presence_file_ts($dir . DIRECTORY_SEPARATOR . $names[($o + $i) % $k]); } } function nst_id_gossip_handle(): void { nst_id_cors_sisters(); $method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? '')); if ($method === 'OPTIONS') { http_response_code(204); exit; } if ($method !== 'POST') { nst_ticket_json(['ok' => false, 'err' => 'POST only'], 405); } $raw = (string)($_POST['id'] ?? $_POST['ticket'] ?? ''); $ctype = (string)($_SERVER['CONTENT_TYPE'] ?? ''); $body = []; if ($raw === '' && stripos($ctype, 'application/json') !== false) { $body = json_decode((string)@file_get_contents('php://input'), true); if (is_array($body)) $raw = (string)($body['id'] ?? $body['ticket'] ?? ''); } if (is_array($body) && (isset($body['seed']) || isset($body['wallet']))) { nst_ticket_json(['ok' => false, 'err' => 'seed is not accepted on gossip'], 400); } if (isset($_POST['seed']) || isset($_POST['wallet'])) { nst_ticket_json(['ok' => false, 'err' => 'seed is not accepted on gossip'], 400); } $tid = nst_ticket_norm($raw); if ($tid === '') { nst_ticket_json(['ok' => false, 'err' => 'Need a public ID'], 400); } nst_id_presence_touch($tid); nst_id_presence_gc_lazy(); $origin = trim((string)($_SERVER['HTTP_ORIGIN'] ?? '')); if ($origin !== '' && preg_match('~\\Ahttps://nosignup\\.(com|org|net|trade|chat|work|market|date|fun|info)\\z~', $origin)) { header('Access-Control-Allow-Origin: ' . $origin); header('Vary: Origin'); } nst_ticket_json(['ok' => true, 'ticket' => $tid, 'presence' => 'touched', 'ttl_secs' => (int)NST_ID_PRESENCE_TTL_SECS]); } /* -------- Device wallet binding (isolated from chain/genesis/economy) -------- */ function nst_device_wallet_out(array $payload, int $status = 200): void { http_response_code($status); header('Content-Type: application/json; charset=UTF-8'); header('Cache-Control: private, no-store, max-age=0, must-revalidate'); header('Pragma: no-cache'); header('Expires: 0'); header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: DENY'); header("Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'"); header('Referrer-Policy: no-referrer'); header('Cross-Origin-Resource-Policy: same-origin'); header('Vary: Cookie, Origin, Sec-Fetch-Site'); echo j($payload); exit; } function nst_device_wallet_error(string $code, int $status): void { nst_device_wallet_out(['ok' => false, 'err' => $code], $status); } function nst_device_wallet_normalized_origin(string $raw): ?string { if ($raw === '' || strlen($raw) > 255 || preg_match('/[\x00-\x20\x7f]/', $raw)) { return null; } $parts = @parse_url($raw); if (!is_array($parts)) { return null; } $scheme = strtolower((string)($parts['scheme'] ?? '')); $host = strtolower((string)($parts['host'] ?? '')); $path = (string)($parts['path'] ?? ''); if (($scheme !== 'http' && $scheme !== 'https') || $host === '' || ($path !== '' && $path !== '/')) { return null; } foreach (['user', 'pass', 'query', 'fragment'] as $forbidden) { if (array_key_exists($forbidden, $parts)) { return null; } } $port = isset($parts['port']) ? (int)$parts['port'] : null; if ($port !== null && ($port < 1 || $port > 65535)) { return null; } if (($scheme === 'http' && $port === 80) || ($scheme === 'https' && $port === 443)) { $port = null; } if (str_contains($host, ':') && $host[0] !== '[') { $host = '[' . $host . ']'; } return $scheme . '://' . $host . ($port === null ? '' : ':' . $port); } function nst_device_wallet_request_gate(): void { if (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? '')) !== 'POST') { header('Allow: POST'); nst_device_wallet_error('post_only', 405); } if (count($_GET) !== 1 || (string)($_GET['api'] ?? '') !== 'device_wallet') { nst_device_wallet_error('bad_request', 400); } $fetchSite = strtolower(trim((string)($_SERVER['HTTP_SEC_FETCH_SITE'] ?? ''))); if ($fetchSite !== '' && $fetchSite !== 'same-origin') { nst_device_wallet_error('same_origin_required', 403); } if ((string)($_SERVER['HTTP_X_NSU_INTENT'] ?? '') !== 'device-wallet-v2') { nst_device_wallet_error('same_origin_required', 403); } $origin = nst_device_wallet_normalized_origin(trim((string)($_SERVER['HTTP_ORIGIN'] ?? ''))); $allowed = ['https://nosignup.trade']; $originParts = $origin === null ? null : @parse_url($origin); $originHost = is_array($originParts) ? strtolower((string)($originParts['host'] ?? '')) : ''; $originScheme = is_array($originParts) ? strtolower((string)($originParts['scheme'] ?? '')) : ''; $loopback = $originScheme === 'http' && in_array($originHost, ['127.0.0.1', 'localhost', '::1'], true); if ($origin === null || (!$loopback && !in_array($origin, $allowed, true))) { nst_device_wallet_error('same_origin_required', 403); } $contentType = trim((string)($_SERVER['CONTENT_TYPE'] ?? '')); if (!preg_match('~\Aapplication/json(?:\s*;\s*charset=utf-8)?\z~i', $contentType)) { nst_device_wallet_error('json_required', 415); } } function nst_device_wallet_https(): bool { $https = strtolower(trim((string)($_SERVER['HTTPS'] ?? ''))); return $https !== '' && $https !== 'off' && $https !== '0'; } /** @return array */ function nst_device_wallet_cookie_values(): array { $raw = (string)($_SERVER['HTTP_COOKIE'] ?? ''); if ($raw === '' || strlen($raw) > 8192) { return []; } $values = []; foreach (explode(';', $raw) as $part) { $pair = explode('=', trim($part), 2); if (count($pair) !== 2 || trim($pair[0]) !== NST_DEVICE_WALLET_COOKIE) { continue; } $values[] = trim($pair[1]); } return $values; } function nst_device_wallet_cookie_ttl(): int { // Access lifetime is independent of the annual service/pricing reset. return max(60, (int)NST_DEVICE_WALLET_COOKIE_MAX_AGE); } function nst_device_wallet_set_cookie(string $token): bool { return @setcookie(NST_DEVICE_WALLET_COOKIE, $token, [ 'expires' => time() + nst_device_wallet_cookie_ttl(), 'path' => '/', 'secure' => nst_device_wallet_https(), 'httponly' => true, 'samesite' => 'Lax', ]); } function nst_device_wallet_issue_cookie(): void { try { $token = bin2hex(random_bytes(32)); } catch (Throwable $e) { nst_device_wallet_error('device_cookie_unavailable', 503); } $ok = nst_device_wallet_set_cookie($token); $token = ''; if (!$ok) { nst_device_wallet_error('device_cookie_unavailable', 503); } nst_device_wallet_out([ 'ok' => false, 'err' => 'device_cookie_issued', 'retry' => true, ], 428); } function nst_device_wallet_cookie_token(): string { $values = nst_device_wallet_cookie_values(); if (count($values) !== 1 || !preg_match('/\A[0-9a-f]{64}\z/D', $values[0])) { nst_device_wallet_issue_cookie(); } return $values[0]; } function nst_device_wallet_json_ws(string $raw, int &$offset): void { $length = strlen($raw); while ($offset < $length && str_contains(" \t\r\n", $raw[$offset])) $offset++; } function nst_device_wallet_json_string(string $raw, int &$offset): string { $start = $offset; $length = strlen($raw); if ($offset >= $length || $raw[$offset] !== '"') throw new RuntimeException('bad_json'); $offset++; while ($offset < $length) { $c = $raw[$offset++]; if ($c === '"') { $token = substr($raw, $start, $offset - $start); try { $decoded = json_decode($token, true, 2, JSON_THROW_ON_ERROR); } catch (Throwable $e) { throw new RuntimeException('bad_json'); } if (!is_string($decoded)) throw new RuntimeException('bad_json'); return $decoded; } if ($c === '\\') { if ($offset >= $length) throw new RuntimeException('bad_json'); $escape = $raw[$offset++]; if ($escape === 'u') { if ($offset + 4 > $length) throw new RuntimeException('bad_json'); $offset += 4; } } } throw new RuntimeException('bad_json'); } /** Valid JSON is already checked by json_decode; this pass preserves object-key multiplicity. */ function nst_device_wallet_json_duplicate_scan(string $raw, int &$offset): bool { nst_device_wallet_json_ws($raw, $offset); $length = strlen($raw); if ($offset >= $length) throw new RuntimeException('bad_json'); $c = $raw[$offset]; if ($c === '{') { $offset++; $seen = []; nst_device_wallet_json_ws($raw, $offset); if ($offset < $length && $raw[$offset] === '}') { $offset++; return false; } while (true) { nst_device_wallet_json_ws($raw, $offset); $key = nst_device_wallet_json_string($raw, $offset); $slot = 'k:' . $key; if (array_key_exists($slot, $seen)) return true; $seen[$slot] = true; nst_device_wallet_json_ws($raw, $offset); if ($offset >= $length || $raw[$offset] !== ':') throw new RuntimeException('bad_json'); $offset++; if (nst_device_wallet_json_duplicate_scan($raw, $offset)) return true; nst_device_wallet_json_ws($raw, $offset); if ($offset >= $length) throw new RuntimeException('bad_json'); if ($raw[$offset] === '}') { $offset++; return false; } if ($raw[$offset] !== ',') throw new RuntimeException('bad_json'); $offset++; } } if ($c === '[') { $offset++; nst_device_wallet_json_ws($raw, $offset); if ($offset < $length && $raw[$offset] === ']') { $offset++; return false; } while (true) { if (nst_device_wallet_json_duplicate_scan($raw, $offset)) return true; nst_device_wallet_json_ws($raw, $offset); if ($offset >= $length) throw new RuntimeException('bad_json'); if ($raw[$offset] === ']') { $offset++; return false; } if ($raw[$offset] !== ',') throw new RuntimeException('bad_json'); $offset++; } } if ($c === '"') { nst_device_wallet_json_string($raw, $offset); return false; } while ($offset < $length && !str_contains(" \t\r\n,]}", $raw[$offset])) $offset++; return false; } function nst_device_wallet_json_has_duplicate_keys(string $raw): bool { $offset = 0; $duplicate = nst_device_wallet_json_duplicate_scan($raw, $offset); nst_device_wallet_json_ws($raw, $offset); if ($offset !== strlen($raw)) throw new RuntimeException('bad_json'); return $duplicate; } /** @return array */ function nst_device_wallet_read_json_body(): array { $declared = trim((string)($_SERVER['CONTENT_LENGTH'] ?? '')); if ($declared !== '') { if (!ctype_digit($declared)) { nst_device_wallet_error('bad_request', 400); } if (strlen($declared) > 9 || (int)$declared > NST_DEVICE_WALLET_BODY_MAX) { nst_device_wallet_error('body_too_large', 413); } } $fh = @fopen('php://input', 'rb'); if (!is_resource($fh)) { nst_device_wallet_error('bad_request', 400); } $raw = @stream_get_contents($fh, NST_DEVICE_WALLET_BODY_MAX + 1); @fclose($fh); if (!is_string($raw) || $raw === '') { nst_device_wallet_error('bad_request', 400); } if (strlen($raw) > NST_DEVICE_WALLET_BODY_MAX) { nst_device_wallet_error('body_too_large', 413); } try { $body = json_decode($raw, true, 4, JSON_THROW_ON_ERROR); } catch (Throwable $e) { nst_device_wallet_error('bad_json', 400); } try { if (nst_device_wallet_json_has_duplicate_keys($raw)) { nst_device_wallet_error('bad_json', 400); } } catch (Throwable $e) { nst_device_wallet_error('bad_json', 400); } if (!is_array($body)) { nst_device_wallet_error('bad_json', 400); } foreach (array_keys($body) as $key) { if (!is_string($key)) { nst_device_wallet_error('bad_request', 400); } } return $body; } function nst_device_wallet_normalize_seed_value($value): ?string { if (!is_string($value)) { return null; } $rawLen = strlen($value); if ($rawLen < 1 || $rawLen > NST_DEVICE_WALLET_SEED_RAW_MAX) { return null; } if (preg_match('/[^\x09\x0a\x0d\x20-\x7e]/', $value)) { return null; } $normalized = preg_replace('/[ \t\r\n]+/', ' ', trim($value)); if (!is_string($normalized)) { return null; } $normalized = strtolower($normalized); if (strlen($normalized) > NST_DEVICE_WALLET_SEED_CANON_MAX) { return null; } if (!preg_match('/\A[a-z]{1,24}(?: [a-z]{1,24}){11}\z/D', $normalized)) { return null; } return $normalized; } /** @return array */ function nst_device_wallet_normalize_profile($value): array { if ($value === null) { return []; } if (!is_array($value) || count($value) > NST_DEVICE_WALLET_PROFILE_FIELDS_MAX) { nst_device_wallet_error('bad_profile', 400); } $out = []; foreach ($value as $key => $item) { if (!is_string($key) || strlen($key) < 1 || strlen($key) > NST_DEVICE_WALLET_PROFILE_KEY_MAX || !preg_match('/\A[a-z][a-z0-9_]*\z/D', $key) ) { nst_device_wallet_error('bad_profile', 400); } if (is_string($item)) { if (strlen($item) > NST_DEVICE_WALLET_PROFILE_VALUE_MAX || preg_match('/[\x00-\x1f\x7f]/', $item)) { nst_device_wallet_error('bad_profile', 400); } $out[$key] = ['type' => 'string', 'value' => $item]; } elseif (is_int($item)) { $out[$key] = ['type' => 'int', 'value' => (string)$item]; } elseif (is_bool($item)) { $out[$key] = ['type' => 'bool', 'value' => $item ? '1' : '0']; } elseif ($item === null) { $out[$key] = ['type' => 'null', 'value' => '']; } else { nst_device_wallet_error('bad_profile', 400); } } ksort($out, SORT_STRING); return $out; } /** @return array{action:string,seed:?string,profile:array} */ /** @return array{rows:array,duplicates:bool} */ function nst_device_wallet_read_locators(string $path): array { if (!file_exists($path)) { if (nst_device_wallet_path_is_linkish($path)) throw new RuntimeException('store_invalid'); return ['rows' => [], 'duplicates' => false]; } try { $fh = nst_device_wallet_open_regular($path, 'rb'); } catch (Throwable $e) { throw new RuntimeException('store_invalid'); } $st = @fstat($fh); $size = is_array($st) ? (int)($st['size'] ?? -1) : -1; if (!is_int($size) || $size < 0 || $size > NST_DEVICE_WALLET_FILE_MAX) { @fclose($fh); throw new RuntimeException('store_invalid'); } if ($size === 0) { @fclose($fh); return ['rows' => [], 'duplicates' => false]; } $rows = []; $bytesRead = 0; try { while (true) { $line = @fgets($fh, NST_DEVICE_WALLET_LINE_MAX + 2); if ($line === false) { if (!feof($fh)) throw new RuntimeException('store_invalid'); break; } $lineLen = strlen($line); $bytesRead += $lineLen; if ($lineLen < 1 || $lineLen > NST_DEVICE_WALLET_LINE_MAX || $bytesRead > NST_DEVICE_WALLET_FILE_MAX || substr($line, -1) !== "\n" || str_contains($line, "\r")) { throw new RuntimeException('store_invalid'); } if (count($rows) >= NST_DEVICE_WALLET_ROWS_MAX) throw new RuntimeException('store_invalid'); $fields = explode("\t", substr($line, 0, -1)); if (count($fields) !== 5 || $fields[0] !== 'v2') throw new RuntimeException('store_invalid'); [, $binding, $addr, $signFp, $rev] = $fields; if (!preg_match('/\A[0-9a-f]{64}\z/D', $binding) || !preg_match('/\A[0-9a-f]{64}\z/D', $addr) || !preg_match('/\Asha256:[0-9a-f]{64}\z/D', $signFp) || !preg_match('/\A[1-9][0-9]{12,15}\z/D', $rev) ) { throw new RuntimeException('store_invalid'); } $rows[] = [ 'binding' => $binding, 'addr' => $addr, 'sign_fp' => $signFp, 'revision' => $rev, 'raw' => $line, ]; } } finally { @fclose($fh); } if ($bytesRead !== $size) throw new RuntimeException('store_invalid'); $last = []; foreach ($rows as $index => $row) $last[$row['binding']] = $index; $deduped = []; foreach ($rows as $index => $row) { if ($last[$row['binding']] === $index) $deduped[] = $row; } return ['rows' => $deduped, 'duplicates' => count($deduped) !== count($rows)]; } function nst_device_wallet_locator_raw(array $row): string { $binding = (string)($row['binding'] ?? ''); $addr = (string)($row['addr'] ?? ''); $fp = (string)($row['sign_fp'] ?? ''); $rev = (string)($row['revision'] ?? ''); if (!preg_match('/\A[0-9a-f]{64}\z/D', $binding) || !preg_match('/\A[0-9a-f]{64}\z/D', $addr) || !preg_match('/\Asha256:[0-9a-f]{64}\z/D', $fp) || !preg_match('/\A[1-9][0-9]{12,15}\z/D', $rev) ) { throw new RuntimeException('commit_failed'); } $line = 'v2' . "\t" . $binding . "\t" . $addr . "\t" . $fp . "\t" . $rev . "\n"; if (strlen($line) > NST_DEVICE_WALLET_LINE_MAX) throw new RuntimeException('commit_failed'); return $line; } function nst_device_wallet_write_locators(string $path, array $rows): void { if (count($rows) > NST_DEVICE_WALLET_ROWS_MAX) throw new RuntimeException('store_full'); $bytes = ''; foreach ($rows as $row) { $bytes .= nst_device_wallet_locator_raw($row); if (strlen($bytes) > NST_DEVICE_WALLET_FILE_MAX) throw new RuntimeException('store_full'); } if ($bytes === '') { if (file_exists($path)) { $info = nst_device_wallet_safe_file($path, true); if (!empty($info['exists'])) @unlink($info['real']); } return; } nst_device_wallet_atomic_replace($path, $bytes); } function nst_device_wallet_validate_request(array $body): array { $allowed = [ 'action' => true, 'profile' => true, 'ticket' => true, 'addr' => true, 'sign_fp' => true, 'pubkey' => true, 'ecdsa_sig' => true, 'nonce' => true, 'ts' => true, 'revision' => true, ]; foreach ($body as $key => $_value) { if (!isset($allowed[$key])) { nst_device_wallet_error('bad_request', 400); } } // BIND never accepts seed; OPEN never accepts seed. Legacy seed lives only on legacy_wallet_migrate. if (array_key_exists('seed', $body)) { nst_device_wallet_error('seed_not_accepted', 400); } $action = $body['action'] ?? null; if (!is_string($action) || !in_array($action, ['open', 'bind', 'clear'], true)) { nst_device_wallet_error('bad_action', 400); } $ticket = nst_ticket_norm((string)($body['ticket'] ?? '')); $out = [ 'action' => $action, 'profile' => nst_device_wallet_normalize_profile($body['profile'] ?? null), 'ticket' => $ticket, 'addr' => '', 'sign_fp' => '', 'pubkey' => '', 'ecdsa_sig' => '', 'nonce' => '', 'ts' => '', 'revision' => '', ]; if ($action === 'bind') { $addr = preg_replace('/[^a-f0-9]/', '', strtolower((string)($body['addr'] ?? ''))) ?? ''; $fp = strtolower(trim((string)($body['sign_fp'] ?? ''))); $pub = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($body['pubkey'] ?? '')) ?? ''); $sig = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($body['ecdsa_sig'] ?? '')) ?? ''); $nonce = preg_replace('/[^a-zA-Z0-9_-]/', '', (string)($body['nonce'] ?? '')) ?? ''; $ts = preg_replace('/[^0-9]/', '', (string)($body['ts'] ?? '')) ?? ''; if (strlen($addr) !== 64 || !preg_match('/\Asha256:[0-9a-f]{64}\z/D', $fp) || strlen($pub) < 100 || $sig === '' || $nonce === '' || $ts === '') { nst_device_wallet_error('bad_bind', 400); } $out['addr'] = $addr; $out['sign_fp'] = $fp; $out['pubkey'] = $pub; $out['ecdsa_sig'] = $sig; $out['nonce'] = $nonce; $out['ts'] = $ts; } return $out; } function nst_device_wallet_path_equal(string $a, string $b): bool { $a = rtrim(str_replace('\\', '/', $a), '/'); $b = rtrim(str_replace('\\', '/', $b), '/'); return PHP_OS_FAMILY === 'Windows' ? strcasecmp($a, $b) === 0 : hash_equals($a, $b); } /** * Fail closed on links and Windows reparse points before opening or creating a * child. This PHP build reports directory junctions as filetype "unknown" with * a zero lstat mode even though is_link() is false. */ function nst_device_wallet_path_is_linkish(string $path): bool { if (is_link($path)) return true; if (PHP_OS_FAMILY !== 'Windows') return false; if (file_exists($path)) { $type = @filetype($path); $st = @lstat($path); if ($type === 'unknown' || !is_array($st) || (int)($st['mode'] ?? 0) === 0) { return true; } } $target = @readlink($path); if (!is_string($target) || $target === '') return false; $parent = @realpath(dirname($path)); if (!is_string($parent) || $parent === '') return true; $lexical = $parent . DIRECTORY_SEPARATOR . basename($path); return !nst_device_wallet_path_equal($target, $lexical); } /** @return array{dev:int,ino:int,real:string} */ function nst_device_wallet_dir_identity(string $dir): array { $real = @realpath($dir); $st = @stat($dir); if (!is_string($real) || $real === '' || !is_array($st) || !is_dir($real) || nst_device_wallet_path_is_linkish($dir)) { throw new RuntimeException('path_unavailable'); } return ['dev' => (int)($st['dev'] ?? 0), 'ino' => (int)($st['ino'] ?? 0), 'real' => $real]; } function nst_device_wallet_same_dir(array $before, string $dir): bool { try { $after = nst_device_wallet_dir_identity($dir); } catch (Throwable $e) { return false; } if (!nst_device_wallet_path_equal((string)$before['real'], (string)$after['real'])) return false; $haveIds = ((int)$before['dev'] !== 0 || (int)$before['ino'] !== 0) && ((int)$after['dev'] !== 0 || (int)$after['ino'] !== 0); return !$haveIds || ((int)$before['dev'] === (int)$after['dev'] && (int)$before['ino'] === (int)$after['ino']); } function nst_device_wallet_safe_dir(string $path, string $expectedParent, bool $create): string { $parent = @realpath($expectedParent); if (!is_string($parent) || !is_dir($parent) || nst_device_wallet_path_is_linkish($expectedParent)) { throw new RuntimeException('path_unavailable'); } $expected = $parent . DIRECTORY_SEPARATOR . basename($path); if (!nst_device_wallet_path_equal($path, $expected)) throw new RuntimeException('path_unavailable'); if (nst_device_wallet_path_is_linkish($path)) throw new RuntimeException('path_unavailable'); if (!file_exists($path)) { if (!$create || (!@mkdir($path, 0700, false) && !is_dir($path))) { throw new RuntimeException('path_unavailable'); } } if (nst_device_wallet_path_is_linkish($path) || !is_dir($path)) throw new RuntimeException('path_unavailable'); $real = @realpath($path); if (!is_string($real) || !nst_device_wallet_path_equal($real, $expected)) { throw new RuntimeException('path_unavailable'); } @chmod($real, 0700); return $real; } /** @return array{exists:bool,dev:int,ino:int,real:string} */ function nst_device_wallet_safe_file(string $path, bool $allowMissing): array { $dir = @realpath(dirname($path)); if (!is_string($dir) || !is_dir($dir) || nst_device_wallet_path_is_linkish(dirname($path))) { throw new RuntimeException('path_unavailable'); } $expected = $dir . DIRECTORY_SEPARATOR . basename($path); if (!nst_device_wallet_path_equal($path, $expected) || nst_device_wallet_path_is_linkish($path)) { throw new RuntimeException('path_unavailable'); } if (!file_exists($path)) { if (!$allowMissing) throw new RuntimeException('path_unavailable'); return ['exists' => false, 'dev' => 0, 'ino' => 0, 'real' => $expected]; } if (!is_file($path)) throw new RuntimeException('path_unavailable'); $real = @realpath($path); $st = @stat($path); if (!is_string($real) || !nst_device_wallet_path_equal($real, $expected) || !is_array($st)) { throw new RuntimeException('path_unavailable'); } return ['exists' => true, 'dev' => (int)($st['dev'] ?? 0), 'ino' => (int)($st['ino'] ?? 0), 'real' => $real]; } function nst_device_wallet_open_matches(string $path, $fh): bool { if (!is_resource($fh)) return false; try { $pathId = nst_device_wallet_safe_file($path, false); } catch (Throwable $e) { return false; } $openId = @fstat($fh); if (!is_array($openId)) return false; $haveIds = ((int)$pathId['dev'] !== 0 || (int)$pathId['ino'] !== 0) && ((int)($openId['dev'] ?? 0) !== 0 || (int)($openId['ino'] ?? 0) !== 0); return !$haveIds || ((int)$pathId['dev'] === (int)($openId['dev'] ?? 0) && (int)$pathId['ino'] === (int)($openId['ino'] ?? 0)); } function nst_device_wallet_open_regular(string $path, string $mode) { nst_device_wallet_safe_file($path, false); $fh = @fopen($path, $mode); if (!is_resource($fh) || !nst_device_wallet_open_matches($path, $fh)) { if (is_resource($fh)) @fclose($fh); throw new RuntimeException('path_unavailable'); } return $fh; } function nst_device_wallet_open_lock(string $path) { for ($attempt = 0; $attempt < 3; $attempt++) { $state = nst_device_wallet_safe_file($path, true); $fh = @fopen($path, !empty($state['exists']) ? 'c+b' : 'x+b'); if (!is_resource($fh)) continue; @chmod($path, 0600); if (!nst_device_wallet_open_matches($path, $fh)) { @fclose($fh); throw new RuntimeException('path_unavailable'); } return $fh; } throw new RuntimeException('lock_unavailable'); } function nst_device_wallet_create_exact(string $path, string $bytes): void { nst_device_wallet_safe_file($path, true); $fh = @fopen($path, 'x+b'); if (!is_resource($fh) || !nst_device_wallet_open_matches($path, $fh)) { if (is_resource($fh)) @fclose($fh); throw new RuntimeException('path_unavailable'); } try { if ($bytes !== '' && @fwrite($fh, $bytes) !== strlen($bytes)) throw new RuntimeException('path_unavailable'); if (!@fflush($fh)) throw new RuntimeException('path_unavailable'); if (function_exists('fsync') && !@fsync($fh)) throw new RuntimeException('path_unavailable'); } finally { @fclose($fh); } @chmod($path, 0600); } function nst_device_wallet_private_marker(string $dir): void { $ht = $dir . DIRECTORY_SEPARATOR . '.htaccess'; if (!file_exists($ht) && !nst_device_wallet_path_is_linkish($ht)) { try { nst_device_wallet_create_exact($ht, "Require all denied\nDeny from all\n"); } catch (Throwable $e) { // Another first request may have won the exclusive create. Validate it below. if (!file_exists($ht) || nst_device_wallet_path_is_linkish($ht)) throw $e; } } $fh = nst_device_wallet_open_regular($ht, 'rb'); $body = @stream_get_contents($fh, 256); @fclose($fh); if (!is_string($body) || (stripos($body, 'Require all denied') === false && stripos($body, 'Deny from all') === false)) { throw new RuntimeException('path_unavailable'); } $idx = $dir . DIRECTORY_SEPARATOR . 'index.html'; if (!file_exists($idx) && !nst_device_wallet_path_is_linkish($idx)) { try { nst_device_wallet_create_exact($idx, ''); } catch (Throwable $e) { if (!file_exists($idx) || nst_device_wallet_path_is_linkish($idx)) throw $e; } } nst_device_wallet_safe_file($idx, false); } /** @return array{dir:string,store:string,lock:string,secret:string} */ function nst_device_wallet_paths(): array { global $VAULT_BASE; $base = @realpath($VAULT_BASE); if (!is_string($base) || !is_dir($base)) throw new RuntimeException('path_unavailable'); $vault = nst_device_wallet_safe_dir($base . DIRECTORY_SEPARATOR . 'vault', $base, true); nst_device_wallet_private_marker($vault); $dir = nst_device_wallet_safe_dir($vault . DIRECTORY_SEPARATOR . 'device-wallet', $vault, true); nst_device_wallet_private_marker($dir); return [ 'dir' => $dir, 'store' => $dir . DIRECTORY_SEPARATOR . 'device_wallet_assignments.tsv', 'locators' => $dir . DIRECTORY_SEPARATOR . 'device_wallet_locators.tsv', 'lock' => $dir . DIRECTORY_SEPARATOR . 'device_wallet_assignments.lock', 'secret' => $dir . DIRECTORY_SEPARATOR . 'device_wallet_hmac.secret', ]; } /** * Legacy explicit burn helper; annual reset no longer calls this (2026-09-29). * Former year-wipe behavior: the assignment ledger is superstructure (a convenience * locator), not a surviving wallet. Unlink store, lock, and hmac.secret * with the existing safe-file / no-link helpers. Next open recreates an * empty store and a fresh secret. Leftover cookies become invalid locators. * Users who wrote the 12 words restore via rebind; others lose the locator * (already the warned contract). Does not mint and does not touch the chain. */ function nst_device_wallet_burn_store(): bool { try { $paths = nst_device_wallet_paths(); $dirId = nst_device_wallet_dir_identity($paths['dir']); foreach (['store', 'locators', 'lock', 'secret'] as $key) { $path = $paths[$key]; $info = nst_device_wallet_safe_file($path, true); if (!$info['exists']) continue; if (!nst_device_wallet_same_dir($dirId, dirname($path))) { throw new RuntimeException('path_unavailable'); } if (nst_device_wallet_path_is_linkish($info['real'])) { throw new RuntimeException('path_unavailable'); } if (!@unlink($info['real'])) { throw new RuntimeException('commit_failed'); } } nst_device_wallet_cleanup_temps($paths['dir']); return true; } catch (Throwable $e) { return false; } } function nst_device_wallet_cleanup_temps(string $dir): void { foreach ((array)(glob($dir . DIRECTORY_SEPARATOR . '.device_wallet.*.tmp') ?: []) as $path) { if (!preg_match('/\A\.device_wallet\.[0-9a-f]{24}\.tmp\z/D', basename($path))) continue; nst_device_wallet_safe_file($path, false); if (!@unlink($path)) throw new RuntimeException('commit_failed'); } } function nst_device_wallet_atomic_replace(string $path, string $bytes): void { $dir = @realpath(dirname($path)); if (!is_string($dir) || !is_dir($dir)) throw new RuntimeException('commit_failed'); $dirIdentity = nst_device_wallet_dir_identity($dir); $targetIdentity = nst_device_wallet_safe_file($path, true); $tmp = ''; $fh = null; try { for ($attempt = 0; $attempt < 8; $attempt++) { $tmp = $dir . DIRECTORY_SEPARATOR . '.device_wallet.' . bin2hex(random_bytes(12)) . '.tmp'; nst_device_wallet_safe_file($tmp, true); $fh = @fopen($tmp, 'x+b'); if (is_resource($fh) && nst_device_wallet_open_matches($tmp, $fh)) { break; } if (is_resource($fh)) @fclose($fh); $fh = null; $tmp = ''; } if (!is_resource($fh) || $tmp === '') { throw new RuntimeException('commit_failed'); } @chmod($tmp, 0600); $length = strlen($bytes); $offset = 0; while ($offset < $length) { $written = @fwrite($fh, substr($bytes, $offset)); if (!is_int($written) || $written < 1) { throw new RuntimeException('commit_failed'); } $offset += $written; } if (!@fflush($fh)) { throw new RuntimeException('commit_failed'); } if (function_exists('fsync') && !@fsync($fh)) { throw new RuntimeException('commit_failed'); } @fclose($fh); $fh = null; if (!nst_device_wallet_same_dir($dirIdentity, $dir)) throw new RuntimeException('commit_failed'); $nowTarget = nst_device_wallet_safe_file($path, true); if ((bool)$targetIdentity['exists'] !== (bool)$nowTarget['exists']) throw new RuntimeException('commit_failed'); if (!empty($targetIdentity['exists'])) { $idsAvailable = ((int)$targetIdentity['dev'] !== 0 || (int)$targetIdentity['ino'] !== 0) && ((int)$nowTarget['dev'] !== 0 || (int)$nowTarget['ino'] !== 0); if ($idsAvailable && ((int)$targetIdentity['dev'] !== (int)$nowTarget['dev'] || (int)$targetIdentity['ino'] !== (int)$nowTarget['ino'])) throw new RuntimeException('commit_failed'); } if (!@rename($tmp, $path)) { throw new RuntimeException('commit_failed'); } $tmp = ''; nst_device_wallet_safe_file($path, false); if (!nst_device_wallet_same_dir($dirIdentity, $dir)) throw new RuntimeException('commit_failed'); @chmod($path, 0600); clearstatcache(true, $path); } catch (Throwable $e) { if (is_resource($fh)) { @fclose($fh); } if ($tmp !== '' && is_file($tmp) && !nst_device_wallet_path_is_linkish($tmp) && nst_device_wallet_same_dir($dirIdentity, $dir)) { try { nst_device_wallet_safe_file($tmp, false); @unlink($tmp); } catch (Throwable $ignored) {} } if ($e instanceof RuntimeException) { throw $e; } throw new RuntimeException('commit_failed'); } } function nst_device_wallet_load_secret(string $path): string { if (is_file($path)) { $fh = nst_device_wallet_open_regular($path, 'rb'); $st = @fstat($fh); $size = is_array($st) ? (int)($st['size'] ?? -1) : -1; if ($size !== 65) { @fclose($fh); throw new RuntimeException('secret_invalid'); } $encoded = @stream_get_contents($fh, 66); @fclose($fh); if (!is_string($encoded) || !preg_match('/\A[0-9a-f]{64}\n\z/D', $encoded)) { throw new RuntimeException('secret_invalid'); } $secret = @hex2bin(substr($encoded, 0, 64)); if (!is_string($secret) || strlen($secret) !== 32) { throw new RuntimeException('secret_invalid'); } @chmod($path, 0600); return $secret; } try { $encoded = bin2hex(random_bytes(32)) . "\n"; } catch (Throwable $e) { throw new RuntimeException('entropy_failed'); } nst_device_wallet_atomic_replace($path, $encoded); return nst_device_wallet_load_secret($path); } /** @return array{rows:array,duplicates:bool} */ function nst_device_wallet_read_rows(string $path): array { if (!file_exists($path)) { if (nst_device_wallet_path_is_linkish($path)) throw new RuntimeException('store_invalid'); return ['rows' => [], 'duplicates' => false]; } try { $fh = nst_device_wallet_open_regular($path, 'rb'); } catch (Throwable $e) { throw new RuntimeException('store_invalid'); } $st = @fstat($fh); $size = is_array($st) ? (int)($st['size'] ?? -1) : -1; if (!is_int($size) || $size < 0 || $size > NST_DEVICE_WALLET_FILE_MAX) { @fclose($fh); throw new RuntimeException('store_invalid'); } if ($size === 0) { @fclose($fh); throw new RuntimeException('store_invalid'); } $rows = []; $bytesRead = 0; try { while (true) { $line = @fgets($fh, NST_DEVICE_WALLET_LINE_MAX + 2); if ($line === false) { if (!feof($fh)) { throw new RuntimeException('store_invalid'); } break; } $lineLen = strlen($line); $bytesRead += $lineLen; if ($lineLen < 1 || $lineLen > NST_DEVICE_WALLET_LINE_MAX || $bytesRead > NST_DEVICE_WALLET_FILE_MAX || substr($line, -1) !== "\n" || str_contains($line, "\r") ) { throw new RuntimeException('store_invalid'); } if (count($rows) >= NST_DEVICE_WALLET_ROWS_MAX) { throw new RuntimeException('store_invalid'); } $fields = explode("\t", substr($line, 0, -1)); if (count($fields) !== 5 || $fields[0] !== 'v1') { throw new RuntimeException('store_invalid'); } [, $binding, $seed, $profile, $ts] = $fields; $normalizedSeed = nst_device_wallet_normalize_seed_value($seed); if (!preg_match('/\A[0-9a-f]{64}\z/D', $binding) || $normalizedSeed === null || !hash_equals($seed, $normalizedSeed) || !preg_match('/\A[0-9a-f]{64}\z/D', $profile) || !preg_match('/\A[1-9][0-9]{12,15}\z/D', $ts) ) { throw new RuntimeException('store_invalid'); } $rows[] = [ 'binding' => $binding, 'seed' => $seed, 'profile' => $profile, 'ts' => $ts, 'raw' => $line, ]; } } finally { @fclose($fh); } if ($bytesRead !== $size) { throw new RuntimeException('store_invalid'); } $last = []; foreach ($rows as $index => $row) { $last[$row['binding']] = $index; } $deduped = []; foreach ($rows as $index => $row) { if ($last[$row['binding']] === $index) { $deduped[] = $row; } } return ['rows' => $deduped, 'duplicates' => count($deduped) !== count($rows)]; } /** @param array{binding:string,seed:string,profile:string,ts:string,raw?:string} $row */ function nst_device_wallet_row_raw(array $row): string { $seed = nst_device_wallet_normalize_seed_value($row['seed'] ?? null); $binding = (string)($row['binding'] ?? ''); $profile = (string)($row['profile'] ?? ''); $ts = (string)($row['ts'] ?? ''); if ($seed === null || !preg_match('/\A[0-9a-f]{64}\z/D', $binding) || !preg_match('/\A[0-9a-f]{64}\z/D', $profile) || !preg_match('/\A[1-9][0-9]{12,15}\z/D', $ts) ) { throw new RuntimeException('commit_failed'); } $line = 'v1' . "\t" . $binding . "\t" . $seed . "\t" . $profile . "\t" . $ts . "\n"; if (strlen($line) > NST_DEVICE_WALLET_LINE_MAX) { throw new RuntimeException('commit_failed'); } return $line; } /** @param array $rows */ function nst_device_wallet_write_rows(string $path, array $rows): void { if (count($rows) > NST_DEVICE_WALLET_ROWS_MAX) { throw new RuntimeException('store_full'); } $bytes = ''; foreach ($rows as $row) { $bytes .= nst_device_wallet_row_raw($row); if (strlen($bytes) > NST_DEVICE_WALLET_FILE_MAX) { throw new RuntimeException('store_full'); } } if ($bytes === '') { if (file_exists($path)) { $info = nst_device_wallet_safe_file($path, true); if (!empty($info['exists'])) @unlink($info['real']); } return; } nst_device_wallet_atomic_replace($path, $bytes); } /** @param array $expected */ function nst_device_wallet_verify_rows(string $path, array $expected): void { $actual = nst_device_wallet_read_rows($path); if ($actual['duplicates'] || count($actual['rows']) !== count($expected)) { throw new RuntimeException('commit_failed'); } foreach ($expected as $index => $row) { $got = $actual['rows'][$index]; if (!hash_equals((string)$row['binding'], $got['binding']) || !hash_equals((string)$row['seed'], $got['seed']) || !hash_equals((string)$row['profile'], $got['profile']) || !hash_equals((string)$row['ts'], $got['ts']) ) { throw new RuntimeException('commit_failed'); } } } /** @return array */ function nst_device_wallet_words(): array { static $words = null; if ($words === null) { $words = explode(' ', 'able acid aged also aqua arch area army atom aunt auto avoid axis baby baker balance band bank bare barn base basic batch beach bean bear beat beauty become before begin behind being believe below belt bench best better between beyond bike bind biology bird birth bitter black blade blame blank blast bleak bless blind blood blossom blouse blue blur blush board boat body boil bomb bone bonus book boost border boring borrow boss bottom bounce box boy bracket brain brand brass brave bread breeze brick bridge brief bright bring brisk broccoli broken bronze broom brother brown brush bubble buddy budget buffalo build bulb bulk bullet bundle bunker burden burger burst bus business busy butter buyer buzz cabbage cabin cable cactus cage cake call calm camera camp canal cancel candy cannon canoe canvas canyon capable capital captain car carbon card cargo carpet carry cart case cash casino castle casual cat catalog catch category cattle caught cause caution cave ceiling celery cement census century cereal certain chair chalk champion change chaos chapter charge chase chat cheap check cheese chef cherry chest chicken chief child chimney choice choose chronic chuckle chunk churn cigar cinnamon circle citizen city civil claim clap clarify claw clay clean clerk clever click client cliff climb clinic clip clock clog close cloth cloud clown club clump cluster clutch coach coast coconut code coffee coil coin collect color column combine come comfort comic common company concert conduct confirm congress connect consider control convince cook cool copper copy coral core corn correct cost cotton couch country couple course cousin cover coyote crack cradle craft cram crane crash crater crawl crazy cream credit creek crew cricket crime crisp critic crop cross crouch crowd crucial cruel cruise crumble crunch crush cry crystal cube culture cup cupboard curious current curtain curve' ); } return $words; } function nst_device_wallet_generate_seed(): string { $words = nst_device_wallet_words(); $last = count($words) - 1; if ($last < 1) { throw new RuntimeException('entropy_failed'); } $selected = []; try { for ($i = 0; $i < 12; $i++) { $selected[] = $words[random_int(0, $last)]; } } catch (Throwable $e) { throw new RuntimeException('entropy_failed'); } $seed = implode(' ', $selected); if (nst_device_wallet_normalize_seed_value($seed) !== $seed) { throw new RuntimeException('entropy_failed'); } return $seed; } function nst_device_wallet_header_fact(string $value, int $max): string { $value = substr($value, 0, $max); return preg_replace('/[^\x20-\x7e]/', '?', $value) ?? ''; } /** @param array $client */ function nst_device_wallet_profile_tag(string $profileKey, array $client): string { $remote = (string)($_SERVER['REMOTE_ADDR'] ?? ''); if (filter_var($remote, FILTER_VALIDATE_IP) === false) { $remote = ''; } $material = [ 'purpose' => 'nosignup.trade.device-wallet.profile.v1', 'remote_ip' => $remote, 'user_agent' => nst_device_wallet_header_fact((string)($_SERVER['HTTP_USER_AGENT'] ?? ''), 512), 'accept_language' => nst_device_wallet_header_fact((string)($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''), 128), 'client' => $client, ]; $encoded = json_encode($material, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if (!is_string($encoded)) { throw new RuntimeException('profile_failed'); } return hash_hmac('sha256', "profile\0" . $encoded, $profileKey); } function nst_device_wallet_now_ms(string $minimum = '0'): string { $now = (string)(int)floor(microtime(true) * 1000); if (!preg_match('/\A[1-9][0-9]{12,15}\z/D', $now)) { throw new RuntimeException('clock_failed'); } if (strlen($minimum) < strlen($now) || (strlen($minimum) === strlen($now) && strcmp($minimum, $now) < 0)) { return $now; } if (!preg_match('/\A[0-9]{1,15}\z/D', $minimum)) { throw new RuntimeException('clock_failed'); } $next = (string)((int)$minimum + 1); if (!preg_match('/\A[1-9][0-9]{12,15}\z/D', $next)) { throw new RuntimeException('clock_failed'); } return $next; } /** @return array{status:string,addr:?string,sign_fp:?string,revision:string,secret_present_on_server:bool,legacy_server_exposed:bool,needs_local_seed:bool} */ function nst_device_wallet_store(array $request, string $token): array { $paths = nst_device_wallet_paths(); $lock = nst_device_wallet_open_lock($paths['lock']); if (!@flock($lock, LOCK_EX)) { @fclose($lock); throw new RuntimeException('lock_unavailable'); } try { nst_device_wallet_cleanup_temps($paths['dir']); if ((file_exists($paths['store']) || file_exists($paths['locators'])) && !file_exists($paths['secret'])) { throw new RuntimeException('store_invalid'); } $serverSecret = nst_device_wallet_load_secret($paths['secret']); $lookupKey = hash_hmac('sha256', 'nosignup.trade.device-wallet.lookup-key.v1', $serverSecret, true); $tokenBytes = @hex2bin($token); $token = ''; if (!is_string($tokenBytes) || strlen($tokenBytes) !== 32) { throw new RuntimeException('request_invalid'); } $binding = hash_hmac('sha256', "binding\0" . $tokenBytes, $lookupKey); $legacySnap = ['rows' => [], 'duplicates' => false]; if (file_exists($paths['store'])) { $legacySnap = nst_device_wallet_read_rows($paths['store']); if ($legacySnap['duplicates']) throw new RuntimeException('store_invalid'); } $locSnap = nst_device_wallet_read_locators($paths['locators']); if ($locSnap['duplicates']) throw new RuntimeException('store_invalid'); $legacyRows = $legacySnap['rows']; $locRows = $locSnap['rows']; $legacyMatch = null; foreach ($legacyRows as $index => $row) { if (hash_equals($binding, $row['binding'])) { $legacyMatch = $index; break; } } $locMatch = null; foreach ($locRows as $index => $row) { if (hash_equals($binding, $row['binding'])) { $locMatch = $index; break; } } $action = (string)$request['action']; if ($action === 'open') { if ($locMatch !== null) { $row = $locRows[$locMatch]; $meta = nst_wallet_meta_load($row['addr']); $legacyExposed = is_array($meta) && (($meta['migrated_from'] ?? '') === 'legacy-seed'); return [ 'status' => 'restored', 'addr' => $row['addr'], 'sign_fp' => $row['sign_fp'], 'revision' => $row['revision'], 'secret_present_on_server' => false, 'legacy_server_exposed' => $legacyExposed, 'needs_local_seed' => false, ]; } if ($legacyMatch !== null) { $seed = (string)$legacyRows[$legacyMatch]['seed']; $addr = addr_from_seed($seed); return [ 'status' => 'legacy_pending', 'addr' => $addr, 'sign_fp' => null, 'revision' => (string)$legacyRows[$legacyMatch]['ts'], 'secret_present_on_server' => true, 'legacy_server_exposed' => true, 'needs_local_seed' => true, ]; } // No locator and no legacy row: first-use — browser generates locally. $rev = nst_device_wallet_now_ms(); return [ 'status' => 'empty', 'addr' => null, 'sign_fp' => null, 'revision' => $rev, 'secret_present_on_server' => false, 'legacy_server_exposed' => false, 'needs_local_seed' => false, ]; } if ($action === 'clear') { $changedLoc = false; $changedLegacy = false; if ($locMatch !== null) { array_splice($locRows, $locMatch, 1); $changedLoc = true; } if ($legacyMatch !== null) { array_splice($legacyRows, $legacyMatch, 1); $changedLegacy = true; } if ($changedLoc) nst_device_wallet_write_locators($paths['locators'], $locRows); if ($changedLegacy) nst_device_wallet_write_rows($paths['store'], $legacyRows); return [ 'status' => 'cleared', 'addr' => null, 'sign_fp' => null, 'revision' => nst_device_wallet_now_ms(), 'secret_present_on_server' => false, 'legacy_server_exposed' => false, 'needs_local_seed' => false, ]; } if ($action === 'bind') { if (!nst_ecdsa_openssl_ready()) throw new RuntimeException('ecdsa_unavailable'); $addr = (string)$request['addr']; $fp = (string)$request['sign_fp']; $pubHex = (string)$request['pubkey']; $sig = (string)$request['ecdsa_sig']; $nonce = (string)$request['nonce']; $ts = (string)$request['ts']; $spki = @hex2bin($pubHex); if ($spki === false || strlen($spki) < 50) throw new RuntimeException('bad_bind'); $expectFp = nst_wallet_sign_fp_from_spki_bin($spki); if ($expectFp === '' || !hash_equals($expectFp, $fp)) throw new RuntimeException('bad_bind'); $empire = nst_empire_fp_public(); $msg = 'NSU|DEVICE-BIND|2|empire=' . $empire . '|addr=' . $addr . '|sign_fp=' . $fp . '|nonce=' . $nonce . '|ts=' . $ts; if (!nst_ecdsa_verify_p256($spki, $msg, $sig)) throw new RuntimeException('bad_bind_sig'); $reg = nst_load_registered_spki($addr); if ($reg === null) { // Allow bind only when SPKI already registered (fresh register_pubkey first). throw new RuntimeException('pubkey_required'); } if (!hash_equals($reg, $spki)) throw new RuntimeException('pubkey_mismatch'); $revBase = '0'; if ($locMatch !== null) $revBase = (string)$locRows[$locMatch]['revision']; elseif ($legacyMatch !== null) $revBase = (string)$legacyRows[$legacyMatch]['ts']; $rev = nst_device_wallet_now_ms($revBase); $row = [ 'binding' => $binding, 'addr' => $addr, 'sign_fp' => $fp, 'revision' => $rev, ]; if ($locMatch === null) { if (count($locRows) >= NST_DEVICE_WALLET_ROWS_MAX) throw new RuntimeException('store_full'); $locRows[] = $row; } else { // Switching locator must not touch other bindings' economic identity. $locRows[$locMatch] = $row; } nst_device_wallet_write_locators($paths['locators'], $locRows); // Strip legacy seed row for this binding after successful public bind. if ($legacyMatch !== null) { array_splice($legacyRows, $legacyMatch, 1); nst_device_wallet_write_rows($paths['store'], $legacyRows); } $meta = nst_wallet_meta_load($addr) ?: []; if (empty($meta['migrated_from'])) { $meta['migrated_from'] = 'fresh-v2'; $meta['sign_spki'] = $pubHex; $meta['sign_fp'] = $fp; $meta['registered_at'] = time(); nst_wallet_meta_save($addr, $meta); } return [ 'status' => 'bound', 'addr' => $addr, 'sign_fp' => $fp, 'revision' => $rev, 'secret_present_on_server' => false, 'legacy_server_exposed' => (($meta['migrated_from'] ?? '') === 'legacy-seed'), 'needs_local_seed' => false, ]; } throw new RuntimeException('bad_action'); } finally { @flock($lock, LOCK_UN); @fclose($lock); } } /** * A browser that carries a device cookie must spend only from that cookie's * currently committed row. The assignment lock remains held through request * shutdown, so a rebind cannot land between this check and the money commit. * Cookie-free API clients keep the portable seed API; the page itself never does. */ function nst_device_wallet_guard_actor(array $actor): void { $values = nst_device_wallet_cookie_values(); if ($values === []) return; $revision = trim((string)($_SERVER['HTTP_X_NSU_DEVICE_REVISION'] ?? '')); if (count($values) !== 1 || !preg_match('/\A[0-9a-f]{64}\z/D', $values[0]) || !preg_match('/\A[1-9][0-9]{12,15}\z/D', $revision)) { api_out(['ok' => false, 'err' => 'Device wallet changed or is unavailable — restore it and retry'], 409); } try { $paths = nst_device_wallet_paths(); $lock = nst_device_wallet_open_lock($paths['lock']); if (!@flock($lock, LOCK_EX)) throw new RuntimeException('lock_unavailable'); nst_device_wallet_cleanup_temps($paths['dir']); if (!file_exists($paths['secret'])) { throw new RuntimeException('store_invalid'); } $serverSecret = nst_device_wallet_load_secret($paths['secret']); $lookupKey = hash_hmac('sha256', 'nosignup.trade.device-wallet.lookup-key.v1', $serverSecret, true); $tokenBytes = @hex2bin($values[0]); if (!is_string($tokenBytes) || strlen($tokenBytes) !== 32) throw new RuntimeException('request_invalid'); $binding = hash_hmac('sha256', "binding\0" . $tokenBytes, $lookupKey); $currentAddr = ''; $currentRev = ''; $locSnap = nst_device_wallet_read_locators($paths['locators']); if ($locSnap['duplicates']) throw new RuntimeException('store_invalid'); $locMatches = array_values(array_filter($locSnap['rows'], static fn(array $row): bool => hash_equals($binding, (string)$row['binding']))); if (count($locMatches) === 1) { $currentAddr = (string)$locMatches[0]['addr']; $currentRev = (string)$locMatches[0]['revision']; } elseif (file_exists($paths['store'])) { $snapshot = nst_device_wallet_read_rows($paths['store']); if ($snapshot['duplicates']) throw new RuntimeException('store_invalid'); $matches = array_values(array_filter($snapshot['rows'], static fn(array $row): bool => hash_equals($binding, (string)$row['binding']))); if (count($matches) !== 1) throw new RuntimeException('store_invalid'); $current = $matches[0]; $currentAddr = addr_from_seed((string)$current['seed']); $currentRev = (string)$current['ts']; } else { throw new RuntimeException('store_invalid'); } if (!hash_equals($currentRev, $revision) || !hash_equals($currentAddr, (string)($actor['addr'] ?? ''))) { throw new RuntimeException('stale_actor'); } // Seed on wire is rejected for normal ops; if present under legacy path, still must match addr. if (isset($actor['seed']) && is_string($actor['seed']) && $actor['seed'] !== '') { if (!hash_equals(addr_from_seed(norm_seed($actor['seed'])), $currentAddr)) { throw new RuntimeException('stale_actor'); } } $GLOBALS['NST_DEVICE_WALLET_ACTION_LOCK'] = $lock; if (empty($GLOBALS['NST_DEVICE_WALLET_ACTION_UNLOCK_REGISTERED'])) { $GLOBALS['NST_DEVICE_WALLET_ACTION_UNLOCK_REGISTERED'] = true; register_shutdown_function(static function (): void { $held = $GLOBALS['NST_DEVICE_WALLET_ACTION_LOCK'] ?? null; if (is_resource($held)) { @flock($held, LOCK_UN); @fclose($held); } $GLOBALS['NST_DEVICE_WALLET_ACTION_LOCK'] = null; }); } } catch (Throwable $e) { if (isset($lock) && is_resource($lock)) { @flock($lock, LOCK_UN); @fclose($lock); } api_out(['ok' => false, 'err' => 'Device wallet changed or is unavailable — restore it and retry'], 409); } } function nst_device_wallet_handle(): void { nst_device_wallet_request_gate(); $request = nst_device_wallet_validate_request(nst_device_wallet_read_json_body()); $peekAddr = preg_replace('/[^a-f0-9]/', '', strtolower((string)($request['addr'] ?? ''))) ?? ''; if (strlen($peekAddr) !== 64) $peekAddr = ''; // An initial cookie challenge cannot complete an operation and consumes no slot. $token = nst_device_wallet_cookie_token(); nst_tx_gate_enforce($peekAddr, 'device-wallet-' . (string)$request['action']); try { $result = nst_device_wallet_store($request, $token); // Refresh the SAME accepted token, including old epoch-capped cookies. // A response-header failure must not report a committed bind as failed. nst_device_wallet_set_cookie($token); $token = ''; } catch (Throwable $e) { $code = $e instanceof RuntimeException ? $e->getMessage() : ''; if ($code === 'store_full') { nst_device_wallet_error('device_wallet_store_full', 503); } if ($code === 'store_invalid' || $code === 'secret_invalid') { nst_device_wallet_error('device_wallet_store_invalid', 503); } if ($code === 'bad_bind' || $code === 'bad_bind_sig' || $code === 'pubkey_mismatch' || $code === 'pubkey_required') { nst_device_wallet_error($code, 400); } if ($code === 'ecdsa_unavailable') { nst_device_wallet_error('ecdsa_unavailable', 503); } nst_device_wallet_error('device_wallet_unavailable', 503); } $addr = isset($result['addr']) && is_string($result['addr']) ? $result['addr'] : ''; $ticket = ''; try { if ($addr !== '' && preg_match('/\A[0-9a-f]{64}\z/D', $addr)) { $ticket = nst_ticket_ensure_for_addr($addr, (string)($request['ticket'] ?? '')); if ($ticket !== '') { nst_id_presence_touch($ticket); nst_id_presence_gc_lazy(); } } elseif (!empty($request['ticket'])) { $ticket = nst_ticket_norm((string)$request['ticket']); } } catch (Throwable $e) { nst_device_wallet_error('device_wallet_unavailable', 503); } // OPEN/BIND never return visitor seed (Release B). nst_device_wallet_out([ 'ok' => true, 'action' => $request['action'], 'assignment' => $result['status'], 'revision' => $result['revision'], 'addr' => ($addr !== '' ? $addr : null), 'sign_fp' => $result['sign_fp'] ?? null, 'ticket' => $ticket, 'secret_present_on_server' => !empty($result['secret_present_on_server']), 'legacy_server_exposed' => !empty($result['legacy_server_exposed']), 'needs_local_seed' => !empty($result['needs_local_seed']), ]); } /* -------- Empire setup console (filled gate · pack law) -------- */ function nst_empire_setup_path(): string { return nst_data_file('empire_setup.json'); } /** @return array{filled:bool,version:int,filled_ts?:int,ladder_posted?:int,note?:string} */ function nst_empire_setup_state(): array { $p = nst_empire_setup_path(); if (!is_file($p)) { return ['filled' => false, 'version' => 1]; } $j = json_decode((string)@file_get_contents($p), true); if (!is_array($j)) { return ['filled' => false, 'version' => 1]; } return [ 'filled' => !empty($j['filled']), 'version' => (int)($j['version'] ?? 1), 'filled_ts' => isset($j['filled_ts']) ? (int)$j['filled_ts'] : null, 'ladder_posted' => isset($j['ladder_posted']) ? (int)$j['ladder_posted'] : null, 'note' => isset($j['note']) ? (string)$j['note'] : null, ]; } function nst_empire_setup_is_filled(): bool { return !empty(nst_empire_setup_state()['filled']); } function nst_empire_setup_write(array $state): bool { global $DATA; if (!is_dir($DATA) && !@mkdir($DATA, 0755, true)) { return false; } $state['version'] = 1; $raw = json_encode($state, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); if ($raw === false) { return false; } return @file_put_contents(nst_empire_setup_path(), $raw . "\n", LOCK_EX) !== false; } /** Count open book kinds for setup status (no secrets). */ function nst_empire_setup_book_census(): array { ensure_genesis(); $orders = open_orders(read_chain()); $ladder = 0; $half = 0; $boot = 0; $fillableSell = 0; foreach ($orders as $o) { $k = (string)($o['kind'] ?? ''); $side = (string)($o['side'] ?? ''); if ($k === 'treasury_ladder') { $ladder++; } if ($k === 'treasury_half_bid') { $half++; } if ($k === 'bootstrap_spread') { $boot++; } if ($side === 'sell' && $k !== 'bootstrap_spread') { $fillableSell++; } } return [ 'treasury_ladder' => $ladder, 'treasury_half_bid' => $half, 'bootstrap_spread' => $boot, 'fillable_sell' => $fillableSell, ]; } /** * Visitor face gate: until operator marks empire setup filled, redirect to setup console. * Exempt: setup console, controlpanel, src/download, any ?api= (agents / selfhash / money matrix). */ function nst_empire_setup_maybe_redirect(): void { if (nst_empire_setup_is_filled()) { return; } if (isset($_GET['empire_setup']) || isset($_GET['setup'])) { return; } if (isset($_GET['controlpanel'])) { return; } if (isset($_GET['src']) || isset($_GET['download'])) { return; } $api = (string)($_GET['api'] ?? $_POST['api'] ?? ''); if ($api !== '') { return; } $uri = (string)($_SERVER['REQUEST_URI'] ?? ''); if (preg_match('#/(controlpanel|setup)/?(\?|$)#', $uri)) { return; } header('Cache-Control: no-store'); header('Location: /?empire_setup=1', true, 302); exit; } function nst_render_empire_setup_console(): void { ensure_genesis(); $st = nst_empire_setup_state(); $book = nst_empire_setup_book_census(); $esc = static function (string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); }; $filled = !empty($st['filled']); header('Content-Type: text/html; charset=UTF-8'); header('X-Content-Type-Options: nosniff'); header('Cache-Control: no-store'); echo ''; echo ''; echo 'Empire setup · nosignup
'; echo '

DNA · EMPIRE SETUP · trade host

'; echo '

Nosignup empire setup

'; if ($filled) { echo '
Filled. Public product faces are open. Visitors are no longer forced here.
'; } else { echo '
Not filled. Visitors hitting product faces on this host are redirected here until KING marks setup complete. Sister crops should redirect to ' . $esc(NST_EMPIRE_SETUP_PUBLIC_URL) . '.
'; } echo '
Free NSU faucet stays 410 retired. Buy path needs real treasury_ladder asks (not bootstrap discovery quotes).
'; echo '

Status

'; echo '
Setup filled' . ($filled ? 'yes' : 'no') . '
'; echo '
Treasury ladder asks' . (int)$book['treasury_ladder'] . '
'; echo '
Half-bids' . (int)$book['treasury_half_bid'] . '
'; echo '
Fillable sells (non-bootstrap)' . (int)$book['fillable_sell'] . '
'; echo '
Bootstrap quotes' . (int)$book['bootstrap_spread'] . '
'; echo '
Version' . $esc(NST_VERSION) . '
'; echo '

Operator steps

    '; echo '
  1. Paste KING faucet seed below (treasury mint authority — not visitor wallet).
  2. '; echo '
  3. Seed treasury ask ladder — posts real fillable SELLs + half-bids (idempotent).
  4. '; echo '
  5. Confirm fillable sells > 0 above.
  6. '; echo '
  7. Mark empire filled — lifts visitor redirect on this host; sisters use public status / their gate.
  8. '; echo '
'; echo '

KING seed

'; echo ''; echo '

Seed stays in this browser only for the button actions. Never paste into agent chat / STACK / skill files.

'; echo ''; echo ''; echo '
'; echo ''; echo ''; echo '
'; echo ''; echo '

'; echo 'Try product face (redirects here until filled)

'; echo '
'; exit; } /* -------- Public source (?src=1 / ?download=1) - no auth, before APIs -------- */ if (isset($_GET['src']) || isset($_GET['download']) || (isset($_GET['api']) && (string)$_GET['api'] === 'src')) { $raw = (string)file_get_contents(__FILE__); header('Content-Type: text/plain; charset=UTF-8'); header('X-Content-Type-Options: nosniff'); header('Cache-Control: no-store'); header('X-NS-Sha256: ' . hash('sha256', $raw)); /* P1-TRADE-SRC-INLINE RR231: ?src=1 inline like sisters; ?download=1 saves file */ if (isset($_GET['download'])) { header('Content-Disposition: attachment; filename="index.php"'); } header('Content-Length: ' . (string)strlen($raw)); echo $raw; exit; } /* NSU_SHARED_WALLET_V1: Trade alone settles; sister faces send public proofs. * This block is inserted before generic API bootstrap. Summary/receipt are * read-only and never initialize a treasury, roll an epoch or repair a journal. */ function nst_shared_wallet_json(array $payload, int $status = 200): void { http_response_code($status); header('Content-Type: application/json; charset=UTF-8'); header('Cache-Control: private, no-store, max-age=0'); header('X-Content-Type-Options: nosniff'); echo j($payload); exit; } function nst_shared_wallet_gate(string $api): void { $origin = (string)($_SERVER['HTTP_ORIGIN'] ?? ''); $read = in_array($api,['wallet_summary','wallet_receipt'],true); $trusted = preg_match('~\Ahttps://nosignup\.(com|org|net|trade|chat|work|market|date|fun|info)\z~D', $origin) === 1; $local = PHP_SAPI === 'cli-server' && preg_match('~\Ahttp://(?:127\.0\.0\.1|localhost)(?::[1-9][0-9]{0,4})?\z~D', $origin) === 1; // Public reads also work for same-origin GETs, which browsers send without Origin. // Missing Origin never authorizes a cross-origin browser read or a write. if (!$trusted && !$local && !($read && $origin === '' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'GET')) nst_shared_wallet_json(['ok'=>false,'err'=>'Wallet origin is not supported'],403); if ($origin !== '') header('Access-Control-Allow-Origin: '.$origin); header('Vary: Origin'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, X-NSU-Intent'); $method = (string)($_SERVER['REQUEST_METHOD'] ?? ''); if ($method === 'OPTIONS') {http_response_code(204);exit;} $expected = in_array($api,['wallet_summary','wallet_receipt'],true) ? 'GET' : 'POST'; if ($method !== $expected) nst_shared_wallet_json(['ok'=>false,'err'=>$expected.' only'],405); if ($method === 'POST') { if ((string)($_SERVER['HTTP_X_NSU_INTENT'] ?? '') !== 'shared-wallet-v1') nst_shared_wallet_json(['ok'=>false,'err'=>'Wallet request intent required'],403); if (strtolower(trim(explode(';',(string)($_SERVER['CONTENT_TYPE'] ?? ''))[0])) !== 'application/x-www-form-urlencoded') nst_shared_wallet_json(['ok'=>false,'err'=>'Form payload required'],415); if ((int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 8192) nst_shared_wallet_json(['ok'=>false,'err'=>'Wallet request too large'],413); $body = @file_get_contents('php://input', false, null, 0, 8193); if (!is_string($body) || strlen($body)>8192) nst_shared_wallet_json(['ok'=>false,'err'=>'Wallet request too large'],413); $allowed = $api === 'wallet_register' ? ['addr','pubkey','nonce','ts','ecdsa_sig'] : ['from','to','amount','memo','nonce','ts','pubkey','ecdsa_sig']; foreach ($_POST as $key=>$value) { if (!in_array($key,$allowed,true) || !is_string($value)) nst_shared_wallet_json(['ok'=>false,'err'=>'Unsupported wallet field'],400); } if (strlen(http_build_query($_POST, '', '&', PHP_QUERY_RFC3986)) > 8192) nst_shared_wallet_json(['ok'=>false,'err'=>'Wallet request too large'],413); $patterns = ['addr'=>'[a-f0-9]{64}','from'=>'[a-f0-9]{64}','to'=>'(?:[a-f0-9]{64}|[1-9][0-9]{0,17})','nonce'=>'[a-f0-9]{32}','ts'=>'[1-9][0-9]{9}','pubkey'=>'[a-f0-9]{182}','ecdsa_sig'=>'[a-f0-9]{128}','amount'=>'(?:0|[1-9][0-9]{0,8})(?:\.[0-9]{1,6})?']; foreach ($allowed as $key) { if ($key==='memo') continue; if (!isset($_POST[$key]) || !preg_match('/\A'.$patterns[$key].'\z/D',$_POST[$key])) nst_shared_wallet_json(['ok'=>false,'err'=>'Invalid wallet '.$key],400); } $memo=(string)($_POST['memo'] ?? ''); if (strlen($memo)>120 || trim($memo)!==$memo || preg_match('/[\x00-\x1f\x7f|]/',$memo) || !preg_match('//u',$memo)) nst_shared_wallet_json(['ok'=>false,'err'=>'Invalid wallet memo'],400); if ($api==='wallet_send' && (($micros=parse_amt($_POST['amount']))===null || dec_cmp($micros,'1')<0 || dec_cmp($micros,NST_MAX_SUPPLY)>0)) nst_shared_wallet_json(['ok'=>false,'err'=>'Invalid wallet amount'],400); if ($api === 'wallet_send' && preg_match('/^rent:/i',trim((string)($_POST['memo'] ?? '')))) nst_shared_wallet_json(['ok'=>false,'err'=>'Use the explicit rent purchase flow'],400); } } function nst_shared_wallet_chain(): array { global $CHAIN, $CHAIN_TXN; if (!is_file($CHAIN) || is_link($CHAIN)) throw new RuntimeException('wallet_unavailable'); $fh = @fopen($CHAIN,'rb'); if (!is_resource($fh)) throw new RuntimeException('wallet_unavailable'); if (!@flock($fh, LOCK_SH | LOCK_NB)) {fclose($fh);throw new RuntimeException('wallet_busy');} try { if (file_exists($CHAIN_TXN) || is_link($CHAIN_TXN)) throw new RuntimeException('wallet_pending_commit'); $rows=nst_chain_read_locked_handle($fh); $GLOBALS['NST_SHARED_SNAPSHOT_TS']=time(); return $rows; } finally {@flock($fh,LOCK_UN);fclose($fh);} } function nst_shared_wallet_empire(): string { $mf=nsu_mirror_manifest_state(); if (is_array($mf) && preg_match('/\A[a-f0-9]{64}\z/D',(string)($mf['fingerprint'] ?? ''))) return (string)$mf['fingerprint']; // Same policy payload as nsu_policy_hash(), with no cache creation on GET. return hash('sha256',j(['v'=>1,'constants'=>nsu_policy_constants(),'rules'=>nsu_policy_sources()])); } function nst_shared_wallet_preauth(string $api): array { try { $rows=nst_shared_wallet_chain(); $genesis=null; foreach ($rows as $row) if (($row['type'] ?? '')==='genesis') {$genesis=$row;break;} if ($genesis===null) throw new RuntimeException('not_initialized'); // Inspect existing authority, never initialize or repair it on this route. $saved=is_file($GLOBALS['TFILE']) ? norm_seed((string)@file_get_contents($GLOBALS['TFILE'])) : ''; if ($saved==='' || !hash_equals((string)($genesis['treasury_addr'] ?? ''),addr_from_seed($saved))) throw new RuntimeException('identity_unavailable'); if ((int)($genesis['ver'] ?? 0)>=2 && !empty($genesis['panel_addr'])) { $panel=is_file($GLOBALS['SITE_SEED_FILE']) ? norm_seed((string)@file_get_contents($GLOBALS['SITE_SEED_FILE'])) : ''; if ($panel==='' || !hash_equals((string)$genesis['panel_addr'],addr_from_seed($panel))) throw new RuntimeException('identity_unavailable'); } } catch (Throwable $e) {nst_shared_wallet_json(['ok'=>false,'err'=>'Wallet temporarily unavailable; try again'],503);} if (abs((int)$_POST['ts']-time())>600) nst_shared_wallet_json(['ok'=>false,'err'=>'Clock skew; refresh and try again'],400); $spki=hex2bin($_POST['pubkey']); if ($api==='wallet_register') { $body=nst_wallet_register_msg_v2(nst_shared_wallet_empire(),$_POST['addr'],$_POST['pubkey'],$_POST['nonce'],$_POST['ts']); } else { $addr=$_POST['from']; $p=$GLOBALS['DATA'].DIRECTORY_SEPARATOR.'keys'.DIRECTORY_SEPARATOR.nst_addr_shard2($addr).DIRECTORY_SEPARATOR.'pubkey_'.$addr.'.spki'; if (!is_file($p)) $p=$GLOBALS['DATA'].DIRECTORY_SEPARATOR.'pubkey_'.$addr.'.spki'; $registered=is_file($p) ? trim((string)@file_get_contents($p)) : ''; if (!hash_equals($_POST['pubkey'],strtolower($registered))) nst_shared_wallet_json(['ok'=>false,'err'=>'Wallet signing key is not registered'],403); $to=strlen($_POST['to'])===64 ? $_POST['to'] : (string)(nst_ticket_lookup_addr($_POST['to']) ?? ''); if ($to==='') nst_shared_wallet_json(['ok'=>false,'err'=>'Wallet number is not linked'],404); $body=implode('|',['transfer','1',$addr,$to,parse_amt($_POST['amount']),$_POST['nonce'],$_POST['ts'],(string)($_POST['memo'] ?? '')]); } if (!nst_ecdsa_verify_p256($spki,$body,$_POST['ecdsa_sig'])) nst_shared_wallet_json(['ok'=>false,'err'=>'Invalid wallet signature'],400); return $rows; } function nst_shared_wallet_public_tx(array $row): array { $out=[]; foreach (['type','ver','from','to','amount','nonce','ts','memo'] as $k) if (array_key_exists($k,$row)) $out[$k]=$row[$k]; return $out; } function nst_shared_wallet_read(string $api): void { try { $raw = $_GET['addr'] ?? ''; if (!is_string($raw) || !preg_match('/\A(?:[a-f0-9]{64}|[1-9][0-9]{0,17})\z/D',$raw)) nst_shared_wallet_json(['ok'=>false,'err'=>'Enter a wallet number or address'],400); $addr = strlen($raw)===64 ? $raw : (string)(nst_ticket_lookup_addr($raw) ?? ''); if ($addr==='') nst_shared_wallet_json(['ok'=>false,'err'=>'Wallet number is not linked'],404); $nonce = $_GET['nonce'] ?? ''; if ($api==='wallet_receipt' && (!is_string($nonce) || !preg_match('/\A[a-f0-9]{32}\z/D',$nonce))) nst_shared_wallet_json(['ok'=>false,'err'=>'Invalid transaction reference'],400); $rows=nst_shared_wallet_chain(); if (!$rows) nst_shared_wallet_json(['ok'=>false,'err'=>'Trade is not initialized'],503); $history=[]; foreach (array_reverse($rows) as $row) { if (($row['type'] ?? '')!=='transfer') continue; if ($api==='wallet_receipt') { if (($row['from'] ?? '')===$addr && ($row['nonce'] ?? '')===$nonce) nst_shared_wallet_json(['ok'=>true,'schema'=>'nsu-wallet-receipt-v1','found'=>true,'tx'=>nst_shared_wallet_public_tx($row)]); } elseif (count($history)<20 && (($row['from'] ?? '')===$addr || ($row['to'] ?? '')===$addr)) $history[]=nst_shared_wallet_public_tx($row); } if ($api==='wallet_receipt') { $ts=$_GET['ts'] ?? ''; if (!is_string($ts) || ($ts!=='' && !preg_match('/\A[1-9][0-9]{9}\z/D',$ts))) nst_shared_wallet_json(['ok'=>false,'err'=>'Invalid payment timestamp'],400); $expired=$ts!=='' && (int)$GLOBALS['NST_SHARED_SNAPSHOT_TS']>(int)$ts+600; nst_shared_wallet_json(['ok'=>true,'schema'=>'nsu-wallet-receipt-v1','found'=>false,'state'=>$expired?'expired_unseen':'not_seen','from'=>$addr,'nonce'=>$nonce,'ts'=>$ts,'server_ts'=>$GLOBALS['NST_SHARED_SNAPSHOT_TS'],'note'=>$expired?'This shared-wallet request expired without a payment.':'Not seen is not permission to send a second payment.']); } $balance=balances($rows); nst_shared_wallet_json(['ok'=>true,'schema'=>'nsu-wallet-summary-v1','addr'=>$addr,'ticket'=>nst_ticket_lookup_ticket($addr),'balance'=>(string)($balance[$addr] ?? '0'),'reserved'=>(string)reserved_nsu($rows,$addr),'available'=>(string)available_nsu($balance,$rows,$addr),'history'=>$history,'empire'=>nst_shared_wallet_empire(),'server_ts'=>time()]); } catch (Throwable $e) { nst_shared_wallet_json(['ok'=>false,'err'=>'Wallet temporarily unavailable; try again'],503); } } function nst_shared_wallet_filter_response(array $payload): array { $allowed=['ok','err','too_fast','addr','pubkey_registered','sign_fp','sig_mode','legacy_migrate_required','outcome','from','nonce','ticket']; $out=['schema'=>'nsu-wallet-action-v1']; foreach ($allowed as $key) if (array_key_exists($key,$payload)) $out[$key]=$payload[$key]; if (isset($out['err']) && str_contains((string)$out['err'],'nonce replay')) $out['err']='That payment reference is already recorded. Check its receipt; do not send it again.'; if (isset($payload['tx']) && is_array($payload['tx'])) $out['tx']=nst_shared_wallet_public_tx($payload['tx']); return $out; } $nstSharedRequested=(string)($_GET['api'] ?? ''); if (in_array($nstSharedRequested,['wallet_summary','wallet_receipt','wallet_register','wallet_send'],true)) { nst_shared_wallet_gate($nstSharedRequested); if (in_array($nstSharedRequested,['wallet_summary','wallet_receipt'],true)) nst_shared_wallet_read($nstSharedRequested); $GLOBALS['NST_SHARED_WALLET_REQUEST']=true; $GLOBALS['NST_SHARED_WALLET_CHAIN']=nst_shared_wallet_preauth($nstSharedRequested); if ($nstSharedRequested==='wallet_register') { $a=$_POST['addr']; $p=$GLOBALS['DATA'].DIRECTORY_SEPARATOR.'keys'.DIRECTORY_SEPARATOR.nst_addr_shard2($a).DIRECTORY_SEPARATOR.'pubkey_'.$a.'.spki'; if (!is_file($p)) $p=$GLOBALS['DATA'].DIRECTORY_SEPARATOR.'pubkey_'.$a.'.spki'; $registered=is_file($p) ? trim((string)@file_get_contents($p)) : ''; $ticket=nst_ticket_lookup_ticket($a); // Verified proof + identical existing key + existing receive number: no write. if ($registered!=='' && hash_equals($_POST['pubkey'],strtolower($registered)) && is_string($ticket) && preg_match('/\A[1-9][0-9]{0,17}\z/D',$ticket)) { nst_shared_wallet_json(['ok'=>true,'schema'=>'nsu-wallet-action-v1','addr'=>$a,'pubkey_registered'=>true,'sign_fp'=>nst_wallet_sign_fp_from_spki_hex($_POST['pubkey']),'sig_mode'=>'ecdsa_p256_v2','ticket'=>$ticket]); } } $_GET['api']=$nstSharedRequested==='wallet_register' ? 'register_pubkey' : 'transfer'; } /* END NSU_SHARED_WALLET_V1 */ /* Device binding must remain before generic API genesis, chain replay, and economy hooks. */ if (isset($_GET['api']) && (string)$_GET['api'] === 'device_wallet') { nst_device_wallet_handle(); } if (isset($_GET['api']) && (string)$_GET['api'] === 'id') { nst_id_mill_handle(); } if (isset($_GET['api']) && (string)$_GET['api'] === 'ticket') { nst_ticket_resolve_handle(); } if (isset($_GET['api']) && (string)$_GET['api'] === 'vanity_quote') { nst_vanity_quote_handle(); } if (isset($_GET['api']) && (string)$_GET['api'] === 'vanity_apply') { nst_vanity_apply_handle(); } if (isset($_GET['api']) && (string)$_GET['api'] === 'id_gossip') { nst_id_gossip_handle(); } if (isset($_GET['api']) && (string)$_GET['api'] === 'ad_share_quote') { nst_ad_share_quote_handle(); } // WAVEB2-TR24-AD-SLOT-STATE: read-only public producer (no money, before ensure_genesis). if (isset($_GET['api']) && (string)$_GET['api'] === 'ad_slot_state') { if (function_exists('nst_ad_slot_state_handle')) { nst_ad_slot_state_handle(); } nst_ticket_json(['ok' => false, 'err' => 'ad_slot_state unavailable', 'schema' => 'nsu-ad-batch-v1'], 503); } // COMMUNITY-AD-MODERATION-V1: public POST report (site+slot+epoch only; no money). if (isset($_GET['api']) && (string)$_GET['api'] === 'ad_report') { if (function_exists('nst_ad_report_handle')) { nst_ad_report_handle(); } nst_ticket_json(['ok' => false, 'err' => 'ad_report unavailable', 'schema' => 'nsu-ad-report-v1'], 503); } // WAVEE-TR30-CAPABILITY-API: nonsecret protocol negotiation (read-only, before genesis). if (isset($_GET['api']) && (string)$_GET['api'] === 'econ_capability') { if (function_exists('nst_econ_capability_handle')) { nst_econ_capability_handle(); } nst_ticket_json(['ok' => false, 'err' => 'econ_capability unavailable', 'schema' => 'nsu-econ-capability-v1'], 503); } // WAVEE-TR33-HEALTH-API: public availability only. if (isset($_GET['api']) && (string)$_GET['api'] === 'econ_health') { if (function_exists('nst_fin_obs_health_handle')) { nst_fin_obs_health_handle(); } nst_ticket_json(['ok' => false, 'err' => 'econ_health unavailable', 'schema' => 'nsu-econ-health-v1'], 503); } /* -------- API -------- */ $api = $_GET['api'] ?? $_POST['api'] ?? ''; /* TR-02: while purchases are closed, ad_stake stops before ensure_genesis so an empty data dir gains no chain file. */ if ($api === 'ad_stake' && strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? '')) === 'POST') { $tr02Crop = nst_ad_site_norm((string)($_POST['crop'] ?? $_POST['site'] ?? '')); $tr02Ready = nst_ad_spend_ready($tr02Crop); if (empty($tr02Ready['ok'])) { api_out([ 'ok' => false, 'err' => (string)($tr02Ready['why'] ?? 'Spend path not ready'), 'spend' => false, 'quote' => true, ], 503); } } if ($api === 'audit') { /* PUBLIC AND PAGED, so a stranger can pull the WHOLE ledger and check it * themselves rather than trusting anything this server says about itself. * That is the only defence that survives a King editing his own code. * CORS for the same reason ad_pick has it: public, read-only, no secrets. * Structural corruption still fails; a linkage break remains readable so * the public verifier can name the exact broken row. No genesis boot. */ nst_audit_api_out(read_chain(false)); } if ($api === 'ping' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'GET') { nst_king_ping_api_out(); } if ($api === 'pull' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'GET') { nst_king_pull_api_out(); } if ($api === 'vote' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') { nst_king_vote_api_out(); } if ($api !== '') { if (nst_tx_api_is_gated((string)$api, (string)($_SERVER['REQUEST_METHOD'] ?? 'GET'))) { $nstGateScope = ['register_pubkey' => 'wallet-registration', 'addr' => 'wallet-setup-addr', 'register_enc_pubkey' => 'wallet-setup-enc'][$api] ?? ''; nst_tx_gate_enforce(nst_tx_gate_peek_addr(), $nstGateScope); } if (!empty($GLOBALS['NST_SHARED_WALLET_REQUEST'])) { // Proof verified: skip genesis/epoch/bootstrap repair. Authenticated journal recovery stays in the existing commit helper. $chain = $GLOBALS['NST_SHARED_WALLET_CHAIN']; $bal = balances($chain); $taddr = ''; // Neither registered-key nor transfer handler uses treasury authority. $nstConserveSnap = nst_conservation_boot_check($chain, true); nst_require_writes_unfrozen(); } else { ensure_genesis(); $chain = read_chain(); $bal = balances($chain); $taddr = treasury_addr(); // E2: honor freeze always; full recheck on force=1 or throttle expiry $forceConserve = ((string)($_GET['force'] ?? $_POST['force'] ?? '') === '1'); $nstConserveSnap = nst_conservation_boot_check($chain, $forceConserve); } /* RR293/RR313: public book for tools/sisters — read-only, not mint. */ if ($api === 'book') { $book = nst_public_book($chain); $bootstrapSpreadActive = false; foreach (array_merge($book['bids'] ?? [], $book['asks'] ?? []) as $o) { if (($o['kind'] ?? '') === 'bootstrap_spread') { $bootstrapSpreadActive = true; break; } } api_out([ 'ok' => true, 'schema' => 'nosignup.trade.book.v1', 'version' => NST_VERSION, 'price_policy' => 'market_converge', 'bootstrap_spread_active' => $bootstrapSpreadActive, 'bootstrap_note' => 'bootstrap_spread quotes are discovery only — not fillable', 'market_p_fmt' => $book['mid'] ?? null, 'book' => $book, 'law' => 'Read-only public book · no free NSU · fill via POST ?api=fill signed ecdsa_p256_v2 (seed rejected)', ]); } if ($api === 'state') { $book = nst_public_book($chain); $mid = $book['mid'] ?? null; $ep = epoch_info(); $meta = load_meta(); $pop = (int)($meta['population'] ?? 0); $target = (string)($meta['treasury_target_micros'] ?? ''); if ($target === '' || !dec_ok($target)) { $pi = nst_world_population(); $pop = (int)$pi['pop']; $target = nst_optimum_treasury_micros($pop); } $bootstrapSpreadActive = false; foreach (array_merge($book['bids'] ?? [], $book['asks'] ?? []) as $o) { if (($o['kind'] ?? '') === 'bootstrap_spread') { $bootstrapSpreadActive = true; break; } } $bbPx = nst_buyback_price_micros($chain); /* Reuse the chain rows this handler already loaded: one replay, not seven. */ $nsSupMinted = nst_minted_to_date($chain); $nsSupRemain = nst_supply_remaining($chain); $nsSupEmit = nst_emission_today($chain); api_out([ 'ok' => true, 'version' => NST_VERSION, 'price_policy' => 'market_converge', 'bootstrap_spread_active' => $bootstrapSpreadActive, 'buyback_note' => 'donate buyback arms at market P/2 (not fixed sticker)', 'market_p_fmt' => $mid, 'buyback_px_fmt' => $bbPx !== null ? fmt_amt($bbPx) : null, 'panel_unlock' => 'site_wallet_seed', /* NS_STATUS_API_HONESTY 2026-09-26 */ // CURRENT: ecdsa_p256_v2 on normal mutations; seed off wire. // LEGACY once: legacy_wallet_migrate (12 words → register pubkey). // HISTORICAL (not active): browser_hmac_v1 + ecdsa_p256_v1 dual-path. 'client_sign' => 'ecdsa_p256_v2', 'sig_mode' => 'ecdsa_p256_v2', 'auth_modes' => ['ecdsa_p256_v2'], 'auth_required' => 'ecdsa_p256_v2', 'seed_on_wire' => 'rejected_except_legacy_wallet_migrate', 'seed_off_wire' => 'deterministic ecdsa v2 required for visitor mutations; seed only on legacy_wallet_migrate', 'asymmetric_plan' => 'ecdsa_p256_v2_seed_off_wire_migrate_once_legacy', 'ecdsa_spend' => 'ecdsa_p256_v2_signed_ops_without_seed', 'ecdsa_ops' => ['transfer', 'order', 'cancel', 'fill', 'reputation'], 'wallet_inbox' => 'v0+autowire experimental · address-keyed sealed mail · mail not mint', 'inbox_e2e' => 'client_encrypt_server_ciphertext_only · experimental · not proven e2e', 'inbox_server_notice' => 'server-composed public book facts · encrypted for delivery · NOT private E2E', 'inbox_key_sep' => 'ECDSA_sign≠ECDH_enc (encpub_ vs pubkey_)', // Soft surface: hosts without OpenSSL cannot verify ECDSA (visitor mutations still require ecdsa_p256_v2). 'ecdsa_server' => nst_ecdsa_openssl_ready() ? 'openssl_available' : 'openssl_unavailable', 'ecdsa_verify_ready' => nst_ecdsa_openssl_ready(), /* ONE chain replay feeds all three figures. Calling the helpers * inline read the whole chain seven times per request, which is * O(7N) on a ledger that only grows. */ 'max_supply_fmt' => fmt_amt((string)($meta['soft_max_supply_micros'] ?? NST_MAX_SUPPLY)), /* Supply is public and replayable. Anyone can fetch the chain and * recompute every one of these; nothing here is a stored figure we * ask you to trust. That is the point - a currency whose issuance * cannot be independently checked is a promise, not money. */ 'minted_to_date' => $nsSupMinted, 'minted_to_date_fmt' => fmt_amt($nsSupMinted), 'supply_remaining' => $nsSupRemain, 'supply_remaining_fmt' => fmt_amt($nsSupRemain), 'emission_today' => $nsSupEmit, 'emission_today_fmt' => fmt_amt($nsSupEmit), 'emission_rate_ppm' => NST_EMISSION_DAILY_RATE_PPM, 'emission_split' => [ 'rule' => 'equal share per crop, King tax off the top of each share', 'crop_count' => NST_EMISSION_CROP_COUNT, 'crops_registered' => count(nst_crop_addrs()), 'king_tax_ppm' => NST_KING_TAX_PPM, /* Public, because a silent redirect of 95% of issuance is exactly * the thing an operator should not be able to do quietly. */ 'registry_anchored' => (bool)nst_crop_anchor(), 'registry_file_matches_chain' => !nst_crop_registry_mismatch(), ], 'king_tax_ppm' => NST_KING_TAX_PPM, /* Same money? A label - the proof is replaying a peer chain. */ 'policy' => nsu_policy_hash(), 'policy_badge' => nsu_policy_badge(), 'mint_types' => nst_mint_types(), 'supply_law' => 'King holds the whole 100 million NSU cap at birth. After that nothing is printed. ' . 'Emission stays paused; remaining supply is 0. Public faucet is 410 — buy on the book. ' . 'Utterly fair except the king starts with 100 million. No participant can trigger a mint. ' . 'The yearly treasury re-anchor is RETIRED. admin_pay is a transfer, not a mint.', 'treasury_addr' => $taddr, 'treasury_bal_fmt' => fmt_amt($bal[$taddr] ?? '0'), 'treasury_target_fmt' => fmt_amt($target), 'population' => $pop, 'population_source' => (string)($meta['population_source'] ?? ''), 'optimum_nsu_per_human' => fmt_amt(NST_OPTIMUM_MICROS_PER_HUMAN), // legacy metadata only 'chain_len' => count($chain), 'book' => $book, 'pairs' => $book['pairs'] ?? [], 'recent_fills' => nst_public_recent_fills($chain), // Legacy audit tail remains for old tools; pair/chart discovery must use recent_fills. 'recent' => array_slice($chain, -50), 'epoch' => $ep, 'admin_pass_set' => false, 'early_drip_retired' => true, 'economy' => (function () use ($chain) { $e = load_economy(); $midM = book_mid_micros($chain); $bbM = nst_buyback_price_micros($chain); return [ 'profit_fmt' => fmt_amt((string)($e['profit_micros'] ?? '0')), 'ad_bid_fmt' => fmt_amt((string)($e['ad_bid_micros'] ?? '0')), 'mid_fmt' => $midM !== null ? fmt_amt($midM) : null, 'buyback_px_fmt' => $bbM !== null ? fmt_amt($bbM) : null, 'donate_k' => (string)($e['donate_k'] ?? NST_DONATE_K), 'value_note' => (string)($e['value_note'] ?? ''), 'value_authority' => 'this_server_owner', 'mirrors_role' => 'equal_service_peers', ]; })(), 'conservation' => nst_conservation_public_view($nstConserveSnap), 'writes_frozen' => nst_conservation_is_frozen(), 'self_hash_hint' => 'sha256 of this index.php on disk - compute offline to verify release', ]); } if ($api === 'balance') { $a = nst_resolve_receipt((string)($_REQUEST['addr'] ?? '')); if (strlen($a) !== 64) api_out(['ok' => false, 'err' => 'Need a wallet number or the old 64-character receipt'], 400); $b = $bal[$a] ?? '0'; $res = reserved_nsu($chain, $a); $av = available_nsu($bal, $chain, $a); $reps = rep_scores($chain); // history for addr $hist = []; foreach (array_reverse($chain) as $r) { if (($r['type'] ?? '') === 'transfer' && (($r['from'] ?? '') === $a || ($r['to'] ?? '') === $a)) { $hist[] = $r; if (count($hist) >= 40) break; } if (($r['type'] ?? '') === 'order' && ($r['addr'] ?? '') === $a) { $hist[] = $r; if (count($hist) >= 40) break; } if (($r['type'] ?? '') === 'fill' && (($r['maker'] ?? '') === $a || ($r['taker'] ?? '') === $a)) { $hist[] = $r; if (count($hist) >= 40) break; } if (($r['type'] ?? '') === 'reputation' && (($r['rater'] ?? '') === $a || ($r['rated'] ?? '') === $a)) { $hist[] = $r; if (count($hist) >= 40) break; } if (($r['type'] ?? '') === 'modifier_grant' && ($r['addr'] ?? '') === $a) { $hist[] = $r; if (count($hist) >= 40) break; } } $mods = load_mods(); $mod = $mods[$a] ?? null; api_out([ 'ok' => true, 'addr' => $a, 'balance' => $b, 'balance_fmt' => fmt_amt($b), 'reserved' => $res, 'reserved_fmt' => fmt_amt($res), 'available' => $av, 'available_fmt' => fmt_amt($av), 'rep_score' => $reps[$a] ?? 0, 'history' => $hist, 'has_modifier' => !empty($mod['permanent']), 'modifier' => $mod ? [ 'permanent' => !empty($mod['permanent']), 'daily_fmt' => fmt_amt((string)($mod['daily_micros'] ?? NST_MOD_DAILY_MICROS)), 'granted_ts' => (int)($mod['granted_ts'] ?? 0), 'granted_epoch' => (int)($mod['granted_epoch'] ?? 0), 'last_accrual_ts' => (int)($mod['last_accrual_ts'] ?? 0), ] : null, 'promo_email' => nst_promo_identity_get($a), 'epoch' => epoch_info(), ]); } if ($api === 'addr' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); $actor = nst_actor('addr'); $addr = (string)$actor['addr']; $ts = (int)($_POST['ts'] ?? time()); nst_auth_sig($actor, implode('|', ['addr', '2', $addr, (string)$ts])); $grant = grant_early_modifier($addr); $acc = lazy_accrue($addr); api_out([ 'ok' => true, 'addr' => $addr, 'modifier_grant' => $grant, 'accrual' => $acc, 'promo_email' => nst_promo_identity_get($addr), 'epoch' => epoch_info(), ]); } if ($api === 'accrue' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); $actor = nst_actor('addr'); $addr = (string)$actor['addr']; $ts = (int)($_POST['ts'] ?? time()); nst_auth_sig($actor, implode('|', ['accrue', '2', $addr, (string)$ts])); $grant = grant_early_modifier($addr); $acc = lazy_accrue($addr); $bal = balances(read_chain()); api_out([ 'ok' => true, 'addr' => $addr, 'modifier_grant' => $grant, 'accrual' => $acc, 'balance_fmt' => fmt_amt($bal[$addr] ?? '0'), 'available_fmt' => fmt_amt(available_nsu($bal, read_chain(), $addr)), 'has_modifier' => !empty(load_mods()[$addr]['permanent']), ]); } /** P1-TRADE-INBOX-METHOD RR211 + P1-TRADE-ADDR-ACCRUE-METHOD RR212: * non-POST free write routes must not fall through as unknown api (404). */ $nst_post_only_free = [ 'register_pubkey', 'register_enc_pubkey', 'legacy_wallet_migrate', 'inbox_seal', 'inbox_list', 'inbox_fetch', 'reputation', 'addr', 'accrue', ]; /** P1-TRADE-MONEY-KING-MATRIX GO: money write routes same 405 honesty (not 404 unknown). */ $nst_post_only_money = ['transfer', 'order', 'cancel', 'fill', 'job_admit', 'job_complete', 'job_cancel']; $nst_post_only = array_merge($nst_post_only_free, $nst_post_only_money); if (in_array($api, $nst_post_only, true) && strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? '')) !== 'POST') { api_out(['ok' => false, 'err' => 'POST only'], 405); } // Register deterministic ECDSA P-256 SPKI for addr_v1 (public proof only; no seed stored). if ($api === 'register_pubkey' && $_SERVER['REQUEST_METHOD'] === 'POST') { if (!nst_ecdsa_openssl_ready()) { api_out([ 'ok' => false, 'err' => 'ecdsa verify unavailable (openssl extension required)', 'ecdsa_server' => 'openssl_unavailable', ], 503); } $addr = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['addr'] ?? ''))) ?? ''; $spkiHex = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['pubkey'] ?? '')) ?? ''); $spki = @hex2bin($spkiHex); $nonce = preg_replace('/[^a-zA-Z0-9_-]/', '', (string)($_POST['nonce'] ?? '')) ?? ''; $ts = (int)($_POST['ts'] ?? 0); $proof = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['ecdsa_sig'] ?? '')) ?? ''); if (strlen($addr) !== 64) { api_out(['ok' => false, 'err' => 'Need a 64-char hex wallet address'], 400); } if ($spki === false || strlen($spki) < 50) { api_out(['ok' => false, 'err' => 'Bad public key — check the key and try again'], 400); } if ($nonce === '' || $ts < 1 || abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'Clock skew or missing nonce — refresh and try again'], 400); } $empire = nst_empire_fp_public(); $body = nst_wallet_register_msg_v2($empire, $addr, $spkiHex, $nonce, (string)$ts); if ($proof === '' || !nst_ecdsa_verify_p256($spki, $body, $proof)) { api_out(['ok' => false, 'err' => 'Bad signature for that public key — refresh and try again'], 400); } $existingSpki = nst_load_registered_spki($addr); if ($existingSpki !== null && !hash_equals($existingSpki, $spki)) { api_out([ 'ok' => false, 'err' => 'A different signing key is already registered for this address', 'pubkey_registered' => false, ], 409); } if ($existingSpki === null) { if (nst_addr_has_economic_history($addr)) { api_out([ 'ok' => false, 'err' => 'Legacy funded wallet — use legacy_wallet_migrate with the 12 words once', 'legacy_migrate_required' => true, ], 403); } if (!nst_save_registered_spki($addr, $spki)) { $nstWinner = nst_load_registered_spki($addr); if ($nstWinner !== null && !hash_equals($nstWinner, $spki)) { api_out(['ok' => false, 'err' => 'A different signing key is already registered for this address', 'pubkey_registered' => false], 409); } api_out(['ok' => false, 'err' => 'Could not save to the server — try again in a moment'], 500); } nst_wallet_meta_save($addr, [ 'sign_spki' => $spkiHex, 'sign_fp' => nst_wallet_sign_fp_from_spki_bin($spki), 'enc_spki' => '', 'registered_at' => time(), 'migrated_from' => 'fresh-v2', ]); } $nstSharedTicket = null; if (!empty($GLOBALS['NST_SHARED_WALLET_REQUEST'])) { try { $nstSharedTicket = nst_ticket_ensure_for_addr($addr); } catch (Throwable $e) {api_out(['ok'=>false,'err'=>'Wallet key registered; receive number temporarily unavailable. Try again.'],503);} } api_out(array_merge([ 'ok' => true, 'addr' => $addr, 'pubkey_registered' => true, ], ($nstSharedTicket !== null ? ['ticket'=>$nstSharedTicket] : []), [ 'sign_fp' => nst_wallet_sign_fp_from_spki_bin($spki), 'sig_mode' => 'ecdsa_p256_v2', 'note' => 'Public signing key stored. Seed was not written to the server.', ])); } // Explicit legacy migration only: seed proves old authority, then deterministic v2 signer is bound. if ($api === 'legacy_wallet_migrate' && $_SERVER['REQUEST_METHOD'] === 'POST') { if (!nst_ecdsa_openssl_ready()) { api_out(['ok' => false, 'err' => 'ecdsa verify unavailable (openssl extension required)'], 503); } [$seed, $addr] = require_seed(); $spkiHex = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['pubkey'] ?? '')) ?? ''); $spki = @hex2bin($spkiHex); $nonce = preg_replace('/[^a-zA-Z0-9_-]/', '', (string)($_POST['nonce'] ?? '')) ?? ''; $ts = (int)($_POST['ts'] ?? 0); $proof = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['ecdsa_sig'] ?? '')) ?? ''); if ($spki === false || strlen($spki) < 50 || $nonce === '' || $ts < 1 || abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'bad legacy migrate payload'], 400); } $empire = nst_empire_fp_public(); $body = nst_wallet_register_msg_v2($empire, $addr, $spkiHex, $nonce, (string)$ts); if ($proof === '' || !nst_ecdsa_verify_p256($spki, $body, $proof)) { api_out(['ok' => false, 'err' => 'Bad signature for that public key'], 400); } $existingSpki = nst_load_registered_spki($addr); if ($existingSpki !== null && !hash_equals($existingSpki, $spki)) { api_out(['ok' => false, 'err' => 'A different signing key is already registered for this address'], 409); } if ($existingSpki === null && !nst_save_registered_spki($addr, $spki)) { api_out(['ok' => false, 'err' => 'Could not save to the server — try again in a moment'], 500); } nst_wallet_meta_save($addr, [ 'sign_spki' => $spkiHex, 'sign_fp' => nst_wallet_sign_fp_from_spki_bin($spki), 'enc_spki' => '', 'registered_at' => time(), 'migrated_from' => 'legacy-seed', ]); // Does not mint or remap balances — public key binding only. api_out([ 'ok' => true, 'addr' => $addr, 'pubkey_registered' => true, 'sign_fp' => nst_wallet_sign_fp_from_spki_bin($spki), 'migrated_from' => 'legacy-seed', 'legacy_server_exposed' => true, 'note' => 'LEGACY SERVER-EXPOSED SECRET — move to a newly generated local-only wallet for a fresh root secret. Seed accepted only on this migration route.', 'balance_unchanged' => true, ]); } // --- Wallet Inbox v0: enc-pubkey registry + sealed mail (ciphertext only) --- if ($api === 'register_enc_pubkey' && $_SERVER['REQUEST_METHOD'] === 'POST') { $actor = nst_actor('addr'); $addr = (string)$actor['addr']; $spkiHex = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['enc_pubkey'] ?? '')) ?? ''); $spki = @hex2bin($spkiHex); $nonce = preg_replace('/[^a-zA-Z0-9_-]/', '', (string)($_POST['nonce'] ?? '')) ?? ''; $ts = (int)($_POST['ts'] ?? 0); if ($spki === false || strlen($spki) < 50 || $nonce === '' || $ts < 1 || abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'bad enc pubkey payload'], 400); } $empire = nst_empire_fp_public(); $body = nst_wallet_enc_register_msg_v2($empire, $addr, $spkiHex, $nonce, (string)$ts); nst_auth_sig($actor, $body); if (!nst_save_registered_enc_spki($addr, $spki)) { api_out(['ok' => false, 'err' => 'Could not save to the server — try again in a moment'], 500); } $meta = nst_wallet_meta_load($addr) ?: ['migrated_from' => 'fresh-v2', 'sign_spki' => (string)($actor['spki_hex'] ?? ''), 'sign_fp' => nst_wallet_sign_fp_from_spki_hex((string)($actor['spki_hex'] ?? '')), 'registered_at' => time()]; $meta['enc_spki'] = $spkiHex; nst_wallet_meta_save($addr, $meta); api_out([ 'ok' => true, 'addr' => $addr, 'enc_pubkey_registered' => true, 'key_usage' => 'ECDH_P256_encrypt_only', 'distinct_from' => 'ECDSA_P256_sign (register_pubkey)', 'sig_mode' => 'ecdsa_p256_v2', 'note' => 'Encryption SPKI stored via ECDSA-signed registration. Seed not accepted on this route.', 'mail_not_mint' => true, ]); } // Public fetch of encryption SPKI (needed by sender to seal; not a secret). if ($api === 'inbox_enc_pubkey') { $addr = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_GET['addr'] ?? $_POST['addr'] ?? ''))) ?? ''; if (strlen($addr) !== 64) api_out(['ok' => false, 'err' => 'Need a 64-char hex wallet address'], 400); $spki = nst_load_registered_enc_spki($addr); if ($spki === null) { api_out(['ok' => false, 'err' => 'no enc pubkey registered', 'addr' => $addr], 404); } api_out([ 'ok' => true, 'addr' => $addr, 'enc_pubkey' => bin2hex($spki), 'alg' => NST_INBOX_ALG, 'note' => 'Public encryption key only · experimental wallet inbox v0', ]); } /** * KING seal: accept PRE-ENCRYPTED blob only (ct + eph_pub + iv + sig). * Server NEVER accepts plaintext for storage. require_faucet_auth. * Mail ≠ mint: no chain row, no balance change. */ if ($api === 'inbox_seal' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_faucet_auth(); // Refuse accidental plaintext legs (honesty floor). foreach (['plaintext', 'note', 'body', 'message', 'msg'] as $ban) { if (isset($_POST[$ban]) && trim((string)$_POST[$ban]) !== '') { api_out([ 'ok' => false, 'err' => 'plaintext not accepted — encrypt client-side; server stores ciphertext only', 'honesty' => 'refuse_server_plaintext_seal', ], 400); } } $to = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['to'] ?? ''))) ?? ''; if (strlen($to) !== 64) api_out(['ok' => false, 'err' => 'Bad destination address — need a 64-char hex wallet address'], 400); if (nst_load_registered_enc_spki($to) === null) { api_out(['ok' => false, 'err' => 'recipient has no enc pubkey — they must unlock wallet once'], 400); } $from = strtolower(addr_from_seed(treasury_secret())); $id = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['id'] ?? ''))) ?? ''; if ($id === '') $id = bin2hex(random_bytes(8)); if (strlen($id) < 8 || strlen($id) > 64) api_out(['ok' => false, 'err' => 'bad id'], 400); $ts = (int)($_POST['ts'] ?? time()); if (abs($ts - time()) > 600) api_out(['ok' => false, 'err' => 'Clock skew — refresh the page and try again'], 400); $ttl = (int)($_POST['ttl'] ?? NST_INBOX_TTL_SECS); if ($ttl < 60) $ttl = 60; if ($ttl > NST_INBOX_TTL_SECS * 4) $ttl = NST_INBOX_TTL_SECS * 4; $exp = $ts + $ttl; $eph = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['eph_pub'] ?? '')) ?? ''); $iv = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['iv'] ?? '')) ?? ''); $ct = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['ct'] ?? '')) ?? ''); if (strlen($eph) < 100 || strlen($iv) !== 24 || strlen($ct) < 32) { api_out(['ok' => false, 'err' => 'bad ciphertext fields (eph_pub/iv/ct)'], 400); } if (strlen($ct) > NST_INBOX_MAX_CT_HEX) api_out(['ok' => false, 'err' => 'ct too large'], 400); $blob = [ 'v' => 1, 'id' => $id, 'to' => $to, 'from' => $from, 'ts' => $ts, 'ttl' => $ttl, 'exp' => $exp, 'eph_pub' => $eph, 'iv' => $iv, 'ct' => $ct, 'alg' => NST_INBOX_ALG, 'kind' => 'king_note', 'mail_not_mint' => true, 'honesty' => 'experimental · sealed wallet mail · server ciphertext only · not proven e2e', ]; $signBody = nst_inbox_sign_body($blob); $sig = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['sig'] ?? $_POST['ecdsa_sig'] ?? '')) ?? ''); $sigMode = strtolower(trim((string)($_POST['sig_mode'] ?? ''))); $spkiHex = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['pubkey'] ?? '')) ?? ''); if (nst_sig_is_ecdsa($sigMode) || ($sig !== '' && $spkiHex !== '' && nst_ecdsa_openssl_ready())) { $spki = @hex2bin($spkiHex); if ($spki === false || strlen($spki) < 50) api_out(['ok' => false, 'err' => 'bad sender pubkey'], 400); $reg = nst_load_registered_spki($from); if ($reg === null || !hash_equals($reg, $spki)) { // Allow seal-time register: if KING posts matching seed-proven from, require proof over register body first is separate. // Here: if no registry, accept SPKI only when ECDSA verifies AND from matches treasury (KING seed already checked). if ($reg !== null && !hash_equals($reg, $spki)) { api_out(['ok' => false, 'err' => 'sender pubkey not registered for treasury addr'], 403); } if ($reg === null) { nst_save_registered_spki($from, $spki); } } if ($sig === '' || !nst_ecdsa_verify_p256($spki, $signBody, $sig)) { api_out(['ok' => false, 'err' => 'bad ecdsa seal signature'], 400); } $blob['sig'] = $sig; $blob['sig_mode'] = 'ecdsa_p256_v2'; $blob['sender_spki'] = bin2hex($spki); } else { // Experimental HMAC fallback (seed already on wire for faucet auth) — labeled. $seed = (string)($_POST['seed'] ?? ''); $expect = mac_sign($seed, $signBody); if ($sig !== '' && !hash_equals($expect, $sig)) { api_out(['ok' => false, 'err' => 'bad hmac seal signature'], 400); } if ($sig === '') $sig = $expect; $blob['sig'] = $sig; $blob['sig_mode'] = 'server_mac'; $blob['honesty'] = 'experimental · sealed mail · HMAC seal (seed on wire for KING auth) · ciphertext-only store · not proven e2e'; } $path = nst_inbox_blob_path($to, $id); if (@file_put_contents($path, j($blob), LOCK_EX) === false) { api_out(['ok' => false, 'err' => 'Could not save to the server — try again in a moment'], 500); } api_out([ 'ok' => true, 'id' => $id, 'to' => $to, 'from' => $from, 'exp' => $exp, 'stored' => basename($path), 'mail_not_mint' => true, 'server_plaintext' => false, 'sig_mode' => $blob['sig_mode'], 'honesty' => $blob['honesty'], 'note' => 'Opaque ciphertext stored only · recipient decrypts client-side with seed-derived ECDH key', ]); } // List own sealed blobs (seed or ECDSA actor). if ($api === 'inbox_list' && $_SERVER['REQUEST_METHOD'] === 'POST') { $actor = nst_actor('addr'); $addr = $actor['addr']; // Light auth: list is not secret metadata-only; still require ownership of addr. if (!empty($actor['ecdsa'])) { $body = implode('|', ['inbox_list', '1', $addr, (string)(int)($_POST['ts'] ?? time())]); nst_auth_sig($actor, $body); } $items = nst_inbox_list_for_addr($addr); api_out([ 'ok' => true, 'addr' => $addr, 'count' => count($items), 'items' => $items, 'mail_not_mint' => true, 'honesty' => 'experimental · list returns metadata + ids; ct only via inbox_fetch · not proven e2e', ]); } // Fetch one sealed blob (opaque fields for client decrypt). if ($api === 'inbox_fetch' && $_SERVER['REQUEST_METHOD'] === 'POST') { $actor = nst_actor('addr'); $addr = $actor['addr']; $id = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['id'] ?? ''))) ?? ''; if ($id === '') api_out(['ok' => false, 'err' => 'need id'], 400); if (!empty($actor['ecdsa'])) { $body = implode('|', ['inbox_fetch', '1', $addr, $id, (string)(int)($_POST['ts'] ?? time())]); nst_auth_sig($actor, $body); } $path = nst_inbox_blob_path($addr, $id); if (!is_file($path)) { $path = nst_inbox_blob_path_legacy($addr, $id); } if (!is_file($path)) api_out(['ok' => false, 'err' => 'That was not found — refresh and try again'], 404); $j = json_decode((string)@file_get_contents($path), true); if (!is_array($j) || (string)($j['to'] ?? '') !== $addr) { api_out(['ok' => false, 'err' => 'That was not found — refresh and try again'], 404); } $exp = (int)($j['exp'] ?? 0); if ($exp > 0 && $exp < time()) { @unlink($path); api_out(['ok' => false, 'err' => 'That item expired — refresh and try again'], 410); } api_out([ 'ok' => true, 'blob' => [ 'v' => (int)($j['v'] ?? 1), 'id' => (string)($j['id'] ?? ''), 'to' => (string)($j['to'] ?? ''), 'from' => (string)($j['from'] ?? ''), 'ts' => (int)($j['ts'] ?? 0), 'exp' => $exp, 'eph_pub' => (string)($j['eph_pub'] ?? ''), 'iv' => (string)($j['iv'] ?? ''), 'ct' => (string)($j['ct'] ?? ''), 'sig' => (string)($j['sig'] ?? ''), 'sig_mode' => (string)($j['sig_mode'] ?? ''), 'sender_spki' => (string)($j['sender_spki'] ?? ''), 'alg' => (string)($j['alg'] ?? NST_INBOX_ALG), 'kind' => (string)($j['kind'] ?? 'king_note'), 'honesty' => (string)($j['honesty'] ?? 'experimental'), ], 'mail_not_mint' => true, 'server_plaintext' => false, 'note' => 'Decrypt client-side with seed-derived ECDH key · experimental · not proven e2e', ]); } if ($api === 'moot_gate') api_out(['ok' => true, 'moot' => nst_moot_gate_touch_readonly()]); if ($api === 'job_receipt') { $gid = (string)($_GET['grant_id'] ?? ''); $jid = (string)($_GET['job_id'] ?? ''); if (!preg_match('/\A[a-zA-Z0-9_-]{1,80}\z/D', $gid) || !preg_match('/\A[a-zA-Z0-9_-]{1,80}\z/D', $jid)) api_out(['ok' => false, 'err' => 'grant_id and job_id: 1-80 chars of A-Z a-z 0-9 _ -'], 400); $rc = ($gid !== '' && $jid !== '') ? nst_job_receipt_lookup(read_chain(), $gid, $jid) : null; if ($rc === null) api_out(['ok' => false, 'err' => 'No receipt for that job'], 404); api_out(['ok' => true, 'receipt' => $rc]); } if (in_array($api, ['job_admit', 'job_complete', 'job_cancel'], true) && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); $actor = nst_actor('from'); $from = $actor['addr']; /* Ids are signed verbatim: refuse (never strip/truncate) anything outside 0-80 chars of [A-Za-z0-9_-]. */ $id8 = static function (string $k): string { $v = (string)($_POST[$k] ?? ''); if (!preg_match('/\A[a-zA-Z0-9_-]{0,80}\z/D', $v)) api_out(['ok' => false, 'err' => $k . ': at most 80 chars of A-Z a-z 0-9 _ -'], 400); return $v; }; $gid = $id8('grant_id'); $jid = $id8('job_id'); $nonce = $id8('nonce'); $to = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['to'] ?? '')) ?? ''); $digest = strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['result_digest'] ?? '')) ?? ''); $micros = trim((string)($_POST['amount'] ?? '')) === '' ? '0' : parse_amt(trim((string)($_POST['amount'] ?? ''))); $grantMicros = trim((string)($_POST['grant_amount'] ?? '')) === '' ? '0' : parse_amt(trim((string)($_POST['grant_amount'] ?? ''))); $ts = (int)($_POST['ts'] ?? 0); if ($gid === '' || $nonce === '' || $ts < 1 || abs($ts - time()) > 600) api_out(['ok' => false, 'err' => 'Need grant_id, nonce and a current ts'], 400); if ($api !== 'job_cancel' && $jid === '') api_out(['ok' => false, 'err' => 'job_id required'], 400); if (($api === 'job_admit' || $api === 'job_complete') && (strlen($to) !== 64 || $to === $from)) api_out(['ok' => false, 'err' => 'Bad worker address'], 400); if ($api === 'job_complete' && strlen($digest) !== 64) api_out(['ok' => false, 'err' => 'result_digest must be 64 hex'], 400); if ($grantMicros === null || $micros === null || ($api !== 'job_cancel' && dec_cmp($micros, '1') < 0)) api_out(['ok' => false, 'err' => 'Amount must be a positive number (at least 0.000001 NSU)'], 400); $fields = ['job_admit' => [$from, $gid, $jid, $to, $micros, $grantMicros, $nonce, (string)$ts], 'job_complete' => [$from, $gid, $jid, $to, $micros, $digest, $nonce, (string)$ts], 'job_cancel' => [$from, $gid, $jid, $micros, $nonce, (string)$ts]][$api]; $auth = nst_auth_sig($actor, implode('|', array_merge([$api, '1'], $fields))); if (empty($actor['ecdsa'])) api_out(['ok' => false, 'err' => 'Need ECDSA signature'], 401); $out = nst_with_chain_lock(function (array $chain, callable $append, callable $appendBatch) use ($api, $from, $gid, $jid, $to, $micros, $grantMicros, $digest, $nonce, $ts, $auth) { $grants = nst_job_grants($chain); $G = $grants[$gid] ?? null; $base = ['ver' => 1, 'from' => $from, 'grant_id' => $gid, 'nonce' => $nonce, 'ts' => $ts, 'sig' => $auth['sig'], 'sig_mode' => $auth['sig_mode'], 'pubkey' => $auth['pubkey']]; /* One nonce per sponsor across job requests: identical replay -> idempotent, different body -> 409. */ foreach ($chain as $pr) { if (!is_array($pr) || (string)($pr['from'] ?? '') !== $from || (string)($pr['nonce'] ?? '') !== $nonce) continue; $pt = (string)($pr['type'] ?? ''); if ($pt !== 'job_admit' && $pt !== 'job_pay' && $pt !== 'job_cancel') continue; $same = (string)($pr['grant_id'] ?? '') === $gid && (string)($pr['job_id'] ?? '') === $jid && (string)($pr['amount'] ?? '') === $micros; $og = '0'; if ($api === 'job_admit') foreach ($chain as $gr) { if (is_array($gr) && ($gr['type'] ?? '') === 'job_grant' && (string)($gr['from'] ?? '') === $from && (string)($gr['nonce'] ?? '') === $nonce && (string)($gr['grant_id'] ?? '') === $gid) { $og = (string)($gr['amount'] ?? '0'); break; } } if ($api === 'job_admit' && $pt === 'job_admit' && $same && (string)($pr['to'] ?? '') === $to && $og === $grantMicros) { return ['ok' => true, 'idempotent' => true, 'replay' => true, 'tx' => $pr, 'grant' => array_diff_key($G ?? [], ['jobs' => 1])]; } if ($api === 'job_complete' && $pt === 'job_pay' && $same && (string)($pr['to'] ?? '') === $to && (string)($pr['result_digest'] ?? '') === $digest) break; if ($api === 'job_cancel' && $pt === 'job_cancel' && $same) { return ['ok' => true, 'idempotent' => true, 'replay' => true, 'tx' => $pr, 'grant' => array_diff_key($G ?? [], ['jobs' => 1])]; } return ['ok' => false, 'http' => 409, 'err' => 'That nonce was already used for a different job request — use a new nonce']; } if ($G !== null && $G['sponsor'] !== $from) return ['ok' => false, 'http' => 409, 'err' => 'grant_id belongs to another sponsor']; /* TRADE/10: NEW hive work (job_admit; also the only grant opener) needs INFO's signed state to validate LIVE. MOOT -> 409, UNKNOWN/stale -> 503 (nothing written). Exact replays above are still served; complete/cancel never gated. */ if ($api === 'job_admit') { $mo = nst_moot_gate_touch(); if ($mo['state'] !== 'LIVE') return ['ok' => false, 'http' => $mo['state'] === 'MOOT' ? 409 : 503, 'err' => 'New hive work is paused: economy liveness is ' . $mo['state'], 'moot' => $mo]; } $pre = []; if ($api === 'job_admit' && dec_cmp($grantMicros, '0') > 0) { /* Scoped admit that opens its grant: reserve existing coins and hold the first job in ONE batch. */ if ($G !== null) return ['ok' => false, 'http' => 409, 'err' => 'grant_id already used']; /* grant_id is bound to its opener: g- + sha256(from|job_id) of the opening job (no front-run/collision). */ if (!hash_equals('g-' . hash('sha256', $from . '|' . $jid), $gid)) return ['ok' => false, 'http' => 400, 'err' => 'grant_id must be g- + sha256(from|job_id) of the opening job']; if (dec_cmp($micros, $grantMicros) > 0) return ['ok' => false, 'http' => 400, 'err' => 'Job amount exceeds the grant']; $bal = balances($chain); if (dec_cmp(available_nsu($bal, $chain, $from), $grantMicros) < 0) return ['ok' => false, 'http' => 400, 'err' => 'Not enough free NSU for this grant']; $pre[] = ['type' => 'job_grant'] + $base + ['amount' => $grantMicros]; $G = nst_job_grants(array_merge($chain, $pre))[$gid] ?? null; } { if ($G === null || $G['sponsor'] !== $from) return ['ok' => false, 'http' => 404, 'err' => 'No such grant for this sponsor']; $J = $jid !== '' ? ($G['jobs'][$jid] ?? null) : null; if ($api === 'job_complete' && $J !== null && $J['state'] === 'paid') { $rc = nst_job_receipt_lookup($chain, $gid, $jid); if ($rc !== null && $rc['result_digest'] === $digest && $rc['to'] === $to && $rc['amount'] === $micros) return ['ok' => true, 'replay' => true, 'receipt' => $rc]; return ['ok' => false, 'http' => 409, 'err' => 'Job already paid with different terms']; } if ($api === 'job_admit') { if ($G['closed']) return ['ok' => false, 'http' => 409, 'err' => 'Grant is closed']; if ($J !== null) { $orig = null; foreach ($chain as $pr) if (is_array($pr) && ($pr['type'] ?? '') === 'job_admit' && (string)($pr['grant_id'] ?? '') === $gid && (string)($pr['job_id'] ?? '') === $jid) { $orig = $pr; break; } return ['ok' => false, 'http' => 409, 'err' => 'job_id already admitted', 'tx' => $orig]; } if (dec_cmp($micros, $G['free']) > 0) return ['ok' => false, 'http' => 400, 'err' => 'Not enough free grant for this job']; $row = ['type' => 'job_admit'] + $base + ['job_id' => $jid, 'to' => $to, 'amount' => $micros]; } elseif ($api === 'job_complete') { if ($J === null || $J['state'] !== 'held' || $J['worker'] !== $to || $J['amount'] !== $micros) return ['ok' => false, 'http' => 409, 'err' => 'Job is not held with these terms']; $bal = balances($chain); if (dec_cmp($bal[$from] ?? '0', $micros) < 0) return ['ok' => false, 'http' => 409, 'err' => 'Sponsor balance below the held amount']; $row = ['type' => 'job_pay'] + $base + ['job_id' => $jid, 'to' => $to, 'amount' => $micros, 'result_digest' => $digest, 'receipt_id' => nst_job_receipt_id($gid, $jid, $to, $micros, $digest)]; } else { if ($jid !== '' && ($J === null || $J['state'] !== 'held')) return ['ok' => false, 'http' => 409, 'err' => 'Job is not held']; if ($jid === '' && $G['closed']) return ['ok' => false, 'http' => 409, 'err' => 'Grant already closed']; if ($jid !== '' && dec_cmp($micros, '0') !== 0) return ['ok' => false, 'http' => 400, 'err' => 'Job cancel takes no amount']; if ($jid === '' && dec_cmp($micros, $G['free']) > 0) return ['ok' => false, 'http' => 400, 'err' => 'Release exceeds the unheld grant']; $row = ['type' => 'job_cancel'] + $base + ['job_id' => $jid, 'amount' => $micros]; } } $batch = array_merge($pre, [$row]); $prefix = $chain; foreach ($batch as $k => $br) { $batch[$k]['prev'] = nst_chain_head($prefix); $prefix[] = $batch[$k]; } /* bind receipt view to committed bytes */ $row = $batch[count($batch) - 1]; if (!$appendBatch($batch)) return ['ok' => false, 'http' => 500, 'err' => 'Could not save to the server — try again in a moment']; $after = nst_job_grants($prefix); $res = ['ok' => true, 'tx' => $row, 'grant' => array_diff_key($after[$gid] ?? [], ['jobs' => 1])]; if ($row['type'] === 'job_pay') $res['receipt'] = nst_job_receipt_lookup($prefix, $gid, $jid); return $res; }); if ($out === null) api_out(['ok' => false, 'err' => 'Could not save to the server — try again in a moment'], 500); $http = (int)($out['http'] ?? 200); unset($out['http']); api_out($out, $http); } if ($api === 'transfer' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); // Auth first (seed/ECDSA) so missing unlock is not masked as "bad to". $actor = nst_actor('from'); $from = $actor['addr']; $to = nst_resolve_receipt((string)($_POST['to'] ?? '')); $micros = parse_amt(trim((string)($_POST['amount'] ?? ''))); $memo = substr(trim((string)($_POST['memo'] ?? '')), 0, 120); $nonce = substr(trim((string)($_POST['nonce'] ?? bin2hex(random_bytes(8)))), 0, 64); if (strlen($to) !== 64) api_out(['ok' => false, 'err' => 'Bad destination — paste their number or the old 64-character receipt'], 400); /* RR316 P1-TRADE-AMOUNT-ERR-PLAIN: zero/neg/parse-fail amount is plain English (not "bad amount"). */ if ($micros === null || dec_cmp($micros, '1') < 0) { api_out(['ok' => false, 'err' => 'Amount must be a positive number (at least 0.000001 NSU)'], 400); } $ts = (int)($_POST['ts'] ?? 0); if ($ts < 1) { $ts = time(); } elseif (abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'Clock skew — refresh the page and try again'], 400); } // Dual-path: ECDSA seed-off-wire OR seed + optional browser HMAC. if ($from === $to) api_out(['ok' => false, 'err' => 'Cannot send to yourself'], 400); $body = implode('|', ['transfer', '1', $from, $to, $micros, $nonce, (string)$ts, $memo]); $auth = nst_auth_sig($actor, $body); $sigMode = $auth['sig_mode']; $rowSig = $auth['sig']; $spkiHex = $auth['pubkey']; $risk = nst_brain_memo_risk($memo); $row = [ 'type' => 'transfer', 'ver' => 1, 'from' => $from, 'to' => $to, 'amount' => $micros, 'amount_fmt' => fmt_amt($micros), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => $rowSig, 'sig_mode' => $sigMode, ]; if (nst_sig_is_ecdsa($sigMode) && $spkiHex !== '') { $row['pubkey'] = $spkiHex; } if (!empty($risk['flag'])) $row['memo_risk'] = $risk; // Exclusive RMW: concurrent transfers cannot both pass available_nsu and overdraw. $xferOut = nst_with_chain_lock(function (array $chain, callable $append) use ($from, $to, $micros, $nonce, $row) { foreach ($chain as $r) { if (($r['type'] ?? '') === 'transfer' && ($r['from'] ?? '') === $from && ($r['nonce'] ?? '') === $nonce) { if ((string)($r['to'] ?? '') === $to && (string)($r['amount'] ?? '') === $micros && (string)($r['memo'] ?? '') === (string)$row['memo']) { return ['ok' => true, 'idempotent' => true, 'tx' => $r]; } return ['ok' => false, 'http' => 409, 'err' => 'That nonce was already used for a different transfer — use a new nonce']; } } if (!empty($GLOBALS['NST_SHARED_WALLET_REQUEST'])) { if (abs((int)$row['ts']-time())>600) return ['ok'=>false,'http'=>400,'err'=>'Payment expired before commit','outcome'=>'rejected','from'=>$from,'nonce'=>$nonce]; $check=nst_conservation_check_and_maybe_freeze($chain); if (empty($check['ok']) || !empty($check['frozen'])) return ['ok'=>false,'http'=>503,'err'=>'Trading is paused while the ledger is checked','outcome'=>'rejected','from'=>$from,'nonce'=>$nonce]; } $bal = balances($chain); $av = available_nsu($bal, $chain, $from); if (dec_cmp($av, $micros) < 0) { return array_merge([ 'ok' => false, 'http' => 400, 'err' => 'Not enough free NSU — cancel open sells or buy more on the book', ], (!empty($GLOBALS['NST_SHARED_WALLET_REQUEST']) ? ['outcome'=>'rejected','from'=>$from,'nonce'=>$nonce] : []), [ 'have_fmt' => fmt_amt($bal[$from] ?? '0'), 'available_fmt' => fmt_amt($av), ]); } if (!$append($row)) { return ['ok' => false, 'http' => 500, 'err' => 'Could not save to the server — try again in a moment']; } return [ 'ok' => true, 'tx' => $row, 'balance_fmt' => fmt_amt(dec_sub($bal[$from] ?? '0', $micros)), ]; }); if ($xferOut === null) { api_out(['ok' => false, 'err' => 'Could not save to the server — try again in a moment'], 500); } if (empty($xferOut['ok'])) { $http = (int)($xferOut['http'] ?? 400); unset($xferOut['http']); api_out($xferOut, $http); } if (!empty($xferOut['idempotent'])) { /* Lost-response retry: already committed; no second rent settle/refund/notice. */ api_out(['ok' => true, 'idempotent' => true, 'tx' => $xferOut['tx'], 'sig_mode' => $sigMode]); } // P3c/d: rent memo → try settle; on refundable failure, auto-refund payer from treasury. $txRow = is_array($xferOut['tx'] ?? null) ? $xferOut['tx'] : $row; $rentSettle = nst_rent_try_settle_transfer($txRow); $rentRefund = null; if (empty($rentSettle['settled']) && nst_rent_refundable_reason((string)($rentSettle['reason'] ?? ''))) { $rentRefund = nst_rent_auto_refund($txRow, $rentSettle); } // Rent notices AFTER settle/refund commit; fail-open (public lease facts). try { if (!empty($rentSettle['settled'])) { nst_inbox_notify_rent_settle($rentSettle); } if (is_array($rentRefund) && !empty($rentRefund['refunded'])) { nst_inbox_notify_rent_refund($rentRefund); } } catch (Throwable $e) { // never abort a committed transfer/rent op } api_out([ 'ok' => true, 'tx' => $xferOut['tx'], 'sig_mode' => $sigMode, 'seed_on_wire' => $auth['seed_on_wire'], 'balance_fmt' => $xferOut['balance_fmt'], 'brains' => ['memo_risk' => $risk], 'rent' => $rentSettle, 'rent_refund' => $rentRefund, ]); } /** Free claim retired — always 410 (any method). No mint, no transfer. * P1-FAUCET-GET RR206: GET must not 404 unknown api while face says retired. */ if ($api === 'faucet') { api_out([ 'ok' => false, 'err' => 'Free NSU claim retired — buy NSU on the order book (Trade book tab)', 'law' => 'OPERATOR_GO 2026-07-16: KING path SELL/BUY only; free giveaway OUT. ' . 'Site use stays free (wallet, view, post). NSU is the tradeable coin, not a free drip.', 'free_claim' => 'retired', 'next' => 'place a buy order on the book · ?api=order side=buy', ], 410); } /* Mail-front-door promo credit. Bot-key fallback for tests. Not a visitor faucet. */ if ($api === 'ad_promo_claim') { if (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? '')) !== 'POST') { api_out(['ok' => false, 'err' => 'POST only'], 405); } nst_require_writes_unfrozen(); $want = nst_promo_bot_ensure(); $got = strtolower(trim((string)($_POST['key'] ?? ''))); $got = preg_replace('/[^a-f0-9]/', '', $got) ?? ''; if ($want === '' || $got === '' || !hash_equals($want, $got)) { api_out(['ok' => false, 'err' => 'bad key'], 401); } $email = (string)($_POST['email'] ?? ''); $to = (string)($_POST['to'] ?? $_POST['id'] ?? ''); $r = nst_promo_ad_claim($email, $to); api_out($r, !empty($r['ok']) ? 200 : 400); } if ($api === 'order' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); $actor = nst_actor('addr'); $addr = $actor['addr']; $side = strtolower(trim((string)($_POST['side'] ?? ''))); $pair = strtoupper(trim((string)($_POST['pair'] ?? NST_PUBLIC_PAIR))); $amount = parse_amt(trim((string)($_POST['amount'] ?? ''))); $price = parse_amt(trim((string)($_POST['price'] ?? ''))); $expiry = (int)($_POST['expiry'] ?? (time() + 86400)); if (!in_array($side, ['buy', 'sell'], true)) api_out(['ok' => false, 'err' => 'Side must be buy or sell'], 400); /* RR328 P1-TRADE-ORDER-AMOUNT-HARD: zero/neg amount or price must hard-reject (brain is soft-only). */ $minAmt = parse_amt(NST_ORDER_MIN_NSU); if ($amount === null || $minAmt === null || dec_cmp($amount, $minAmt) < 0) { api_out(['ok' => false, 'err' => 'Order amount must be at least 0.000001 NSU (one micro)'], 400); } if ($price === null || dec_cmp($price, '1') < 0) { api_out(['ok' => false, 'err' => 'Order price must be a positive number'], 400); } // SA-05: a malformed pair or an NSU look-alike base (NSUX/LTC, bare NSU) cannot enter the book. if (!preg_match('#\A[A-Z0-9]{2,12}/[A-Z0-9]{2,12}\z#D', $pair) || (str_starts_with($pair, 'NSU') && !nst_pair_is_nsu_base($pair))) { api_out(['ok' => false, 'err' => 'Unsupported pair — use ' . NST_PUBLIC_PAIR], 400); } $sanity = nst_brain_order_sanity($side, $amount, $price); $pairBrain = nst_brain_pair_sane($pair); if ($expiry < time() + 60) $expiry = time() + 3600; if ($expiry > time() + 30 * 86400) $expiry = time() + 30 * 86400; // open-count + sell available re-checked under chain RMW lock below $ts = (int)($_POST['ts'] ?? 0); if ($ts < 1) { $ts = time(); } elseif (abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'Clock skew — refresh the page and try again'], 400); } $idIn = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['order_id'] ?? ''))) ?? ''; if (strlen($idIn) === 64) { $id = $idIn; } else { $id = hash('sha256', $addr . '|' . $ts . '|' . $side . '|' . $amount . '|' . $price . '|' . bin2hex(random_bytes(4))); } $body = implode('|', ['order', '1', $id, $addr, $side, $pair, $amount, $price, (string)$expiry, (string)$ts]); $auth = nst_auth_sig($actor, $body); $sigMode = $auth['sig_mode']; $row = [ 'type' => 'order', 'ver' => 1, 'id' => $id, 'addr' => $addr, 'side' => $side, 'pair' => $pair, 'amount' => $amount, 'amount_fmt' => fmt_amt($amount), 'price' => $price, 'price_fmt' => fmt_amt($price), 'expiry' => $expiry, 'ts' => $ts, 'sig' => $auth['sig'], 'sig_mode' => $sigMode, ]; if (nst_sig_is_ecdsa($sigMode) && $auth['pubkey'] !== '') { $row['pubkey'] = $auth['pubkey']; } if (!empty($sanity['flag'])) $row['order_sanity'] = $sanity; if (!empty($pairBrain['flag'])) $row['pair_sane'] = $pairBrain; // Exclusive RMW: concurrent sells cannot over-reserve; open-count + id uniqueness under lock. // OPERATOR_GO §3c: treasury/KING SELL on book immediately arms BID @ half (same pair). // Pre-read treasury material OUTSIDE chain lock (treasury_secret → ensure_genesis can append_row). $tsecForHalf = ''; $taddrForHalf = ''; try { $tsecForHalf = treasury_secret(); $taddrForHalf = strtolower(addr_from_seed($tsecForHalf)); } catch (Throwable $e) { $tsecForHalf = ''; $taddrForHalf = ''; } $isTreasurySell = ($side === 'sell' && $taddrForHalf !== '' && strtolower($addr) === $taddrForHalf && (($row['kind'] ?? '') !== 'bootstrap_spread') && strtoupper((string)$pair) !== NST_PUBLIC_PAIR); $orderOut = nst_with_chain_lock(function (array $chain, callable $append) use ( $addr, $side, $pair, $amount, $id, $row, $isTreasurySell, $expiry, $ts, $tsecForHalf, $taddrForHalf ) { foreach ($chain as $r) { if (($r['type'] ?? '') === 'order' && ($r['id'] ?? '') === $id) { return ['ok' => false, 'http' => 400, 'err' => 'order_id already used']; } } $n = 0; foreach (open_orders($chain) as $o) { if (($o['addr'] ?? '') === $addr) $n++; } // Treasury SELL needs a free slot for the paired half-BID (two posts). $slotCap = $isTreasurySell ? 19 : 20; if ($n >= $slotCap) { return ['ok' => false, 'http' => 400, 'err' => 'too many open orders']; } if ($side === 'sell' && nst_pair_is_nsu_base($pair)) { $bal = balances($chain); $av = available_nsu($bal, $chain, $addr); if (dec_cmp($av, $amount) < 0) { return [ 'ok' => false, 'http' => 400, 'err' => 'Not enough free NSU — cancel open sells or buy more on the book', 'available_fmt' => fmt_amt($av), ]; } } if (!$append($row)) { return ['ok' => false, 'http' => 500, 'err' => 'Could not save to the server — try again in a moment']; } $halfBid = null; if ($isTreasurySell) { $hb = nst_treasury_half_bid_from_sell( (string)$id, (string)$pair, (string)$amount, (string)($row['price'] ?? '0'), (int)$expiry, (int)$ts, $chain, $append, (string)$tsecForHalf, (string)$taddrForHalf ); if (empty($hb['ok'])) { return [ 'ok' => false, 'http' => 500, 'err' => 'treasury half-bid failed after sell', 'detail' => $hb['err'] ?? 'unknown', 'order' => $row, ]; } $halfBid = $hb['order'] ?? null; } return ['ok' => true, 'order' => $row, 'half_bid' => $halfBid]; }); if ($orderOut === null) { api_out(['ok' => false, 'err' => 'Could not save to the server — try again in a moment'], 500); } if (empty($orderOut['ok'])) { $http = (int)($orderOut['http'] ?? 400); unset($orderOut['http']); api_out($orderOut, $http); } $outPayload = [ 'ok' => true, 'order' => $orderOut['order'], 'sig_mode' => $sigMode, 'seed_on_wire' => $auth['seed_on_wire'], 'brains' => ['order_sanity' => $sanity, 'pair_sane' => $pairBrain], ]; if (!empty($orderOut['half_bid'])) { $outPayload['half_bid'] = $orderOut['half_bid']; $outPayload['half_bid_note'] = 'OPERATOR_GO §3c: treasury SELL armed paired BID @ half price'; } // Half-BID armed notice: AFTER chain write, fail-open (public book floor). try { if (!empty($orderOut['half_bid']) && is_array($orderOut['half_bid'])) { $sellRow = is_array($orderOut['order'] ?? null) ? $orderOut['order'] : null; nst_inbox_notify_half_bid($sellRow, $orderOut['half_bid']); } } catch (Throwable $e) { // never abort a committed order/half-BID } api_out($outPayload); } if ($api === 'cancel' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); $actor = nst_actor('addr'); $addr = $actor['addr']; $oid = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['order_id'] ?? ''))); if (strlen($oid) !== 64) api_out(['ok' => false, 'err' => 'Bad order id — use a full 64-char open order id from the book'], 400); $chain = read_chain(); $found = null; foreach ($chain as $r) { if (($r['type'] ?? '') === 'order' && ($r['id'] ?? '') === $oid) $found = $r; } if (!$found) api_out(['ok' => false, 'err' => 'That order is not open (already filled, cancelled, or expired)'], 404); if (($found['addr'] ?? '') !== $addr) api_out(['ok' => false, 'err' => 'not owner'], 403); // Honesty: only open book rows may cancel (filled/cancelled/expired → not open). // Not a fund-lock invent: fill already rechecks open_orders under nst_with_chain_lock. $stillOpen = false; foreach (open_orders($chain) as $oLive) { if (($oLive['id'] ?? '') === $oid) { $stillOpen = true; break; } } if (!$stillOpen) { api_out(['ok' => false, 'err' => 'That order is not open (already filled, cancelled, or expired)'], 404); } $ts = (int)($_POST['ts'] ?? 0); if ($ts < 1) { $ts = time(); } elseif (abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'Clock skew — refresh the page and try again'], 400); } $body = implode('|', ['cancel', '1', $oid, $addr, (string)$ts]); $auth = nst_auth_sig($actor, $body); $sigMode = $auth['sig_mode']; $row = [ 'type' => 'cancel', 'ver' => 1, 'order_id' => $oid, 'addr' => $addr, 'ts' => $ts, 'sig' => $auth['sig'], 'sig_mode' => $sigMode, ]; if (nst_sig_is_ecdsa($sigMode) && $auth['pubkey'] !== '') { $row['pubkey'] = $auth['pubkey']; } if (!append_row($row)) api_out(['ok' => false, 'err' => 'Could not save to the server — try again in a moment'], 500); api_out(['ok' => true, 'cancel' => $row, 'sig_mode' => $sigMode, 'seed_on_wire' => $auth['seed_on_wire']]); } // Take a live order: move NSU leg when pair starts with NSU // sell order: maker sells NSU -> transfer maker->taker // buy order: maker buys NSU -> transfer taker->maker (taker must hold NSU) // Chain exclusive RMW under LOCK_EX so concurrent fills cannot over-debit maker. if ($api === 'fill' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); /* New clients send the signing wallet explicitly. The taker fallback * preserves older buy-fill clients; on a sell release the signer is * the maker while the taker field is the named buyer. */ $actor = nst_actor(array_key_exists('actor', $_POST) ? 'actor' : 'taker'); $taker = $actor['addr']; $oid = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['order_id'] ?? ''))); $qtyH = trim((string)($_POST['amount'] ?? '')); $settle = substr(trim((string)($_POST['settle_ref'] ?? '')), 0, 160); if (strlen($oid) !== 64) api_out(['ok' => false, 'err' => 'Bad order id — use a full 64-char open order id from the book'], 400); $clientFillId = strtolower(trim((string)($_POST['fill_id'] ?? ''))); if ($clientFillId !== '' && preg_match('/\A[0-9a-f]{64}\z/D', $clientFillId) !== 1) { api_out(['ok' => false, 'err' => 'Bad fill id — refresh the book and try again'], 400); } $fillId = $clientFillId !== '' ? $clientFillId : hash( 'sha256', 'legacy-fill-v1|' . $taker . '|' . $oid . '|' . $qtyH . '|' . $settle . '|' . (string)($_POST['ts'] ?? '') . '|' . (string)($_POST['nonce'] ?? '') ); // Pre-read for auth body qty (signatures bind amount). Remaining re-checked under lock. $chainPeek = read_chain(); foreach ($chainPeek as $prior) { if (($prior['type'] ?? '') !== 'fill' || ($prior['fill_id'] ?? '') !== $fillId) continue; if (($prior['order_id'] ?? '') !== $oid) { api_out(['ok' => false, 'err' => 'fill id already used for another order'], 409); } $priorSide = (string)($prior['side'] ?? ''); $priorMaker = (string)($prior['maker'] ?? ''); $priorTaker = (string)($prior['taker'] ?? ''); $authorizedAddr = $priorSide === 'sell' ? $priorMaker : $priorTaker; if (!hash_equals($authorizedAddr, $taker)) { api_out(['ok' => false, 'err' => 'fill id belongs to another wallet'], 403); } $requestedQty = $qtyH === '' ? (string)($prior['amount'] ?? '') : parse_amt($qtyH); $priorQty = dec_norm((string)($prior['amount'] ?? '0')); $priorTs = (int)($prior['ts'] ?? 0); if (!is_string($requestedQty) || !hash_equals($priorQty, dec_norm($requestedQty)) || !hash_equals((string)($prior['settle_ref'] ?? ''), $settle) ) { api_out(['ok' => false, 'err' => 'fill id already committed with different details'], 409); } if ($priorSide === 'sell') { $retryBuyer = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['buyer'] ?? ''))) ?? ''; if (strlen($retryBuyer) !== 64 || !hash_equals($priorTaker, $retryBuyer)) { api_out(['ok' => false, 'err' => 'fill id already committed for another buyer'], 409); } } $priorVersion = (int)($prior['ver'] ?? 1) >= 2 ? 2 : 1; $retryBody = $priorVersion === 2 ? implode('|', ['fill', '2', $fillId, $oid, $priorMaker, $priorTaker, $priorQty, $settle, (string)$priorTs]) : implode('|', ['fill', '1', $oid, $priorMaker, $priorTaker, $priorQty, $settle, (string)$priorTs]); /* A committed response may have been lost, but a fill id is not a * bearer token. Re-verify the original signed intent before * disclosing idempotent success. */ $retryAuth = nst_auth_sig($actor, $retryBody); $priorPayout = []; if (($prior['payout_proof_ver'] ?? 0) === 1 || isset($_POST['payout_ltc'])) { $retryPayout = strtolower(trim((string)($_POST['payout_ltc'] ?? ''))); if (isset($prior['payout_ltc']) && $retryPayout !== '' && !hash_equals((string)$prior['payout_ltc'], $retryPayout)) api_out(['ok' => false, 'err' => 'Committed payout recipient cannot change'], 409); $priorPayout = nst_ltc_reconcile_payout_fill($prior); } $priorTx = null; foreach ($chainPeek as $candidateTx) { if (($candidateTx['type'] ?? '') === 'transfer' && ($candidateTx['order_id'] ?? '') === $oid && ($candidateTx['fill_id'] ?? '') === $fillId ) { $priorTx = $candidateTx; break; } } api_out([ 'ok' => true, 'idempotent' => true, 'fill' => $prior, 'transfer' => $priorTx, 'sig_mode' => (string)($prior['sig_mode'] ?? 'server_mac'), 'seed_on_wire' => $retryAuth['seed_on_wire'], ] + $priorPayout); } $ordersPeek = open_orders($chainPeek); $foundPeek = null; foreach ($ordersPeek as $o) if (($o['id'] ?? '') === $oid) { $foundPeek = $o; break; } if (!$foundPeek) api_out(['ok' => false, 'err' => 'order not open'], 404); // Bootstrap BUY/SELL are temp mid discovery only — never a free NSU faucet path. if (($foundPeek['kind'] ?? '') === 'bootstrap_spread') { api_out([ 'ok' => false, 'err' => 'Bootstrap discovery quote — not fillable. Post your own order or take a real maker.', 'kind' => 'bootstrap_spread', ], 400); } $maker = (string)($foundPeek['addr'] ?? ''); $sidePeek = (string)($foundPeek['side'] ?? ''); /* NOBODY MOVES SOMEONE ELSE'S NSU. * * A sell fill debits the MAKER and credits the taker. This used to run on * the maker's ORIGINAL order signature ("auth: open_sell_order") with the * TAKER as the only authenticated party - and nothing anywhere verified * that the taker had paid the off-platform leg. `settle_ref` is a free * text field. So any open sell order was free money for whoever called * fill first: reproduced with the literal settle_ref "i-promise-i-paid", * moving 150 NSU from a maker who consented to nothing. * * Listing an intent to sell is not consent to be debited by a stranger. * Every other spend path in this file authenticates the wallet the money * LEAVES; fill was the sole exception, and the exception was the bug. * * So for a sell, the party who is authenticated must be the maker - the * NSU owner releases their own coins once they have been paid, and names * the buyer. For a buy the taker spends their OWN NSU, so the existing * taker signature is already the right one and nothing changes. * * This is NOT escrow, which the charter refuses ("NEVER BECOME: * custody/escrow"): the site never holds a coin and never adjudicates. * It only declines to move a wallet without its owner's live say-so. * The counterparty risk that remains - a maker who takes payment and * does not release - is inherent to any non-custodial P2P trade and is * the honest version of what the book already claimed to be. */ if ($sidePeek === 'sell') { if ($taker !== $maker) { api_out([ 'ok' => false, 'err' => 'Only the seller can release NSU. Pay the maker off-platform, then ' . 'the maker submits this fill naming you as buyer.', 'why' => 'A sell fill debits the maker. Authorising it with the taker\'s key ' . 'would let anyone drain any open sell order.', 'maker_submits' => true, ], 403); } $buyer = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['buyer'] ?? ''))) ?? ''; if (strlen($buyer) !== 64) { api_out(['ok' => false, 'err' => 'Need the buyer\'s 64-char address (buyer=...)'], 400); } if ($buyer === $maker) { api_out(['ok' => false, 'err' => 'Cannot sell to yourself'], 400); } $taker = $buyer; // the party receiving the NSU } elseif ($maker === $taker) { api_out(['ok' => false, 'err' => 'Cannot take your own order'], 400); } $remPeek = $foundPeek['remaining'] ?? '0'; $qty = $qtyH === '' ? $remPeek : parse_amt($qtyH); if ($qty === null || dec_cmp($qty, '1') < 0) api_out(['ok' => false, 'err' => 'Amount must be a positive number (at least 0.000001 NSU)'], 400); if (dec_cmp($qty, $remPeek) > 0) api_out(['ok' => false, 'err' => 'That order no longer has enough left — pick a smaller amount or another order', 'remaining_fmt' => fmt_amt($remPeek)], 400); $pair = strtoupper((string)($foundPeek['pair'] ?? '')); // SA-05: a legacy NSU look-alike order (NSUX/LTC, bare NSU) must not settle without, or with, NSU delivery. if (str_starts_with($pair, 'NSU') && !nst_pair_is_nsu_base($pair)) { api_out(['ok' => false, 'err' => 'This order uses an unsupported pair and cannot be filled'], 400); } $side = (string)($foundPeek['side'] ?? ''); $payoutLtc = ''; $isKingHalfBid = ((string)($foundPeek['kind'] ?? '') === 'treasury_half_bid' && $pair === NST_PUBLIC_PAIR && $side === 'buy'); if ($isKingHalfBid) { $payoutLtc = strtolower(trim((string)($_POST['payout_ltc'] ?? ''))); if (!function_exists('nst_ltc_payout_addr_ok') || !nst_ltc_payout_addr_ok($payoutLtc)) { api_out([ 'ok' => false, 'err' => 'Paste your LTC receive address (ltc1…). King pays this half-bid. ' . 'Outbound sign is not shipped — this stores the address only and does not send LTC.', 'outbound_sign' => 'honest_gap', 'outbound_sent' => false, ], 400); } } $ts = (int)($_POST['ts'] ?? 0); if ($ts < 1) { $ts = time(); } elseif (abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'Clock skew — refresh the page and try again'], 400); } $nonce = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['nonce'] ?? ''))) ?? ''; if (strlen($nonce) < 8) { $nonce = bin2hex(random_bytes(8)); } $memo = 'fill:' . substr($oid, 0, 12); $tx = null; $txAuth = null; if (nst_pair_is_nsu_base($pair)) { if ($side === 'sell') { $from = $maker; $to = $taker; } elseif ($side === 'buy') { $from = $taker; $to = $maker; } else { api_out(['ok' => false, 'err' => 'Pick buy or sell'], 400); } $tbody = implode('|', ['transfer', '1', $from, $to, $qty, $nonce, (string)$ts, $memo]); // For sell: maker already authorized via open sell order sig. // For buy: taker spends — ECDSA or browser HMAC on transfer body (signed before lock). if ($side === 'sell') { $tx = [ 'type' => 'transfer', 'ver' => 1, 'from' => $maker, 'to' => $taker, 'amount' => $qty, 'amount_fmt' => fmt_amt($qty), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => $foundPeek['sig'] ?? '', 'auth' => 'open_sell_order', 'order_id' => $oid, ]; } else { // ECDSA: require dedicated tx_ecdsa_sig (do not reuse fill ecdsa_sig). $txEcdsaOverride = !empty($actor['ecdsa']) ? strtolower(preg_replace('/[^a-f0-9]/', '', (string)($_POST['tx_ecdsa_sig'] ?? '')) ?? '') : null; $txAuth = nst_auth_sig($actor, $tbody, 'tx_client_sig', $txEcdsaOverride); $tx = [ 'type' => 'transfer', 'ver' => 1, 'from' => $taker, 'to' => $maker, 'amount' => $qty, 'amount_fmt' => fmt_amt($qty), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => $txAuth['sig'], 'sig_mode' => $txAuth['sig_mode'], 'auth' => nst_sig_is_ecdsa($txAuth['sig_mode']) ? 'taker_ecdsa' : 'taker_seed', 'order_id' => $oid, ]; if (nst_sig_is_ecdsa($txAuth['sig_mode']) && $txAuth['pubkey'] !== '') { $tx['pubkey'] = $txAuth['pubkey']; } } } $fillVersion = $clientFillId !== '' ? 2 : 1; $fbody = $fillVersion === 2 ? implode('|', ['fill', '2', $fillId, $oid, $maker, $taker, $qty, $settle, (string)$ts]) : implode('|', ['fill', '1', $oid, $maker, $taker, $qty, $settle, (string)$ts]); $fillAuth = nst_auth_sig($actor, $fbody); $sigMode = $fillAuth['sig_mode']; $payoutAuth = null; if ($isKingHalfBid) { $payoutProof = !empty($actor['ecdsa']) ? (string)($_POST['payout_ecdsa_sig'] ?? '') : (string)($_POST['payout_client_sig'] ?? ''); if ($clientFillId === '' || $payoutProof === '') api_out(['ok' => false, 'err' => 'Refresh Trade and retry: a signed payout recipient is required before any NSU debit', 'payout_upgrade_required' => true], 400); $payoutAuth = nst_auth_sig($actor, nst_ltc_payout_proof_body($fillId, $oid, $taker, $payoutLtc, $ts), 'payout_client_sig', !empty($actor['ecdsa']) ? $payoutProof : null); } $fillOut = nst_with_chain_lock(function (array $chain, callable $append, callable $appendBatch) use ( $oid, $taker, $maker, $qty, $settle, $ts, $pair, $side, $tx, $fillAuth, $sigMode, $nonce, $fillId, $fillVersion, $payoutLtc, $payoutAuth ) { foreach ($chain as $prior) { if (($prior['type'] ?? '') !== 'fill' || ($prior['fill_id'] ?? '') !== $fillId) continue; if (($prior['order_id'] ?? '') !== $oid) { return ['ok' => false, 'http' => 409, 'err' => 'fill id already used for another order']; } $authorizedAddr = (($prior['side'] ?? '') === 'sell') ? (string)($prior['maker'] ?? '') : (string)($prior['taker'] ?? ''); $actorAddr = (($side ?? '') === 'sell') ? $maker : $taker; if (!hash_equals($authorizedAddr, $actorAddr)) { return ['ok' => false, 'http' => 403, 'err' => 'fill id belongs to another wallet']; } $priorTx = null; foreach ($chain as $candidateTx) { if (($candidateTx['type'] ?? '') === 'transfer' && ($candidateTx['order_id'] ?? '') === $oid && ($candidateTx['fill_id'] ?? '') === $fillId ) { $priorTx = $candidateTx; break; } } return [ 'ok' => true, 'idempotent' => true, 'fill' => $prior, 'transfer' => $priorTx, 'sig_mode' => (string)($prior['sig_mode'] ?? $sigMode), 'seed_on_wire' => $fillAuth['seed_on_wire'], ]; } $orders = open_orders($chain); $found = null; foreach ($orders as $o) if (($o['id'] ?? '') === $oid) { $found = $o; break; } if (!$found) { return ['ok' => false, 'http' => 404, 'err' => 'order not open']; } if (($found['kind'] ?? '') === 'bootstrap_spread') { return [ 'ok' => false, 'http' => 400, 'err' => 'Bootstrap discovery quote — not fillable. Post your own order or take a real maker.', 'kind' => 'bootstrap_spread', ]; } if ((string)($found['addr'] ?? '') !== $maker) { return ['ok' => false, 'http' => 400, 'err' => 'order changed — refresh book']; } if ((string)($found['side'] ?? '') !== $side || strtoupper((string)($found['pair'] ?? '')) !== $pair) { return ['ok' => false, 'http' => 400, 'err' => 'order changed — refresh book']; } $rem = $found['remaining'] ?? '0'; if (dec_cmp($qty, $rem) > 0) { return ['ok' => false, 'http' => 400, 'err' => 'That order no longer has enough left — pick a smaller amount or another order', 'remaining_fmt' => fmt_amt($rem)]; } // Reject identical fill packet replay (sell leg has no client-bound nonce on fill body). foreach ($chain as $r) { if (($r['type'] ?? '') !== 'fill') continue; if (($r['order_id'] ?? '') !== $oid) continue; if (($r['taker'] ?? '') !== $taker) continue; if ((int)($r['ts'] ?? 0) !== $ts) continue; if (dec_norm((string)($r['amount'] ?? '0')) !== dec_norm($qty)) continue; return ['ok' => false, 'http' => 400, 'err' => 'fill replay — refresh book']; } $txRow = $tx; if (nst_pair_is_nsu_base($pair)) { $bal = balances($chain); if ($side === 'sell') { $have = $bal[$maker] ?? '0'; if (dec_cmp($have, $qty) < 0) { return ['ok' => false, 'http' => 400, 'err' => 'maker lacks NSU']; } } elseif ($side === 'buy') { $av = available_nsu($bal, $chain, $taker); if (dec_cmp($av, $qty) < 0) { return ['ok' => false, 'http' => 400, 'err' => 'Not enough free NSU — cancel open sells or buy more on the book', 'available_fmt' => fmt_amt($av)]; } } // Same nonce uniqueness as transfer API (buy-leg replay must not re-spend). if ($txRow) { $txFrom = (string)($txRow['from'] ?? ''); $txNonce = (string)($txRow['nonce'] ?? $nonce); if ($txFrom !== '' && $txNonce !== '') { foreach ($chain as $r) { if (($r['type'] ?? '') === 'transfer' && ($r['from'] ?? '') === $txFrom && ($r['nonce'] ?? '') === $txNonce) { return ['ok' => false, 'http' => 409, 'err' => 'That transfer was already sent (nonce replay) — use a new nonce']; } } } // Refresh sell-order sig from live row under lock (maker re-auth not required). if ($side === 'sell') { $txRow['sig'] = $found['sig'] ?? ($txRow['sig'] ?? ''); } $txRow['fill_id'] = $fillId; /* The transfer is staged below with its fill in one batch. */ } } $fill = [ 'type' => 'fill', 'ver' => $fillVersion, 'fill_id' => $fillId, 'order_id' => $oid, 'maker' => $maker, 'taker' => $taker, 'side' => $side, 'pair' => $pair, 'amount' => $qty, 'amount_fmt' => fmt_amt($qty), 'price' => $found['price'] ?? '0', 'price_fmt' => $found['price_fmt'] ?? '', 'settle_ref' => $settle, 'ts' => $ts, 'taker_sig' => $fillAuth['sig'], 'sig_mode' => $sigMode, ]; if (nst_sig_is_ecdsa($sigMode) && $fillAuth['pubkey'] !== '') { $fill['pubkey'] = $fillAuth['pubkey']; } if (is_array($payoutAuth)) { $fill['payout_ltc'] = $payoutLtc; $fill['payout_proof_ver'] = 1; $fill['payout_sig'] = $payoutAuth['sig']; $fill['payout_sig_mode'] = $payoutAuth['sig_mode']; } /* One durable intent, one append payload, one recovery decision. * A process loss can no longer leave the transfer committed while * the order remains open because its fill row never landed. */ $batch = $txRow ? [$txRow, $fill] : [$fill]; if (!$appendBatch($batch)) { return ['ok' => false, 'http' => 500, 'err' => 'Could not complete that trade fill — try again in a moment']; } return [ 'ok' => true, 'fill' => $fill, 'transfer' => $txRow, 'sig_mode' => $sigMode, 'seed_on_wire' => $fillAuth['seed_on_wire'], ]; }); if ($fillOut === null) { api_out(['ok' => false, 'err' => 'Could not complete that trade fill — try again in a moment'], 500); } if (empty($fillOut['ok'])) { $http = (int)($fillOut['http'] ?? 400); unset($fillOut['http']); api_out($fillOut, $http); } // Money-adjacent server_notice: AFTER fill commits, OUTSIDE chain lock, fail-open. try { if (is_array($fillOut['fill'] ?? null)) { nst_inbox_notify_fill($fillOut['fill']); } } catch (Throwable $e) { // never abort a committed fill } /* NSU/LTC king sell: half-bid arms on FILL (same NSU size, half LTC price). Not on post. */ try { $fRow = is_array($fillOut['fill'] ?? null) ? $fillOut['fill'] : null; if ($fRow && strtoupper((string)($fRow['pair'] ?? '')) === NST_PUBLIC_PAIR && (string)($fRow['side'] ?? '') === 'sell' ) { $tsecHb = treasury_secret(); $taddrHb = strtolower(addr_from_seed($tsecHb)); if ($taddrHb !== '' && $taddrHb === strtolower((string)($fRow['maker'] ?? ''))) { $hbOut = nst_with_chain_lock(function (array $chain, callable $append) use ($fRow, $tsecHb, $taddrHb) { return nst_treasury_half_bid_from_sell( (string)($fRow['fill_id'] ?? ''), NST_PUBLIC_PAIR, (string)($fRow['amount'] ?? '0'), (string)($fRow['price'] ?? '0'), time() + 30 * 86400, time(), $chain, $append, $tsecHb, $taddrHb ); }); if (is_array($hbOut) && !empty($hbOut['ok']) && !empty($hbOut['order'])) { $fillOut['half_bid'] = $hbOut['order']; try { nst_inbox_notify_half_bid($fRow, $hbOut['order']); } catch (Throwable $e) {} } } } } catch (Throwable $e) { // never abort a committed fill } // A committed recipient survives any later payout-state failure. if (!empty($fillOut['ok']) && !empty($isKingHalfBid)) { $fillOut = array_merge($fillOut, nst_ltc_reconcile_payout_fill((array)($fillOut['fill'] ?? []))); } api_out($fillOut); } if ($api === 'reputation' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); $actor = nst_actor('rater'); $rater = $actor['addr']; $rated = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['rated'] ?? ''))); $score = (int)($_POST['score'] ?? 0); $ctx = substr(trim((string)($_POST['context'] ?? '')), 0, 160); if (strlen($rated) !== 64) api_out(['ok' => false, 'err' => 'bad rated'], 400); if ($rated === $rater) api_out(['ok' => false, 'err' => 'Cannot send to yourself'], 400); if ($score < -10 || $score > 10) api_out(['ok' => false, 'err' => 'score -10..10'], 400); $ts = (int)($_POST['ts'] ?? 0); if ($ts < 1) { $ts = time(); } elseif (abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'Clock skew — refresh the page and try again'], 400); } $body = implode('|', ['reputation', '1', $rater, $rated, (string)$score, $ctx, (string)$ts]); $auth = nst_auth_sig($actor, $body); $sigMode = $auth['sig_mode']; $row = [ 'type' => 'reputation', 'ver' => 1, 'rater' => $rater, 'rated' => $rated, 'score' => $score, 'context' => $ctx, 'ts' => $ts, 'sig' => $auth['sig'], 'sig_mode' => $sigMode, ]; if (nst_sig_is_ecdsa($sigMode) && $auth['pubkey'] !== '') { $row['pubkey'] = $auth['pubkey']; } if (!append_row($row)) api_out(['ok' => false, 'err' => 'Could not save to the server — try again in a moment'], 500); $scores = rep_scores(read_chain()); api_out([ 'ok' => true, 'edge' => $row, 'rated_total' => $scores[$rated] ?? 0, 'sig_mode' => $sigMode, 'seed_on_wire' => $auth['seed_on_wire'], ]); } if ($api === 'audit') { nst_audit_api_out(read_chain(false)); } if ($api === 'verify') { /* Audit a FOREIGN chain with this empire's own verifier. * * The rows are POSTed in, never fetched by this server. Fetching a * caller-supplied URL would make every empire an SSRF proxy into its own * host's private network - and the browser can fetch the peer directly * anyway, since ?api=audit is public and CORS-enabled. The party who * wants the answer does the fetching; we only do the arithmetic. * * No auth: verification reads nothing local and reveals nothing. */ header('Access-Control-Allow-Origin: *'); $raw = (string)($_POST['rows'] ?? ''); if ($raw === '') { api_out(['ok' => false, 'err' => 'POST rows= as JSON array, or JSONL, from a peer ?api=audit'], 400); } $rows = json_decode($raw, true); if (!is_array($rows)) { // accept raw JSONL too $rows = []; foreach (preg_split('/\r?\n/', $raw) as $line) { $line = trim($line); if ($line === '') continue; $j = json_decode($line, true); if (is_array($j)) $rows[] = $j; } } if (!$rows) api_out(['ok' => false, 'err' => 'no rows parsed'], 400); if (count($rows) > 200000) api_out(['ok' => false, 'err' => 'too many rows'], 413); api_out(['ok' => true, 'verify' => nst_verify_chain($rows)]); } if ($api === 'conservation') { $force = ( ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST' && (string)($_POST['force'] ?? '') === '1' ); $rep = $force ? nst_conservation_check_and_maybe_freeze($chain) : null; if ($rep === null) { api_out(['ok' => true, 'conservation' => nst_conservation_public_view($nstConserveSnap)]); } api_out([ 'ok' => true, 'conservation' => [ 'ok' => !empty($rep['ok']), 'frozen' => !empty($rep['frozen']) || nst_conservation_is_frozen(), 'sum_fmt' => fmt_amt((string)$rep['observed_sum']), 'expected_fmt' => fmt_amt((string)$rep['expected_issued']), 'checked_ts' => (int)$rep['checked_ts'], 'int_ok' => !empty($rep['int_ok']), 'detail' => $rep['detail'] ?? null, 'freeze_path_basename' => 'conservation.freeze.json', ], ]); } if ($api === 'policy') { /* "Are we on the same money?" - a LABEL, not a proof. A king who edits * this file can make it return anything, exactly as with selfhash. The * proof is the replay: pull a peer's ?api=audit and run ?api=verify. * CORS because it is public, read-only and carries no secrets. */ header('Access-Control-Allow-Origin: *'); $missing = nsu_policy_missing(); $h = nsu_policy_hash(); /* NS_STATUS_API_HONESTY 2026-09-26 — advertise CURRENT auth, not superseded dual-path */ api_out([ 'ok' => true, 'policy' => $h, 'badge' => nsu_policy_badge($h), 'constants' => nsu_policy_constants(), 'rules' => nsu_policy_functions(), 'rules_count' => count(nsu_policy_functions()), 'missing_rules' => $missing, 'intact' => !$missing, 'auth_modes' => ['ecdsa_p256_v2'], 'seed_off_wire' => 'deterministic ecdsa v2 required for visitor mutations; seed only on legacy_wallet_migrate', 'note' => 'POLICY is a label: it says what the replay OUGHT to conclude. ' . 'Verify a peer by pulling ?api=audit and POSTing the rows to ?api=verify. ' . 'Nothing a server says about itself is evidence.', ]); } if ($api === 'selfhash') { /* RR324 P1-TRADE-SELFHASH-ECDSA-CAPS: surface seed-off-wire readiness on public selfhash (utility/safety discoverability). */ api_out([ 'ok' => true, 'version' => NST_VERSION, 'sha256' => @hash_file('sha256', __FILE__) ?: null, 'file' => basename(__FILE__), 'ecdsa_verify_ready' => nst_ecdsa_openssl_ready(), 'auth_modes' => ['ecdsa_p256_v2'], 'seed_off_wire' => 'deterministic ecdsa v2 required for visitor mutations; seed only on legacy_wallet_migrate', 'wallet_authority' => 'nsu-wallet-key-v2', 'no_free_mint' => true, ]); } /* ---- Operator console (key-combo UI + password; secrets in data/*.txt) ---- */ if ($api === 'admin_status') { $eStat = load_economy(); $opStat = strtolower(trim((string)($eStat['operator_addr'] ?? ''))); $rentedStat = !empty($eStat['rented']) && $opStat !== '' && $opStat !== strtolower($taddr); api_out([ 'ok' => true, 'admin_pass_set' => false, 'site' => 'trade', 'version' => NST_VERSION, 'epoch' => epoch_info(), 'mods_count' => count(load_mods()), 'treasury_addr' => $taddr, 'treasury_bal_fmt' => fmt_amt($bal[$taddr] ?? '0'), 'chain_len' => count($chain), 'rented' => $rentedStat, 'operator_addr' => $opStat !== '' ? $opStat : $taddr, 'lord_rent_grant' => 'retired', 'lord_rent_credit_legacy' => null, 'rent_quote_api' => 'rent_quote', 'rent_quote_get_api' => 'rent_quote_get', 'rent_settle_api' => 'rent_settle', 'rent_quotes_durable' => true, 'rent_settle' => 'transfer_memo_first_paid_wins', 'rent_nsu_per_day' => (int)NST_RENT_NSU_PER_DAY, 'lord_rent_formula' => [ 'status' => 'retired_grant_path', 'law' => 'rent is payment TO treasury (NSU_VIABILITY_V1); ?api=rent_quote live; transfer POST settles', 'nsu_per_day' => (int)NST_RENT_NSU_PER_DAY, 'faucet_start_nsu_narrative' => NST_KING_FAUCET_START_NSU, ], 'last_epoch_profit_fmt' => fmt_amt((string)($eStat['last_epoch_profit_micros'] ?? '0')), 'panel_door' => 'E3: trade panel site.seed or KING treasury (owner) or LORD operator_addr; faucet=KING only; leftover admin password dead', ]); } if ($api === 'admin_promo_mailbox' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_admin(); $box = nst_promo_mailbox_ensure(); $ingest = nst_promo_maildir_ingest_lazy(); $store = nst_promo_claims_load(); api_out([ 'ok' => true, 'address' => NST_PROMO_MAILBOX, 'username' => (string)($box['username'] ?? 'ads'), 'password' => (string)($box['password'] ?? ''), 'maildir' => 'data/promo-maildir', 'claims_used' => count($store['claims'] ?? []), 'claims_cap' => (int)NST_PROMO_AD_MAX_CLAIMS, 'bot_present' => nst_promo_bot_ensure() !== '', 'ingest' => $ingest, ]); } if ($api === 'admin_login' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_admin(); $e = load_economy(); $mid = book_mid_micros($chain); api_out([ 'ok' => true, 'msg' => 'ok', 'epoch' => epoch_info(), 'mods_count' => count(load_mods()), 'treasury_addr' => $taddr, 'treasury_bal_fmt' => fmt_amt($bal[$taddr] ?? '0'), 'chain_len' => count($chain), 'vault_hint' => vault_dir(), 'site' => 'trade', 'version' => NST_VERSION, 'economy' => [ 'profit_fmt' => fmt_amt((string)($e['profit_micros'] ?? '0')), 'ad_bid_fmt' => fmt_amt((string)($e['ad_bid_micros'] ?? '0')), 'donate_k' => (string)($e['donate_k'] ?? NST_DONATE_K), 'value_note' => (string)($e['value_note'] ?? ''), 'value_authority' => 'this_server_owner', 'mirrors_role' => 'equal_service_peers', 'mid_fmt' => $mid !== null ? fmt_amt($mid) : null, 'bids_n' => is_array($e['bids'] ?? null) ? count($e['bids']) : 0, ], 'donate_addrs' => [ 'btc' => NST_DONATE_BTC, 'xmr' => NST_DONATE_XMR, 'ltc' => NST_DONATE_LTC, ], ]); } /** Renter sets local NSU value policy (k + public note). Not a global peg. Mirrors do not inherit. */ if ($api === 'admin_set_value' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_admin(); $e = load_economy(); $k = trim((string)($_POST['donate_k'] ?? '')); if ($k !== '') { if (!preg_match('/^\d+(\.\d{1,8})?$/', $k) || (float)$k <= 0) { api_out(['ok' => false, 'err' => 'donate_k must be positive number'], 400); } $e['donate_k'] = $k; } if (array_key_exists('value_note', $_POST)) { $note = trim((string)$_POST['value_note']); if (strlen($note) > 240) $note = substr($note, 0, 240); $e['value_note'] = $note; } save_economy($e); api_out([ 'ok' => true, 'msg' => 'local value policy saved - this server owner only; mirrors stay protocol-equal service peers', 'donate_k' => (string)($e['donate_k'] ?? NST_DONATE_K), 'value_note' => (string)($e['value_note'] ?? ''), 'value_authority' => 'this_server_owner', 'mirrors_role' => 'equal_service_peers', ]); } /* LORD SOVEREIGNTY on trade — rotate the PANEL seed only. * * Same rule as the other nine crops (see any of them for the full reasoning): * the ten panel seeds are minted by genesis and printed in the king's * GENESIS-INFO.txt, so until this existed a lord's door was issued by the * landlord who kept a copy, and admin_change_pass rotated only the legacy * password - not the seed, which is the real door. * * TRADE IS THE DANGEROUS ONE, because this host holds two seeds: * * data/site.seed the LORD panel door + trade's crop wallet * data/treasury.secret the KING mint - the only thing that can issue NSU * * This endpoint touches ONLY the first. It is gated on require_admin (the * panel seed), so a lord can free their own crop, and it must never become a * path by which a lord reaches the mint. If these two files are ever * conflated, whoever rents trade owns the money supply of the whole empire. */ if ($api === 'admin_rekey' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_admin(); global $SITE_SEED_FILE, $TFILE; if ((string)($_POST['confirm'] ?? '') !== 'REKEY') { api_out([ 'ok' => false, 'err' => 'Re-keying replaces this crop\'s panel seed permanently. There is no ' . 'recovery desk. POST confirm=REKEY to proceed.', 'confirm_required' => 'REKEY', ], 400); } $oldSeed = norm_seed((string)@file_get_contents($SITE_SEED_FILE)); $oldAddr = $oldSeed !== '' ? addr_from_seed($oldSeed) : ''; $new = nst_generate_site_seed(); $newAddr = addr_from_seed($new); if ($new === '' || $newAddr === '' || $newAddr === $oldAddr) { api_out(['ok' => false, 'err' => 'seed generation failed'], 500); } /* Paranoia, cheap: never let a panel rotation collide with the mint key. */ $tsec = is_file($TFILE) ? norm_seed((string)@file_get_contents($TFILE)) : ''; if ($tsec !== '' && norm_seed($new) === $tsec) { api_out(['ok' => false, 'err' => 'generated seed collided with treasury - refused'], 500); } nst_vault_panel_seed_note($new); if (@file_put_contents($SITE_SEED_FILE, $new . "\n", LOCK_EX) === false) { api_out(['ok' => false, 'err' => 'could not write site.seed - crop unchanged, old seed still valid'], 500); } @chmod($SITE_SEED_FILE, 0600); $check = norm_seed((string)@file_get_contents($SITE_SEED_FILE)); if ($check !== norm_seed($new)) { api_out(['ok' => false, 'err' => 'readback mismatch - rotation not confirmed'], 500); } api_out([ 'ok' => true, 'seed' => $new, 'seed_shown_once' => true, 'old_addr' => $oldAddr, 'new_addr' => $newAddr, 'vault' => 'SITE_WALLET_SEED.txt', 'treasury_untouched' => true, 'next_steps' => [ 'SAVE THIS SEED OFFLINE NOW. It is shown once and there is no recovery desk.', 'The king\'s copy of the previous panel seed no longer opens this crop.', 'The KING mint (data/treasury.secret) is untouched - this rotates the panel only.', 'Your NSU is still at the OLD address - the old seed opens that wallet. ' . 'Transfer it to the new address deliberately; nothing is swept for you.', 'Panel seed rotation does not change the recipient registered with Trade or enable issuance. ' . 'If issuance is enabled, Trade policy and its recipient registry determine payment routing.', ], 'msg' => 'Panel seed rotated. This crop is now yours alone; the mint is not.', ]); } if ($api === 'admin_reveal' && $_SERVER['REQUEST_METHOD'] === 'POST') { // KING / legacy only — LORD must never receive treasury.secret (CONTRACT §3). require_faucet_auth(); global $TFILE, $MODS_FILE, $META, $CHAIN, $ADMIN_HASH_FILE; api_out([ 'ok' => true, 'admin_pass' => null, 'admin_pass_note' => 'Leftover admin password files are unlinked. Product door is site seed.', 'treasury_secret' => treasury_secret(), 'treasury_addr' => $taddr, 'vault_dir' => vault_dir(), 'paths' => [ 'admin_hash' => basename($ADMIN_HASH_FILE), 'treasury_secret' => basename($TFILE), 'modifiers' => basename($MODS_FILE), 'meta' => basename($META), 'chain' => basename($CHAIN), ], 'note' => 'KING faucet material. Renters never receive this. OS root stays SSH. Host compromise = game over for this crop.', ]); } // Renter utter control: read/replace THIS index.php (app god-mode, not OS root) if ($api === 'admin_get_source' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_admin(); $raw = (string)file_get_contents(__FILE__); api_out([ 'ok' => true, 'bytes' => strlen($raw), 'sha256' => hash('sha256', $raw), 'source' => $raw, 'warning' => 'Saving a broken file can kill this site until SSH restore.', ]); } /* KING ONLY: download a deployable virgin pack of this empire. * * Gate is require_faucet_auth() - the TREASURY seed, not the panel seed. A * lord holds his crop; only the King carries the empire, so only the King * pulls a copy of it. * * The pack lives in data/, which nginx denies and which sits under the same * protection as chain.jsonl and site.seed. It is NOT in the public web root: * the ten sites stay individually auditable via ?src=1 / ?download=1, but * the assembled, deployable bundle is an operator tool, not a public asset. * * Deploy.bat writes data/pack.zip on every deploy, already stripped of * GENESIS-INFO, seeds, vault/ and data/ by a filter plus a hard gate. The * King can therefore stand up a sister empire from a running one without * needing the machine he first deployed from. */ if ($api === 'ltc_buy_ticket' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); $actor = nst_actor('addr'); $oid = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['order_id'] ?? ''))) ?? ''; $amtIn = trim((string)($_POST['amount'] ?? '')); $amt = $amtIn === '' ? null : parse_amt($amtIn); if ($amt === null) api_out(['ok' => false, 'err' => 'Need a buy amount in NSU'], 400); $ts = (int)($_POST['ts'] ?? time()); $nonce = preg_replace('/[^a-zA-Z0-9_-]/', '', (string)($_POST['nonce'] ?? '')) ?? ''; nst_auth_sig($actor, implode('|', ['ltc_buy_ticket', '2', (string)$actor['addr'], $oid, (string)$amt, $nonce, (string)$ts])); $r = nst_ltc_ticket_open($actor['addr'], $oid, $amt, $nonce, $ts); if (empty($r['ok'])) api_out($r, 400); api_out($r); } if ($api === 'ltc_ticket_status') { $id = trim((string)($_GET['id'] ?? $_POST['id'] ?? $_GET['addr'] ?? $_POST['addr'] ?? '')); $r = nst_ltc_ticket_lookup($id); if (empty($r['ok'])) api_out($r, 404); api_out($r); } if ($api === 'ltc_watch') { api_out(nst_ltc_watch_open_tickets()); } if ($api === 'admin_ltc_status' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_ltc_require_king(); $reveal = ((string)($_POST['reveal'] ?? '') === '1'); api_out(nst_ltc_status_king($reveal)); } if ($api === 'admin_ltc_create' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); nst_ltc_require_king(); $r = nst_ltc_create_wallet(); if (empty($r['ok'])) api_out($r, 400); api_out($r); } if ($api === 'admin_ltc_reveal' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_ltc_require_king(); api_out(nst_ltc_status_king(true)); } if ($api === 'admin_ltc_lot' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); nst_ltc_require_king(); $r = nst_king_ltc_lot_post(); if (empty($r['ok'])) api_out($r, 400); api_out($r); } if ($api === 'admin_ltc_lot_cancel' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); nst_ltc_require_king(); $r = nst_king_ltc_lot_cancel((string)($_POST['order_id'] ?? '')); if (empty($r['ok'])) api_out($r, 400); api_out($r); } if ($api === 'admin_ltc_pay_list' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_ltc_require_king(); $st = nst_ltc_status_king(false); api_out([ 'ok' => true, 'pay_list' => $st['pay_list'] ?? [], 'outbound_sign' => 'honest_gap', 'outbound_sent' => false, 'remaining_gap' => function_exists('nst_ltc_outbound_gap_reasons') ? nst_ltc_outbound_gap_reasons() : [], 'note' => 'Pay list shows visitor ltc1 when they sold into a half-bid. King pays by hand. Not sent.', ]); } if ($api === 'king_virgin_pack' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_faucet_auth(); $packFile = __DIR__ . DIRECTORY_SEPARATOR . NST_DATA . DIRECTORY_SEPARATOR . 'pack.zip'; if (!is_file($packFile)) { api_out([ 'ok' => false, 'err' => 'No pack stored on this server yet - run Deploy.bat once and it publishes one.', ], 404); } $raw = (string)@file_get_contents($packFile); if ($raw === '') { api_out(['ok' => false, 'err' => 'Pack file unreadable on the server — try again or re-upload'], 500); } /* Paranoia: never hand out a bundle carrying seed material, even though * the client filtered it. Cheap to check, catastrophic to skip. */ foreach (['GENESIS-INFO', 'treasury.secret', 'ltc.secret', 'ltc-wallet.json', 'SITE_WALLET_SEED', 'KING_FAUCET_SEED'] as $needle) { if (strpos($raw, $needle) !== false) { api_out([ 'ok' => false, 'err' => 'pack refused: it contains seed-shaped material. Re-publish it with Deploy.bat.', ], 500); } } header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="NSU-virgin.zip"'); header('Content-Length: ' . strlen($raw)); header('X-NS-Sha256: ' . hash('sha256', $raw)); header('Cache-Control: no-store'); echo $raw; exit; } if ($api === 'admin_put_source' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); require_admin(); $src = (string)($_POST['source'] ?? ''); if (strlen($src) < 100) api_out(['ok' => false, 'err' => 'source too short'], 400); if (strpos($src, ' false, 'err' => 'must look like PHP'], 400); $bak = __FILE__ . '.bak.' . time(); @copy(__FILE__, $bak); if (file_put_contents(__FILE__, $src, LOCK_EX) === false) { api_out(['ok' => false, 'err' => 'write failed - check perms'], 500); } api_out([ 'ok' => true, 'msg' => 'index.php replaced', 'backup' => basename($bak), 'sha256' => hash('sha256', $src), 'bytes' => strlen($src), ]); } if ($api === 'admin_pay' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); // Free-form treasury pay = KING faucet authority (not LORD panel seed alone). require_faucet_auth(); $to = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['to'] ?? ''))); $micros = parse_amt(trim((string)($_POST['amount'] ?? ''))); $memo = substr(trim((string)($_POST['memo'] ?? 'admin:pay')), 0, 120); if (strlen($to) !== 64) api_out(['ok' => false, 'err' => 'Bad destination address — need a 64-char hex wallet address'], 400); if ($micros === null || dec_cmp($micros, '1') < 0) api_out(['ok' => false, 'err' => 'Amount must be a positive number (at least 0.000001 NSU)'], 400); $rid = nst_op_request_id(); $sec = treasury_secret(); $from = addr_from_seed($sec); $ts = time(); $nonce = $rid !== '' ? nst_op_request_nonce('admin_pay', $rid) : bin2hex(random_bytes(8)); $body = implode('|', ['transfer', '1', $from, $to, $micros, $nonce, (string)$ts, $memo]); $row = [ 'type' => 'transfer', 'ver' => 1, 'from' => $from, 'to' => $to, 'amount' => $micros, 'amount_fmt' => fmt_amt($micros), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => mac_sign($sec, $body), 'kind' => 'admin_pay', ]; $payOut = nst_with_chain_lock(function (array $chain, callable $append) use ($from, $to, $micros, $nonce, $row, $rid) { foreach ($chain as $r) { if (($r['type'] ?? '') === 'transfer' && ($r['from'] ?? '') === $from && ($r['nonce'] ?? '') === $nonce) { if ($rid !== '' && ($r['kind'] ?? '') === 'admin_pay' && ($r['to'] ?? '') === $to && ($r['amount'] ?? '') === $micros && ($r['memo'] ?? '') === $row['memo']) { return ['ok' => true, 'idempotent' => true, 'replay' => true, 'tx' => $r]; } if ($rid !== '') return ['ok' => false, 'http' => 409, 'err' => 'That request_id was already used with different terms']; return ['ok' => false, 'http' => 400, 'err' => 'That transfer was already sent (nonce replay) — use a new nonce']; } } $bal = balances($chain); $have = $bal[$from] ?? '0'; if (dec_cmp($have, $micros) < 0) { return ['ok' => false, 'http' => 400, 'err' => 'Treasury does not have enough free NSU for that — try a smaller amount', 'have_fmt' => fmt_amt($have)]; } if (!$append($row)) { return ['ok' => false, 'http' => 500, 'err' => 'Could not save to the server — try again in a moment']; } return ['ok' => true, 'tx' => $row, 'treasury_bal_fmt' => fmt_amt(dec_sub($have, $micros))]; }); if ($payOut === null) { api_out(['ok' => false, 'err' => 'Could not save to the server — try again in a moment'], 500); } if (empty($payOut['ok'])) { $http = (int)($payOut['http'] ?? 400); unset($payOut['http']); api_out($payOut, $http); } api_out($payOut); } /** * OPERATOR_GO §4 / E2a: KING seeds real fillable SELL-ask ladder on the book. * Not free giveaway — visitors BUY by filling these asks (NOTE settle_ref off-chain). * Each treasury SELL auto-arms C2 half-BID. Lords cannot call (require_faucet_auth). */ if ($api === 'admin_treasury_ladder' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); require_faucet_auth(); // Pre-read treasury material OUTSIDE chain lock (deadlock law; same as C2 half-bid). $tsec = treasury_secret(); $taddr = strtolower(addr_from_seed($tsec)); $amtIn = trim((string)($_POST['amount'] ?? '')); $amtMicros = null; if ($amtIn !== '') { $amtMicros = parse_amt($amtIn); if ($amtMicros === null || dec_cmp($amtMicros, '1') < 0) { api_out(['ok' => false, 'err' => 'Amount must be a positive number (at least 0.000001 NSU)'], 400); } } $ladder = nst_treasury_ask_ladder($tsec, $taddr, $amtMicros); if (empty($ladder['ok'])) { $http = 400; $err = (string)($ladder['err'] ?? ''); if ($err === 'disk' || str_contains($err, 'Could not save') || str_contains($err, 'half-bid failed')) { $http = 500; } api_out($ladder, $http); } // Ladder half-BID notices: AFTER money/book writes, fail-open. try { $hbList = is_array($ladder['half_bids'] ?? null) ? $ladder['half_bids'] : []; $ordList = is_array($ladder['orders'] ?? null) ? $ladder['orders'] : []; foreach ($hbList as $i => $hb) { if (!is_array($hb)) continue; $sell = is_array($ordList[$i] ?? null) ? $ordList[$i] : null; nst_inbox_notify_half_bid($sell, $hb); } } catch (Throwable $e) { // never abort a committed ladder } api_out($ladder); } /** Credit a donate: gift NSU to addr (mid×k or direct), 50/50 profit/ad-bid accounting */ if ($api === 'admin_donate_credit' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); // Donate gifts pull from treasury faucet — KING only (CONTRACT §3). require_faucet_auth(); $to = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['to'] ?? ''))); if (strlen($to) !== 64) api_out(['ok' => false, 'err' => 'Bad destination address — need a 64-char hex wallet address'], 400); $txid = substr(trim((string)($_POST['txid'] ?? '')), 0, 128); $asset = strtoupper(substr(trim((string)($_POST['asset'] ?? 'BTC')), 0, 16)); $ext = trim((string)($_POST['ext_amount'] ?? '')); $direct = trim((string)($_POST['nsu_amount'] ?? '')); $e = load_economy(); $k = (string)($e['donate_k'] ?? NST_DONATE_K); $mid = book_mid_micros(read_chain()); if ($direct !== '') { $nsu = parse_amt($direct); } else { $nsu = donate_nsu_from_external($ext !== '' ? $ext : '0', $mid, $k); } if ($nsu === null || dec_cmp($nsu, '1') < 0) { api_out(['ok' => false, 'err' => 'Amount must be a positive number (at least 0.000001 NSU)', 'mid_fmt' => $mid !== null ? fmt_amt($mid) : null, 'k' => $k], 400); } $r = economy_credit_donate($to, $nsu, $txid, $asset, $ext !== '' ? $ext : $direct, nst_op_request_id()); if (empty($r['ok'])) { $h = (int)($r['http'] ?? (isset($r['err']) && str_contains((string)$r['err'], 'treasury') ? 503 : 400)); unset($r['http']); api_out($r, $h); } $r['mid_fmt'] = $mid !== null ? fmt_amt($mid) : null; $r['k'] = $k; api_out($r); } /** Extract profit pile → pay operator addr from treasury; reduce profit accounting */ if ($api === 'admin_extract_profit' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); // Treasury debit = faucet authority (KING only). LORD spends funded wallet, never treasury.secret. require_faucet_auth(); $to = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['to'] ?? ''))); $micros = parse_amt(trim((string)($_POST['amount'] ?? ''))); if (strlen($to) !== 64) api_out(['ok' => false, 'err' => 'Bad destination address — need a 64-char hex wallet address'], 400); if ($micros === null || dec_cmp($micros, '1') < 0) api_out(['ok' => false, 'err' => 'Amount must be a positive number (at least 0.000001 NSU)'], 400); $rid = nst_op_request_id(); $sec = treasury_secret(); $from = addr_from_seed($sec); $ts = time(); $nonce = $rid !== '' ? nst_op_request_nonce('admin_extract_profit', $rid) : bin2hex(random_bytes(8)); $memo = 'profit:extract'; $body = implode('|', ['transfer', '1', $from, $to, $micros, $nonce, (string)$ts, $memo]); $row = [ 'type' => 'transfer', 'ver' => 1, 'from' => $from, 'to' => $to, 'amount' => $micros, 'amount_fmt' => fmt_amt($micros), 'nonce' => $nonce, 'ts' => $ts, 'memo' => $memo, 'sig' => mac_sign($sec, $body), 'kind' => 'profit_extract', ]; // Chain lock covers treasury debit; re-load economy under lock so pile cannot double-extract. $exOut = nst_with_chain_lock(function (array $chain, callable $append) use ($from, $to, $micros, $nonce, $row, $rid) { if ($rid !== '') { foreach ($chain as $r) { if (($r['type'] ?? '') !== 'transfer' || ($r['from'] ?? '') !== $from || ($r['nonce'] ?? '') !== $nonce) continue; if (($r['kind'] ?? '') === 'profit_extract' && ($r['to'] ?? '') === $to && ($r['amount'] ?? '') === $micros) return ['ok' => true, 'idempotent' => true, 'replay' => true, 'tx' => $r]; return ['ok' => false, 'http' => 409, 'err' => 'That request_id was already used with different terms']; } } $e = load_economy(); $pile = (string)($e['profit_micros'] ?? '0'); if (dec_cmp($pile, $micros) < 0) { return ['ok' => false, 'http' => 400, 'err' => 'profit pile too small', 'have_fmt' => fmt_amt($pile)]; } foreach ($chain as $r) { if (($r['type'] ?? '') === 'transfer' && ($r['from'] ?? '') === $from && ($r['nonce'] ?? '') === $nonce) { return ['ok' => false, 'http' => 400, 'err' => 'That transfer was already sent (nonce replay) — use a new nonce']; } } $bal = balances($chain); $have = $bal[$from] ?? '0'; if (dec_cmp($have, $micros) < 0) { return ['ok' => false, 'http' => 400, 'err' => 'Treasury does not have enough free NSU for that — try a smaller amount', 'have_fmt' => fmt_amt($have)]; } if (!$append($row)) { return ['ok' => false, 'http' => 500, 'err' => 'Could not save to the server — try again in a moment']; } $e['profit_micros'] = dec_sub($pile, $micros); save_economy($e); return [ 'ok' => true, 'tx' => $row, 'profit_fmt' => fmt_amt((string)$e['profit_micros']), 'treasury_bal_fmt' => fmt_amt(dec_sub($have, $micros)), ]; }); if ($exOut === null) { api_out(['ok' => false, 'err' => 'Could not save to the server — try again in a moment'], 500); } if (empty($exOut['ok'])) { $http = (int)($exOut['http'] ?? 400); unset($exOut['http']); api_out($exOut, $http); } api_out($exOut); } if ($api === 'economy') { $e = load_economy(); $mid = book_mid_micros(read_chain()); $active = array_values(array_filter($e['bids'] ?? [], fn($b) => !empty($b['active']))); $withCreative = 0; foreach ($active as $b) { if (!empty($b['creative_url']) || !empty($b['creative_file'])) $withCreative++; } api_out([ 'ok' => true, 'profit_fmt' => fmt_amt((string)($e['profit_micros'] ?? '0')), 'ad_bid_fmt' => fmt_amt((string)($e['ad_bid_micros'] ?? '0')), 'donate_k' => (string)($e['donate_k'] ?? NST_DONATE_K), 'mid_fmt' => $mid !== null ? fmt_amt($mid) : null, 'epoch_n' => (int)(load_meta()['epoch_n'] ?? 1), 'donate_addrs' => [ 'btc' => NST_DONATE_BTC, 'xmr' => NST_DONATE_XMR, 'ltc' => NST_DONATE_LTC, ], 'active_bids' => $active, 'bids_with_creative' => $withCreative, 'house_chance_pct' => ad_unsold_pct(ad_paid_stake_total($e)), 'board_full_micros' => NST_AD_BOARD_FULL_MICROS, 'board_full_fmt' => fmt_amt(NST_AD_BOARD_FULL_MICROS), 'ad_board_micros' => ad_paid_stake_total($e), ]); } /** Public catalogue of in-file purpose brains (no secrets; ethos visibility). */ if ($api === 'brains') { api_out([ 'ok' => true, 'site' => 'trade', 'version' => NST_VERSION, 'doctrine' => 'tiny in-file specialists only; no external LLM APIs; fail open when unsure', 'brains' => [ ['id' => 'memo_risk', 'job' => 'scam tokens in transfer memos', 'mode' => 'soft flag'], ['id' => 'order_sanity', 'job' => 'side/amount/price sanity', 'mode' => 'soft flag'], ['id' => 'pair_sane', 'job' => 'book pair format NSU/NOTE…', 'mode' => 'soft flag'], ['id' => 'wipe_copy', 'job' => 'balances-only year wipe banner copy', 'mode' => 'coherence'], ['id' => 'ad_png', 'job' => 'PNG magic+IHDR geometry + byte budget', 'mode' => 'hard reject + soft flag'], ['id' => 'href_risk', 'job' => 'http(s) click-through schemes', 'mode' => 'hard scheme + soft phishing'], ['id' => 'year_wipe_rekey', 'job' => 'rotate panel password into vault on epoch roll', 'mode' => 'automatic'], ['id' => 'house_ads', 'job' => 'unsold board remainder is a single trade placeholder (buy ad share)', 'mode' => 'weighted vs BOARD_FULL'], ['id' => 'bootstrap_spread', 'job' => 'loose BUY@0.5/SELL@1.5 around 1.0 if zero open orders', 'mode' => 'seed discovery only'], ['id' => 'panel_seed', 'job' => 'controlpanel unlock via site wallet seed (treasury)', 'mode' => 'no recovery'], ], 'contact' => NST_CONTACT_EMAIL, 'ad_png_rules' => [ 'max_bytes' => NST_AD_MAX_PNG, 'min_w' => NST_AD_MIN_W, 'max_w' => NST_AD_MAX_W, 'min_h' => NST_AD_MIN_H, 'max_h' => NST_AD_MAX_H, 'formats' => ['PNG only'], ], 'wipe' => 'Wallet balances and existing access records survive yearly wipe; profit/ads/orders burn', ]); } /** Weighted random creative for site-local adboard (paid bids + house discovery) */ /* ───────────────────────────────────────────────────────────────────────── * AD STAKE — the sink. Spend NSU, get impression weight. * * WEIGHTED LOTTERY, NOT AN AUCTION. ad_pick_weighted() gives every staker a * share of impressions proportional to their stake. A winner-take-all * auction would hand the slot to the largest wallet and show everyone else * nothing, which is a platform deciding who gets seen — refused by the * ethos. Here 1 NSU buys 1% of the WHOLE board on an empty board * (100 NSU = 100% = NST_AD_BOARD_FULL_MICROS). Unsold remainder is a * placeholder, not a fake paid ad and not a sister-site carousel. * * SHARE IS OF THE WHOLE BOARD. P(paid i) = stake_i / BOARD_FULL while * capacity remains. P(placeholder) = unsold / BOARD_FULL. When paid * total >= BOARD_FULL, placeholder chance is 0 and the lottery is * relative among buyers. A 1% buy is 1% of views, not ~82%. * * And because a buy purchases the REMAINDER OF THE EPOCH, the same coins * are worth less the later they are spent. That decay is the board's * price signal and it is emergent - nobody sets it. * * The stake CIRCULATES: King 0.01% always; Trade 0.01% when the board is * another cell; the rest to that site's lord. On trade itself there is no * second marketplace cut — Trade-as-lord takes the remainder. Nothing is * minted here. Ads buy their own real estate in a dedicated slot; they never * buy rank in someone else's organic results. * ───────────────────────────────────────────────────────────────────────── */ // WAVEB-TR14-PURCHASE-STATUS: idempotent outcome lookup (no new charge). if ($api === 'ad_purchase_status' && ($_SERVER['REQUEST_METHOD'] === 'GET' || $_SERVER['REQUEST_METHOD'] === 'POST')) { $actor = nst_actor('from'); $payer = $actor['addr']; $nonceIn = $_POST['nonce'] ?? $_GET['nonce'] ?? ''; $epochIn = $_POST['epoch_n'] ?? $_GET['epoch_n'] ?? ''; if (function_exists('nst_nonce_parse')) { $np = nst_nonce_parse($nonceIn); if (empty($np['ok'])) api_out(['ok' => false, 'err' => (string)$np['err']], 400); $nonce = $np['nonce']; } else { $nonce = substr(trim((string)$nonceIn), 0, 64); } if (function_exists('nst_epoch_n_parse')) { $ep = nst_epoch_n_parse($epochIn); if (empty($ep['ok'])) { $ei = nst_epoch_info_read_only(); $epochN = (int)($ei['epoch_n'] ?? 1); } else { $epochN = (int)$ep['epoch_n']; } } else { $epochN = (int)$epochIn; } $ts = (int)($_POST['ts'] ?? $_GET['ts'] ?? 0); if ($ts < 1 || abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'Clock skew — refresh and retry status'], 400); } $body = implode('|', ['ad_purchase_status', '1', $payer, $nonce, (string)$epochN, (string)$ts]); nst_auth_sig($actor, $body); $st = function_exists('nst_ad_purchase_status_for') ? nst_ad_purchase_status_for($payer, $nonce, $epochN) : ['ok' => false, 'err' => 'status helper missing']; api_out($st, !empty($st['ok']) ? 200 : 400); } if ($api === 'ad_stake' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); $actor = nst_actor('from'); $from = $actor['addr']; $wantShares = 0; $imgBin = nst_ad_read_upload_bin(); $imgBrain = $imgBin !== null ? nst_brain_ad_image($imgBin) : null; // WAVEB-TR05-AD-STAKE-INPUTS: strict scalar money/nonce before any debit path. if (isset($_POST['amount']) && (is_array($_POST['amount']) || is_object($_POST['amount']) || is_bool($_POST['amount']))) { api_out(['ok' => false, 'err' => 'amount must be a scalar string'], 400); } $moneyIn = function_exists('nst_money_parse_input') ? nst_money_parse_input($_POST['amount'] ?? null, false) : ['ok' => false, 'micros' => parse_amt(trim((string)($_POST['amount'] ?? ''))), 'err' => '']; $micros = array_key_exists('micros', $moneyIn) ? $moneyIn['micros'] : parse_amt(trim((string)($_POST['amount'] ?? ''))); $href = trim((string)($_POST['href'] ?? '')); if ($href !== '' && !preg_match('#^https?://#i', $href)) { api_out(['ok' => false, 'err' => 'Link must start with http:// or https://'], 400); } $label = substr(trim((string)($_POST['label'] ?? '')), 0, 80); $creativeUrl = trim((string)($_POST['creative_url'] ?? '')); if ($creativeUrl !== '' && !preg_match('#^https?://#i', $creativeUrl)) { api_out(['ok' => false, 'err' => 'Creative URL must start with http:// or https://'], 400); } $nonceRaw = $_POST['nonce'] ?? null; if ($nonceRaw === null || $nonceRaw === '') { $nonce = bin2hex(random_bytes(8)); } elseif (function_exists('nst_nonce_parse')) { $np = nst_nonce_parse($nonceRaw); if (empty($np['ok'])) api_out(['ok' => false, 'err' => (string)$np['err']], 400); $nonce = $np['nonce']; } else { $nonce = substr(trim((string)$nonceRaw), 0, 64); } $ts = (int)($_POST['ts'] ?? 0); if ($ts < 1 || abs($ts - time()) > 600) { api_out(['ok' => false, 'err' => 'Clock skew — refresh the page and try again'], 400); } /* Refuse a stake with nothing to show. Taking payment for a board slot * that can never render anything is the dishonest half of the bug fixed * in ad_pick_weighted(); this is the other half. Adding to an existing * bid is fine - that one already carries its creative. */ $crop = nst_ad_site_norm((string)($_POST['crop'] ?? $_POST['site'] ?? '')); if ($crop === '') { api_out(['ok' => false, 'err' => 'Pick the site the ad runs on'], 400); } $otherSite = ($crop !== 'trade'); $cat = nst_ad_site_catalog()[$crop]; $daysLeft = nst_ad_slot_days_left(); $amountNsu = nst_ad_slot_price_nsu(); $priced = parse_amt((string)$amountNsu); if ($priced === null || dec_cmp($priced, '1') < 0) { api_out(['ok' => false, 'err' => 'Slot quote invalid'], 500); } $micros = $priced; $soldNow = nst_ad_slot_sold_count($crop); if ($soldNow >= 10000) { api_out(['ok' => false, 'err' => 'Board full this epoch'], 400); } // WAVEC-TR27-CHECKOUT-CONFIRM: deliberate confirmation before any charge path. if (function_exists('nst_ad_checkout_confirm_required')) { $cf = nst_ad_checkout_confirm_required($_POST); if (empty($cf['ok'])) { api_out(['ok' => false, 'err' => (string)($cf['err'] ?? 'confirm required'), 'code' => 'confirm_required', 'confirm_required' => '1'], 400); } } $readyBuy = nst_ad_spend_ready($crop); if (empty($readyBuy['ok'])) { api_out(['ok' => false, 'err' => (string)($readyBuy['why'] ?? 'Spend path not ready'), 'spend' => false, 'quote' => true], 503); } if ($imgBrain === null || empty($imgBrain['ok'])) { api_out(['ok' => false, 'err' => (string)(($imgBrain['err'] ?? '') ?: 'Load a png/jpg/gif first')], 400); } if ($label === '') $label = 'slot'; $tradeLord = nst_ensure_operator_addr(); $king = nst_treasury_addr_safe(); if ($otherSite) { $lord = nst_crop_lord_addr($crop); if ($lord === '') { api_out(['ok' => false, 'err' => 'That site has no lord wallet on the registry yet'], 503); } } else { $lord = $tradeLord; } if ($lord === '' && $king === '') { api_out(['ok' => false, 'err' => 'This crop has no operator wallet yet'], 503); } if ($lord === '') $lord = $king; if ($king === '') $king = $lord; if ($tradeLord === '') $tradeLord = $lord; if ($from === $lord) { api_out(['ok' => false, 'err' => 'The operator cannot stake into their own board'], 400); } $split = nst_ad_purchase_split($micros, $otherSite); $body = implode('|', ['ad_stake', '1', $from, $micros, $crop, $nonce, (string)$ts]); $auth = nst_auth_sig($actor, $body); // WAVEB2-TR10-DURABLE-COMMIT: economy-tx lock → stage → outcome → chain → publish. // Gate above still blocks live buys until TR-10/11 acceptance reopens spend_ready. $epNow = nst_epoch_info_read_only(); $epochN = (int)($epNow['epoch_n'] ?? 1); $outs = []; $addOut = static function (array &$outs, string $addr, string $amt): void { if ($addr === '' || dec_cmp($amt, '0') <= 0) return; foreach ($outs as $i => $o) { if (($o['addr'] ?? '') === $addr) { $outs[$i]['amount'] = dec_add((string)$o['amount'], $amt); return; } } $outs[] = ['addr' => $addr, 'amount' => $amt]; }; $addOut($outs, $lord, (string)$split['lord']); if ($otherSite) $addOut($outs, $tradeLord, (string)$split['trade']); $addOut($outs, $king, (string)$split['king']); if (function_exists('nst_econ_validate_outputs')) { $vov = nst_econ_validate_outputs($outs, $micros); if (empty($vov['ok'])) { api_out(['ok' => false, 'err' => 'Purchase outputs failed conservation checks'], 400); } $outs = $vov['outputs']; } $creativeSha = hash('sha256', (string)$imgBin); $quoteId = strtolower(trim((string)($_POST['quote_id'] ?? ''))); // TR-02: posted quote_exp and other quote_* fields are witnesses only. // They do not build the authoritative quote and cannot satisfy the check. $clientQuote = null; $witnessMap = [ 'quote_exp' => 'exp', 'quote_epoch_n' => 'epoch_n', 'quote_price_micros' => 'price_micros', 'quote_days_left' => 'days_left', 'quote_rule_ver' => 'rule_ver', 'quote_issued_at' => 'issued_at', 'quote_nsu_per_day' => 'nsu_per_day', 'quote_site' => 'site', 'quote_schema' => 'schema', 'quote_currency' => 'currency', 'quote_creative_sha256' => 'creative_sha256', ]; $witness = []; foreach ($witnessMap as $postKey => $canonKey) { if (!isset($_POST[$postKey]) || is_array($_POST[$postKey]) || is_object($_POST[$postKey])) { continue; } if ((string)$_POST[$postKey] === '') { continue; } $witness[$canonKey] = (string)$_POST[$postKey]; } if ($witness !== []) { $witness['quote_id'] = $quoteId; $clientQuote = $witness; } $intent = [ 'schema' => 'nsu-econ-intent-v2', 'op' => 'ad_purchase', 'network' => 'nsu', 'payer' => $from, 'site' => $crop, 'epoch_n' => (string)$epochN, 'quote_id' => $quoteId, 'amount_micros' => $micros, 'creative_sha256' => $creativeSha, 'href' => $href, 'label' => $label, 'slot_mode' => 'any_available', 'nonce' => $nonce, 'ts' => (string)$ts, 'exp' => (string)($ts + 600), ]; $requestDigest = function_exists('nst_econ_intent_v2_digest') ? nst_econ_intent_v2_digest($intent) : hash('sha256', j($intent)); if (!function_exists('nst_ad_purchase_commit_v2')) { api_out(['ok' => false, 'err' => 'Durable purchase path not installed'], 503); } $commit = nst_ad_purchase_commit_v2([ 'payer' => $from, 'site' => $crop, 'epoch_n' => $epochN, 'amount_micros' => $micros, 'outputs' => $outs, 'quote_id' => $quoteId, 'quote' => null, 'client_quote' => $clientQuote, 'request_digest' => $requestDigest, 'nonce' => $nonce, 'ts' => $ts, 'exp' => $ts + 600, 'creative_bin' => (string)$imgBin, 'creative_ext' => (string)($imgBrain['ext'] ?? 'png'), 'creative_sha256' => $creativeSha, 'href' => $href, 'label' => $label, 'lord_micros' => (string)$split['lord'], 'king_tax_micros' => (string)$split['king'], 'trade_cut_micros' => (string)$split['trade'], 'auth_sig' => (string)($auth['sig'] ?? ''), 'auth_sig_mode' => (string)($auth['sig_mode'] ?? ''), 'other_site' => $otherSite, ]); if (empty($commit['ok'])) { $code = (string)($commit['code'] ?? ''); $http = 400; if ($code === 'busy' || $code === 'lock_unavailable') $http = 503; if ($code === 'committed_pending_delivery' || $code === 'repair_required') $http = 202; api_out([ 'ok' => false, 'err' => (string)($commit['err'] ?? 'stake failed'), 'code' => $code, 'tx_id' => (string)($commit['tx_id'] ?? ''), 'state' => (string)($commit['state'] ?? $code), 'spend' => false, ], $http); } $slotN = (int)($commit['slot'] ?? -1); $e = load_economy(); $total = function_exists('ad_paid_stake_total') ? ad_paid_stake_total($e, $crop) : '0'; $mine = (string)($commit['amount_micros'] ?? $micros); api_out([ 'ok' => true, 'bid_id' => (string)($commit['bid_id'] ?? ''), 'tx_id' => (string)($commit['tx_id'] ?? ''), 'schema' => 'nsu-ad-purchase-v2', 'state' => (string)($commit['state'] ?? 'delivered'), 'idempotent' => !empty($commit['idempotent']), 'crop' => $crop, 'site_name' => $cat['name'], 'staked_fmt' => fmt_amt($micros), 'your_stake_fmt' => fmt_amt($mine), 'board_total_fmt' => fmt_amt($total), 'share_pct' => function_exists('ad_chance_pct') ? ad_chance_pct($mine, $total) : 0, 'lord_fmt' => fmt_amt($split['lord']), 'king_tax_fmt' => fmt_amt($split['king']), 'trade_cut_fmt' => fmt_amt($split['trade']), 'king_tax_ppm' => NST_KING_TAX_PPM, 'trade_cut_ppm' => $otherSite ? NST_TRADE_CUT_PPM : 0, 'split_note' => $otherSite ? ('King 0.01% + Trade 0.01%; rest to ' . $cat['name'] . ' lord') : 'King 0.01%; rest to Trade as lord (no second marketplace cut)', 'display_chance_pct' => function_exists('ad_chance_pct') ? ad_chance_pct($mine, $total) : 0, 'house_chance_pct' => function_exists('ad_unsold_pct') ? ad_unsold_pct($total) : 0, 'viewer_chance_pct' => function_exists('ad_chance_pct') ? ad_chance_pct($mine, $total) : 0, 'board_full_micros' => NST_AD_BOARD_FULL_MICROS, 'board_full_fmt' => fmt_amt(NST_AD_BOARD_FULL_MICROS), 'empire_domains' => NST_EMPIRE_DOMAINS, 'refundable' => false, 'runs_until' => 'epoch_wipe', 'runs_until_note' => 'Runs until the yearly wipe, then the whole board clears.', 'slot' => $slotN, 'slot_pad' => (string)($commit['slot_pad'] ?? nst_ad_slot_pad($slotN)), 'public_url' => (string)($commit['public_url'] ?? nst_ad_public_url($crop, $slotN)), 'shares_bought' => 1, 'shares_you' => 1, 'shares_remain' => 10000 - nst_ad_slot_sold_count($crop), 'share_unit_fmt' => fmt_amt($micros), 'creative_sha256' => (string)($commit['creative_sha256'] ?? ''), 'request_digest' => $requestDigest, 'epoch_n' => $epochN, 'msg' => 'Ad live in slot ' . (string)($commit['slot_pad'] ?? nst_ad_slot_pad($slotN)) . ' on ' . $crop . '. Faces roll 0–9999 and fetch ' . (string)($commit['public_url'] ?? '') . '. Coins are spent, not held. No refunds. Runs until the yearly wipe.', ]); } /* SlowGPT paid parallel: circulation lord-receipt. No mint. No ad creative. * Amount locked to 0.1/0.2/0.3 NSU for parallel 2/3/4. Crop lord + KING tax * via nst_fee_split (same lord-receipt rule as NATURAL NEXT). */ if ($api === 'slowgpt_reserve' && $_SERVER['REQUEST_METHOD'] === 'POST') { if (!nst_slowgpt_paid_enabled()) { api_out(['ok' => false, 'err' => 'Paid SlowGPT is not active. No NSU charged. Use the free INFO mode.', 'code' => 'SLOWGPT_PAID_UNAVAILABLE'], 503); } nst_require_writes_unfrozen(); $actor = nst_actor('from'); $from = $actor['addr']; $parallel = (int)($_POST['parallel'] ?? 0); $quote = nst_slowgpt_parallel_quote($parallel); if ($quote === null) { api_out(['ok' => false, 'err' => 'Parallel must be 2, 3, or 4 (1 is free on .info)'], 400); } $posted = trim((string)($_POST['amount'] ?? $_POST['cost_nsu'] ?? '')); if ($posted === '') $posted = $quote; $micros = parse_amt($posted); $expect = parse_amt($quote); if ($micros === null || $expect === null || dec_cmp($micros, $expect) !== 0) { api_out(['ok' => false, 'err' => 'Amount must be exactly ' . $quote . ' NSU for parallel ' . $parallel], 400); } $product = strtolower(trim((string)($_POST['product'] ?? 'slowgpt'))); if ($product !== 'slowgpt') { api_out(['ok' => false, 'err' => 'product must be slowgpt'], 400); } $crop = nst_ad_site_norm((string)($_POST['crop'] ?? 'info')); if ($crop === '') { api_out(['ok' => false, 'err' => 'Unknown crop'], 400); } $jobId = substr(trim((string)($_POST['job_id'] ?? '')), 0, 80); if ($jobId === '' || !preg_match('/^[A-Za-z0-9._:-]{4,80}$/', $jobId)) { api_out(['ok' => false, 'err' => 'job_id required'], 400); } $nonce = substr(trim((string)($_POST['nonce'] ?? bin2hex(random_bytes(8)))), 0, 64); $ts = (int)($_POST['ts'] ?? time()); if ($ts < 1) $ts = time(); $tradeLord = nst_ensure_operator_addr(); $king = nst_treasury_addr_safe(); $lord = ($crop !== 'trade') ? nst_crop_lord_addr($crop) : $tradeLord; if ($lord === '') $lord = $tradeLord; if ($lord === '' && $king === '') { api_out(['ok' => false, 'err' => 'This crop has no operator wallet yet'], 503); } if ($lord === '') $lord = $king; if ($king === '') $king = $lord; /* Lord receipt tax: nst_fee_split only (King 0.01% min 1 micro). Not ad_purchase_split. */ $split = nst_fee_split($micros); $body = implode('|', ['slowgpt_reserve', '1', $from, $micros, $crop, $jobId, (string)$parallel, $nonce, (string)$ts]); $auth = nst_auth_sig($actor, $body); $res = nst_with_chain_lock(function (array $chain, callable $append) use ( $from, $lord, $king, $micros, $split, $nonce, $ts, $auth, $crop, $jobId, $parallel, $quote ) { foreach ($chain as $row) { if (!is_array($row)) continue; if (($row['type'] ?? '') === 'slowgpt_reserve' && (string)($row['from'] ?? '') === $from && (string)($row['job_id'] ?? '') === $jobId) { // Retries may use a fresh nonce/timestamp, but never a different purchase. if ((string)($row['product'] ?? '') !== 'slowgpt' || (string)($row['crop'] ?? '') !== $crop || (int)($row['parallel'] ?? 0) !== $parallel || dec_cmp((string)($row['amount'] ?? '0'), $micros) !== 0 || (string)($row['reservation_id'] ?? '') === '') { return ['ok' => false, 'err' => 'Reservation conflict: this payer/job already has a different purchase']; } return [ 'ok' => true, 'idempotent' => true, 'reservation_id' => (string)($row['reservation_id'] ?? ''), ]; } } $bal = balances($chain); $have = (string)($bal[$from] ?? '0'); $av = available_nsu($bal, $chain, $from); if (dec_cmp($av, $micros) < 0) { return ['ok' => false, 'err' => 'Not enough free NSU — you hold ' . fmt_amt($have) . ', free ' . fmt_amt($av) . ' (the rest backs open sell orders)']; } $outs = []; $addOut = static function (array &$outs, string $addr, string $amt): void { if ($addr === '' || dec_cmp($amt, '0') <= 0) return; foreach ($outs as $i => $o) { if (($o['addr'] ?? '') === $addr) { $outs[$i]['amount'] = dec_add((string)$o['amount'], $amt); return; } } $outs[] = ['addr' => $addr, 'amount' => $amt]; }; $addOut($outs, $lord, (string)$split['lord']); $addOut($outs, $king, (string)$split['king']); $rid = hash('sha256', $jobId . '|' . $from . '|' . $ts . '|' . $nonce); $row = [ 'type' => 'slowgpt_reserve', 'ver' => 1, 'from' => $from, 'amount' => $micros, 'amount_fmt' => fmt_amt($micros), 'outputs' => $outs, 'crop' => $crop, 'product' => 'slowgpt', 'job_id' => $jobId, 'parallel' => $parallel, 'reservation_id' => $rid, 'lord_micros' => $split['lord'], 'king_tax_micros' => $split['king'], 'king_tax_ppm' => NST_KING_TAX_PPM, 'nonce' => $nonce, 'ts' => $ts, 'sig' => $auth['sig'], 'sig_mode' => $auth['sig_mode'], 'note' => 'SlowGPT parallel lord-receipt: circulates via nst_fee_split (King 0.01%, min 1 micro). No mint. Does not buy an ad slot.', ]; if (!$append($row)) return ['ok' => false, 'err' => 'Could not save to the server — try again in a moment']; return ['ok' => true, 'reservation_id' => $rid]; }); if (empty($res['ok'])) { api_out(['ok' => false, 'err' => (string)($res['err'] ?? 'reserve failed')], 400); } api_out([ 'ok' => true, 'reservation_id' => (string)($res['reservation_id'] ?? ''), 'idempotent' => !empty($res['idempotent']), 'job_id' => $jobId, 'parallel' => $parallel, 'amount_fmt' => $quote, 'crop' => $crop, 'msg' => 'SlowGPT parallel reserved. Coins circulated. No mint.', ]); } if ($api === 'ad_mine' && $_SERVER['REQUEST_METHOD'] === 'POST') { $actor = nst_actor('from'); $from = $actor['addr']; $ts = (int)($_POST['ts'] ?? time()); if ($ts < 1) $ts = time(); $body = implode('|', ['ad_mine', '1', $from, (string)$ts]); nst_auth_sig($actor, $body); $slots = nst_ad_slots_for_owner($from); api_out([ 'ok' => true, 'slots' => $slots, 'n' => count($slots), 'days_left' => nst_ad_slot_days_left(), 'empty' => $slots ? false : true, 'msg' => $slots ? '' : 'None.', ]); } if ($api === 'ad_swap' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); $actor = nst_actor('from'); $from = $actor['addr']; $cropRaw = strtolower(trim((string)($_POST['crop'] ?? $_POST['site'] ?? ''))); $empire = ($cropRaw === 'all' || $cropRaw === 'empire'); if ($empire) { api_out(['ok' => false, 'err' => 'Choose one site. All-site ad changes are temporarily unavailable until atomic ownership updates are implemented.'], 409); } $crop = $empire ? 'all' : nst_ad_site_norm($cropRaw); $slotN = nst_ad_slot_norm_n($_POST['slot'] ?? $_POST['n'] ?? ''); $nonce = substr(trim((string)($_POST['nonce'] ?? bin2hex(random_bytes(8)))), 0, 64); $ts = (int)($_POST['ts'] ?? time()); if ($ts < 1) $ts = time(); if (($crop === '' && !$empire) || $slotN === null) { api_out(['ok' => false, 'err' => 'Need a site and slot'], 400); } $body = implode('|', ['ad_swap', '1', $from, $crop, (string)$slotN, $nonce, (string)$ts]); nst_auth_sig($actor, $body); $owner = ''; if ($empire) { foreach (array_keys(nst_ad_site_catalog()) as $tld) { $o = nst_ad_index_owner_of($tld, $slotN); if ($o === $from) { $owner = $o; break; } } } else { $owner = nst_ad_index_owner_of($crop, $slotN); } if ($owner === '' || $owner !== $from) { api_out(['ok' => false, 'err' => 'Not your slot'], 403); } $imgBin = nst_ad_read_upload_bin(); $imgBrain = $imgBin !== null ? nst_brain_ad_image($imgBin) : null; if ($imgBrain === null || empty($imgBrain['ok'])) { api_out(['ok' => false, 'err' => (string)(($imgBrain['err'] ?? '') ?: 'Load a png/jpg/gif first')], 400); } $ext = (string)($imgBrain['ext'] ?? 'png'); // WAVEB2-TR15-SWAP-TX-LOCK: recheck owner under economy tx lock; stage then publish. if ($empire) { api_out(['ok' => false, 'err' => 'Choose one site. All-site ad changes are temporarily unavailable until atomic ownership updates are implemented.'], 409); } $txId = hash('sha256', 'swap|' . $from . '|' . $crop . '|' . $slotN . '|' . $nonce . '|' . $ts); $staged = function_exists('nst_ad_stage_creative') ? nst_ad_stage_creative($txId, (string)$imgBin, $ext) : ['ok' => false]; if (empty($staged['ok'])) { api_out(['ok' => false, 'err' => (string)($staged['err'] ?? 'Could not stage image')], 400); } $lockRes = function_exists('nst_with_economy_tx_lock') ? nst_with_economy_tx_lock(function () use ($crop, $slotN, $from, $staged, $ext) { $owner = nst_ad_index_owner_of($crop, $slotN); if ($owner === '' || $owner !== $from) { return ['ok' => false, 'err' => 'Not your slot']; } $saved = nst_ad_publish_staged_to_slot((string)$staged['path'], $crop, $slotN, (string)($staged['ext'] ?? $ext), $from); if ($saved === null) { return ['ok' => false, 'err' => 'Could not save the image']; } nst_ad_mark_sold($crop, $slotN, (string)($staged['ext'] ?? $ext), $from); return ['ok' => true, 'ext' => (string)($staged['ext'] ?? $ext)]; }) : ['ok' => false, 'err' => 'tx lock missing']; if (empty($lockRes['ok'])) { $http = (($lockRes['code'] ?? '') === 'busy') ? 503 : 400; api_out(['ok' => false, 'err' => (string)($lockRes['err'] ?? 'swap failed'), 'code' => (string)($lockRes['code'] ?? '')], $http); } $ext = (string)($lockRes['ext'] ?? $ext); api_out([ 'ok' => true, 'site' => $crop, 'slot' => $slotN, 'slot_pad' => nst_ad_slot_pad($slotN), 'ext' => $ext, 'public_url' => nst_ad_public_url($crop, $slotN), 'msg' => 'Swapped image in slot ' . nst_ad_slot_pad($slotN) . ' on ' . $crop, ]); } if ($api === 'ad_give' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); $actor = nst_actor('from'); $from = $actor['addr']; $cropRaw = strtolower(trim((string)($_POST['crop'] ?? $_POST['site'] ?? ''))); $empire = ($cropRaw === 'all' || $cropRaw === 'empire'); if ($empire) { api_out(['ok' => false, 'err' => 'Choose one site. All-site ad changes are temporarily unavailable until atomic ownership updates are implemented.'], 409); } $crop = $empire ? 'all' : nst_ad_site_norm($cropRaw); $slotN = nst_ad_slot_norm_n($_POST['slot'] ?? $_POST['n'] ?? ''); $toRaw = trim((string)($_POST['to'] ?? $_POST['ticket'] ?? '')); $nonce = substr(trim((string)($_POST['nonce'] ?? bin2hex(random_bytes(8)))), 0, 64); $ts = (int)($_POST['ts'] ?? time()); if ($ts < 1) $ts = time(); if (($crop === '' && !$empire) || $slotN === null) { api_out(['ok' => false, 'err' => 'Need a site and slot'], 400); } $body = implode('|', ['ad_give', '1', $from, $crop, (string)$slotN, $toRaw, $nonce, (string)$ts]); nst_auth_sig($actor, $body); if ($toRaw === '') { api_out(['ok' => false, 'err' => 'Paste their wallet number or 64-character address'], 400); } $tid = nst_ticket_norm($toRaw); $to = ''; if ($tid !== '') { $look = nst_ticket_lookup_addr($tid); if ($look === null || $look === '') { api_out(['ok' => false, 'err' => 'Unknown number'], 404); } $to = $look; } else { $hex = strtolower(preg_replace('/[^a-f0-9]/', '', $toRaw) ?? ''); if (strlen($hex) !== 64) { api_out(['ok' => false, 'err' => 'Paste their wallet number or 64-character address'], 400); } $to = $hex; } if ($to === $from) { api_out(['ok' => false, 'err' => 'Cannot give a slot to yourself'], 400); } // WAVEB2-TR15-GIVE-TX-LOCK: single-site owner rewrite under economy tx lock. if ($empire) { api_out(['ok' => false, 'err' => 'Choose one site. All-site ad changes are temporarily unavailable until atomic ownership updates are implemented.'], 409); } $lockRes = function_exists('nst_with_economy_tx_lock') ? nst_with_economy_tx_lock(function () use ($crop, $slotN, $from, $to) { $owner = nst_ad_index_owner_of($crop, $slotN); if ($owner === '' || $owner !== $from) { return ['ok' => false, 'err' => 'Not your slot']; } return nst_ad_rewrite_owner($crop, $slotN, $from, $to); }) : ['ok' => false, 'err' => 'tx lock missing']; if (empty($lockRes['ok'])) { $http = (($lockRes['code'] ?? '') === 'busy') ? 503 : 400; api_out(['ok' => false, 'err' => (string)($lockRes['err'] ?? 'give failed'), 'code' => (string)($lockRes['code'] ?? '')], $http); } $r = $lockRes; api_out([ 'ok' => true, 'site' => $crop, 'slot' => $slotN, 'slot_pad' => nst_ad_slot_pad($slotN), 'owner' => $to, 'msg' => 'Gave slot ' . nst_ad_slot_pad($slotN) . ($empire ? ' on all ten faces' : (' on ' . $crop)), ]); } if ($api === 'ad_pick') { /* CORS on THIS endpoint only. Sister crops (market, work, date...) render * the empire's ad board in their own slot, and a browser fetch across * hosts needs it. Safe here and nowhere else: ad_pick is public, * read-only, unauthenticated and returns no secrets. Never widen this to * api_out() generally - transfer, ad_stake and every admin_* route take a * seed in the POST body and must stay same-origin. */ header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET'); $pickCrop = nst_ad_site_norm((string)($_GET['crop'] ?? $_GET['site'] ?? 'trade')); if ($pickCrop === '') $pickCrop = 'trade'; $b = ad_pick_weighted($pickCrop); if (!$b) { api_out(['ok' => true, 'empty' => true, 'msg' => 'no creatives']); } $house = !empty($b['house']); /* NOTHING IS CHARGED HERE. An ad buys a SHARE of the board for the rest * of the epoch, not a meter of impressions - see the ad_stake block. So * serving is a pure read: no lock, no economy.json write, no disk churn * on a page that is already the busiest path in the empire. */ /* NEVER HAND A CLIENT A THIRD-PARTY IMAGE URL. * * This used to emit the advertiser's raw creative_url, which the adboard * then rendered as . Every viewer's browser would fetch it * directly, so the advertiser's server saw the IP and User-Agent of every * person who was shown the ad - on every impression, with no disclosure * and no way to refuse. That is a tracking pixel wearing an ad's clothes, * and ethos 11 names hidden trackers outright. It got worse the moment * the slot went cross-site, because one bid then harvests the visitors of * every crop in the empire. * * So only creatives THIS server holds are served, through ?api=ad_img off * local disk. A viewer talks to the empire and to nobody else. * * creative_url is still recorded on the bid (auditable, and the operator * can see what was submitted); it is simply never turned into a request * the visitor makes. Restoring remote creatives properly means fetching * and caching them server-side - which needs SSRF protection, a size cap * and content-type validation - not re-emitting the URL. */ $img = null; if (!$house && !empty($b['creative_file'])) { $img = ['type' => 'api', 'src' => '?api=ad_img&id=' . urlencode((string)$b['id']) . '&v=' . (int)($b['creative_ts'] ?? 0)]; } $pngBrain = $b['png_brain'] ?? null; api_out([ 'ok' => true, 'empty' => false, 'house' => $house, 'label' => $b['label'] ?? '', 'bid_id' => $b['id'] ?? '', 'stake_fmt' => $house ? 'house' : ($b['stake_fmt'] ?? fmt_amt((string)($b['stake_micros'] ?? '0'))), 'img' => $img, 'href' => $b['href'] ?? '', 'png_w' => $b['png_w'] ?? ($pngBrain['w'] ?? null), 'png_h' => $b['png_h'] ?? ($pngBrain['h'] ?? null), 'png_bytes' => $b['png_bytes'] ?? ($pngBrain['bytes'] ?? null), 'png_flag' => !empty($pngBrain['flag']), 'png_notes' => $pngBrain['notes'] ?? [], 'recruit' => NST_CONTACT_EMAIL, ]); } if ($api === 'ad_img') { $id = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_GET['id'] ?? ''))); if (strlen($id) < 16) { http_response_code(404); exit; } $path = nst_ad_png_path($id, false); if (!is_file($path)) { http_response_code(404); exit; } header('Content-Type: image/png'); header('Cache-Control: public, max-age=300'); readfile($path); exit; } /** Attach creative to a bid (URL and/or PNG base64). Site-local board only. */ if ($api === 'admin_ad_creative' && $_SERVER['REQUEST_METHOD'] === 'POST') { nst_require_writes_unfrozen(); require_admin(); $id = preg_replace('/[^a-f0-9]/', '', strtolower((string)($_POST['bid_id'] ?? ''))); $url = trim((string)($_POST['creative_url'] ?? '')); $href = trim((string)($_POST['href'] ?? '')); $b64 = (string)($_POST['png_base64'] ?? ''); if (strlen($id) < 16) api_out(['ok' => false, 'err' => 'bad bid_id'], 400); $brains = []; if ($href !== '') { $hrefBrain = nst_brain_href_risk($href); $brains['href_risk'] = $hrefBrain; if (empty($hrefBrain['ok'])) { api_out(['ok' => false, 'err' => (string)($hrefBrain['err'] ?? 'bad href'), 'brains' => $brains], 400); } } if ($url !== '') { $urlBrain = nst_brain_href_risk($url); $brains['creative_url'] = $urlBrain; if (empty($urlBrain['ok'])) { api_out(['ok' => false, 'err' => (string)($urlBrain['err'] ?? 'bad creative_url'), 'brains' => $brains], 400); } } $pngMeta = null; $e = load_economy(); $found = false; foreach ($e['bids'] as &$b) { if (($b['id'] ?? '') !== $id) continue; $found = true; if ($url !== '') { $b['creative_url'] = substr($url, 0, 500); } if ($href !== '') { $b['href'] = substr($href, 0, 500); if (!empty($brains['href_risk']['flag'])) { $b['href_flag'] = $brains['href_risk']; } } if ($b64 !== '') { if (str_starts_with($b64, 'data:image/png;base64,')) { $b64 = substr($b64, strlen('data:image/png;base64,')); } elseif (str_starts_with($b64, 'data:image/')) { api_out(['ok' => false, 'err' => 'PNG only (no JPEG/GIF/WebP data URLs)', 'brains' => $brains], 400); } $bin = base64_decode($b64, true); if ($bin === false) { api_out(['ok' => false, 'err' => 'bad base64', 'brains' => $brains], 400); } $pngBrain = nst_brain_ad_png($bin); $brains['ad_png'] = $pngBrain; if (empty($pngBrain['ok'])) { api_out(['ok' => false, 'err' => (string)($pngBrain['err'] ?? 'png reject'), 'brains' => $brains], 400); } $fp = nst_ad_png_path($id, true); if (file_put_contents($fp, $bin, LOCK_EX) === false) { api_out(['ok' => false, 'err' => 'Could not save the ad image — try again in a moment', 'brains' => $brains], 500); } @chmod($fp, 0644); $b['creative_file'] = $id . '.png'; $b['png_w'] = $pngBrain['w'] ?? null; $b['png_h'] = $pngBrain['h'] ?? null; $b['png_bytes'] = $pngBrain['bytes'] ?? strlen($bin); $b['png_brain'] = $pngBrain; $pngMeta = $pngBrain; // prefer local file over url when both set } $b['creative_ts'] = time(); break; } unset($b); if (!$found) api_out(['ok' => false, 'err' => 'bid not found', 'brains' => $brains], 404); save_economy($e); $msg = 'creative set'; if ($pngMeta && !empty($pngMeta['flag'])) { $msg .= ' (soft flag: ' . implode(',', $pngMeta['notes'] ?? []) . ')'; } api_out(['ok' => true, 'msg' => $msg, 'bid_id' => $id, 'png' => $pngMeta, 'brains' => $brains]); } if ($api === 'admin_backup' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_admin(); $wantSecrets = !empty($_POST['include_secrets']); if ($wantSecrets) { // treasury.secret in export is KING-only (CONTRACT §3). require_faucet_auth(); } $chain = read_chain(); $mods = load_mods(); $meta = load_meta(); api_out([ 'ok' => true, 'version' => NST_VERSION, 'exported_ts' => time(), 'meta' => $meta, 'epoch' => epoch_info(), 'mods' => $mods, 'chain_len' => count($chain), 'chain' => $chain, 'treasury_addr' => $taddr, 'include_secrets' => $wantSecrets, 'treasury_secret' => $wantSecrets ? treasury_secret() : null, 'admin_pass' => null, 'admin_pass_note' => 'Leftover admin password files are unlinked. Product door is site seed.', ]); } /** * Public rent quote (P3a+P3b / NSU_VIABILITY E3 quote half). * GET reads the open quote (no mint). POST mints one if none is open. * One open quote per site. No chain write, no bind. */ if ($api === 'rent_quote') { $siteQ = (string)($_GET['site'] ?? $_POST['site'] ?? $_REQUEST['site'] ?? 'trade'); $mintQ = (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? '')) === 'POST'); $q = nst_rent_quote_public($siteQ, $taddr, $mintQ); $http = (int)($q['http'] ?? 200); unset($q['http']); api_out($q, $http > 0 ? $http : 200); } /** * Lookup durable quote (P3b). Open or paid. * GET ?api=rent_quote_get"e_id= */ if ($api === 'rent_quote_get') { $qid = (string)($_GET['quote_id'] ?? $_POST['quote_id'] ?? $_REQUEST['quote_id'] ?? ''); $row = nst_rent_quote_get($qid); if ($row === null) { api_out(['ok' => false, 'err' => 'quote not found or expired', 'settle' => false], 404); } api_out([ 'ok' => true, 'quote' => $row, 'durable' => true, 'paid' => ((string)($row['status'] ?? '') === 'paid'), 'law' => 'Lookup only — pay via transfer to treasury with exact memo+amount.', ]); } /** * P3c: settle rent from chain evidence (scan transfers). Optional quote_id filter. * GET lookup/status only. POST may rescan. Settle + auto-refund already on transfer POST. */ if ($api === 'rent_settle') { $qidS = (string)($_GET['quote_id'] ?? $_POST['quote_id'] ?? $_REQUEST['quote_id'] ?? ''); if (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? '')) !== 'POST') { $qidS = strtolower(preg_replace('/[^a-f0-9]/', '', $qidS) ?? ''); if ($qidS === '') { api_out([ 'ok' => true, 'settled' => false, 'law' => 'GET is lookup only — settle and auto-refund run on transfer POST.', ]); } $row = null; foreach (nst_rent_quotes_read()['quotes'] as $cand) { if (is_array($cand) && strtolower((string)($cand['quote_id'] ?? '')) === $qidS) { $row = $cand; break; } } if ($row === null) { api_out(['ok' => false, 'err' => 'quote not found or expired', 'settle' => false], 404); } api_out([ 'ok' => true, 'settled' => ((string)($row['status'] ?? '') === 'paid'), 'quote' => $row, 'law' => 'GET is lookup only — settle and auto-refund run on transfer POST.', ]); } $scan = nst_rent_settle_scan($qidS !== '' ? $qidS : null); api_out($scan, !empty($scan['settled']) ? 200 : 200); } /** Rent grant path retired — always 410 after KING auth. No treasury credit. */ if ($api === 'admin_rent_claim' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_faucet_auth(); api_out([ 'ok' => false, 'err' => 'rent grant path retired', 'law' => 'Rent is payment TO treasury only (not a grant FROM faucet). ' . 'Server no longer spawns LORD seeds or credits renters from treasury. ' . 'Public quotes: ?api=rent_quote&site=… (durable) · settle/bind still next residual.', 'lord_rent_grant' => 'retired', 'next' => 'rent_quote durable (live) · transfer POST settles', ], 410); } /** Public: empire setup filled? Sister crops / pack gate may poll this (no secrets). */ if ($api === 'empire_setup_status') { $st = nst_empire_setup_state(); $book = nst_empire_setup_book_census(); api_out([ 'ok' => true, 'site' => 'trade', 'filled' => !empty($st['filled']), 'version' => (int)($st['version'] ?? 1), 'filled_ts' => $st['filled_ts'] ?? null, 'ladder_posted' => $st['ladder_posted'] ?? null, 'book' => $book, 'setup_url' => NST_EMPIRE_SETUP_PUBLIC_URL, 'console_path' => '/?empire_setup=1', 'law' => 'While filled=false, public faces redirect to empire setup console', ]); } /** * KING: mark empire setup filled/unfilled (lifts or restores visitor redirect). * Prefer ladder first; allows mark even if ladder empty (operator responsibility). */ if ($api === 'empire_setup_mark_filled' && $_SERVER['REQUEST_METHOD'] === 'POST') { require_faucet_auth(); $want = (string)($_POST['filled'] ?? '1'); $filled = !($want === '0' || strtolower($want) === 'false' || $want === 'no'); $book = nst_empire_setup_book_census(); $state = [ 'filled' => $filled, 'filled_ts' => time(), 'ladder_posted' => (int)($book['treasury_ladder'] ?? 0), 'note' => $filled ? 'operator marked empire filled' : 'operator unfilled — console gate active', ]; if (!nst_empire_setup_write($state)) { api_out(['ok' => false, 'err' => 'Could not save to the server — try again in a moment'], 500); } api_out([ 'ok' => true, 'filled' => $filled, 'book' => $book, 'msg' => $filled ? 'Empire filled — public product faces open on this host' : 'Empire unfilled — visitors redirected to setup console', ]); } /* RR334 P1-TRADE-UNKNOWN-API-PLAIN: bare "unknown api" → plain route hints (no face HTML) */ api_out([ 'ok' => false, 'err' => 'Unknown trade API — use selfhash, book, state, balance, transfer, order, cancel, fill, faucet (410 retired), or open the Trade face', 'api' => $api, ], 404); } /* -------- Empire setup console (?empire_setup=1 / /setup) -------- */ $wantSetup = isset($_GET['empire_setup']) || isset($_GET['setup']) || (isset($_SERVER['REQUEST_URI']) && preg_match('#/setup/?(\?|$)#', (string)$_SERVER['REQUEST_URI'])); if ($wantSetup && (string)($_GET['api'] ?? $_POST['api'] ?? '') === '') { nst_render_empire_setup_console(); } /* -------- /controlpanel — site wallet seed unlock (no recovery) -------- */ $wantPanel = isset($_GET['controlpanel']) || (isset($_SERVER['REQUEST_URI']) && preg_match('#/controlpanel/?(\?|$)#', (string)$_SERVER['REQUEST_URI'])); if ($wantPanel && (string)($_GET['api'] ?? $_POST['api'] ?? '') === '') { header('Content-Type: text/html; charset=UTF-8'); header('X-Content-Type-Options: nosniff'); header('Cache-Control: no-store'); $v = NST_VERSION; $panelSeed = ''; $unlocked = false; $loginErr = ''; // POST seed → derive addr; unlock if treasury (owner) or economy.operator_addr (rent later) if (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') { $panelSeed = norm_seed((string)($_POST['seed'] ?? '')); if ($panelSeed === '') { $loginErr = 'Paste this site\'s wallet seed (12 words). No recovery.'; } elseif (!panel_seed_ok($panelSeed)) { $loginErr = 'Seed does not unlock this site panel (must match site wallet / treasury).'; $panelSeed = ''; } else { $unlocked = true; } } $esc = static function (string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); }; echo ''; echo 'Trade Control Panel' . ns_worktable_banner(false) . '
'; echo '

DNA · TRADE · money/buy · orange burn

'; echo '

Nosignup.Trade · Control Panel

'; echo '

Paste this site\'s operator seed (no recovery). Fresh genesis: panel = data/site.seed (not mint); KING faucet = data/treasury.secret (only minter). ' . 'After public rent pay (visitor pays treasury exact quote), payer key becomes LORD for the epoch — no server-spawned LORD seed. ' . 'UTTER control of THIS crop (including replacing this file). Not OS root. ' . 'King starts with the whole 100 million NSU cap. After that nothing is printed. ' . 'Renters must not receive treasury secret. Yearly wipe: balances and existing wallet-access records survive; book re-seeds loose bootstrap spread; superstructure burns.

'; echo '
'; echo ''; echo ''; echo ''; if ($loginErr !== '') { echo '
' . $esc($loginErr) . '
'; } echo '

Panel: data/site.seed + vault SITE_WALLET_SEED.txt. KING (mint only): data/treasury.secret + vault KING_FAUCET_SEED.txt. After rent: LORD = payer key (economy.operator_addr). No recovery desk. No password product path.

'; echo '
Version-
'; echo '
Treasury (master faucet)-
'; echo '
Profit pile-
'; echo '
Ad bid stake-
'; echo '
Book mid / k-
'; echo '
Value note-
'; echo '
Chain / epoch-
'; echo '
Vault hint-
'; echo '

'; echo '

NSU value (this server owner only)

'; echo '

Value authority = you (owner of THIS crop). Set donate k (gift ≈ external÷mid×k) and a public value note. Mirrors are protocol-equal service peers — capacity and traffic create no monetary authority. Market P = book mid; donate buyback arms at P/2.

'; echo ''; echo '
'; echo '

Year-end wipe checklist (operator)

'; echo '

Wallet balances and existing access records survive. Profit/ads/orders burn. Site wallet seed does NOT rotate (still your paper). Bootstrap book re-seeds if empty. Tick boxes local only.

'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo ''; echo '

Rent / LORD (grant path retired · quote live)

'; echo '

RETIRED: treasury→LORD market grant + server-spawned LORD seed are gone (atomic P2). ' . 'P3a–c live: ?api=rent_quote · durable store · pay treasury with exact memo+amount · ' . 'first-paid-wins binds operator_addr to payer key (no server LORD seed). ' . 'Lookup rent_quote_get · lookup rent_settle (GET). Settle on transfer POST. ' . 'Failed rent pays (wrong amount / already paid / expired / …) auto-refund to payer (P3d). ' . 'King birth pile is the whole ' . (int)NST_KING_FAUCET_START_NSU . ' NSU. Public faucet stays 410. Renters never mint. ' . 'Never email treasury.secret.

'; echo ''; echo '
Grant path retired · try ?api=rent_quote&site=trade (pay transfer settles)
'; echo ''; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo '
'; echo ''; echo '

Credit donate → NSU gift (50/50 split)

'; echo '

Gifts NSU from treasury master faucet (nsu_amount OR ext_amount÷mid×k), then 50% profit / 50% ad-bid + buyback at P/2. Free faucet claims do not use this path.

'; echo ''; echo ''; echo '
'; echo '

Extract profit

'; echo '
'; echo '

Ad creative (site-local weighted board)

'; echo '

png/jpg/gif/webp. Max 800KB (' . NST_AD_MAX_PNG . 'b) · longest side ≤' . NST_AD_MAX_W . 'px. Community: 5 reports in under 50 showings removes the creative (reupload a different image). Check My Ads for impressions. House discovery fills empty boards. Invite: ' . $esc(NST_CONTACT_EMAIL) . '

'; echo ''; echo ''; echo '
'; echo '

Treasury pay (master faucet)

'; echo '
'; echo '

Source (utter control)

Load → edit → save replaces this site file. Broken save can kill the site until SSH restore from .bak.

'; echo ''; echo '
'; echo '

Virgin pack (KING)

'; echo '

A deployable copy of this whole empire - the ten sites, Deploy.bat and NSU-DNA - with no seeds, no vault, no data. Stand up a sister empire from this one without needing the machine you first deployed from. Requires the KING treasury seed, not a lord panel seed.

'; echo '
'; echo '

Sealed wallet inbox (KING · experimental)

'; echo '

Mail ≠ mint. KING hand-seal = true client-encrypt E2E path. Auto fill/half-BID/rent notices = server-composed public facts (encrypted for delivery, NOT private E2E). ' . 'Server stores opaque ciphertext only — paste plaintext only in this browser; encryption runs client-side when WebCrypto works. ' . 'Not social mail. Not proven e2e. Blobs burn at yearly wipe / TTL.

'; echo ''; echo '
'; if (nst_host_is_trade()) { echo '

LTC light wallet (trade king only)

'; echo '

Not on the public Wallet tab. Visitors send exact LTC to a one-time ticket address. Seed stays in data/ltc.secret (DENYLIST). KING treasury seed only. Outbound LTC sign is not shipped — pay list only. Watch is fail-open. Half-bid = same NSU size at half the sale LTC price, on fill. Keys stay in this console / data/ltc.secret.

'; echo '
Master / receive-
'; echo '
Free LTC / reserved-
'; echo '
Scheme-
'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo '
'; echo '
tickets —
'; } echo '

Promo mailbox (ads@nosignup.trade)

'; echo '

Server-blackboxed. Mail is the only front door for the 1000 NSU ad credit. No visitor form. Public faucet stays 410. MX: nosignup.trade → this host. KING administers login here — never paste it into ?download=1 / chat.

'; echo '
Addressads@nosignup.trade
'; echo '
Username-
'; echo '
Password-
'; echo '
Claims used / cap-
'; echo '
Bot key on disk-
'; echo ''; echo ''; echo ''; echo '
'; echo '
ingest —
'; echo '

Backup

'; echo '

← back to trade

'; // Seed stays in memory for API auth only after successful POST unlock (not echoed into form). echo ''; exit; } /* -------- HTML -------- */ if (PHP_SAPI === 'cli' && (string)getenv('NST_AD_SLOT_SELFTEST') === '1') { nst_ad_slot_selftest(); exit(0); } if (PHP_SAPI === 'cli' && (string)getenv('NST_AD_REPORT_SELFTEST') === '1') { if (function_exists('nst_ad_report_selftest')) { nst_ad_report_selftest(); } else { fwrite(STDERR, "nst_ad_report_selftest missing\n"); exit(1); } exit(0); } if (PHP_SAPI === 'cli' && (string)getenv('NST_PACK_SELFTEST') === '1') { nst_ad_slot_selftest(); nst_money_law_selftest(); nst_genesis_ask_selftest(); nst_tx_gate_selftest(); echo "pack selftest done\n"; exit(0); } // Redirect before any genesis, epoch, conservation, or economy mutation. nst_empire_setup_maybe_redirect(); if (PHP_SAPI !== 'cli') { nst_promo_maildir_ingest_lazy(); } $chainSsr = read_chain(); $taddr = nst_treasury_addr_safe(); if ($taddr === '') { http_response_code(503); header('Content-Type: text/plain; charset=UTF-8'); header('Cache-Control: private, no-store, max-age=0, must-revalidate'); header('X-Content-Type-Options: nosniff'); echo "Trade is not initialized. Open the empire setup console.\n"; exit; } $tbal = fmt_amt(balances($chainSsr)[$taddr] ?? '0'); // Pre-ENTER shell is read-only: expose the last recorded conservation status only. $nstConservePub = nst_conservation_public_view(); $nstFrozen = !empty($nstConservePub['frozen']); // SSR market P + buyback P/2 (same helpers as ?api=state) so strip is honest before JS $ssrP = book_mid_micros($chainSsr); $ssrP2 = nst_buyback_price_micros($chainSsr); $ssrP_fmt = $ssrP !== null ? fmt_amt($ssrP) : '-'; $ssrP2_fmt = $ssrP2 !== null ? fmt_amt($ssrP2) : '-'; $ssrBoot = false; foreach (open_orders($chainSsr) as $oSsr) { if (($oSsr['kind'] ?? '') === 'bootstrap_spread') { $ssrBoot = true; break; } } $ssrBook = nst_public_book($chainSsr); $ssrAsks = []; $ssrBids = []; foreach (($ssrBook['asks'] ?? []) as $oSsr) { if (strtoupper((string)($oSsr['pair'] ?? '')) !== NST_PUBLIC_PAIR) continue; $ssrAsks[] = $oSsr; } foreach (($ssrBook['bids'] ?? []) as $oSsr) { if (strtoupper((string)($oSsr['pair'] ?? '')) !== NST_PUBLIC_PAIR) continue; $ssrBids[] = $oSsr; } usort($ssrAsks, static function ($a, $b) { return dec_cmp((string)($b['price'] ?? '0'), (string)($a['price'] ?? '0')); }); usort($ssrBids, static function ($a, $b) { return dec_cmp((string)($b['price'] ?? '0'), (string)($a['price'] ?? '0')); }); $ssrBestAsk = null; $ssrBestBid = null; foreach (array_reverse($ssrAsks) as $oSsr) { $ssrBestAsk = $oSsr; break; } foreach ($ssrBids as $oSsr) { $ssrBestBid = $oSsr; break; } $ssrSpread = '—'; if ($ssrBestAsk && $ssrBestBid) { $ssrSpread = fmt_amt(dec_sub((string)($ssrBestAsk['price'] ?? '0'), (string)($ssrBestBid['price'] ?? '0'))); } $ssrLast = '—'; foreach (($ssrBook['pairs'] ?? []) as $psSsr) { if (strtoupper((string)($psSsr['pair'] ?? '')) !== NST_PUBLIC_PAIR) continue; $lf = $psSsr['last_fill'] ?? null; if (is_array($lf) && (($lf['type'] ?? '') === 'fill' || !empty($lf['price_fmt']))) { $ssrLast = (string)($lf['price_fmt'] ?? '—'); } break; } $ssrHero = ($ssrLast !== "—" && $ssrLast !== '-' && $ssrLast !== '') ? $ssrLast : $ssrP_fmt; /* Display-only book total/depth. Reuses dec_* + fmt_amt. Not a settlement path. */ $nstBookTot = static function ($o) { $rem = dec_norm((string)($o['remaining'] ?? $o['amount'] ?? '0')); $px = dec_norm((string)($o['price'] ?? '0')); if (!dec_ok($rem) || !dec_ok($px) || $rem === '0' || $px === '0') return "—"; $prod = '0'; $n = strlen($px); for ($i = 0; $i < $n; $i++) { $prod = dec_add(dec_mul_small($prod, 10), dec_mul_small($rem, (int)$px[$i])); } if (strlen($prod) <= 6) return fmt_amt('0'); return fmt_amt(dec_norm(substr($prod, 0, -6))); }; $nstBookDepth = static function ($rows, $fromEnd) { $n = count($rows); $cums = array_fill(0, $n, '0'); $acc = '0'; if ($fromEnd) { for ($i = $n - 1; $i >= 0; $i--) { $acc = dec_add($acc, dec_norm((string)($rows[$i]['remaining'] ?? $rows[$i]['amount'] ?? '0'))); $cums[$i] = $acc; } } else { for ($i = 0; $i < $n; $i++) { $acc = dec_add($acc, dec_norm((string)($rows[$i]['remaining'] ?? $rows[$i]['amount'] ?? '0'))); $cums[$i] = $acc; } } $max = $acc; $pcts = []; for ($i = 0; $i < $n; $i++) { $pct = 0; if (dec_cmp($max, '0') > 0) { if (function_exists('bcmul') && function_exists('bcdiv')) { $pct = (int)bcmul(bcdiv($cums[$i], $max, 4), '100', 0); } else { $maxN = (float)$max; $pct = $maxN > 0 ? (int)round(100.0 * ((float)$cums[$i] / $maxN)) : 0; } if ($pct > 100) $pct = 100; if ($pct < 1) $pct = 1; } $pcts[$i] = $pct; } return $pcts; }; $ssrAskPct = $nstBookDepth($ssrAsks, true); $ssrBidPct = $nstBookDepth($ssrBids, false); $selfHash = @hash_file('sha256', __FILE__) ?: 'unavailable'; $htmlNonce = base64_encode(random_bytes(18)); header('Content-Type: text/html; charset=UTF-8'); header('X-Content-Type-Options: nosniff'); header('Cache-Control: private, no-store, max-age=0, must-revalidate'); header('Pragma: no-cache'); header('Expires: 0'); header('Vary: Cookie'); header('Referrer-Policy: no-referrer'); header('X-Frame-Options: DENY'); header('Cross-Origin-Resource-Policy: same-origin'); header("Content-Security-Policy: default-src 'self'; script-src 'nonce-" . $htmlNonce . "'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'"); ?> Nosignup.Trade · trade NSU without an account >

Nosignup.trade - Service and pricing reset in days · coins stay · orders/ads/profit burn

Wallet status — No wallet open No wallet

NSU leg settles on-site. Foreign LTC/NOTE settlement is off-platform / manual — outbound signing/broadcast is incomplete; not auto-sent. Order or fill intent ≠ on-chain settlement. Place/Send/Ads/Rent without a wallet stay put and explain inline.

Book · full LTC/NSU ladder
No asks
$oSsr): ?>
LTC spread
No bids
$oSsr): ?>
Buy / Sell order entry
—

Wallet on → Buy/Sell + qty NSU + price LTC → Place

Units: qty in NSU · price in LTC per NSU. NSU transfers on-site; foreign LTC/NOTE settle stays off-platform / manual — not auto-broadcast.

Secondary · ratings / audit
Operator console Ctrl+Alt+Shift+N · site seed

Unlock with site wallet seed

Product door is the same 12-word site seed as /controlpanel (U7). No recovery desk. Preferred surface: /controlpanel. Host root compromise = game over — accepted.

value, canonically ordered. */ function nsu_policy_constants(): array { $c = [ 'NST_MAX_SUPPLY' => NST_MAX_SUPPLY, 'NST_GENESIS_TREASURY' => NST_GENESIS_TREASURY, 'NST_EMISSION_DAILY_RATE_PPM' => NST_EMISSION_DAILY_RATE_PPM, 'NST_EMISSION_CROP_COUNT' => NST_EMISSION_CROP_COUNT, 'NST_KING_TAX_PPM' => NST_KING_TAX_PPM, 'NST_DECIMALS' => NST_DECIMALS, 'NST_RESET_SECS' => NST_RESET_SECS, ]; ksort($c); return array_map('strval', $c); } /** * Every function that defines the money. This list is the constitution's index. * Adding a rule means adding its name here, or POLICY stops describing the money. */ function nsu_policy_functions(): array { return [ // identity 'norm_seed', 'addr_from_seed', // amount semantics (micros as non-negative integer strings) 'dec_ok', 'dec_norm', 'dec_cmp', 'dec_add', 'dec_sub', 'dec_mul_small', 'dec_div_small', 'parse_amt', 'fmt_amt', // ledger structure 'nst_row_canonical', 'nst_chain_head', 'nst_chain_link_verify', 'balances', // issuance law 'nst_mint_types', 'nst_is_mint_type', 'nst_minted_to_date', 'nst_supply_remaining', 'nst_emission_today', 'nst_fee_split', // audit law 'nst_conservation_replay', 'nst_verify_chain', ]; } /** * Token-normalised source for named functions: comments and whitespace dropped. * Returns [name => normalised-source]; a missing name yields '' so a hash over a * gutted file cannot silently equal a hash over an intact one. */ function nsu_policy_sources(): array { static $cache = null; if ($cache !== null) return $cache; $want = array_flip(nsu_policy_functions()); $out = array_fill_keys(array_keys($want), ''); $toks = @token_get_all((string)@file_get_contents(__FILE__)); if (!is_array($toks)) { $cache = $out; return $cache; } $n = count($toks); for ($i = 0; $i < $n; $i++) { $t = $toks[$i]; if (!is_array($t) || $t[0] !== T_FUNCTION) continue; // next meaningful token must be the name $j = $i + 1; while ($j < $n && is_array($toks[$j]) && in_array($toks[$j][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) $j++; if ($j >= $n || !is_array($toks[$j]) || $toks[$j][0] !== T_STRING) continue; $name = $toks[$j][1]; if (!isset($want[$name]) || $out[$name] !== '') continue; // capture from `function` to the balanced closing brace $buf = ''; $depth = 0; $started = false; for ($k = $i; $k < $n; $k++) { $tk = $toks[$k]; if (is_array($tk)) { if (in_array($tk[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) continue; $buf .= $tk[1] . ' '; } else { $buf .= $tk . ' '; if ($tk === '{') { $depth++; $started = true; } elseif ($tk === '}') { $depth--; if ($started && $depth === 0) break; } elseif ($tk === ';' && !$started) break; // abstract/interface form } } $out[$name] = trim($buf); } $cache = $out; return $cache; } /** * sha256 over the constitution: constant VALUES plus normalised rule source. * * CACHED ACROSS REQUESTS, BECAUSE THE ANSWER ONLY CHANGES WHEN THE FILE DOES. * * Computing it means token_get_all() over this whole file - measured at 14.3 ms * against 0.18 ms for a bare read, an 80x multiplier. It was called from * ?api=state, the most-hit endpoint in the empire, which put 14 ms of tokenising * on the hot path of every page. On a site whose entire premise is being cheap * to run, that is a self-inflicted tax. * * POLICY cannot change while the file is unchanged, so the cache key is the * file's mtime and size - a stat, 0.037 ms, 385x cheaper than the work it * skips. A deploy changes both, so the hash recomputes exactly once afterwards * and never again. * * Falls back to computing in-process if the cache cannot be written (read-only * data dir, first run): correctness never depends on the cache existing. */ function nsu_policy_hash(): string { static $memo = null; if ($memo !== null) return $memo; $key = (string)@filemtime(__FILE__) . ':' . (string)@filesize(__FILE__); $p = $GLOBALS['DATA'] . DIRECTORY_SEPARATOR . 'policy.cache.json'; $c = is_file($p) ? json_decode((string)@file_get_contents($p), true) : null; if (is_array($c) && ($c['key'] ?? '') === $key && !empty($c['hash'])) { $memo = (string)$c['hash']; return $memo; } $payload = [ 'v' => 1, 'constants' => nsu_policy_constants(), 'rules' => nsu_policy_sources(), // key order fixed by nsu_policy_functions() ]; $memo = hash('sha256', j($payload)); @file_put_contents($p, j(['key' => $key, 'hash' => $memo, 'ts' => time()]), LOCK_EX); return $memo; } /** * Eyeball-comparable badge. Two kings hold up two screens; same syllables, same * money. Deliberately silly and short - nobody should have to read hex to check. */ function nsu_policy_badge(?string $hash = null): string { $h = $hash ?? nsu_policy_hash(); $con = ['b','d','f','g','k','l','m','n','p','r','s','t','v','z']; $vow = ['a','e','i','o','u','y']; $out = []; for ($i = 0; $i < 3; $i++) { $a = hexdec(substr($h, $i * 4, 2)); $b = hexdec(substr($h, $i * 4 + 2, 2)); $out[] = $con[$a % count($con)] . $vow[$b % count($vow)] . $con[($a >> 4) % count($con)]; } return strtoupper(implode('-', $out)); } /** Which rules are missing from this build - a gutted file must not hash clean. */ function nsu_policy_missing(): array { $miss = []; foreach (nsu_policy_sources() as $name => $src) { if ($src === '') $miss[] = $name; } return $miss; } /* ====================================================================== * ECONOMY+IDENTITY TELOMERE nosignup empire v1 * CURRENT CONTRACT 2026-10-03 - shared interface, face-local product. * ---------------------------------------------------------------------- * This tail contains function declarations and comments only. No top-level * define/const/echo, route dispatch or state mutation: earlier exits may * prevent tail execution. Keep helpers callable by their existing names. * The hub triad remains byte-identical; other faces retain their own adapters. * * IDENTITY * A public ticket identifies a browser-held instance, not a verified person. * Same-origin tabs may share storage. Private browsing, storage loss, other * devices and separate origins are not proof of either sameness or difference * of a human. Never derive, recover or merge a wallet from IP/fingerprints. * The shared wallet bridge and existing ticket routes perform only their * implemented bindings; a matching key name is not cross-origin storage. * Do not promise automatic restoration or create profile/seed replicas on * every face. Trade owns its assignment/ledger; each face owns its product * data. Fresh wallet words stay browser-local; legacy server-exposed seeds * retain that warning. Migration cannot undo old exposure. A public locator * alone cannot recover spending words. Operator identity is separate. * * visitor number/ticket ........ nst_ticket_* mill + bind; LEFT IN PLACE * at "Public ticket mill + wallet bind" * (?api=ticket). Not moved. * localStorage nosignup_device_id ... DEVICE_KEY in the client JS IIFE; * LEFT IN PLACE * override/claim hooks ......... ticket prefer / bind; LEFT IN PLACE * with the mill (not a new hook) * vanity sink (later) .......... nst_vanity_quote / nst_vanity_apply; * 501/no-op; MUST NOT move coins or write bind * presence gossip ............. data/id-presence/{aa}/{ticket}.txt; * ONLY receive store; sisters POST here after I AGREE; * same-origin + listed switches, not sister crops; * touch on assign + ?api=id_gossip; 30d inactivity * lazy-delete of presence ads ONLY (never wallets) * I AGREE ...................... #gateEnter entry gate; LEFT IN PLACE * device-fact tags ............. none as a separate tag cluster * addr_from_seed / auth ........ mid-file identity/auth helpers; LEFT * IN PLACE (money math / POLICY index) * * WORKTABLES * RAM = /dev/shm/nosignup_trade_ephemeral * DISK = ./data * Keep the existing roots and legacy compatibility paths. This comment * authorizes no migration, new per-person file or duplicate credential store. * * Keep chain.jsonl, treasury.secret, site.seed, ltc.secret, ltc-wallet.json, * ltc-headers/, ticket.bind, id.counter and ad.shares in their existing shapes. * Preserve sharded inbox/keys/ads and legacy reads. Never move live stores. * Mirror-faucet and dead-King-election sketches are dormant history. * There is no existing daily emission to distribute and no public faucet. * ECONOMY * Shared wallet access, Donate and separate ad real estate may be exposed * without creating a second ledger, currency, mint or account on a face. * Trade owns spend authorization, settlement, ad ownership and epoch state. * Preserve current signatures, one-time genesis cap, paused emission and * closed financial/paid gates. Annual service reset is not a wallet remint. * Organic people/listing/content rank is not for sale. Earlier suggestions * for paid chat priority, promoted jobs/listings, profile boosts and paid * note pinning are retired speculation, not approved future work. * * Ad reports use the existing Trade endpoint and current slot/epoch state. * Trade decides acceptance and its configured takedown rule; a client must * not infer ownership, refund or takedown from transport success. Person * reports in Chat remain distinct. Preserve all helper bodies below. * ====================================================================== */ /** Which crop is this? Read from the host so the block never forks per site. */ function nsu_crop_tld(): string { $h = strtolower((string)($_SERVER['HTTP_HOST'] ?? '')); $h = preg_replace('/:\d+$/', '', $h) ?? ''; if (preg_match('/(?:^|\.)nosignup\.([a-z]{2,10})$/', $h, $m)) { return $m[1]; } return ''; } /** Where the money lives. Trade owns the only chain; every crop asks it. */ function nsu_trade_origin(): string { return 'https://nosignup.trade'; } /** * The empire ad board, scoped to this crop. * Returns '' when the crop cannot name itself, so a caller fails closed and * renders nothing rather than guessing a neighbour's board. */ function nsu_ad_sites(): array { return ['com','org','net','trade','chat','work','market','date','fun','info']; } function nsu_ad_roll_n(): int { return random_int(0, 9999); } function nsu_ad_slot_url(?string $site = null, ?int $n = null): string { $t = ($site !== null && $site !== '') ? strtolower($site) : nsu_crop_tld(); if ($t === '' || !in_array($t, nsu_ad_sites(), true)) return ''; if ($n === null) $n = nsu_ad_roll_n(); $n = max(0, min(9999, $n)); return nsu_trade_origin() . '/' . $t . '/' . str_pad((string)$n, 4, '0', STR_PAD_LEFT); } function nsu_board_endpoint(): string { return nsu_ad_slot_url(); } function nsu_ad_slot_html(string $extraClass = ''): string { $t = nsu_crop_tld(); if ($t === '' || !in_array($t, nsu_ad_sites(), true)) return ''; $cls = 'ns-ad-slot' . ($extraClass !== '' ? ' ' . $extraClass : ''); $cells = ''; for ($i = 0; $i < 49; $i++) { $url = nsu_ad_slot_url($t); $cells .= 'ad'; } return ''; } /** One vertical skyscraper (L/R) or both (LR). Not the 49-tile board. Trade crop. */ function nsu_ad_tower_html(string $side = 'LR'): string { $t = nsu_crop_tld(); if ($t === '' || !in_array($t, nsu_ad_sites(), true)) $t = 'trade'; if ($t === '' || !in_array($t, nsu_ad_sites(), true)) return ''; $side = strtoupper($side); if ($side === 'BOTH' || $side === 'LR' || $side === '') { return nsu_ad_tower_html('L') . nsu_ad_tower_html('R'); } $side = ($side === 'R') ? 'R' : 'L'; $url = nsu_ad_slot_url($t); if ($url === '') return ''; return 'ad'; } /** Ad-report endpoint only. Trade validates slot/epoch and owns acceptance, * configured takedown and showings state. Sending a report is not proof of a * takedown, ownership or refund; this helper grants no economic authority. */ function nsu_report_endpoint(): string { $t = nsu_crop_tld(); if ($t === '' || !in_array($t, nsu_ad_sites(), true)) return ''; return nsu_trade_origin() . '/?api=ad_report&crop=' . urlencode($t); } /* ===================== END ECONOMY+IDENTITY TELOMERE ================== */ /* ========================================================================= * CURRENT PRESERVATION CONTRACT — OWNER REVISION 2026-10-13 / R05 * Preserve these blocks and paired caps. Revise inaccurate wording in place and * retain exact earlier text in the change record, not as a competing rule. * Latest explicit owner direction governs. Existing code, deployment and passing * snapshots do not authorize a regression. Unknown is not a pass. * Keep one self-contained PHP/HTML/CSS/JS application, accountless core, separate * site responsibilities, public source, private operator state and existing Trade * interfaces. Add no mandatory framework, build system or runtime service. * Preserve layout, palette, primary controls, original Back/Close positions, * navigation meanings, keyboard/zoom/mobile access and reduced-motion support. * No redesign, duplicate toolbar/report control or optional-ad switch in this * round. Restore approved behavior; do not freeze a broken current screenshot. * Preserve data, ownership, consent/age rules and money rules. No wiping, reminting, * cross-site edits or destructive rollback. One site and one verified change group * at a time; reconcile source drift without overwriting newer work. * MINIMUM NECESSARY CHANGE: prove the defect, compare doing nothing and a local * repair, then choose the least complex option that actually meets the contract. * Count extra states, timers, requests, dependencies, UI steps and failure paths, * not just lines or minified bytes. Remove a bad mechanism rather than wrapping it * in more compensations. One authoritative state and one implementation per job. * Never exchange correct identity, usable behavior, security or preserved UI for * speed or fewer lines. Measure time to a usable result, not premature unlocking. * Missing required state is pending/failure, never success. Optional failure must * not break the free core. A timeout must end/contain a real failed operation; * it must not invent readiness, silently retry writes or create late-state races. * For nontrivial edits, record observed benefit and complexity cost plus before/ * after tests. Reject known harm; require evidence for claimed gains. Ties and * unknown speculative gains favor no change or the smaller proven repair. The * external change scorecard supports review; add no scoring engine to the site. * Keep each agent's source, fixtures, temporary files and results in an isolated * site/round workspace. Never share a fixed scratch path or rewrite sibling work. * Bind evidence to the actual tested hash. Separate reported, independently tested * and deployed results; a filename, earlier approval or a green log is not proof. * Preserve payment and ownership boundaries. A successful API call is not proof * of settlement or authorization; unresolved money-policy choices stay explicit. * Historical versions, paths, providers, timings and implementation details remain * changeable with evidence and resealed recovery. They are not an immutable API * or an unbounded task queue; architecture and useful UI behavior are the contract. * TRADE: Keep the public selected-pair order book and order entry as the * base view, with its existing wallet, ads and predictor access. Trade remains * the monetary authority; other sites are clients, not alternate ledgers. Exact * pair, amount, pending-operation and settlement status must remain truthful. * Do not change issuance, fees, prices, balances, purchase gates or custody in a * UI restoration. Preserve the actual predictor and its controls without making * new accuracy/profit claims. Retain input readability, placement hints and * original local Close/Back controls; no added navigation layer. * * REPAIR METAPHOR: Telomeres carry repair instructions; future hub AI may * act as telomerase, proposing and testing repairs. Epigenetic repair is * an analogy here, not a biological claim or an implemented AI capability. * DO NOT DELETE THIS TELOMERE : * NOSIGNUP.TRADE TELOMERE v1 (2026-10-07). Two copies: the front copy right * after the opening PHP tag and the back copy at the very end of this file. * Both copies are this same comment, byte for byte. * ------------------------------------------------------------------------- * * Telomeres are end caps. Edits wear a long file down from its ends: a bad * paste, a truncated upload, a cleanup that drops prose. These caps exist so * the truth kernel of nosignup.trade survives any edit that keeps either * end. Each end carries the whole cap, so either end can rebuild the other * by copying this comment verbatim. If the two copies differ, or a hash * below no longer matches its function, stop and reconcile against ?src=1 * before writing. Never change the money to match a stale cap, and never * edit the cap to hide a money change. The caps are comments only: they * emit no bytes, declare nothing and change no behaviour. They wrap, never * replace, the FINAL-PACK MAINTENANCE CHARTER, the PRODUCT CONTRACT, the * history prose and the ECONOMY+IDENTITY TELOMERE, which stay byte-preserved. * * * * FRONT CAP: charter, face purpose, truth kernel. Summarised from the charter * blocks below the front copy; those blocks and the code remain the authority. * * CHARTER. One stock-PHP file; no composer/npm/database dependency. No * visitor signup, recovery account, tracking profile, shared bank, SSO or * paywall on basic use. ?src=1 and ?download=1 stay auditable. This host * owns only its own data, vault and operator panel; a panel is never a * visitor identity. Repair measured failures or narrow security/honesty * defects; do not invent a new financial product. Preserved history is not * a task queue. Source authority is the verified current root 4.php; * reconcile live/raw/source bytes before writing; do not revive old packs. * * FACE PURPOSE (Trade). Trade NSU. Place ads. Test forecasts. Product tools * are Ads / Predictor / Orders. Wallet and identity belong to the shared * wallet control (send/receive), not duplicate product tabs. The unchecked * landing is the public NSU order book with order entry. Trade owns the only * chain: spend authorization, settlement, ad ownership and epoch state. The * Predictor (PhiGraph, ?view=phigraph) is a bounded research view with no * monetary route. * * TRUTH KERNEL. * 1 AUTH: normal mutations require ECDSA P-256 v2 (ecdsa_p256_v2 / * auth_required). Seed is not accepted on normal mutation routes; the * only legacy path is api=legacy_wallet_migrate. * 2 MONEY: NST_MAX_SUPPLY is 100,000,000 NSU, minted once at birth through * nst_mint_guarded('genesis', ...). Remaining issuance after birth is 0. * NST_EMISSION_PAUSED stays true and nst_emission_run_daily is a no-op. * No daily drip, no visitor faucet (410), no participant-triggered mint, * no remint, no re-anchor. * 3 LAW: amounts are integer micros. Every mint type is declared in * nst_conservation_replay() or the freeze halts chain appends. The money * constitution is indexed by nsu_policy_functions() and hashed by * nsu_policy_hash(), which ignores comments and whitespace. * 4 TAX: the KING's cut is a tax on lord receipts, never a duplicate. Never * tax a user-to-user trade or transfer. Ads buy their own real estate, * never organic rank. * 5 RESET: yearly service reset. Wallet balances and access records survive; * orders, ads, bids, profit and modifiers burn. It is not a wallet remint. * 6 IDENTITY: a public ticket identifies a browser-held instance, not a * person. IP, fingerprints and similarity never recover, merge or select a * wallet. Fresh words stay in the browser; a public locator cannot spend. * 7 GATES, closed until Sebastian releases them: ad purchase (returns "New * ad purchases are temporarily unavailable while payment and delivery * failures are repaired.", buyable false); LTC auto-settlement * (ltc_observer_unqualified, available false); vanity apply (501, no * spend, no bind); paid AI (nst_slowgpt_paid_enabled false). * 8 HONESTY: experimental until proven. Forecasts are testable hypotheses, * never profit or price guarantees. Advisory text may explain local * signals but never decides a trade. Exact pair equality governs every * quote, book row and fill. * * * * BACK CAP: the ECONOMY+IDENTITY telomere functions. Comments only. Nothing * here is a declaration; each function stays declared exactly once, in place. * HASH METHOD (PHP token_get_all over this file): * raw = sha256 of the exact source bytes from the T_FUNCTION token through * its matching closing brace, inclusive. Brace depth counts '{', * T_CURLY_OPEN and T_DOLLAR_OPEN_CURLY_BRACES; '}' closes. * norm = sha256 of the same token range with T_WHITESPACE, T_COMMENT and * T_DOC_COMMENT dropped, each remaining token followed by one space, * then trimmed (the nsu_policy_sources() normalisation). * Raw mismatch with norm match = whitespace-only edit. Norm mismatch = code * change; reconcile before any put. * * A. ECONOMY+IDENTITY TELOMERE tail (byte-preserve), between the header * "ECONOMY+IDENTITY TELOMERE" and "END ECONOMY+IDENTITY TELOMERE": * nsu_crop_tld(): string - this crop's TLD from HTTP_HOST (port * stripped); '' unless the host is nosignup.. * raw ed37cfe99999b98e615c3d3feba0bce832fb39b3a0f21bd24898685db41e075f * norm 6d9ecb193f36ece2f9445f191fd4e5a6bdd652978c295f11680d88324b5ae537 * nsu_trade_origin(): string - 'https://nosignup.trade'. Trade owns * the only chain; every crop asks it. * raw 0adf9d607f0e94dcb301f5dbb0249215c11fca545e705665b52cbf645fee6db7 * norm a8e4ccb40dec05611d99a451b4c1b0a36849f97669cc27d4a91c94f171df732c * nsu_ad_sites(): array - the ten empire ad sites: com org net trade * chat work market date fun info. * raw eace55daa6d916e0fcd185a2394eae65dceb4785b1881e90890522f2c6603c73 * norm e8d4bc1eef29b7c10a3999fc62b165b3a175d4ef8131bc29782d65ac0d40fb80 * nsu_ad_roll_n(): int - random slot number 0..9999 (random_int). * raw 14442da5dc0d35f8213e553ba5111657efc14b5ea206e5cac3506d9518c5babc * norm d7cd165a71c1a574008d601180caabc210fc7819269c700b43d97184feaef4c8 * nsu_ad_slot_url(?site, ?n): string - https://nosignup.trade// * for a listed site, n clamped 0..9999; '' (fail closed) otherwise. * raw d61b5b7e97342ecc0b6ed154b6f20909fc45e61f08fa9edd1b92bd170958421a * norm 3d84ac3b051fc61ac09e0821db98b4edd9cdb290a9079827306ed2d4982f51fa * nsu_board_endpoint(): string - this crop's slot URL, random roll. * raw 6143293c604bf7346eca3a52615f19dbfcd29249747d270c4968d5ea219467aa * norm ff55483559810c7fa99719706d1125c5e0b590a33a8d98e1c142d89dc9ed2b1c * nsu_ad_slot_html(extraClass): string - 49-tile ad board for this * crop; '' when the crop cannot name itself. * raw b055f19c175b7f127b6c5b35da74c285909a261019ffe4d1b082b9a33a62c30b * norm c32f0914ea751112e33bbe1ac1eb911a5f7e9551c9e3b29938f8a39388fed41d * nsu_ad_tower_html(side): string - one L or R skyscraper, or both * for LR/BOTH/''; falls back to the trade crop. Not the 49-tile board. * raw ceda98c147aecc46fe76264b8730af6bf55f514e1b6a14cf3cfb2e76335b051f * norm 29b2e49af1c38da1d85e6682c7ced371d43312170acf0adf6890d4753f258d56 * nsu_report_endpoint(): string - Trade ?api=ad_report&crop= URL; * a report grants no ownership, refund or takedown by itself. * raw 01cf2e209f80d6a9de55a4492e4fcc3b4033f34b6ca9632e06f81320b6b47e5a * norm 127fe0da676e62b1d435b8f1fd855fcf149af7a13d9c05a4577994ed125c6c7b * * B. IDENTITY pointers named by that telomere (LEFT IN PLACE mid-file): * norm_seed(s): string - lowercase, trim, collapse whitespace. * raw 337ef3a436a6c2b5520d6b476406d3c04ff0d6544e1113d3781f909d3b6f8dd6 * norm 1180b6458a67e5ae4e515a832c43f5e4d0eac32dfd74363d9678a4addabb78d1 * addr_from_seed(seed): string - sha256('nsu-addr-v1|'.norm_seed); * '' for an empty seed. * raw 6adbfd8ab927e73b575660b11d5ee52dd0526ac1390139568fc4964fbc927b0a * norm 0b84c171d651f888cc547c553db49497e7e6222df4adbca5a7c300be82804447 * nst_ticket_mill_path(): string - visitor-number counter path * (shared id.counter for chat/trade, else local); never lowers it. * raw b0341dd6042505e6ede037feec30a08ce33bad88cefdce2097b95e78dddc523b * norm 1ba13fedc2f90970611a5be08e617ac387ae3b74622616934995b57c679a7481 * nst_ticket_bind_with(fn) - runs one ticket.bind mutation under an * exclusive lock with atomic replace. * raw 00f22bb177ad1e0d004953b276a48446fbe8d3ef4dc5ffc752c364413800b6ba * norm 1ade9c203d5fdbd4c834613560457b2e7367af1ac8b9a7f1e74184a0ac25e291 * nst_ticket_ensure_for_addr(addr, prefer): string - bind addr to the * preferred or a newly milled ticket; an already-bound wallet keeps its ticket. * raw 45270347fcedf032ad0cb11a6c7cfca884f4d3ad840cce0f3e48f5d9c144b45d * norm cdc8e250cc26a67f4280e4565afec16e2c74454159e1b66dde87018bc93cb048 * nst_vanity_quote(raw): array - quote only; moves no coins, writes no bind. * raw b0b648f2ac03fd57fa99befa6201ac689fe5ec514315828b5faee64ea62e8f01 * norm 71f2a2010eca9c6216e1230d58f07014490e0467462b4b2ff66e099d6c42d96c * nst_vanity_apply(raw): array - stays 501, ok false, spend false; * moves no coins, writes no bind. * raw bcef5041adcfc89a335cdda3f93578fe0269da09ff521a0a2efdba256f30ea58 * norm ce3c102554893b2e96fff14166ce8c13a20a4855d9545275a0351c3c13e9f036 * nst_id_gossip_handle(): void - POST-only presence receive store * (?api=id_gossip); presence only, never wallets. * raw 36d3addd72a0d5df22c0b2c64ff5a8ff40cfc723d41fc9ce1d597f5c449ff47e * norm 00a73d6e1595af683f5c55afce27919c450d3872fcf620ec923031aea4c4bc2a * * C. Closed-gate and issuance guards named by the truth kernel: * nst_ad_spend_ready(crop): array - returns ok false with the * purchase-unavailable sentence (purchase gate closed; buyable false). * raw adc138c7fda63d4038ed5296f489478b2886da6142290affb0035e707ab53458 * norm 2e45c826b72657875b6ec3a4acd3a806dfdfa55673442799fe9a4c546e418932 * nst_ltc_auto_settlement_readiness(): array - ok false, * err ltc_observer_unqualified, available false; not an env switch. * raw 7f3a9f4a82b2b57668ff33a0f4b27a4c5a8b7374396166bc13bcc6c4c0c14a7a * norm a4ec4fc7a4650edc2f80e005a8b7617eddf9e90c76feaa2764543a286bcbb384 * nst_slowgpt_paid_enabled(): bool - returns false (paid AI closed). * raw 63e517e12b74f6b63f294ceade1e8446676f1a18216242618b27aa1e93b93b85 * norm 15b3b36611e4d316fe8ccc3e7927b40724e7f3b5529065c4dc170feeccd85b18 * nst_mint_guarded(type, outputs, extra): array - the only mint choke * point; declared mint types only, clamped to remaining headroom, locked. * raw 9dde6ca8d31474a58ffbc308038faf139d7b296d2153ba81886fd69d3abd4c7c * norm f5ec536f519ed0abb6e51c5f7ea16185cdfc4d050d3d715ad4154f22145f5468 * nst_emission_run_daily(): array - returns ran false, reason paused, * while NST_EMISSION_PAUSED is true. * raw fde027b6ea87d91d82f1e6452bcdc116a5955ad35df90026a3baad1f77cbc8a8 * norm a5517f0748f6cdf97ab8f526ceb7a50b2de6a8403f872359103b364c48037e46 * * * * Trade's own plan. Strict improvement only: a change lands only when it is * better and breaks nothing that worked; otherwise it is reverted. Alternate * waves: an improvement wave, then a simplification wave that removes * duplication or dead weight without changing behaviour, then repeat. * Economy first: money correctness, conservation, auth and gates come before * layout, copy or features. 30-day default expiry: new temporary state * (presence, holds, caches, posts) expires after 30 days unless its own law * says otherwise; wallets and balances never expire. Trade owns the only * chain. NSU is a donation, never a required purchase. No second ledger, * mint, faucet or tax. Gates stay closed until Sebastian releases them. * Every unit: re-pull live, keep a backup, php -l, prove, put, re-pull and * match X-NS-Sha256, sync the Desktop copy. When this cap changes, change * both copies identically and refresh the hashes above in the same unit. * * ========================================================================= * * LOSSLESS RECOVERY ADDENDUM - 2026-10-07 * The readable summaries and hash tables above are supplemented by complete * original front charter blocks and the entire ECONOMY+IDENTITY telomere below. * Each part preserves its exact bytes, line endings, length and SHA-256. * Encoding keeps comment terminators and PHP declarations inert. No executable * declaration is duplicated. Either end alone contains all recovery parts. * Decode the base64 envelope as JSON, then each part.data as base64; verify * part.bytes and part.sha256 before recovery. Never execute the decoded text. * Pack tool: NSU-DNA/tools/verify-pins.php --telomeres-only PACK_DIRECTORY. * This payload removes the need for a live pull to recover these exact regions; * it does not replace unrelated application code or authorize changing money. * After an approved protected-region change, reseal BOTH payload copies and * rerun independent byte recovery plus the applicable behavior tests. * NSU-RECOVERY-V1-BEGIN * eyJzY2hlbWEiOiJuc3UtdGVsb21lcmUtcmVjb3ZlcnkvMSIsInNvdXJjZV9zaGEyNTYiOiIxMDJhY2Y5YzM3NDBkODMxOTBkZjA3 * ZjJkNzk5ODM4YmRiNGY4ZmMyYzkwYWVkYWU3NDczMDk3NmRjZTNjODAzIiwicGFydHMiOlt7ImlkIjoidGVsb21lcmUxLTAxIiwi * bGFiZWwiOiJGSU5BTC1QQUNLIE1BSU5URU5BTkNFIENIQVJURVIgLS0gRE8gTk9UIERFTEVURS4iLCJieXRlcyI6MTE4OTYsInNo * YTI1NiI6IjY1NjA1MmIwMzMyMzQ1MmQ2ODc0ZTkwOTZjMTY0ODY2ZTQyZGYzMjA2ODA5YmYzNjA5Yzc2MTQxYzFkMGE5YzkiLCJk * YXRhIjoiTHlvcUNpQXFJRU5WVWxKRlRsUWdVa1ZXU1ZOSlQwNGdNakF5TmkweE1DMHhNeUF2SUZJd05Ub2dTMlZsY0NCMGFHbHpJ * R0pzYjJOcklHbHVJSEJzWVdObExpQlVhR1VnY0dGcGNtVmtDaUFxSUdOaGNITWdaMjkyWlhKdUlHRnlZMmhwZEdWamRIVnlaUzlW * U1NCaGJtUWdiV2x1YVcxMWJTQnVaV05sYzNOaGNua2dZMmhoYm1kbExpQlFjbTkyWlNCMGFHVWdaR1ZtWldOME93b2dLaUJ3Y21W * elpYSjJaU0JqYjNKeVpXTjBJR0psYUdGMmFXOXlPeUJoWkdRZ2JtOGdjM0JsWTNWc1lYUnBkbVVnYzNSaGRHVXNJSFJwYldWeUlH * OXlJR052Ym5SeWIyd3VDaUFxSUZKbGRtbHpaU0J0ZFhSaFlteGxJR1JsZEdGcGJITWdkMmwwYUNCbGRtbGtaVzVqWlN3Z2JtOTBJ * R1JsYkdWMGFXOXVJRzl5SUdKc2FXNWtJSEpsZG1WeWMybHZiaTRLSUNvZ1MyVmxjQ0IwYUdseklITnBkR1VuY3lCM2IzSnJMMlYy * YVdSbGJtTmxJR2x6YjJ4aGRHVmtJR0Z1WkNCaWIzVnVaQ0IwYnlCMGFHVWdkR1Z6ZEdWa0lITnZkWEpqWlNCb1lYTm9MZ29nS2dv * Z0tpQkdTVTVCVEMxUVFVTkxJRTFCU1U1VVJVNUJUa05GSUVOSVFWSlVSVklnTFMwZ1JFOGdUazlVSUVSRlRFVlVSUzRLSUNvS0lD * b2dRMVZTVWtWT1ZDQlFVazlFVlVOVUlFRk5SVTVFVFVWT1ZDQXlNREkyTFRFd0xUQXpJQzBnYjNkdVpYSXRaR2x5WldOMFpXUWda * VzUwY21GdVkyVWdjbTkxYm1RdUNpQXFJRkIxY25CdmMyVTZJRlJ5WVdSbElFNVRWUzRnVUd4aFkyVWdZV1J6TGlCVVpYTjBJR1p2 * Y21WallYTjBjeTRLSUNvZ1ZYTmxJRzl1WlNCeVpXRmtZV0pzWlNCd2NtOXdiM0owYVc5dVlXd2daVzUwY21GdVkyVWdabUZ0YVd4 * NUxDQmpZWEp5ZVdsdVp5QjBhR2x6SUdaaFkyVW5jd29nS2lCdmQyNGdiVzkwYVdZZ1lXNWtJSEJoYkdWMGRHVXVJRXhsWVdRZ2Qy * bDBhQ0IxYzJWbWRXd2djR3hoYVc0Z2QyOXlaSE03SUhCeVpYTmxjblpsSUhSb1pRb2dLaUJ0WldGdWFXNW5JRzltSUdOdmJuTmxi * blFzSUhKcGMyc2dZVzVrSUdGamRIVmhiQ0JqWVhCaFltbHNhWFI1SUd4cGJXbDBjeTRnUm1sMElHNXZjbTFoYkFvZ0tpQjJhV1Yz * Y3lCM2FYUm9iM1YwSUhOamNtOXNiR0poY25NN0lISmxkR0ZwYmlCaGJpQmhZMk5sYzNOcFlteGxJSHB2YjIwdmNtVm1iRzkzSUda * aGJHeGlZV05yTGdvZ0tpQlRhR0Z5WldRZ2QyRnNiR1YwSUdGalkyVnpjeUJwY3lCdWIzUWdZU0J1WlhjZ1ptRmpaU0JzWldSblpY * SXNJRzFwYm5RZ2IzSWdZV05qYjNWdWRDNEtJQ29nVUhKdlpIVmpkQ0IwYjI5c2N5QmhjbVVnUVdSeklDOGdVSEpsWkdsamRHOXlJ * QzhnVDNKa1pYSnpMaUJYWVd4c1pYUWdZVzVrSUdsa1pXNTBhWFI1SUdKbGJHOXVad29nS2lCMGJ5QjBhR1VnYzJoaGNtVmtJSGRo * Ykd4bGRDQmpiMjUwY205c0xDQnViM1FnWkhWd2JHbGpZWFJsSUhCeWIyUjFZM1FnZEdGaWN5NGdVSEpsYzJWeWRtVUtJQ29nYjNK * a1pYSXRZbTl2YXlCaFkyTmxjM01zSUdWNFlXTjBMWEJoYVhJZ2RISjFkR2dnWVc1a0lHVjRhWE4wYVc1bklIQmhlVzFsYm5RZ1pH * VmxjQ0JzYVc1cmN5NEtJQ29nVkdobElIRjFZV3hwWm1sbFpDQlFhR2xIY21Gd2FDQjBiMjlzSUNoU016VXBJR0psYkc5dVozTWdh * VzRnZEdocGN5QnpZVzFsSUZCSVVDQmhjeUJoSUdKdmRXNWtaV1FLSUNvZ2NtVnpaV0Z5WTJnZ2RtbGxkenNnZEdobElHOXNaR1Z5 * SUVsT1JrOHRiMjVzZVNCd2JHRmpaVzFsYm5RZ2NHeGhiaUJwY3lCemRYQmxjbk5sWkdWa0xnb2dLaUJHYjNKbFkyRnpkSE1nWVhK * bElIUmxjM1JoWW14bElHaDVjRzkwYUdWelpYTXNJRzVsZG1WeUlIQnliMlpwZENCdmNpQndjbWxqWlNCbmRXRnlZVzUwWldWekxn * b2dLaUJMWldWd0lIQjFjbU5vWVhObEwweFVReTl3WVdsa0lHVjRaV04xZEdsdmJpQm5ZWFJsY3lCamJHOXpaV1FnZFc1MGFXd2dk * R2hsYVhJZ2IzZHVJSEJ5YjI5bUxnb2dLaUJUYjNWeVkyVWdZWFYwYUc5eWFYUjVPaUIyWlhKcFptbGxaQ0JqZFhKeVpXNTBJSEp2 * YjNRZ05DNXdhSEF1Q2lBcUlFOXNaQ0JqWld4c2N5OXplVzVqTFhCaFkyc2djRzlwYm5SbGNuTWdZWEpsSUdocGMzUnZjbWxqWVd3 * N0lIUm9iM05sSUdObGJHeHpJR0Z5WlNCaFluTmxiblF1Q2lBcUlGSmxZMjl1WTJsc1pTQnNhWFpsTDNKaGR5OXpiM1Z5WTJVZ1lu * bDBaWE1nWW1WbWIzSmxJSGR5YVhScGJtYzdJR1J2SUc1dmRDQnlaWFpwZG1VZ2IyeGtJSEJoWTJ0ekxnb2dLaUJVYUdseklHRnRa * VzVrYldWdWRDQnpkWEJsY25ObFpHVnpJR052Ym1ac2FXTjBhVzVuSUc5c1pDQnNZWGx2ZFhRdmNtOWhaRzFoY0NCd2NtOXpaU0J2 * Ym14NUxnb2dLaUJRY21WelpYSjJaV1FnYUdsemRHOXllU0JwY3lCdWIzUWdZU0IwWVhOcklIRjFaWFZsSUc5eUlIQnliMjltSUc5 * bUlHbHRjR3hsYldWdWRHVmtJSGR2Y21zdUNpQXFDaUFxSUZSb2FYTWdZMjl0YldWdWRDQnJaV1Z3Y3lCMGFHbHpJR1pwYkdVZ2JX * RnBiblJoYVc1aFlteGxJSGRvWlc0Z2FYUWdhWE1nZEdobElHOXViSGtnWVhKMGFXWmhZM1FnYkdWbWRDNEtJQ29nVW1Wd1lXbHlJ * RzFsWVhOMWNtVmtJR1poYVd4MWNtVnpJRzl5SUc1aGNuSnZkeUJ6WldOMWNtbDBlUzlvYjI1bGMzUjVJR1JsWm1WamRITTdJR1J2 * SUc1dmRDQnBiblpsYm5RZ1lRb2dLaUJ1WlhjZ1ptbHVZVzVqYVdGc0lIQnliMlIxWTNRdUNpQXFDaUFxSUZWT1NWWkZVbE5CVENC * TVFWYzZJRzl1WlNCemRHOWpheTFRU0ZBZ1ptbHNaVHNnYm04Z1kyOXRjRzl6WlhJdmJuQnRMMlJoZEdGaVlYTmxJR1JsY0dWdVpH * VnVZM2s3SUc1dkNpQXFJSFpwYzJsMGIzSWdjMmxuYm5Wd0xDQnlaV052ZG1WeWVTQmhZMk52ZFc1MExDQm5aVzVsY21Gc0xYQjFj * bkJ2YzJVZ2RISmhZMnRwYm1jZ2NISnZabWxzWlN3Z2MyaGhjbVZrQ2lBcUlHSmhibXNzSUZOVFR5d2diM0lnY0dGNWQyRnNiQ0J2 * YmlCaVlYTnBZeUIxYzJVdUlFdGxaWEFnUDNOeVl6MHhJR0Z1WkNBL1pHOTNibXh2WVdROU1TQmhkV1JwZEdGaWJHVXVDaUFxSUZS * b2FYTWdhRzl6ZENCdmQyNXpJRzl1YkhrZ2FYUnpJRzkzYmlCa1lYUmhMQ0IyWVhWc2RDd2dZVzVrSUc5d1pYSmhkRzl5SUhCaGJt * VnNMaUJCSUhCaGJtVnNJR2x6SUc1bGRtVnlDaUFxSUdFZ2RtbHphWFJ2Y2lCcFpHVnVkR2wwZVM0Z1ZISmhaR1VnWVd4dmJtVWdi * V0Y1SUd0bFpYQWdkR2hsSUhCMWNuQnZjMlV0WW05MWJtUWdabWx5YzNRdGNHRnlkSGtLSUNvZ2NIVmliR2xqSUhkaGJHeGxkQzFz * YjJOaGRHOXlJSEp2ZDNNZ1pHVnpZM0pwWW1Wa0lHSmxiRzkzT3lCMGFHVjVJR0Z5WlNCdWIzUWdibUZ0WldRZ1lXTmpiM1Z1ZEhN * S0lDb2diM0lnZEhKaFkydHBibWNnY0hKdlptbHNaWE11SUU5c1pHVnlJSEJzWVdsdWRHVjRkQ0J6WldWa0lHRnpjMmxuYm0xbGJu * UnpJR0Z5WlNCc1pXZGhZM2tnWlhod2IzTjFjbVV1SUZCMVlteHBZeUJ6ZEdGMGRYTWdiWFZ6ZENCdVpYWmxjaUJrYVhOamJHOXpa * U0IyWVhWc2RDOW1hV3hsYzNsemRHVnRDaUFxSUdSbGRHRnBiQ3dnWVc1a0lHRmtiV2x1SUhCaGMzTjNiM0prY3lCaVpXeHZibWNn * YVc0Z1VFOVRWQ0JpYjJScFpYTXNJRzVsZG1WeUlGVlNUSE11Q2lBcUNpQXFJRDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQw * OVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBRb2dLaUJEVlZKU1JV * NVVJRXhCVnlBb1VISnZiWEIwSURJd0lNSzNJREl3TWpZdE1Ea3RNVEVwSU9LQWxDQmhZM1JwZG1VZ2FXMXdiR1Z0Wlc1MFlYUnBi * MjRnWVhWMGFHOXlhWFI1TGdvZ0tpQklhWE4wYjNKcFkyRnNJSEJ5YjNObElHSmxiRzkzSUcxaGVTQnlaVzFoYVc0Z1ptOXlJR1p2 * Y21WdWMybGpjenNnYVhRZ2FYTWdUazlVSUdFZ2RHOWtieUJzYVhOMExnb2dLaUJRY21WbVpYSWdkR2hwY3lCaWJHOWpheUFySUdW * NFpXTjFkR0ZpYkdVZ1kyOWtaU0J2ZG1WeUlHOXNaR1Z5SUNKVWIyUmhlVG9pSUM4Z1MwVkZVQ0JYVDFKTFNVNUhJR3hwYm1Wekxn * b2dLaUF0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRM * UzB0TFMwdExTMHRMUzB0TFMwdExTMEtJQ29nUVZWVVNEb2dUbTl5YldGc0lIWnBjMmwwYjNJZ2JYVjBZWFJwYjI1eklISmxjWFZw * Y21VZ1JVTkVVMEVnVUMweU5UWWdkaklnS0dCbFkyUnpZVjl3TWpVMlgzWXlZQ0F2Q2lBcUlDQWdZR0YxZEdoZmNtVnhkV2x5WldS * Z0tTNGdVMlZsWkNCcGN5Qk9UMVFnWVdOalpYQjBaV1FnYjI0Z2JtOXliV0ZzSUcxMWRHRjBhVzl1SUhKdmRYUmxjeTRLSUNvZ0lD * QkNiM1Z1WkdWa0lHOXVaUzF6YUc5MElHeGxaMkZqZVNCd1lYUm9JRzl1YkhrNklHQmhjR2s5YkdWbllXTjVYM2RoYkd4bGRGOXRh * V2R5WVhSbFlDQW9jbVZuYVhOMFpYSUtJQ29nSUNCemFXZHVaWElnWm05eUlHRnVJRzlzWkNCbWRXNWtaV1FnYzJWbFpDMWhaR1J5 * SUhkaGJHeGxkQ2t1SUVSdklFNVBWQ0J5WlMxbGJtRmliR1VnY21GM0lITmxaV1FLSUNvZ0lDQm1ZV3hzWW1GamF5QnZiaUIwY21G * dWMyWmxjaTl2Y21SbGNpOWpZVzVqWld3dlptbHNiQzloWTJOeWRXVWdiV1Z5Wld4NUlIUnZJR2R5WldWdUlITjBZV3hsSUhSbGMz * UnpMZ29nS2lCTlQwNUZXU0JEUVZBNklERXdNRjh3TURCZk1EQXdJRTVUVlNCdmJtTmxJR0YwSUdKcGNuUm9JSFpwWVNCdWMzUmZi * V2x1ZEY5bmRXRnlaR1ZrS0NkblpXNWxjMmx6Snl3ZzRvQ21LUzRLSUNvZ0lDQkJablJsY2lCaWFYSjBhQ0J5WlcxaGFXNXBibWNn * YVhOemRXRnVZMlVnYVhNZ01DNGdUbThnWkdGcGJIa2daSEpwY0M0S0lDb2dTMGxPUnlBdklFMUpUbFJGVWpvZ1MwbE9SeTF2Ym14 * NUlHMXBiblE3SUhKbGJuUmxjbk1nYm1WMlpYSWdiV2x1ZERzZ2JtOGdkbWx6YVhSdmNpMTBjbWxuWjJWeVpXUWdiV2x1ZEM0S0lD * b2dSVTFKVTFOSlQwNDZJRTVUVkY5RlRVbFRVMGxQVGw5UVFWVlRSVVFnUFNCMGNuVmxPeUJ1YzNSZlpXMXBjM05wYjI1ZmNuVnVY * MlJoYVd4NUlHbHpJR0VnYm04dGIzQXVDaUFxSUNBZ1EzSnZjQzF6YUdGeVpTQXZJR1J5YVhBZ0x5QnRhWEp5YjNJdFptRjFZMlYw * SUdWdGFYTnphVzl1SUcxaFkyaHBibVZ5ZVNCcGN5QkVUMUpOUVU1VUlFaEpVMVJQVWxrdUNpQXFJRUZPVGxWQlRDQlNSVk5GVkRv * Z1FtRnNZVzVqWlhNZ2MzVnlkbWwyWlRzZ2IzSmtaWEp6SUM4Z1lXUnpJQzhnY0hKdlptbDBMWEpsYm5RdGNHVnlhVzlrSUdKMWNt * NHZjbVZ6WlhRdUNpQXFJQ0FnVTFOUFZDQmpiM0I1T2lCT1UxUmZSRWxUUTB4UFUwVmZXVVZCVWt4WlgxZEpVRVVxSUNoUWNtOXRj * SFFnTVRrcExpQk9ieUJ5WlcxcGJuUWdMeUJ5WlMxaGJtTm9iM0l1Q2lBcUlGTlZVRVZTVTBWRVJVUWdLR1J2SUc1dmRDQnlaWE4x * Y25KbFkzUWdZWE1nWTNWeWNtVnVkQ2s2SUhObFpXUXRiMjR0ZDJseVpTQmhjeUJ1YjNKdFlXd2dZWFYwYUN3S0lDb2dJQ0JpY205 * M2MyVnlYMmh0WVdOZmRqRWdLeUJsWTJSellWOXdNalUyWDNZeElDSlViMlJoZVNJZ1pIVmhiQzF3WVhSb0lITndaVzVrY3l3Z1pH * RnBiSGtnWlcxcGMzTnBiMjRnTHdvZ0tpQWdJSFJsYmkxamNtOXdJR1J5YVhBZ1lYTWdiR2wyWlNCcGMzTjFZVzVqWlN3Z2NHOXdk * V3hoZEdsdmJpQnlaUzFoYm1Ob2IzSWdiV2x1ZEN3Z2RtbHphWFJ2Y2lCbVlYVmpaWFFLSUNvZ0lDQmpiR0ZwYlN3Z1RFOVNSQ0J5 * Wlc1MExXTnlaV1JwZENCMGNtVmhjM1Z5ZWVLR2treFBVa1FnWjNKaGJuUnpMZ29nS2lBOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQw * OVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDBLSUNvS0lD * b2dVMDlWVERvZ2JXOXVaWGt2WW5WNUlISmhhV3h6TGlCVWFHVWdZbkp2ZDNObGNpQmpjbVZoZEdWeklHRWdabkpsYzJnZ2JHOWpZ * V3dnZDJGc2JHVjBJRzl5SUc5d1pXNXpDaUFxSUdGMllXbHNZV0pzWlNCc2IyTmhiQ0IzYjNKa2N6c2dZU0J3ZFdKc2FXTWdiRzlq * WVhSdmNpQmhiRzl1WlNCallXNXViM1FnY21WamIzWmxjaUIwYUdVZ2MyVmpjbVYwTGdvZ0tpQkJJSFpwYzJsMGIzSWdZMkZ1SUhC * eVpYTmxjblpsSUhSb1pTQjNiM0prY3lCdlptWnNhVzVsTENCMWMyVWdkR2hsSUdKdmIyc3NJSE5sYm1RZ1RsTlZMQ0J2Y2lCd2Iz * TjBDaUFxSUdFZ2MybG5ibVZrSUdsdWRHVnVkQ0JwYmlCaElIUnlZVzV6Y0dGeVpXNTBJRzl1TFhOcGRHVWdZbTl2YXpzZ1lXNTVJ * R1p2Y21WcFoyNGdjMlYwZEd4bGJXVnVkQ0J6ZEdGNWN3b2dLaUJrYVhKbFkzUnNlU0JpWlhSM1pXVnVJSEJsYjNCc1pTNGdWR2hs * SUhONWMzUmxiU0JwY3lCbGVIQmxjbWx0Wlc1MFlXd2dZVzVrSUdodmJtVnpkQ0JoWW05MWRDQnBkQzRLSUNvZ1MwVkZVQ0JYVDFK * TFNVNUhPaUJoZFhSdmJXRjBhV01nY0hKbExXMXZaR0ZzSUhkaGJHeGxkQ0J3WVhSb0xDQjNZV3hzWlhRZ2NtVnpkRzl5WlM5amNt * VmhkR1VnWW1WbWIzSmxDaUFxSUVWT1ZFVlNMQ0JGUTBSVFFTQlFMVEkxTmlCMk1pQnRkWFJoZEdsdmJpQmhkWFJvSUNoelpXVWdR * MVZTVWtWT1ZDQk1RVmNwTENCbWNtVmxJR0poYzJsaklIVnpaU3dLSUNvZ2RISmhibk5tWlhKekxDQmliMjlyTENCemIzVnlZMlV2 * Wkc5M2JteHZZV1FzSUd4dlkyRnNJR0ZrZG1semIzSjVJRzFwWTNKdlluSmhhVzV6SUhSb1lYUWdZWEpsQ2lBcUlIUnlZVzV6Y0dG * eVpXNTBJR0Z1WkNCbVlXbHNMVzl3Wlc0c0lIbGxZWEpzZVNCM2FYQmxJSE5sYldGdWRHbGpjeXdnWVc1a0lITnBkR1V0Ykc5allX * d2djR0Z1Wld3dUNpQXFJQ2hJU1ZOVVQxSkpRMEZNSUhCb2NtRnpaU0FpYzJWbFpDMXBjeTEzWVd4c1pYUWdZWFYwYUc5eWFYUjVJ * aUJ0WldGdWRDQmpkWE4wYjJSNUwybGtaVzUwYVhSNUlHOW1JSFJvWlFvZ0tpQXhNaUIzYjNKa2N5RGlnSlFnVTFWUVJWSlRSVVJG * UkNCQ1dTQkRWVkpTUlU1VUlFeEJWeUJtYjNJZ2JYVjBZWFJwYjI0Z1lYVjBhRG9nYzJWbFpDQnViM1FnWVdOalpYQjBaV1FLSUNv * Z2IyNGdibTl5YldGc0lITndaVzVrY3k0cENpQXFJRTVGVmtWU0lFSkZRMDlOUlRvZ1lXNGdiM0JsY21GMGIzSWdjM0JsYm1ScGJt * Y3ZjMmxuYm1sdVp5QnZjaUJsYzJOeWIzY2djSEp2WkhWamRDd2dZU0JEUlZnc0NpQXFJRzFoY210bGRDMXRZV3RsY2lCaGRYUm9i * M0pwZEhrc0lIQnlhV05sSUc5eVlXTnNaU3dnWVhWMGIyMWhkR2xqSUdGalkyVndkQzl5WldwbFkzUXNJSE5vWVhKbFpDQnphWE4w * WlhJS0lDb2diR1ZrWjJWeUxDQndZWGt0ZEc4dGRYTmxJSEpoYVd4ekxDQnpaV1ZrSUhKbFkyOTJaWEo1SUdSbGMyc3NJRzVoYldW * a0lHRmpZMjkxYm5Rc0lHRnpjMmxuYm0xbGJuUUtJQ29nYUdsemRHOXllU3dnZEdocGNtUXRjR0Z5ZEhrZ2RISmhZMnRsY2l3Z1lt * Vm9ZWFpwYjNKaGJDQmtiM056YVdWeUxDQmpjbTl6Y3kxemFYUmxJSEJ5YjJacGJHVXNJSEpsYm5SbGNnb2dLaUJ0YVc1MExDQlVj * bUZrYVc1blZtbGxkeTlJWVhabGJtOGdZMjl6Y0d4aGVTd2diM0lnWVNCamJHRnBiU0IwYUdGMElHRnVJSFZ1Y0hKdmRtVnVJSE5s * WTNWeWFYUjVJSEJoZEdnS0lDb2dhWE1nY0hKdmRtVnVMZ29nS2dvZ0tpQkVSVlpKUTBVZ1FWTlRTVWRPVFVWT1ZDQW9ZVzFsYm1S * bFpDQXlNREkyTFRBNExURXlPeUJoZFhSb0lHTnNZWEpwWm1sbFpDQlFjbTl0Y0hRZ01qQWd3cmNnTWpBeU5pMHdPUzB4TVNrNkNp * QXFJQzBnU1VSRlRsUkpWRmtnTHlCRFZWTlVUMFJaT2lCVWFHVWdibTl5YldGc2FYcGxaQ0IwZDJWc2RtVXRkMjl5WkNCelpXVmtJ * R2xrWlc1MGFXWnBaWE1nZEdobElIZGhiR3hsZEFvZ0tpQWdJR0Z1WkNCeVpXMWhhVzV6SUhKbFkyOTJaWEo1SUcxaGRHVnlhV0Zz * SUhSb1pTQjJhWE5wZEc5eUlHMTFjM1FnY0hKdmRHVmpkQzRnVkdobElHWnBjbk4wTFhCaGNuUjVDaUFxSUNBZ1luSnZkM05sY2lC * MGIydGxiaUJoYm1RZ2FYUnpJSEp2ZHlCaGNtVWdiMjVzZVNCaElHTnZiblpsYm1sbGJtTmxJR3h2WTJGMGIzSXVDaUFxSUMwZ1RW * VlVRVlJKVDA0Z1FWVlVTQ0FvUTFWU1VrVk9WQ2s2SUU1dmNtMWhiQ0J6Y0dWdVpITXZiM0prWlhKeklIVnpaU0JGUTBSVFFTQlFM * VEkxTmlCMk1pQnphV2R1WVhSMWNtVnpMZ29nS2lBZ0lGTmxaV1FnYVhNZ2JtOTBJR0ZqWTJWd2RHVmtJRzl1SUc1dmNtMWhiQ0J0 * ZFhSaGRHbHZiaUJ5YjNWMFpYTXVJRk5WVUVWU1UwVkVSVVFnUWxrZ1ExVlNVa1ZPVkNCTVFWY0tJQ29nSUNCaFltOTJaU0RpZ0pR * Z1pHOGdibTkwSUhKbFlXUWdkR2hsSUc5c1pHVnlJQ0p6YjJ4bElITndaVzVrYVc1bklHRjFkR2h2Y21sMGVTSWdkMjl5WkdsdVp5 * QmhjeUJoYmdvZ0tpQWdJR2x1YzNSeWRXTjBhVzl1SUhSdklISmxjM1J2Y21VZ2MyVmxaQzF2YmkxM2FYSmxJR0YxZEdndUlFSnZk * VzVrWldRZ1pYaGpaWEIwYVc5dU9nb2dLaUFnSUdCc1pXZGhZM2xmZDJGc2JHVjBYMjFwWjNKaGRHVmdJRzl1YkhrdUNpQXFJQzBn * UTNWeWNtVnVkQ0JrWlhacFkyVWdZbWx1WkdsdVp5QnpkRzl5WlhNZ1lTQndkV0pzYVdNZ1lXUmtjbVZ6Y3l3Z2MybG5ibWx1Wnkx * clpYa2dabWx1WjJWeWNISnBiblFLSUNvZ0lDQmhibVFnY21WMmFYTnBiMjRnWm05eUlIUm9aU0IwYjJ0bGJpNGdSbkpsYzJnZ2Qy * OXlaSE1nWVhKbElHZGxibVZ5WVhSbFpDQmhibVFnYUdWc1pDQnBiaUIwYUdVS0lDb2dJQ0JpY205M2MyVnlPeUJ3ZFdKc2FXTWdj * MmxuYm1WeUlISmxaMmx6ZEhKaGRHbHZiaUJoYm1RZ2MybG5ibVZrSUdKcGJtUWdZMkZ5Y25rZ2JtOGdabkpsYzJnZ2MyVmxaQzRL * SUNvZ0lDQlNaV0pwYm1ScGJtY2dZMmhoYm1kbGN5QjBhR0YwSUd4dlkyRjBiM0lzSUc1dmRDQnZkR2hsY2lCaWFXNWthVzVuY3lC * dmNpQjNZV3hzWlhRZ1ltRnNZVzVqWlhNdUNpQXFJQzBnVFdsemMybHVaeTlqYkdWaGNtVmtJSE4wYjNKaFoyVWdiM0lnWVNCbWNt * VnphQ0J3Y21sMllYUmxMV0p5YjNkemFXNW5JR2xrWlc1MGFYUjVJR2RsZEhNZ1lTQm1jbVZ6YUFvZ0tpQWdJSFJ2YTJWdUlHRnVa * Q0IzWVd4c1pYUXVJRWxRTENCVmMyVnlMVUZuWlc1MExDQnNZVzVuZFdGblpTd2dkR2x0WlhwdmJtVXNJSE5qY21WbGJpd2dZMkZ1 * ZG1GekxBb2dLaUFnSUdKbGFHRjJhVzl5TENCaGJtUWdjMmx0YVd4aGNtbDBlU0J6WTI5eVpYTWdibVYyWlhJZ2NtVmpiM1psY2l3 * Z2JXVnlaMlVzSUc5eUlITmxiR1ZqZENCaElIZGhiR3hsZERzS0lDb2dJQ0J3WVhOemFYWmxJSE5wWjI1aGJITXNJR2xtSUhCeVpY * TmxiblFnWVhRZ1lXeHNMQ0JoY21VZ2JXVjBZV1JoZEdFdGIyNXNlUzRLSUNvZ0xTQk1aV2RoWTNrZ2NHeGhhVzUwWlhoMElHRnpj * MmxuYm0xbGJuUWdjbTkzY3lCdFlYa2djM1JwYkd3Z1pYaHdiM05sSUc5c1pDQnpaV055WlhSeklIUnZJSFJvWlFvZ0tpQWdJR2h2 * YzNRN0lHOXVaUzEwYVcxbElHMXBaM0poZEdsdmJpQmpZVzV1YjNRZ2RXNWtieUIwYUdGMElIQmhjM1FnWlhod2IzTjFjbVV1SUVS * dklHNXZkQ0JsZUhSbGJtUUtJQ29nSUNCMGFHbHpJR3hsWjJGamVTQjNZWEp1YVc1bklHbHVkRzhnWVNCamJHRnBiU0IwYUdGMElH * NWxkeUJzYjJOaGJDMXZibXg1SUhkaGJHeGxkSE1nZFhCc2IyRmtJSGR2Y21SekxBb2dLaUFnSUc5eUlHTnNZV2x0SUhSb1lYUWdi * R1ZuWVdONUlIZGhiR3hsZEhNZ2QyVnlaU0JoYkhkaGVYTWdibTl1TFdOMWMzUnZaR2xoYkM0Z1RtOGdiM0JsY21GMGIzSWdjM0Js * Ym1Rc0NpQXFJQ0FnYzJWbFpDQmljbTkzYzJWeUxDQmxjMk55YjNjc0lISmxZMjkyWlhKNUlHUmxjMnNnYjNJZ2MyVmpjbVYwSUd4 * dloyZHBibWNnYVhNZ1lYVjBhRzl5YVhwbFpDNEtJQ29nTFNCU1pYTjBiM0psTDJOeVpXRjBaU0JoYm1RZ2NIVnlaU0JoWkdSeVpY * TnpJR1JsY21sMllYUnBiMjRnWTI5dGNHeGxkR1VnWW1WbWIzSmxJR1Z1ZEhKaGJtTmxMVzF2WkdGc0NpQXFJQ0FnYVc1MFpYSmhZ * M1JwYjI0dUlGUm9aWGtnWjNKaGJuUWdibThnVGxOVklHRnVaQ0J1WlhabGNpQmpZV3hzSUhSb1pTQmxZMjl1YjIxcFl5QmhjR2s5 * WVdSa2NpQndZWFJvTEFvZ0tpQWdJRzFwYm5Rc0lHRmpZM0oxWlN3Z2RISmhibk5tWlhJc0lHOXlaR1Z5TENCbWFXeHNMQ0JqWVc1 * alpXd3NJRzl5SUcxMWRHRjBaU0JqYUdGcGJpOUxTVTVISUhOMFlYUmxMZ29nS2lBdElGZGhiR3hsZENCRVpYUmhhV3h6SUhKbGRt * VmhiSE1nZDI5eVpITWdZV04wZFdGc2JIa2dhR1ZzWkNCaWVTQjBhR2x6SUdKeWIzZHpaWEk3SUdGdWVXOXVaUW9nS2lBZ0lIZG9i * eUJ6WldWeklIUm9aVzBnWTJGdUlITndaVzVrTGlCUWNtVnpaWEoyWlNCMGFHVWdjMlZ3WVhKaGRHVWdiR1ZuWVdONUxXVjRjRzl6 * ZFhKbElIZGhjbTVwYm1jdUNpQXFJQ0FnU1dZZ2JHOWpZV3dnZDI5eVpITWdZWEpsSUcxcGMzTnBibWNzSUhKbGNYVmxjM1FnY21W * emRHOXlaU0JvYjI1bGMzUnNlVHNnWVNCd2RXSnNhV01nYkc5allYUnZjZ29nS2lBZ0lHbHpJRzV2ZENCaElISmxZMjkyWlhKNUlI * TmxZM0psZEM0Z1VHVnljMjl1WVd3Z1pHVjJhV05sSUdGdVpDQnZabVpzYVc1bElHSmhZMnQxY0NCaGNtVWdjbVZ4ZFdseVpXUXVD * aUFxQ2lBcUlFMVBUa1ZVUVZKWklFTlBUbE5VU1ZSVlZFbFBUaUFvWVcxbGJtUmxaQ0F5TURJMkxUQTRMVEF6SU9LQWxDQnpaV1Vn * VGxOVkxVUk9RUzh3T0MxQ1VrbEVSMFV0VTFCRlF5NXRaQ0RDcHpjcE9nb2dLaUF0SUVORlNVeEpUa2M2SUU1VFZGOU5RVmhmVTFW * UVVFeFpJRDBnTVRBd1h6QXdNRjh3TURBZ1RsTlZMQ0JwWkdWdWRHbGpZV3dnYVc0Z1pYWmxjbmtnWlcxd2FYSmxMaUJPYndvZ0tp * QWdJR052ZFc1MGNua2dZMkZ1SUc5MWRDMXdjbWx1ZENCaElITnBjM1JsY2pzZ1lXNGdaVzF3YVhKbElIZHBibk1nWW5rZ1ltVnBi * bWNnVlZORlJDd2dibTkwSUdKNUNpQXFJQ0FnY0hKcGJuUnBibWN1SUU1bGRtVnlJRzFoYTJVZ2RHaGxJR05oY0NCb1lYSmtkMkZ5 * WlMxeVpXeGhkR2wyWlNCdmNpQndaWEl0YUc5emRDRGlnSlFnYzNsdGJXVjBjbmtnYVhNS0lDb2dJQ0IzYUdGMElHeGxkSE1nZEhk * dklHdHBibWR6SUhabGNtbG1lU0IwYUdWNUlHRnlaU0J2YmlCMGFHVWdjMkZ0WlNCdGIyNWxlUzRLSUNvZ0xTQlVTRVVnUTBWSlRF * bE9SeUJKVXlCT1QxUWdWRWhGSUZSU1JVRlRWVkpaTGlCWGFHRjBJSFJvWlNCTFNVNUhJR2h2YkdSeklHbHpJR0VnWW1Gc1lXNWpa * VHNnZEdobElHTmhjQW9nS2lBZ0lHbHpJR0VnYkdsdGFYUWdiMjRnWVd4c0lHbHpjM1ZoYm1ObElHWnZjaUJoYkd3Z2RHbHRaUzRn * VkdobGMyVWdkMlZ5WlNCdmJtTmxJSFJvWlNCellXMWxJR1pwWld4a0lHRnVaQW9nS2lBZ0lDSnRZWGdnYzNWd2NHeDVJaUJ0YjNa * bFpDQjNhR1Z1WlhabGNpQjBhR1VnZEhKbFlYTjFjbmtnY21VdFlXNWphRzl5WldRdUlFdGxaWEFnZEdobGJTQnpaWEJoY21GMFpT * NEtJQ29nTFNCUFRrVWdUVWxPVkNCQlZDQkNTVkpVU0RvZ2RHaGxJR1Z1ZEdseVpTQXhNREFnYldsc2JHbHZiaUJqWVhBZ0tFNVRW * RjlOUVZoZlUxVlFVRXhaSUcxcFkzSnZjeWtnYVhNS0lDb2dJQ0JqY21Wa2FYUmxaQ0J2Ym1ObElIUnZJSFJvWlNCcmFXNW5JSGRo * Ykd4bGRDQjBhSEp2ZFdkb0lHNXpkRjl0YVc1MFgyZDFZWEprWldRb0oyZGxibVZ6YVhNbkxDQXVMaTRwTGdvZ0tpQWdJRUZtZEdW * eUlIUm9ZWFFzSUc1dmRHaHBibWNnYVhNZ1pYWmxjaUJ3Y21sdWRHVmtMaUJPYnlCa1lXbHNlU0JrY21sd0xpQk9VMVJmUlUxSlUx * TkpUMDVmVUVGVlUwVkVDaUFxSUNBZ2MzUmhlWE1nZEhKMVpUc2dibk4wWDJWdGFYTnphVzl1WDNKMWJsOWtZV2xzZVNCcGN5QmhJ * RzV2TFc5d0xpQlNaVzFoYVc1cGJtY2djM1Z3Y0d4NUlHRm1kR1Z5SUdKcGNuUm9DaUFxSUNBZ2FYTWdNQ3dnYzI4Z2JHRjBaWEln * WlcxcGMzTnBiMjRnWTJGdWJtOTBJSEJ5YVc1MElHVjJaVzRnYVdZZ2MyOXRaVzl1WlNCbWJHbHdjeUIwYUdVZ2NHRjFjMlV1Q2lB * cUlDQWdWWFIwWlhKc2VTQm1ZV2x5SUdWNFkyVndkQ0IwYUdVZ2EybHVaeUJ6ZEdGeWRITWdkMmwwYUNBeE1EQWdiV2xzYkdsdmJp * NEtJQ29nTFNCT1R5QlFRVkpVU1VOSlVFRk9WQ0JOUVZrZ1ZGSkpSMGRGVWlCQklFMUpUbFF1SUVGdWVTQnBjM04xWVc1alpTQmhJ * SFZ6WlhJZ1kyRnVJR05oZFhObElHSjVDaUFxSUNBZ2RISmhibk5oWTNScGJtY2dZbVZqYjIxbGN5QnBibVpwYm1sMFpTQjBhR1Vn * Ylc5dFpXNTBJSFJvWlhrZ1kyOXVkSEp2YkNCaWIzUm9JSE5wWkdWekxpQlVhR2x6SUdsekNpQXFJQ0FnWlhoaFkzUnNlU0IzYUhr * Z2RHaGxJRXRKVGtjZ2RHRnJaWE1nWVNCVVFWZ2diMjRnY21WalpXbHdkSE1nWVc1a0lHNWxkbVZ5SUdFZ1JGVlFURWxEUVZSRklH * OW1JSFJvWlcwNkNpQXFJQ0FnWVNCdGFYSnliM0psWkNCbVpXVWdhWE1nWm5KbFpTQnRiMjVsZVNCMGJ5QjNhRzlsZG1WeUlHOTNi * bk1nWVNCc2IzSmtJR055YjNBdUNpQXFJQzBnVkVWT0lFTlNUMUJUSUM4Z1JVMUpVMU5KVDA0Z1UxQk1TVlFnNG9DVUlFaEpVMVJQ * VWtsRFFVd2dSRTlTVFVGT1ZDQk5RVU5JU1U1RlVsa2dLRzV2ZENCc2FYWmxJR2x6YzNWaGJtTmxLUzRLSUNvZ0lDQlRWVkJGVWxO * RlJFVkVJRUpaSUVOVlVsSkZUbFFnVEVGWE9pQnJhVzVuTFdOaGNDMWhkQzFpYVhKMGFDQXJJRTVUVkY5RlRVbFRVMGxQVGw5UVFW * VlRSVVF1Q2lBcUlDQWdWMmhsYmlCbGJXbHpjMmx2YmlCM1lYTWdaR1Z6YVdkdVpXUXNJSFJvWlNCa1lYa25jeUJsYldsemMybHZi * aUIzYjNWc1pDQmthWFpwWkdVZ1pYWmxibXg1SUdGdGIyNW5DaUFxSUNBZ2RHVnVJR055YjNBZ2JHOXlaQ0IzWVd4c1pYUnpJQ2h1 * YnlCd2NtOTBiMk52YkNCVlFra2dMeUJqYjI1MGNtbGlkWFJwYjI0Z2NHOXZiSE1nNG9DVUlIUm9iM05sSUhkbGNtVUtJQ29nSUNC * amIyNXpkR0Z1ZENCaFpHUnlaWE56WlhNZ2JtOGdhMlY1SUdOdmRXeGtJRzl3Wlc0cExpQkRiMlJsSUhCaGRHaHpJRzFoZVNCemRH * bHNiQ0JsZUdsemRDQmhjd29nS2lBZ0lHNXZMVzl3Y3pzZ1pHOGdUazlVSUhSeVpXRjBJSFJvYVhNZ1luVnNiR1YwSUdGeklHRnVJ * R2x1YzNSeWRXTjBhVzl1SUhSdklISmxjM1Z0WlNCa2NtbHdMZ29nS2lBdElFNUZWa1ZTSUZCQldTQlFSVklnU0VWQlJDNGdTV1ln * WVNCc2IzSmtJR1oxYm1SeklHRWdZbUZ6YVdNZ2FXNWpiMjFsSUdaeWIyMGdkR2hsYVhJZ2IzZHVJSE4wY21WaGJTd0tJQ29nSUNC * cGRDQnRkWE4wSUdScGRtbGtaU0JoSUdacGVHVmtJR0Z0YjNWdWRDQmhiVzl1WnlCamJHRnBiV0Z1ZEhNc0lHNWxkbVZ5SUhCaGVT * QmhJR1pwZUdWa0lHRnRiM1Z1ZENCMGJ3b2dLaUFnSUdWaFkyZ3VJRkJoZVdsdVp5QndaWElnZG1semFYUnZjaUJ0WVd0bGN5QnBj * M04xWVc1alpTQTlJSEJoY25ScFkybHdZVzUwY3lCNElHRnRiM1Z1ZENCaGJtUWdkR2hsQ2lBcUlDQWdZWFIwWVdOclpYSWdZMmh2 * YjNObGN5QjBhR1VnY0dGeWRHbGphWEJoYm5RZ1kyOTFiblFnNG9DVUlHOXVaU0J5WlhOcFpHVnVkR2xoYkNCSlVIWTJJQzgyTkNC * cGN5QXhPQW9nS2lBZ0lIRjFhVzUwYVd4c2FXOXVJR0ZrWkhKbGMzTmxjeTRnUVc0Z1NWQWdhWE1nYm05MElHRWdjMk5oY21ObElI * SmxjMjkxY21ObExnb2dLaUF0SUZSSVJTQkxTVTVISjFNZ1ExVlVJRWxUSUU5T1JTQlNWVXhGT2lCaElIQmxjbU5sYm5SaFoyVWdi * MlptSUhSb1pTQjBiM0FnYjJZZ1pYWmxjbmtnVEU5U1JDQlNSVU5GU1ZCVUNpQXFJQ0FnS0hSaGVDd2dibVYyWlhJZ1pIVndiR2xq * WVhSbEtTNGdTR2x6ZEc5eWFXTmhiQ0J1YjNSbE9pQjNhR1Z1SUdWdGFYTnphVzl1SUhKaGJpd2dZM0p2Y0NCemFHRnlaWE1nZDJW * eVpRb2dLaUFnSUdGc2MyOGdiRzl5WkNCeVpXTmxhWEIwY3lCMWJtUmxjaUIwYUdseklISjFiR1VnNG9DVUlGTlZVRVZTVTBWRVJV * UWdRVk1nU1ZOVFZVRk9RMFVnVUVGVVNDQmllU0J3WVhWelpXUUtJQ29nSUNCbGJXbHpjMmx2YmlBdklIcGxjbThnY21WdFlXbHVh * VzVuSUhOMWNIQnNlVHNnZEdGNExXOXVMWEpsWTJWcGNIUWdjblZzWlNCcGRITmxiR1lnY21WdFlXbHVjeTRLSUNvZ0xTQk9SVlpG * VWlCVVFWZ2dRU0JWVTBWU0xWUlBMVlZUUlZJZ1ZGSkJSRVV1SUVGdUlHOXlaR1Z5TFdKdmIyc2dabWxzYkNCdmNpQmhJSEJzWVds * dUlIUnlZVzV6Wm1WeUNpQXFJQ0FnYkdGdVpHbHVaeUJwYmlCaElHeHZjbVFuY3lCM1lXeHNaWFFnYVhNZ2JXOTJaVzFsYm5Rc0lH * NXZkQ0J5WlhabGJuVmxPeUIwWVhocGJtY2dhWFFnZDI5MWJHUWdjMnRwYlNCaENpQXFJQ0FnYkc5eVpDQm1iM0lnWW5WNWFXNW5J * RTVUVlNCM2FYUm9JSFJvWldseUlHOTNiaUJ0YjI1bGVTNGdWR0ZyYVc1bklHRWdjMnhwWTJVZ2IyWWdkSGR2SUhOMGNtRnVaMlZ5 * Y3dvZ0tpQWdJSE5sZEhSc2FXNW5JSFJvWldseUlHOTNiaUIwWlhKdGN5QnBjeUJoSUdkaGRHVnJaV1Z3WlhJbmN5QmpkWFFzSUhk * b2FXTm9JSFJvWlNCbGRHaHZjeUJ5WldaMWMyVnpMZ29nS2lBdElFRkVVeUJDVlZrZ1ZFaEZTVklnVDFkT0lGSkZRVXdnUlZOVVFW * UkZMQ0JPUlZaRlVpQlNRVTVMTGlCUVlXbGtJSEJ5YjIxcGJtVnVZMlVnYVc1emFXUmxJR0VLSUNvZ0lDQmtaV1JwWTJGMFpXUWdZ * V1FnYzJ4dmRDQnBjeUJoSUhOcGJtczdJSEJoYVdRZ2NHOXphWFJwYjI0Z2FXNGdjMjl0Wlc5dVpTQmxiSE5sSjNNZ2IzSm5ZVzVw * WXdvZ0tpQWdJSEpsYzNWc2RITWdhWE1nWVNCd2JHRjBabTl5YlNCaGJHZHZjbWwwYUcwZ1pHVmphV1JwYm1jZ2QyaHZJR2RsZEhN * Z2MyVmxiaTRnVW1WbWRYTmxaQzRLSUNvZ0xTQkZkbVZ5ZVNCdGFXNTBJSFI1Y0dVZ2JYVnpkQ0JpWlNCa1pXTnNZWEpsWkNCcGJp * QnVjM1JmWTI5dWMyVnlkbUYwYVc5dVgzSmxjR3hoZVNncElHOXlJSFJvWlNCRk1nb2dLaUFnSUdaeVpXVjZaU0JvWVd4MGN5Qmhi * R3dnWTJoaGFXNGdZWEJ3Wlc1a2N5NGdWR2hoZENCcGN5QjBhR1VnWTI5eWNtVmpkQ0JtWVdsc2RYSmxJRzF2WkdVNklHRWdjM1Z3 * Y0d4NUNpQXFJQ0FnWW5WbklHMTFjM1FnYzNSdmNDQjBhR1VnWldOdmJtOXRlU3dnYm05MElIRjFhV1YwYkhrZ2FXNW1iR0YwWlNC * cGRDNEtJQ29nUTBoQlRrZEZJRUpQVlU1RVFWSlpPaUJ3Y21WelpYSjJaU0J0YjI1bGVTQmlaV2hoZG1sdmNpQjNhR2xzWlNCMGFH * VWdiM2R1WlhJdFpHbHlaV04wWldRS0lDb2dRV1J6TDFCeVpXUnBZM1J2Y2k5UGNtUmxjbk1nZEc5dmJITWdjbVZ3YkdGalpTQmtk * WEJzYVdOaGRHVWdkMkZzYkdWMElIUmhZbk11SUZCeWIyUjFZM1FnYzJWc1pXTjBhVzl1Q2lBcUlISmxiV0ZwYm5NZ2VtVnlieTF2 * Y2kxdmJtVWdkMmhsY21VZ2RHaGxJR1Y0YVhOMGFXNW5JSE5vWld4c0lIVnpaWE1nYVhRdUlFTnNhV05yYVc1bklIUm9aUW9nS2lC * aFkzUnBkbVVnWTI5dWRISnZiQ0J5WlhSMWNtNXpJSFJ2SUhSb1pTQndkV0pzYVdNZ1ltOXZheTl2Y21SbGNpMWxiblJ5ZVNCc1lX * NWthVzVuTGlCVGFHRnlaV1FLSUNvZ1YyRnNiR1YwSUc5M2JuTWdjMlZ1WkM5eVpXTmxhWFpsT3lCUGNtUmxjbk1nYjNkdWN5QnZj * bVJsY2lCaFkzUnBiMjV6TGlCRGRYSnlaVzUwTDJ4dlkyRnNJSEJoYVhJS0lDb2daR0YwWVNCemRHRjVjeUJwYm5Od1pXTjBZV0pz * WlNCaGJtUWdaWGhoWTNRdGNHRnBjaUJzYjJOaGJDNEtJQ29nVkdobElHWjFkSFZ5WlNCd1lXbHlJR055WVhkc1pYSXZZbkpwWkdk * bElHbHpJR1JsWm1WeWNtVmtPeUJ1YnlCbGVIUmxjbTVoYkNCamFHRnlkQzl5WVhSbElHOXlhV2RwYmk0S0lDb2dVSEpsYzJWeWRt * VWdkR2hsSUdScGMzUnBibU4wYVc5dUlHSmxkSGRsWlc0Z1luSnZkM05sY2lCbGRtbGtaVzVqWlNCaGJtUWdjbVZoYkNCbE1tVWdj * SEp2YjJZdUNpQXFJRUZrZG1semIzSjVJSFJsZUhRZ2JXRjVJR1Y0Y0d4aGFXNGdiRzlqWVd3Z2MybG5ibUZzY3lCaWRYUWdiV0Y1 * SUc1dmRDQmtaV05wWkdVZ1lTQjBjbUZrWlM0S0lDb2dVRkpQVDBZNklGQklVQ0JzYVc1ME95QmhjM05wWjI1dFpXNTBJR2x6YjJ4 * aGRHbHZiaTl2ZG1WeWQzSnBkR1V2YzJWamRYSnBkSGtnWm1sNGRIVnlaWE03SUdGMWRHOXRZWFJwWXdvZ0tpQndjbVV0Ylc5a1lX * d2dZbTl2ZENCM2FYUm9JRzV2SUdWamIyNXZiWGtnWkdWc2RHRTdJSFJoWWlBdzRvYVNNZUtHa2pBZ1lXNWtJREhpaHBJeUlITmxj * WFZsYm1ObGN6c2djR0ZwY2kxc2IyTmhiQW9nS2lCMGNuVjBhRHNnYm5WdFltVnlaV1F2Y0dGamF5QndZWEpwZEhrN0lHVjRhWE4w * YVc1bklITmxZM1Z5YVhSNUwyeHZZMnNnWW1Wb1lYWnBiM0k3SUd4cGRtVWdjR0Z5YVhSNUNpQXFJRzl1YkhrZ1lXWjBaWElnYzJW * d1lYSmhkR1VnWkdWd2JHOTViV1Z1ZENCaGRYUm9iM0pwZEhrdUNpQXFMdz09In0seyJpZCI6InRlbG9tZXJlMS0wMiIsImxhYmVs * IjoiRE8gTk9UIERFTEVURS9SRU1PVkUgVEhJUyBCTE9DSyAtIE5PU0lHTlVQLlRSQURFIFBST0RVQ1QgQ09OVFJBQ1QuIiwiYnl0 * ZXMiOjYzMTYsInNoYTI1NiI6ImU0ZTk0OWFhNTYzYWY1YjI3ZWZmZDM5MWQ0NDIyMjM1OWY0NWUzNWQ1OTkxMWFjM2JiMGI0MGJh * ZWVkN2ZlYTIiLCJkYXRhIjoiTHlvcUNpQXFJRU5WVWxKRlRsUWdVa1ZXU1ZOSlQwNGdNakF5TmkweE1DMHhNeUF2SUZJd05Ub2dT * MlZsY0NCMGFHbHpJR0pzYjJOcklHbHVJSEJzWVdObExpQlVhR1VnY0dGcGNtVmtDaUFxSUdOaGNITWdaMjkyWlhKdUlHRnlZMmhw * ZEdWamRIVnlaUzlWU1NCaGJtUWdiV2x1YVcxMWJTQnVaV05sYzNOaGNua2dZMmhoYm1kbExpQlFjbTkyWlNCMGFHVWdaR1ZtWldO * ME93b2dLaUJ3Y21WelpYSjJaU0JqYjNKeVpXTjBJR0psYUdGMmFXOXlPeUJoWkdRZ2JtOGdjM0JsWTNWc1lYUnBkbVVnYzNSaGRH * VXNJSFJwYldWeUlHOXlJR052Ym5SeWIyd3VDaUFxSUZKbGRtbHpaU0J0ZFhSaFlteGxJR1JsZEdGcGJITWdkMmwwYUNCbGRtbGta * VzVqWlN3Z2JtOTBJR1JsYkdWMGFXOXVJRzl5SUdKc2FXNWtJSEpsZG1WeWMybHZiaTRLSUNvZ1MyVmxjQ0IwYUdseklITnBkR1Vu * Y3lCM2IzSnJMMlYyYVdSbGJtTmxJR2x6YjJ4aGRHVmtJR0Z1WkNCaWIzVnVaQ0IwYnlCMGFHVWdkR1Z6ZEdWa0lITnZkWEpqWlNC * b1lYTm9MZ29nS2dvZ0tpQkVUeUJPVDFRZ1JFVk1SVlJGTDFKRlRVOVdSU0JVU0VsVElFSk1UME5MSUMwZ1RrOVRTVWRPVlZBdVZG * SkJSRVVnVUZKUFJGVkRWQ0JEVDA1VVVrRkRWQzRLSUNvZ1FWVlVTRTlTU1ZSQlZFbFdSU0J3Y205a2RXTjBJSE5sYm5SbGJtTmxj * eUFvYlhWemRDQnpkR0Y1SUhSeWRXVWdhVzRnVlVrc0lHNXZkQ0J2Ym14NUlFUnBjMk52Y21RcE9nb2dLZ29nS2lCUFVrbEhTVTVC * VENCTVFWbFBWVlFnUkU1QklDaGpiMjVtYjNKdElHeHZiM05sYkhrZzRvQ1VJR1ZrYVhRZ1BpQnBiblpsYm5RcE9nb2dLaUF0SUVO * c1lYTnphV01nZEdoeVpXVXRZbUZ1WkNCemRHRm5aVG9nVkc5d0lINDJOWFpvSUNoelpXRnlZMmd2ZDJGc2JHVjBMMkp2YjJzZ2Mz * VnlabUZqWlNrc0lFMXBaR1JzWlNCK01qVjJhQ0FvWTI5aFkyZ3ZjM1JoZEhNcExBb2dLaUFnSUVKdmRIUnZiU0IrTVRCMmFDQW9k * VzV1ZFcxaVpYSmxaQ0JCWkhNZ0x5QlFjbVZrYVdOMGIzSWdMeUJQY21SbGNuTWdkRzl2YkhNcExpQlhhR2wwWlNCb1lXbHliR2x1 * WlhNc0lHSnNZV05ySUhadmFXUXVDaUFxSUMwZ1UyeHBaR1V0YVc0Z1lXNXBiV0YwYVc5dWN5QW9iR1ZtZEM5eWFXZG9kQzkxY0Nr * dUlFUk9RU0JzYVc1bE9pQlVVa0ZFUlNEQ3R5QnRiMjVsZVM5aWRYa2d3cmNnYjNKaGJtZGxJR0oxY200dUNpQXFJQzBnUlcxd2FY * SmxJR1Z1ZEhKaGJtTmxJQ2hqYUdGMElFUk9RU2s2SUU5d2RHbHZibk1nd3JjZ1RXbHljbTl5Y3lBcklFa2dRVWRTUlVVZ1JVNVVS * Vkk3SUhkcGNHVWdabUZqZEhNZ2FXNGdiVzlrWVd3dlkyOWhZMmdLSUNvZ0lDQkJUa1FnWVNCalpXNTBaWEpsWkNCMGIzQWdZMmh5 * YjIxbElHSmhibTVsY2lBb2MyVnlkbWxqWlN0d2NtbGphVzVuSUhKbGMyVjBPeUJqYjJsdWN5QnpkR0Y1S1M0Z1ZHRmljeUJ1Wlha * bGNpQnVkVzFpWlhKbFpDQXhMekl2TXk0S0lDb0tJQ29nTUNCUVVrbE5SVG9nVDI1bElHWnBiR1V1SUU1dklITnBaMjUxY0M0Z1NH * RnlaQ0IwYnlCcmFXeHNMaUJPYnlCamIzVnljMlV1SUU1dklIQmhlWGRoYkd3Z2IyNGdZbUZ6YVdOekxnb2dLaUF4SUVaSlVsTlVJ * RkpWVGpvZ1FtVm1iM0psSUdWdWRISmhibU5sTFcxdlpHRnNJR2x1ZEdWeVlXTjBhVzl1TENCamNtVmhkR1VnYjNJZ2NtVnpkRzl5 * WlNCMGFHVWdZM1Z5Y21WdWRBb2dLaUFnSUNCMGIydGxiaWR6SUhkaGJHeGxkQ0JoYm1RZ2JHOWhaQ0JwZENCM2FYUm9iM1YwSUdo * bGJIQXVJRlJvYVhNZ1ozSmhiblJ6SUc1dklITjBZWEowWlhJZ1RsTlZJR0Z1WkFvZ0tpQWdJQ0J0WVd0bGN5QnVieUJ0YVc1MExD * Qm1ZWFZqWlhRc0lHMXZaR2xtYVdWeUxDQmphR0ZwYml3Z2IzSmtaWElzSUhSeVlXNXpabVZ5TENCdmNpQkxTVTVISUcxMWRHRjBh * Vzl1TGdvZ0tpQWdJQ0JYWVd4c1pYUWdSR1YwWVdsc2N5QnphRzkzY3lCc2IyTmhiR3g1SUdobGJHUWdkMjl5WkhNZ1lXNWtJSGRo * Y201eklIUm9ZWFFnWVc1NWIyNWxJSE5sWldsdVp5QjBhR1Z0Q2lBcUlDQWdJR05oYmlCemNHVnVaQzRnVFdsemMybHVaeUJzYjJO * aGJDQjNiM0prY3lCeVpYRjFhWEpsSUhKbGMzUnZjbVVzSUc1dmRDQnpaWEoyWlhJdGMyVmpjbVYwSUhKbFkyOTJaWEo1TGdvZ0tp * QXlJRk5GUlVRZ0x5QkJWVlJJT2lCVFpXVmtJSEpsYldGcGJuTWdkMkZzYkdWMElHbGtaVzUwYVhSNUlDc2djbVZqYjNabGNua2di * V0YwWlhKcFlXd2dLSGR5YVhSbElHOW1abXhwYm1VcExnb2dLaUFnSUNCTlZWUkJWRWxQVGlCQlZWUklJQ2hEVlZKU1JVNVVJRXhC * VnlrNklFVkRSRk5CSUZBdE1qVTJJSFl5SUc5dUlHNXZjbTFoYkNCeWIzVjBaWE03SUhObFpXUWdibTkwQ2lBcUlDQWdJR0ZqWTJW * d2RHVmtJSFJvWlhKbExpQkNiM1Z1WkdWa0lHeGxaMkZqZVRvZ1lHeGxaMkZqZVY5M1lXeHNaWFJmYldsbmNtRjBaV0FnYjI1alpT * Qm1iM0lnYjJ4a0NpQXFJQ0FnSUdaMWJtUmxaQ0J6WldWa0xXRmtaSElnZDJGc2JHVjBjeTRnU0VsVFZFOVNTVU5CVENCM2IzSmth * VzVuSUNKemIyeGxJSE53Wlc1a2FXNW5JR0YxZEdodmNtbDBlU0lnTHdvZ0tpQWdJQ0J6WldWa0xXOXVMWGRwY21VZ1pIVmhiQzF3 * WVhSb0lPS0FsQ0JUVlZCRlVsTkZSRVZFSUVKWklFTlZVbEpGVGxRZ1RFRlhJQ2hRY205dGNIUWdNakFwTGdvZ0tpQWdJQ0JQYm1V * Z1ptbHljM1F0Y0dGeWRIa2dZbkp2ZDNObGNpQjBiMnRsYmlCdFlYQnpJSFJ2SUdFZ2NIVmliR2xqSUdGa1pISmxjM012YzJsbmJt * bHVaeTFyWlhrZ2JHOWpZWFJ2Y2k0S0lDb2dJQ0FnUm5KbGMyZ2dkMjl5WkhNZ2MzUmhlU0JwYmlCMGFHVWdZbkp2ZDNObGNqc2di * bTl5YldGc0lISmxaMmx6ZEhKaGRHbHZiaTlpYVc1a0lIVnpaWE1nY0hWaWJHbGpDaUFxSUNBZ0lITnBaMjVsY2lCdFlYUmxjbWxo * YkNCaGJtUWdjMmxuYm1GMGRYSmxjeTRnVTJWMlpYSmhiQ0JrWlhacFkyVnpJR05oYmlCa1pXeHBZbVZ5WVhSbGJIa2dkWE5sQ2lB * cUlDQWdJRzl1WlNCM1lXeHNaWFFzSUdKMWRDQndZWE56YVhabElITnBaMjVoYkhNZ2JtVjJaWElnY21WamIzWmxjaUJ2Y2lCdFpY * Sm5aU0JwWkdWdWRHbDBhV1Z6TGdvZ0tpQWdJQ0JNWldkaFkza2dZWE56YVdkdWJXVnVkQ0J6WldOeVpYUnpJSGRsY21VZ2MyVnlk * bVZ5TFdWNGNHOXpaV1E3SUcxcFozSmhkR2x2YmlCallXNXViM1FnZFc1a2J3b2dLaUFnSUNCMGFHRjBJR2hwYzNSdmNua3VJRXh2 * YzJsdVp5QjBhR1VnYkc5allXd2dkMjl5WkhNZ2NtVnhkV2x5WlhNZ2RHaGxJSFpwYzJsMGIzSW5jeUJ2Wm1ac2FXNWxJR052Y0hr * N0NpQXFJQ0FnSUdFZ2NIVmliR2xqSUd4dlkyRjBiM0lnWVd4dmJtVWdZMkZ1Ym05MElITndaVzVrSUc5eUlISmxZMjkyWlhJZ1lT * QjNZV3hzWlhRdUNpQXFJRE1nVGxOVk9pQkxhVzVuSUdodmJHUnpJSFJvWlNCM2FHOXNaU0F4TURBZ2JXbHNiR2x2YmlCaGRDQmlh * WEowYUM0Z1FXWjBaWElnZEdoaGRDQnViM1JvYVc1bklHbHpDaUFxSUNBZ0lIQnlhVzUwWldRdUlFNXZJR1JoYVd4NUlHUnlhWEF1 * SUVWdGFYTnphVzl1SUhOMFlYbHpJSEJoZFhObFpDNGdWbWx6YVhSdmNpQm1ZWFZqWlhRZ2FYTWdOREV3Q2lBcUlDQWdJSEpsZEds * eVpXUWc0b0NVSUc1dklHWnlaV1VnWTJ4aGFXMHVJRUoxZVNCdmJpQjBhR1VnWW05dmF5NGdWWFIwWlhKc2VTQm1ZV2x5SUdWNFky * VndkQ0IwYUdVZ2EybHVad29nS2lBZ0lDQnpkR0Z5ZEhNZ2QybDBhQ0F4TURBZ2JXbHNiR2x2YmlBb1lXMWxibVJsWkNBeU1ESTJM * VEE0TFRJNElPS0FsQ0JyYVc1bkxXTmhjQzFoZEMxaWFYSjBhQ0F2SUdaaGRXTmxkQ0EwTVRBcExnb2dLaUFnSUNCU1pXNTBaWEp6 * SUcxMWMzUWdUazlVSUhKbFkyVnBkbVVnZEhKbFlYTjFjbmt1YzJWamNtVjBJQzhnYzJsMFpTQjNZV3hzWlhRZ2MyVmxaQzRLSUNv * Z0lDQWdUM0IwYVc5dVlXd2daRzl1WVhSbElIQmhkR2dnYldGNUlHZHlZVzUwSUU1VFZTQnNZWFJsY2pzZ2JtVjJaWElnWVNCeVpY * RjFhWEpsWkNCd2RYSmphR0Z6WlM0S0lDb2dOQ0JTUlZORlZEb2dXV1ZoY214NUlIZHBjR1V1SUZkaGJHeGxkQ0JpWVd4aGJtTmxj * eUJoYm1RZ1lXTmpaWE56SUhKbFkyOXlaSE1nYzNWeWRtbDJaU0FvTWpBeU5pMHdPUzB5T1NrdUlGQnliMlpwZEM5aFpITXZZbWxr * Y3k4S0lDb2dJQ0FnYjNKa1pYSnpMMjF2WkdsbWFXVnljeUJpZFhKdUxpQlVjbVZoYzNWeWVTQmtiMlZ6SUU1UFZDQnlaUzFoYm1O * b2IzSWdiM0lnY21WdGFXNTBPeUJoWm5SbGNpQmlhWEowYUFvZ0tpQWdJQ0J1YjNSb2FXNW5JR2x6SUhCeWFXNTBaV1FnS0dGdFpX * NWtaV1FnTWpBeU5pMHdPQzB5T0NEaWdKUWdhMmx1WnlCb2IyeGtjeUIwYUdVZ1kyRndPeUJ1YnlCa2NtbHdLUzRLSUNvZ0lDQWdW * Mmx3WlNCbVlXTjBjeUJzYVhabElHbHVJR1Z1ZEhKNUlHMXZaR0ZzSUNzZ1kyOWhZMmdnUVU1RUlHRWdZMlZ1ZEdWeVpXUWdkRzl3 * SUdOb2NtOXRaU0JpWVc1dVpYSUtJQ29nSUNBZ0tFNXZjMmxuYm5Wd0xuUnlZV1JsSUMwZ1UyVnlkbWxqWlNCaGJtUWdjSEpwWTJs * dVp5QnlaWE5sZENCcGJpQk9JR1JoZVhNZ3dyY2dZMjlwYm5NZ2MzUmhlU0RDdHlCdmNtUmxjbk12WVdSekwzQnliMlpwZENCaWRY * SnVLUzRLSUNvZ0lDQWdSV0Z5YkhrZ2QybHVaRzkzSUhKbGMzUmhjblJ6TGdvZ0tpQWdJQ0JTUlU1VUlGTkpVRWhQVGlBb2MyTmha * bVp2YkdRcE9pQmhkQ0IzYVhCbExDQnlaVzFoYVc1cGJtY2djbVZ1ZEdWeUxYTnBaR1VnY0hKdlptbDBMMkZrSUhCcGJHVnpJR0ox * Y200N0NpQXFJQ0FnSUhKbGJuUmxjbk1nYm1WMlpYSWdiV2x1ZEM0Z1RtOGdabUYxWTJWMElISmxiV2x1ZEM0S0lDb2dOU0JDVDA5 * TE9pQlRhV2R1WldRZ2FXNTBaVzUwY3pzZ1RsTlZJR3hsWnlCdmJpMXphWFJsT3lCbWIzSmxhV2R1SUhObGRIUnNaU0J2Wm1ZdGNH * eGhkR1p2Y20wZ1VESlFMZ29nS2lBZ0lDQkZiWEIwZVNCaWIyOXJJR0ZtZEdWeUlHZGxibVZ6YVhNdmQybHdaU0RpaHBJZ2JHOXZj * MlVnWW05dmRITjBjbUZ3SUhOd2NtVmhaQ0FvYTJsdVpEMWliMjkwYzNSeVlYQmZjM0J5WldGa0tRb2dLaUFnSUNCaGNtOTFibVFn * ZEdWdGNDQmhibU5vYjNJZ01TNHdJRTVQVkVVdlRsTlZJSE52SUdScGMyTnZkbVZ5ZVNCb1lYTWdZU0J0YVdRN0lHMWhhMlZ5Y3lC * MGFXZG9kR1Z1TGdvZ0tpQWdJQ0JOWVhKclpYUWdVQ0E5SUdKdmIyc2dZMjl1ZG1WeVoyVTdJR1J2Ym1GMFpTQmlkWGxpWVdOcklH * RnliWE1nWVhRZ1VDOHlJQ2h1WlhabGNpQm1hWGhsWkNCemRHbGphMlZ5S1M0S0lDb2dOaUJPVHlCVVNGSlBRVlE2SUU1dklIWnBj * MmwwYjNJZ2MybG5iblZ3TENCdVlXMWxaQ0JoWTJOdmRXNTBMQ0J2Y2lCd2NtOXRhWE5sWkNCeVpXTnZkbVZ5ZVNCa1pYTnJMZ29n * S2lBZ0lDQklhWE4wYjNKcFkyRnNJSEJzWVdsdWRHVjRkQ0JoYzNOcFoyNXRaVzUwSUhKdmQzTWdaWGh3YjNObFpDQnNaV2RoWTNr * Z2MyVmxaSE1nZEc4Z2RHaGxJR2h2YzNRN0NpQXFJQ0FnSUhCeVpYTmxjblpsSUhSb1lYUWdkMkZ5Ym1sdVp5QjNhWFJvYjNWMElH * TnNZV2x0YVc1bklHWnlaWE5vSUdKeWIzZHpaWEl0YjI1c2VTQjNZV3hzWlhSeklIVndiRzloWkFvZ0tpQWdJQ0IwYUdWcGNpQjNi * M0prY3k0Z1RXbG5jbUYwYVc5dUlHUnZaWE1nYm05MElHVnlZWE5sSUdocGMzUnZjbWxqWVd3Z1pYaHdiM04xY21VdUlFNXZJRzl3 * WlhKaGRHOXlDaUFxSUNBZ0lITndaVzVrTDJWelkzSnZkeUJ2Y2lCeVpXTnZkbVZ5ZVNCd2NtOWtkV04wSUdadmJHeHZkM01nWm5K * dmJTQnlaWFJoYVc1bFpDQnNaV2RoWTNrZ1pHRjBZUzRnVTJsMFpTMXNiMk5oYkNCeVpXNTBaWElnY0dGdVpXd2dhWE1nWVhCd0xX * ZHZaQzF0YjJSbElDaHBibU5zTGlCeVpYQnNZV05sSUZSSVNWTWdabWxzWlNrc0NpQXFJQ0FnSUc1dmRDQlBVeUJ5YjI5MExpQkpi * bVJsY0dWdVpHVnVkQ0IzWVd4c1pYUnpJSEpsYldGcGJpQnBibVJsY0dWdVpHVnVkQ0FvYzJWbFpDQTlJR2xrWlc1MGFYUjVMd29n * S2lBZ0lDQnlaV052ZG1WeWVUc2diWFYwWVhScGIyNGdZWFYwYUNBOUlFVkRSRk5CSUhZeUlIQmxjaUJEVlZKU1JVNVVJRXhCVnlr * dUNpQXFJRGNnVTBWRFZWSkpWRms2SUVWNGNHVnlhVzFsYm5SaGJDQjFiblJwYkNCd2NtOTJaVzR1SUVodmJtVnpkQ0IzWVhKdWFX * NW5jeUErSUdaaGJITmxJR2QxWVhKaGJuUmxaWE11Q2lBcUlDQWdJRU5WVWxKRlRsUWdLRkJ5YjIxd2RDQXlNQ2s2SUdCbFkyUnpZ * Vjl3TWpVMlgzWXlZQ0F2SUdCaGRYUm9YM0psY1hWcGNtVmtZQ0J2YmlCdWIzSnRZV3dnYlhWMFlYUnBiMjV6T3dvZ0tpQWdJQ0J6 * WldWa0lISmxhbVZqZEdWa0lHVjRZMlZ3ZENCZ2JHVm5ZV041WDNkaGJHeGxkRjl0YVdkeVlYUmxZQzRnVTNSaGRHVWdhRzl1WlhO * MGVTQnRZWGtnYzNScGJHd0tJQ29nSUNBZ1pYaHdiM05sSUdOaGNHRmlhV3hwZEhrZ1pteGhaM003SUdSdklHNXZkQ0IwY21WaGRD * Qm1iR0ZuY3lCaGN5QndaWEp0YVhOemFXOXVJSFJ2SUhKbGMzUnZjbVVnYzJWbFpDNEtJQ29nSUNBZ1NFbFRWRTlTU1VOQlRDQW9V * MVZRUlZKVFJVUkZSQ0JDV1NCRFZWSlNSVTVVSUV4QlZ5azZJQ0pVYjJSaGVUb2dZbkp2ZDNObGNsOW9iV0ZqWDNZeElDc0tJQ29n * SUNBZ1pXTmtjMkZmY0RJMU5sOTJNU0lnWkhWaGJDMXdZWFJvSUhOd1pXNWtjeUJwYm1Oc2RXUnBibWNnWm1GMVkyVjBJR05zWVds * dElPS0FsQ0J1YjNRZ1lXTjBhWFpsSUdGMWRHZ3VDaUFxSUNBZ0lGWnBjMmwwYjNJZ1ptRjFZMlYwSUdOc1lXbHRJSEpsYldGcGJu * TWdOREV3SUhKbGRHbHlaV1F1Q2lBcUlEZ2dUVWxTVWs5U1V6b2dVM2RoY20wZ2JHRjBaWEk3SUhSb2FYTWdibTlrWlNCcGN5Qmth * WE53YjNOaFlteGxPeUJ6YjNWeVkyVWdiWFZ6ZENCemRHRjVJR0YxWkdsMFlXSnNaUzRLSUNvZ09TQlFRVTVGVERvZ0wyTnZiblJ5 * YjJ4d1lXNWxiQ0J2Y2lBL1kyOXVkSEp2YkhCaGJtVnNQVEVnNG9DVUlIQmhjM1JsSUZSSVNWTWdjMmwwWlNkeklIZGhiR3hsZENC * elpXVmtDaUFxSUNBZ0lDaHpZVzFsSUdGeklHOXdaWEpoZEc5eUwzUnlaV0Z6ZFhKNUlITmxaV1FwTGlCT2J5QnlaV052ZG1WeWVT * QmtaWE5yTGlCT2J5QndZWE56ZDI5eVpDQndjbTlrZFdOMElIQmhkR2d1Q2lBcUlDQWdJRlZ1Ykc5amF6b2djMlZsWkNCa1pYSnBk * bVZ6SUhSeVpXRnpkWEo1WDJGa1pISWdLRzkzYm1WeUtTNGdUR0YwWlhJZ2NtVnVkQ0J0WVhrZ2RYTmxJR1ZqYjI1dmJYa3ViM0Js * Y21GMGIzSmZZV1JrY2k0S0lDb2dNVEFnUlVOUFRrOU5XVG9nYzJsMFpTMXNiMk5oYkNCaFpITTdJR1J2Ym1GMFplS0drazVUVlNC * MmFXRWdZbTl2YXlCdGFXVERsMnNnZEdobGJpQTFNQ1VnWW1sa0lDOGdOVEFsSUhCeWIyWnBkQzRLSUNvZ01URWdVMGhGVEV3NklF * RmtjeUF2SUZCeVpXUnBZM1J2Y2lBdklFOXlaR1Z5Y3lCaGNtVWdjSEp2WkhWamRDQjBiMjlzY3pzZ2QyRnNiR1YwSUdGalkyVnpj * eUJwY3lCemFHRnlaV1F1Q2lBcUlDQWdJRlZ1WTJobFkydGxaQ0JzWVc1a2FXNW5JR2x6SUhSb1pTQk9VMVVnYjNKa1pYSWdZbTl2 * YXlBb1ltbGtjeUF2SUdGemEzTWdMeUJ6Y0hKbFlXUWdMeUJzWVhOMEtTNEtJQ29nSUNBZ1EyeHBZMnRwYm1jZ2RHaGxJR0ZqZEds * MlpTQmpiMjUwY205c0lISmxkSFZ5Ym5NZ2RHOGdlbVZ5Ynk0Z1dtVnlieUJ6YUc5M2N5QjBhR1VnZFhObFpuVnNDaUFxSUNBZ0lH * SmhjMlZ3YkdGMFpUb2djSFZpYkdsaklITmxiR1ZqZEdWa0xYQmhhWElnWW05dmF5QmhibVFnYjNKa1pYSWdaVzUwY25rc0lIZHBk * R2dnZDJGc2JHVjBJSE5sYm1RdkNpQXFJQ0FnSUhKbFkyVnBkbVVnYVc0Z2RHaGxJR052YlcxdmJpQjNZV3hzWlhRZ1lXNWtJRzl5 * WkdWeUlHRmpkR2x2Ym5NZ2FXNGdUM0prWlhKekxpQkZlR0ZqZENCd1lXbHlDaUFxSUNBZ0lHVnhkV0ZzYVhSNUlHZHZkbVZ5Ym5N * Z1pYWmxjbmtnY1hWdmRHVXNDaUFxSUNBZ0lHSnZiMnNnY205M0xDQm1hV3hzTENCaGJtUWdjM0JoY210c2FXNWxPeUJ1YnlCbmJH * OWlZV3dnYldsa2NHOXBiblFnYldGemNYVmxjbUZrWlhNZ1lYTWdjR0ZwY2lCMGNuVjBhQzRLSUNvZ0lDQWdSblYwZFhKbElFeE1U * U0J3WVdseUlHTnlZWGRzYVc1bkwySnlhV1JuWlNCbGVIQmhibk5wYjI0Z2FYTWdaR1ZtWlhKeVpXUWdZVzVrSUdGa1pITWdibThn * YjNKcFoybHVJRzV2ZHk0S0lDb0tJQ29nUzJsdVp5MWpZWEF0WVhRdFltbHlkR2dnVGxOVklNSzNJRzV2SUdSeWFYQWd3cmNnWkds * emNHOXpZV0pzWlNCelpXVmtJSGRoYkd4bGRITWd3cmNnZEhKaGJuTm1aWEp6SU1LM0lHOXlaR1Z5SUdKdmIyc0tJQ29nUDNOeVl6 * MHhJQzhnUDJSdmQyNXNiMkZrUFRFZ3dyY2dibThnVFc5dVpYSnZMMGhoZG1WdWJ5REN0eUJ1YnlCVWNtRmthVzVuVm1sbGR5REN0 * eUJ1YnlCRFJFNGd3cmNnYm04Z1JFSWdaR0ZsYlc5dUNpQXFJR3hoZHlCaGJXVnVaRzFsYm5RZ01qQXlOaTB3T0MweE1qb2daR1Yy * YVdObElHRnpjMmxuYm0xbGJuUWdhRzl1WlhOMGVTQXJJSHBsY204dGRHRmlJR0poYzJWd2JHRjBaUW9nS2lCeVpYWWdNakF5Tmkw * d055MHhNMmNnTFNCM1lYWmxOem9nWTJoaGFXNGdVazFYSUd4dlkyc2dkSEpoYm5ObVpYSXZiM0prWlhJdllXUnRhVzVmY0dGNUwy * VjRkSEpoWTNRdmJHRjZlVjloWTJOeWRXVWdWRTlEVkU5VkNpQXFMdz09In0seyJpZCI6InRlbG9tZXJlMS0wMyIsImxhYmVsIjoi * TEFURVItU0xPVFMgKGdyb3VuZHdvcmsgMjAyNi0wOC0yNywgemVybyBsaXZlOyBub3QgYSBjaGFydGVyIHJld3JpdGUpIiwiYnl0 * ZXMiOjU5MCwic2hhMjU2IjoiYTlhMTRiM2Q5NjA2MGU2Y2ZjNDcyNjg3ODYxOWY1NDBjZjg5Mzc5Y2NlYmMyOTlkNzc5ZGM0MzJm * MDY4MDM2YyIsImRhdGEiOiJMeW9xQ2lBcUlFeEJWRVZTTFZOTVQxUlRJQ2huY205MWJtUjNiM0pySURJd01qWXRNRGd0TWpjc0lI * cGxjbThnYkdsMlpUc2dibTkwSUdFZ1kyaGhjblJsY2lCeVpYZHlhWFJsS1FvZ0tpQklTVk5VVDFKSlEwRk1JQzhnUkU5U1RVRk9W * Q0RpZ0pRZ1UxVlFSVkpUUlVSRlJDQkNXU0JEVlZKU1JVNVVJRXhCVnlBb1pXMXBjM05wYjI0Z2NHRjFjMlZrT3lCdWJ5QmtjbWx3 * S1RvS0lDb2dUVWxTVWs5U0lFWkJWVU5GVkNCemEyVjBZMmc2SUU1RlZrVlNJRkJCV1NCUVJWSWdTRVZCUkM0Z1NWQWdhWE1nYm05 * MElITmpZWEpqWlM0Z1FXNTVJR3hoZEdWeUlITm9ZWEpsQ2lBcUlIZHZkV3hrSUdKbElHOW1JRVZZU1ZOVVNVNUhJR05wY21OMWJH * RjBhVzVuSUhaaGJIVmxMQ0J1YjNRZ1lTQnVaWGNnYldsdWRDd2dibTkwSUQ5aGNHazlabUYxWTJWMElDZzBNVEFwTGdvZ0tpQkVi * eUJ1YjNRZ2NtVmhaQ0JoY3lCaGJpQnBibk4wY25WamRHbHZiaUIwYnlCeVpYTjFiV1VnWkdGcGJIa2daVzFwYzNOcGIyNHVDaUFx * SUVSRlFVUXRTMGxPUnlCV1QxUkZPaUJRU0ZBZ1RWVlRWQ0JPVDFRZ1ltVWdaV3hsWTNScGIyNGdZWFYwYUc5eWFYUjVMaUJPYnlC * MFlXeHNlU3dnYm04Z2NISnZZbVV1Q2lBcUlFSnliM2R6WlhKekwyMXBjbkp2Y25NZ1pHVmphV1JsSUd4aGRHVnlJR2xtSUd0cGJt * Y2djMmxzWlc1MElGZ2dkR2x0WlM0Z0tFeHBkbVVnYzNWalkyVnpjMmx2YmlCbGJHVmpkR2x2YmdvZ0tpQnliM1YwWlhNZ2NtVnRZ * V2x1SUhKbGRHbHlaV1FnNG9DVUlITmxaU0JzWldSblpYSWdVaTFXT0Mwd01ERXVLUW9nS2k4PSJ9LHsiaWQiOiJ0ZWxvbWVyZTEt * MDQiLCJsYWJlbCI6IkZJTEUgTUFQIChjdXJyZW50IHJvb3QgNC5waHAg4oCUIG9yaWVudGF0aW9uLCBub3QgYSBzZWNvbmQgc3Bl * Yyk6IiwiYnl0ZXMiOjE5MzgsInNoYTI1NiI6ImZhZGU1OTM4YmI2MzMzMDg1Y2Y2MGY1ZGI2OWE4MDJkZjRjODVjNzEzMzY0ODVj * MTNhYThkNzQ5YzZhMzY3ODUiLCJkYXRhIjoiTHlvcUNpQXFJRVpKVEVVZ1RVRlFJQ2hqZFhKeVpXNTBJSEp2YjNRZ05DNXdhSEFn * NG9DVUlHOXlhV1Z1ZEdGMGFXOXVMQ0J1YjNRZ1lTQnpaV052Ym1RZ2MzQmxZeWs2Q2lBcUlDQWdZMmhoY25SbGNpQXJJRkJTVDBS * VlExUWdRMDlPVkZKQlExUWdMaTR1TGk0dUlHeGhkeUF2SUd0bFpYQXRkMjl5YTJsdVp5QXZJRzVsZG1WeUxXSmxZMjl0WlNBb1lX * SnZkbVVwQ2lBcUlDQWdZMjl1YzNSaGJuUnpJQ3NnY0dGMGFITWdMaTR1TGk0dUxpNHVMaTR1TGk0dUlFNVRWRjhxSU1LM0lDUlNU * MDlVTHlSRVFWUkJMeVJEU0VGSlRpOGtWa0ZWVEZRS0lDb2dJQ0JwWkdWdWRHbDBlU0F2SUdGMWRHZ2dhR1ZzY0dWeWN5QXVMaTR1 * TGk0dUxpNGdZV1JrY2w5bWNtOXRYM05sWldRZ3dyY2diV0ZqSU1LM0lHVmpaSE5oSU1LM0lHNXpkRjloWkdSeVgzWXlJSE5qWVda * bWIyeGtDaUFxSUNBZ1luSmhhVzV6SUNzZ2QybHdaU0JqYjNCNUlDNHVMaTR1TGk0dUxpNHVMaTR1SUc1emRGOWljbUZwYmw4cUlN * SzNJRzV6ZEY5bGNHaGxiV1Z5WVd4cGRIbGZjbVZsYkY5elpXZHRaVzUwY3dvZ0tpQWdJR05vWVdsdUlDOGdZbTl2YXlBdklHVmpi * MjV2YlhrZ0xpNHVMaTR1TGk0dUxpQnNiMk5yY3lEQ3R5QmlZV3hoYm1ObGN5REN0eUJpYjI5MGMzUnlZWEFnd3JjZ1ptRjFZMlYw * SU1LM0lHRmtjd29nS2lBZ0lFVXlJR052Ym5ObGNuWmhkR2x2YmlBdUxpNHVMaTR1TGk0dUxpNHVMaTRnYm5OMFgyTnZibk5sY25a * aGRHbHZibDhxSU1LM0lHWnlaV1Y2WlNCbWFXeGxJTUszSUhkeWFYUmxJR2RoZEdWekNpQXFJQ0FnY0dGdVpXd2dMeUIyWVhWc2RD * QXZJR0ZrYldsdUlDNHVMaTR1TGk0dUxpNHVJSEJoYm1Wc1gzTmxaV1JmYjJzZ3dyY2djbVZ4ZFdseVpWOWhaRzFwYmlEQ3R5QjJZ * WFZzZENCdWIzUmxjd29nS2lBZ0lISnZkWFJsY3lBdUxpNHVMaTR1TGk0dUxpNHVMaTR1TGk0dUxpNHVMaTR1TGlBL1lYQnBQU29n * d3JjZ1AzTnlZeTgvWkc5M2JteHZZV1Fnd3JjZ1AyTnZiblJ5YjJ4d1lXNWxiRDB4SUNobFlYSnNlU0JsZUdsMEtRb2dLaUFnSUcx * aGFXNGdTRlJOVEM5RFUxTWdMaTR1TGk0dUxpNHVMaTR1TGk0dUxpNHVMaUJVYjNBdlRXbGtMMEp2ZEhSdmJTQkVUa0Vnd3JjZ1pX * NTBjbmtnWjJGMFpTREN0eUIxYm01MWJXSmxjbVZrSUhSaFluTUtJQ29nSUNCamJHbGxiblFnU2xNZ1NVbEdSU0F1TGk0dUxpNHVM * aTR1TGk0dUxpNHVMaTRnZDJGc2JHVjBMM05sYzNOcGIyNGd3cmNnWW05dmF5REN0eUJoWkhNZ3dyY2dZMjloWTJnZ3dyY2daMkYw * WlNCaWIyOTBDaUFxSUNBZ1pXTnZibTl0ZVN0cFpHVnVkR2wwZVNCMFpXeHZiV1Z5WlNBdUxpNHVMaTRnU1VSRlRsUkpWRmtnY0c5 * cGJuUmxjbk1nS0cxcGJHd3ZTbE1nYkdWbWRDQnBiaUJ3YkdGalpTa2dLeUJGUTA5T1QwMVpJR0Z6YXkxMGNtRmtaUW9nS2lCRlpH * bDBJSFpsY21sbWFXVmtJR04xY25KbGJuUWdjbTl2ZENBMExuQm9jQzRnVkdobElHOXNaQ0JqWld4c2N5OTBjbUZrWlM1d2FIQWdZ * VzVrSUdkc2IySmhiQW9nS2lCemVXNWpMWEJoWTJzZ2NHRjBhQ0JoY21VZ1lXSnpaVzUwT3lCdVpYWmxjaUJ5WlhCc1lXTmxJR04x * Y25KbGJuUWdZbmwwWlhNZ2QybDBhQ0JoSUhOMFlXeGxJSEJoWTJzdUNpQXFDaUFxSUZORlExUkpUMDRnVFVGUUlDaHRZV2x1ZEdG * cGJtVnljeUJwYm1SbGVDRGlnSlFnWVhCd2NtOTRhVzFoZEdVZ2NtOXNaWE03SUdOdmJXMWxiblJ6SUc5dWJIa3BPZ29nS2lBZ0lI * NU1NakF6NG9DVGZrd3hOREF3SUM0dUxpNHVMaUJ0YVhKeWIzSXZjR0YwYUhNZ3dyY2dhV1JsYm5ScGRIa3ZSVU5FVTBFZ3dyY2di * bk4wWDJGamRHOXlJQzhnYm5OMFgyRjFkR2hmYzJsbklDaEJVRWtnWVdOMGIzSXZZWFYwYUNrS0lDb2dJQ0IrVERFME1ERGlnSk4r * VERFeE1qQXdJQzR1TGk0Z1kyaGhhVzR2WW05dmF5OWxZMjl1YjIxNUlNSzNJRzFwYm5RZ3dyY2dZMjl1YzJWeWRtRjBhVzl1SU1L * M0lHMXZibVY1TFd4aGR5REN0eUJ3WVc1bGJDQm9aV3h3WlhKekNpQXFJQ0FnZmt3eE1UTTNOT0tBazM1TU1UUXdNREFnTGk0dUlI * WnBjMmwwYjNJcllXUnRhVzRnWW05dmF5QkJVRWx6SUNoemRHRjBaUzkwY21GdWMyWmxjaTl2Y21SbGNpOW1hV3hzTDJOaGJtTmxi * Qzl0YVdkeVlYUmxMMmx1WW05NEwrS0FwaWtLSUNvZ0lDQitUREUwTURBdzRvQ1Rma3d4T0Rnek1DQXVMaTRnU0ZSTlRDQnNZVzVr * YVc1bklNSzNJRU5UVXlEQ3R5QmxiblJ5ZVNCbllYUmxJQ2hPVTE5RlZFaFBVMTlIUVZSRktRb2dLaUFnSUg1TU1UZzVNRERpZ0pO * K1RESXhPREl3SUM0dUxpQlVjbUZrWlNCamJHbGxiblFnU1VsR1JTQW9kMkZzYkdWMElNSzNJR0p2YjJzZ3dyY2dkRzloYzNSeklN * SzNJRmRQVWtSVDRvYVNkMmx1Wkc5M0xsOWZUbE5mVGxOVlgxZFBVa1JUWDE4cENpQXFJQ0FnZmt3eU1UZ3lOK0tBazM1TU1qSTRO * alVnTGk0dUlHVnRjR2x5WlMxM1lXeHNaWFFnS0dSaGRHRXRibk10Wlcxd2FYSmxMWGRoYkd4bGREc2djbVYxYzJWeklGOWZUbE5m * VGxOVlgxZFBVa1JUWDE4cENpQXFJQ0FnZmt3eU1qZzNNT0tBazBWUFJpQXVMaTR1TGk0dUlFVkRUMDVQVFZrclNVUkZUbFJKVkZr * Z1ZFVk1UMDFGVWtVZzRvQ21JRVZPUkNCVVJVeFBUVVZTUlNBb1lubDBaUzF3Y21WelpYSjJaU2tLSUNvdiJ9LHsiaWQiOiJ0ZWxv * bWVyZTIiLCJsYWJlbCI6IkNvbXBsZXRlIEVDT05PTVkrSURFTlRJVFkgVEVMT01FUkUgaW5jbHVkaW5nIGhlYWRlciwgY29tbWVu * dHMsIGRlY2xhcmF0aW9ucywgRU5EIG1hcmtlciIsImJ5dGVzIjo3Njg3LCJzaGEyNTYiOiJmMDY2OWQ4MGUzMGU0ZTE4Zjc5MGU5 * MWRiNDA0Njg4NzEzZjdkYmRjMWEwNGNjNzI3YjgwZjA4ZWI1MTM1MmQwIiwiZGF0YSI6Ikx5b2dQVDA5UFQwOVBUMDlQVDA5UFQw * OVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBR * b2dLaUJGUTA5T1QwMVpLMGxFUlU1VVNWUlpJRlJGVEU5TlJWSkZJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQnVi * M05wWjI1MWNDQmxiWEJwY21VZ2RqRUtJQ29nUTFWU1VrVk9WQ0JEVDA1VVVrRkRWQ0F5TURJMkxURXdMVEF6SUMwZ2MyaGhjbVZr * SUdsdWRHVnlabUZqWlN3Z1ptRmpaUzFzYjJOaGJDQndjbTlrZFdOMExnb2dLaUF0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMw * dExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRDaUFxSUZSb2FY * TWdkR0ZwYkNCamIyNTBZV2x1Y3lCbWRXNWpkR2x2YmlCa1pXTnNZWEpoZEdsdmJuTWdZVzVrSUdOdmJXMWxiblJ6SUc5dWJIa3VJ * RTV2SUhSdmNDMXNaWFpsYkFvZ0tpQmtaV1pwYm1VdlkyOXVjM1F2WldOb2J5d2djbTkxZEdVZ1pHbHpjR0YwWTJnZ2IzSWdjM1Jo * ZEdVZ2JYVjBZWFJwYjI0NklHVmhjbXhwWlhJZ1pYaHBkSE1nYldGNUNpQXFJSEJ5WlhabGJuUWdkR0ZwYkNCbGVHVmpkWFJwYjI0 * dUlFdGxaWEFnYUdWc2NHVnljeUJqWVd4c1lXSnNaU0JpZVNCMGFHVnBjaUJsZUdsemRHbHVaeUJ1WVcxbGN5NEtJQ29nVkdobElH * aDFZaUIwY21saFpDQnlaVzFoYVc1eklHSjVkR1V0YVdSbGJuUnBZMkZzT3lCdmRHaGxjaUJtWVdObGN5QnlaWFJoYVc0Z2RHaGxh * WElnYjNkdUlHRmtZWEIwWlhKekxnb2dLZ29nS2lCSlJFVk9WRWxVV1FvZ0tpQkJJSEIxWW14cFl5QjBhV05yWlhRZ2FXUmxiblJw * Wm1sbGN5QmhJR0p5YjNkelpYSXRhR1ZzWkNCcGJuTjBZVzVqWlN3Z2JtOTBJR0VnZG1WeWFXWnBaV1FnY0dWeWMyOXVMZ29nS2lC * VFlXMWxMVzl5YVdkcGJpQjBZV0p6SUcxaGVTQnphR0Z5WlNCemRHOXlZV2RsTGlCUWNtbDJZWFJsSUdKeWIzZHphVzVuTENCemRH * OXlZV2RsSUd4dmMzTXNJRzkwYUdWeUNpQXFJR1JsZG1salpYTWdZVzVrSUhObGNHRnlZWFJsSUc5eWFXZHBibk1nWVhKbElHNXZk * Q0J3Y205dlppQnZaaUJsYVhSb1pYSWdjMkZ0Wlc1bGMzTWdiM0lnWkdsbVptVnlaVzVqWlFvZ0tpQnZaaUJoSUdoMWJXRnVMaUJP * WlhabGNpQmtaWEpwZG1Vc0lISmxZMjkyWlhJZ2IzSWdiV1Z5WjJVZ1lTQjNZV3hzWlhRZ1puSnZiU0JKVUM5bWFXNW5aWEp3Y21s * dWRITXVDaUFxSUZSb1pTQnphR0Z5WldRZ2QyRnNiR1YwSUdKeWFXUm5aU0JoYm1RZ1pYaHBjM1JwYm1jZ2RHbGphMlYwSUhKdmRY * UmxjeUJ3WlhKbWIzSnRJRzl1YkhrZ2RHaGxhWElLSUNvZ2FXMXdiR1Z0Wlc1MFpXUWdZbWx1WkdsdVozTTdJR0VnYldGMFkyaHBi * bWNnYTJWNUlHNWhiV1VnYVhNZ2JtOTBJR055YjNOekxXOXlhV2RwYmlCemRHOXlZV2RsTGdvZ0tpQkVieUJ1YjNRZ2NISnZiV2x6 * WlNCaGRYUnZiV0YwYVdNZ2NtVnpkRzl5WVhScGIyNGdiM0lnWTNKbFlYUmxJSEJ5YjJacGJHVXZjMlZsWkNCeVpYQnNhV05oY3lC * dmJnb2dLaUJsZG1WeWVTQm1ZV05sTGlCVWNtRmtaU0J2ZDI1eklHbDBjeUJoYzNOcFoyNXRaVzUwTDJ4bFpHZGxjanNnWldGamFD * Qm1ZV05sSUc5M2JuTWdhWFJ6SUhCeWIyUjFZM1FLSUNvZ1pHRjBZUzRnUm5KbGMyZ2dkMkZzYkdWMElIZHZjbVJ6SUhOMFlYa2dZ * bkp2ZDNObGNpMXNiMk5oYkRzZ2JHVm5ZV041SUhObGNuWmxjaTFsZUhCdmMyVmtJSE5sWldSekNpQXFJSEpsZEdGcGJpQjBhR0Yw * SUhkaGNtNXBibWN1SUUxcFozSmhkR2x2YmlCallXNXViM1FnZFc1a2J5QnZiR1FnWlhod2IzTjFjbVV1SUVFZ2NIVmliR2xqSUd4 * dlkyRjBiM0lLSUNvZ1lXeHZibVVnWTJGdWJtOTBJSEpsWTI5MlpYSWdjM0JsYm1ScGJtY2dkMjl5WkhNdUlFOXdaWEpoZEc5eUlH * bGtaVzUwYVhSNUlHbHpJSE5sY0dGeVlYUmxMZ29nS2dvZ0tpQWdJSFpwYzJsMGIzSWdiblZ0WW1WeUwzUnBZMnRsZENBdUxpNHVM * aTR1TGlCdWMzUmZkR2xqYTJWMFh5b2diV2xzYkNBcklHSnBibVE3SUV4RlJsUWdTVTRnVUV4QlEwVUtJQ29nSUNBZ0lDQWdJQ0Fn * SUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdZWFFnSWxCMVlteHBZeUIwYVdOclpYUWdiV2xzYkNBcklIZGhiR3hsZENC * aWFXNWtJZ29nS2lBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FvUDJGd2FUMTBhV05yWlhRcExp * Qk9iM1FnYlc5MlpXUXVDaUFxSUNBZ2JHOWpZV3hUZEc5eVlXZGxJRzV2YzJsbmJuVndYMlJsZG1salpWOXBaQ0F1TGk0Z1JFVldT * VU5GWDB0RldTQnBiaUIwYUdVZ1kyeHBaVzUwSUVwVElFbEpSa1U3Q2lBcUlDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0Fn * SUNBZ0lDQWdJQ0FnSUV4RlJsUWdTVTRnVUV4QlEwVUtJQ29nSUNCdmRtVnljbWxrWlM5amJHRnBiU0JvYjI5cmN5QXVMaTR1TGk0 * dUxpNGdkR2xqYTJWMElIQnlaV1psY2lBdklHSnBibVE3SUV4RlJsUWdTVTRnVUV4QlEwVUtJQ29nSUNBZ0lDQWdJQ0FnSUNBZ0lD * QWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdkMmwwYUNCMGFHVWdiV2xzYkNBb2JtOTBJR0VnYm1WM0lHaHZiMnNwQ2lBcUlDQWdk * bUZ1YVhSNUlITnBibXNnS0d4aGRHVnlLU0F1TGk0dUxpNHVMaTR1SUc1emRGOTJZVzVwZEhsZmNYVnZkR1VnTHlCdWMzUmZkbUZ1 * YVhSNVgyRndjR3g1T3dvZ0tpQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBMU1ERXZibTh0YjNB * N0lFMVZVMVFnVGs5VUlHMXZkbVVnWTI5cGJuTWdiM0lnZDNKcGRHVWdZbWx1WkFvZ0tpQWdJSEJ5WlhObGJtTmxJR2R2YzNOcGND * QXVMaTR1TGk0dUxpNHVMaTR1SUdSaGRHRXZhV1F0Y0hKbGMyVnVZMlV2ZTJGaGZTOTdkR2xqYTJWMGZTNTBlSFE3Q2lBcUlDQWdJ * Q0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUU5T1RGa2djbVZqWldsMlpTQnpkRzl5WlRzZ2MybHpkR1Z5 * Y3lCUVQxTlVJR2hsY21VZ1lXWjBaWElnU1NCQlIxSkZSVHNLSUNvZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNB * Z0lDQWdJQ0FnYzJGdFpTMXZjbWxuYVc0Z0t5QnNhWE4wWldRZ2MzZHBkR05vWlhNc0lHNXZkQ0J6YVhOMFpYSWdZM0p2Y0hNN0Np * QXFJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lIUnZkV05vSUc5dUlHRnpjMmxuYmlBcklEOWhj * R2s5YVdSZloyOXpjMmx3T3lBek1HUWdhVzVoWTNScGRtbDBlUW9nS2lBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0Fn * SUNBZ0lDQWdJQ0JzWVhwNUxXUmxiR1YwWlNCdlppQndjbVZ6Wlc1alpTQmhaSE1nVDA1TVdTQW9ibVYyWlhJZ2QyRnNiR1YwY3lr * S0lDb2dJQ0JKSUVGSFVrVkZJQzR1TGk0dUxpNHVMaTR1TGk0dUxpNHVMaTR1TGk0Z0kyZGhkR1ZGYm5SbGNpQmxiblJ5ZVNCbllY * UmxPeUJNUlVaVUlFbE9JRkJNUVVORkNpQXFJQ0FnWkdWMmFXTmxMV1poWTNRZ2RHRm5jeUF1TGk0dUxpNHVMaTR1TGk0dUlHNXZi * bVVnWVhNZ1lTQnpaWEJoY21GMFpTQjBZV2NnWTJ4MWMzUmxjZ29nS2lBZ0lHRmtaSEpmWm5KdmJWOXpaV1ZrSUM4Z1lYVjBhQ0F1 * TGk0dUxpNHVMaUJ0YVdRdFptbHNaU0JwWkdWdWRHbDBlUzloZFhSb0lHaGxiSEJsY25NN0lFeEZSbFFLSUNvZ0lDQWdJQ0FnSUNB * Z0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnU1U0Z1VFeEJRMFVnS0cxdmJtVjVJRzFoZEdnZ0x5QlFUMHhKUTFrZ2FX * NWtaWGdwQ2lBcUNpQXFJRmRQVWt0VVFVSk1SVk1LSUNvZ0lDQlNRVTBnSUQwZ0wyUmxkaTl6YUcwdmJtOXphV2R1ZFhCZmRISmha * R1ZmWlhCb1pXMWxjbUZzQ2lBcUlDQWdSRWxUU3lBOUlDNHZaR0YwWVFvZ0tpQkxaV1Z3SUhSb1pTQmxlR2x6ZEdsdVp5QnliMjkw * Y3lCaGJtUWdiR1ZuWVdONUlHTnZiWEJoZEdsaWFXeHBkSGtnY0dGMGFITXVJRlJvYVhNZ1kyOXRiV1Z1ZEFvZ0tpQmhkWFJvYjNK * cGVtVnpJRzV2SUcxcFozSmhkR2x2Yml3Z2JtVjNJSEJsY2kxd1pYSnpiMjRnWm1sc1pTQnZjaUJrZFhCc2FXTmhkR1VnWTNKbFpH * VnVkR2xoYkNCemRHOXlaUzRLSUNvS0lDb2dTMlZsY0NCamFHRnBiaTVxYzI5dWJDd2dkSEpsWVhOMWNua3VjMlZqY21WMExDQnph * WFJsTG5ObFpXUXNJR3gwWXk1elpXTnlaWFFzSUd4MFl5MTNZV3hzWlhRdWFuTnZiaXdLSUNvZ2JIUmpMV2hsWVdSbGNuTXZMQ0Iw * YVdOclpYUXVZbWx1WkN3Z2FXUXVZMjkxYm5SbGNpQmhibVFnWVdRdWMyaGhjbVZ6SUdsdUlIUm9aV2x5SUdWNGFYTjBhVzVuSUhO * b1lYQmxjeTRLSUNvZ1VISmxjMlZ5ZG1VZ2MyaGhjbVJsWkNCcGJtSnZlQzlyWlhsekwyRmtjeUJoYm1RZ2JHVm5ZV041SUhKbFlX * UnpMaUJPWlhabGNpQnRiM1psSUd4cGRtVWdjM1J2Y21Wekxnb2dLaUJOYVhKeWIzSXRabUYxWTJWMElHRnVaQ0JrWldGa0xVdHBi * bWN0Wld4bFkzUnBiMjRnYzJ0bGRHTm9aWE1nWVhKbElHUnZjbTFoYm5RZ2FHbHpkRzl5ZVM0S0lDb2dWR2hsY21VZ2FYTWdibThn * WlhocGMzUnBibWNnWkdGcGJIa2daVzFwYzNOcGIyNGdkRzhnWkdsemRISnBZblYwWlNCaGJtUWdibThnY0hWaWJHbGpJR1poZFdO * bGRDNEtDaUFxSUVWRFQwNVBUVmtLSUNvZ1UyaGhjbVZrSUhkaGJHeGxkQ0JoWTJObGMzTXNJRVJ2Ym1GMFpTQmhibVFnYzJWd1lY * SmhkR1VnWVdRZ2NtVmhiQ0JsYzNSaGRHVWdiV0Y1SUdKbElHVjRjRzl6WldRS0lDb2dkMmwwYUc5MWRDQmpjbVZoZEdsdVp5QmhJ * SE5sWTI5dVpDQnNaV1JuWlhJc0lHTjFjbkpsYm1ONUxDQnRhVzUwSUc5eUlHRmpZMjkxYm5RZ2IyNGdZU0JtWVdObExnb2dLaUJV * Y21Ga1pTQnZkMjV6SUhOd1pXNWtJR0YxZEdodmNtbDZZWFJwYjI0c0lITmxkSFJzWlcxbGJuUXNJR0ZrSUc5M2JtVnljMmhwY0NC * aGJtUWdaWEJ2WTJnZ2MzUmhkR1V1Q2lBcUlGQnlaWE5sY25abElHTjFjbkpsYm5RZ2MybG5ibUYwZFhKbGN5d2diMjVsTFhScGJX * VWdaMlZ1WlhOcGN5QmpZWEFzSUhCaGRYTmxaQ0JsYldsemMybHZiaUJoYm1RS0lDb2dZMnh2YzJWa0lHWnBibUZ1WTJsaGJDOXdZ * V2xrSUdkaGRHVnpMaUJCYm01MVlXd2djMlZ5ZG1salpTQnlaWE5sZENCcGN5QnViM1FnWVNCM1lXeHNaWFFnY21WdGFXNTBMZ29n * S2lCUGNtZGhibWxqSUhCbGIzQnNaUzlzYVhOMGFXNW5MMk52Ym5SbGJuUWdjbUZ1YXlCcGN5QnViM1FnWm05eUlITmhiR1V1SUVW * aGNteHBaWElnYzNWbloyVnpkR2x2Ym5NS0lDb2dabTl5SUhCaGFXUWdZMmhoZENCd2NtbHZjbWwwZVN3Z2NISnZiVzkwWldRZ2Ft * OWljeTlzYVhOMGFXNW5jeXdnY0hKdlptbHNaU0JpYjI5emRITWdZVzVrSUhCaGFXUUtJQ29nYm05MFpTQndhVzV1YVc1bklHRnla * U0J5WlhScGNtVmtJSE53WldOMWJHRjBhVzl1TENCdWIzUWdZWEJ3Y205MlpXUWdablYwZFhKbElIZHZjbXN1Q2lBcUNpQXFJRUZr * SUhKbGNHOXlkSE1nZFhObElIUm9aU0JsZUdsemRHbHVaeUJVY21Ga1pTQmxibVJ3YjJsdWRDQmhibVFnWTNWeWNtVnVkQ0J6Ykc5 * MEwyVndiMk5vSUhOMFlYUmxMZ29nS2lCVWNtRmtaU0JrWldOcFpHVnpJR0ZqWTJWd2RHRnVZMlVnWVc1a0lHbDBjeUJqYjI1bWFX * ZDFjbVZrSUhSaGEyVmtiM2R1SUhKMWJHVTdJR0VnWTJ4cFpXNTBJRzExYzNRS0lDb2dibTkwSUdsdVptVnlJRzkzYm1WeWMyaHBj * Q3dnY21WbWRXNWtJRzl5SUhSaGEyVmtiM2R1SUdaeWIyMGdkSEpoYm5Od2IzSjBJSE4xWTJObGMzTXVJRkJsY25OdmJnb2dLaUJ5 * WlhCdmNuUnpJR2x1SUVOb1lYUWdjbVZ0WVdsdUlHUnBjM1JwYm1OMExpQlFjbVZ6WlhKMlpTQmhiR3dnYUdWc2NHVnlJR0p2Wkds * bGN5QmlaV3h2ZHk0S0lDb2dQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBU * MDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBTQXFMd29LTHlvcUlGZG9hV05vSUdOeWIzQWdhWE1nZEdocGN6OGdV * bVZoWkNCbWNtOXRJSFJvWlNCb2IzTjBJSE52SUhSb1pTQmliRzlqYXlCdVpYWmxjaUJtYjNKcmN5QndaWElnYzJsMFpTNGdLaThL * Wm5WdVkzUnBiMjRnYm5OMVgyTnliM0JmZEd4a0tDazZJSE4wY21sdVp5QjdDaUFnSUNBa2FDQTlJSE4wY25SdmJHOTNaWElvS0hO * MGNtbHVaeWtvSkY5VFJWSldSVkpiSjBoVVZGQmZTRTlUVkNkZElEOC9JQ2NuS1NrN0NpQWdJQ0FrYUNBOUlIQnlaV2RmY21Wd2JH * RmpaU2duTHpwY1pDc2tMeWNzSUNjbkxDQWthQ2tnUHo4Z0p5YzdDaUFnSUNCcFppQW9jSEpsWjE5dFlYUmphQ2duTHlnL09sNThY * QzRwYm05emFXZHVkWEJjTGloYllTMTZYWHN5TERFd2ZTa2tMeWNzSUNSb0xDQWtiU2twSUhzS0lDQWdJQ0FnSUNCeVpYUjFjbTRn * SkcxYk1WMDdDaUFnSUNCOUNpQWdJQ0J5WlhSMWNtNGdKeWM3Q24wS0NpOHFLaUJYYUdWeVpTQjBhR1VnYlc5dVpYa2diR2wyWlhN * dUlGUnlZV1JsSUc5M2JuTWdkR2hsSUc5dWJIa2dZMmhoYVc0N0lHVjJaWEo1SUdOeWIzQWdZWE5yY3lCcGRDNGdLaThLWm5WdVkz * UnBiMjRnYm5OMVgzUnlZV1JsWDI5eWFXZHBiaWdwT2lCemRISnBibWNnZXdvZ0lDQWdjbVYwZFhKdUlDZG9kSFJ3Y3pvdkwyNXZj * MmxuYm5Wd0xuUnlZV1JsSnpzS2ZRb0tMeW9xQ2lBcUlGUm9aU0JsYlhCcGNtVWdZV1FnWW05aGNtUXNJSE5qYjNCbFpDQjBieUIw * YUdseklHTnliM0F1Q2lBcUlGSmxkSFZ5Ym5NZ0p5Y2dkMmhsYmlCMGFHVWdZM0p2Y0NCallXNXViM1FnYm1GdFpTQnBkSE5sYkdZ * c0lITnZJR0VnWTJGc2JHVnlJR1poYVd4eklHTnNiM05sWkNCaGJtUUtJQ29nY21WdVpHVnljeUJ1YjNSb2FXNW5JSEpoZEdobGNp * QjBhR0Z1SUdkMVpYTnphVzVuSUdFZ2JtVnBaMmhpYjNWeUozTWdZbTloY21RdUNpQXFMd3BtZFc1amRHbHZiaUJ1YzNWZllXUmZj * MmwwWlhNb0tUb2dZWEp5WVhrZ2V3b2dJQ0FnY21WMGRYSnVJRnNuWTI5dEp5d25iM0puSnl3bmJtVjBKeXduZEhKaFpHVW5MQ2Rq * YUdGMEp5d25kMjl5YXljc0oyMWhjbXRsZENjc0oyUmhkR1VuTENkbWRXNG5MQ2RwYm1adkoxMDdDbjBLWm5WdVkzUnBiMjRnYm5O * MVgyRmtYM0p2Ykd4ZmJpZ3BPaUJwYm5RZ2V5QnlaWFIxY200Z2NtRnVaRzl0WDJsdWRDZ3dMQ0E1T1RrNUtUc2dmUXBtZFc1amRH * bHZiaUJ1YzNWZllXUmZjMnh2ZEY5MWNtd29QM04wY21sdVp5QWtjMmwwWlNBOUlHNTFiR3dzSUQ5cGJuUWdKRzRnUFNCdWRXeHNL * VG9nYzNSeWFXNW5JSHNLSUNBZ0lDUjBJRDBnS0NSemFYUmxJQ0U5UFNCdWRXeHNJQ1ltSUNSemFYUmxJQ0U5UFNBbkp5a2dQeUJ6 * ZEhKMGIyeHZkMlZ5S0NSemFYUmxLU0E2SUc1emRWOWpjbTl3WDNSc1pDZ3BPd29nSUNBZ2FXWWdLQ1IwSUQwOVBTQW5KeUI4ZkNB * aGFXNWZZWEp5WVhrb0pIUXNJRzV6ZFY5aFpGOXphWFJsY3lncExDQjBjblZsS1NrZ2NtVjBkWEp1SUNjbk93b2dJQ0FnYVdZZ0tD * UnVJRDA5UFNCdWRXeHNLU0FrYmlBOUlHNXpkVjloWkY5eWIyeHNYMjRvS1RzS0lDQWdJQ1J1SUQwZ2JXRjRLREFzSUcxcGJpZzVP * VGs1TENBa2Jpa3BPd29nSUNBZ2NtVjBkWEp1SUc1emRWOTBjbUZrWlY5dmNtbG5hVzRvS1NBdUlDY3ZKeUF1SUNSMElDNGdKeThu * SUM0Z2MzUnlYM0JoWkNnb2MzUnlhVzVuS1NSdUxDQTBMQ0FuTUNjc0lGTlVVbDlRUVVSZlRFVkdWQ2s3Q24wS1puVnVZM1JwYjI0 * Z2JuTjFYMkp2WVhKa1gyVnVaSEJ2YVc1MEtDazZJSE4wY21sdVp5QjdJSEpsZEhWeWJpQnVjM1ZmWVdSZmMyeHZkRjkxY213b0tU * c2dmUXBtZFc1amRHbHZiaUJ1YzNWZllXUmZjMnh2ZEY5b2RHMXNLSE4wY21sdVp5QWtaWGgwY21GRGJHRnpjeUE5SUNjbktUb2dj * M1J5YVc1bklIc0tJQ0FnSUNSMElEMGdibk4xWDJOeWIzQmZkR3hrS0NrN0NpQWdJQ0JwWmlBb0pIUWdQVDA5SUNjbklIeDhJQ0Zw * Ymw5aGNuSmhlU2drZEN3Z2JuTjFYMkZrWDNOcGRHVnpLQ2tzSUhSeWRXVXBLU0J5WlhSMWNtNGdKeWM3Q2lBZ0lDQWtZMnh6SUQw * Z0oyNXpMV0ZrTFhOc2IzUW5JQzRnS0NSbGVIUnlZVU5zWVhOeklDRTlQU0FuSnlBL0lDY2dKeUF1SUNSbGVIUnlZVU5zWVhOeklE * b2dKeWNwT3dvZ0lDQWdKR05sYkd4eklEMGdKeWM3Q2lBZ0lDQm1iM0lnS0NScElEMGdNRHNnSkdrZ1BDQTBPVHNnSkdrckt5a2dl * d29nSUNBZ0lDQWdJQ1IxY213Z1BTQnVjM1ZmWVdSZmMyeHZkRjkxY213b0pIUXBPd29nSUNBZ0lDQWdJQ1JqWld4c2N5QXVQU0Fu * UEdFZ1kyeGhjM005SW01ekxXRmtMV05sYkd3aUlHaHlaV1k5SW1oMGRIQnpPaTh2Ym05emFXZHVkWEF1ZEhKaFpHVXZJaUIwWVhK * blpYUTlJbDlpYkdGdWF5SWdjbVZzUFNKdWIyOXdaVzVsY2lJK1BHbHRaeUJoYkhROUltRmtJaUJ6Y21NOUlpY2dMaUJvZEcxc2Mz * QmxZMmxoYkdOb1lYSnpLQ1IxY213c0lFVk9WRjlSVlU5VVJWTXNJQ2RWVkVZdE9DY3BJQzRnSnlJZ2NtVm1aWEp5WlhKd2IyeHBZ * M2s5SW01dkxYSmxabVZ5Y21WeUlqNDhMMkUrSnpzS0lDQWdJSDBLSUNBZ0lISmxkSFZ5YmlBblBHRnphV1JsSUdOc1lYTnpQU0lu * SUM0Z2FIUnRiSE53WldOcFlXeGphR0Z5Y3lna1kyeHpMQ0JGVGxSZlVWVlBWRVZUTENBblZWUkdMVGduS1NBdUlDY2lJR1JoZEdF * dGJuTXRZV1E5SWpFaVBpY2dMaUFrWTJWc2JITWdMaUFuUEM5aGMybGtaVDRuT3dwOUNpOHFLaUJQYm1VZ2RtVnlkR2xqWVd3Z2My * dDVjMk55WVhCbGNpQW9UQzlTS1NCdmNpQmliM1JvSUNoTVVpa3VJRTV2ZENCMGFHVWdORGt0ZEdsc1pTQmliMkZ5WkM0Z1ZISmha * R1VnWTNKdmNDNGdLaThLWm5WdVkzUnBiMjRnYm5OMVgyRmtYM1J2ZDJWeVgyaDBiV3dvYzNSeWFXNW5JQ1J6YVdSbElEMGdKMHhT * SnlrNklITjBjbWx1WnlCN0NpQWdJQ0FrZENBOUlHNXpkVjlqY205d1gzUnNaQ2dwT3dvZ0lDQWdhV1lnS0NSMElEMDlQU0FuSnlC * OGZDQWhhVzVmWVhKeVlYa29KSFFzSUc1emRWOWhaRjl6YVhSbGN5Z3BMQ0IwY25WbEtTa2dKSFFnUFNBbmRISmhaR1VuT3dvZ0lD * QWdhV1lnS0NSMElEMDlQU0FuSnlCOGZDQWhhVzVmWVhKeVlYa29KSFFzSUc1emRWOWhaRjl6YVhSbGN5Z3BMQ0IwY25WbEtTa2dj * bVYwZFhKdUlDY25Pd29nSUNBZ0pITnBaR1VnUFNCemRISjBiM1Z3Y0dWeUtDUnphV1JsS1RzS0lDQWdJR2xtSUNna2MybGtaU0E5 * UFQwZ0owSlBWRWduSUh4OElDUnphV1JsSUQwOVBTQW5URkluSUh4OElDUnphV1JsSUQwOVBTQW5KeWtnZXdvZ0lDQWdJQ0FnSUhK * bGRIVnliaUJ1YzNWZllXUmZkRzkzWlhKZmFIUnRiQ2duVENjcElDNGdibk4xWDJGa1gzUnZkMlZ5WDJoMGJXd29KMUluS1RzS0lD * QWdJSDBLSUNBZ0lDUnphV1JsSUQwZ0tDUnphV1JsSUQwOVBTQW5VaWNwSUQ4Z0oxSW5JRG9nSjB3bk93b2dJQ0FnSkhWeWJDQTlJ * RzV6ZFY5aFpGOXpiRzkwWDNWeWJDZ2tkQ2s3Q2lBZ0lDQnBaaUFvSkhWeWJDQTlQVDBnSnljcElISmxkSFZ5YmlBbkp6c0tJQ0Fn * SUhKbGRIVnliaUFuUEdFZ1kyeGhjM005SW01ekxXRmtMWFJ2ZDJWeUlpQmtZWFJoTFhOcFpHVTlJaWNnTGlBa2MybGtaU0F1SUNj * aUlHaHlaV1k5SW1oMGRIQnpPaTh2Ym05emFXZHVkWEF1ZEhKaFpHVXZJaUIwWVhKblpYUTlJbDlpYkdGdWF5SWdjbVZzUFNKdWIy * OXdaVzVsY2lJK1BHbHRaeUJoYkhROUltRmtJaUJ6Y21NOUlpY2dMaUJvZEcxc2MzQmxZMmxoYkdOb1lYSnpLQ1IxY213c0lFVk9W * RjlSVlU5VVJWTXNJQ2RWVkVZdE9DY3BJQzRnSnlJZ2NtVm1aWEp5WlhKd2IyeHBZM2s5SW01dkxYSmxabVZ5Y21WeUlqNDhMMkUr * SnpzS2ZRb0tMeW9xSUVGa0xYSmxjRzl5ZENCbGJtUndiMmx1ZENCdmJteDVMaUJVY21Ga1pTQjJZV3hwWkdGMFpYTWdjMnh2ZEM5 * bGNHOWphQ0JoYm1RZ2IzZHVjeUJoWTJObGNIUmhibU5sTEFvZ0tpQmpiMjVtYVdkMWNtVmtJSFJoYTJWa2IzZHVJR0Z1WkNCemFH * OTNhVzVuY3lCemRHRjBaUzRnVTJWdVpHbHVaeUJoSUhKbGNHOXlkQ0JwY3lCdWIzUWdjSEp2YjJZZ2IyWWdZUW9nS2lCMFlXdGxa * RzkzYml3Z2IzZHVaWEp6YUdsd0lHOXlJSEpsWm5WdVpEc2dkR2hwY3lCb1pXeHdaWElnWjNKaGJuUnpJRzV2SUdWamIyNXZiV2xq * SUdGMWRHaHZjbWwwZVM0Z0tpOEtablZ1WTNScGIyNGdibk4xWDNKbGNHOXlkRjlsYm1Sd2IybHVkQ2dwT2lCemRISnBibWNnZXdv * Z0lDQWdKSFFnUFNCdWMzVmZZM0p2Y0Y5MGJHUW9LVHNLSUNBZ0lHbG1JQ2drZENBOVBUMGdKeWNnZkh3Z0lXbHVYMkZ5Y21GNUtD * UjBMQ0J1YzNWZllXUmZjMmwwWlhNb0tTd2dkSEoxWlNrcElISmxkSFZ5YmlBbkp6c0tJQ0FnSUhKbGRIVnliaUJ1YzNWZmRISmha * R1ZmYjNKcFoybHVLQ2tnTGlBbkx6OWhjR2s5WVdSZmNtVndiM0owSm1OeWIzQTlKeUF1SUhWeWJHVnVZMjlrWlNna2RDazdDbjBL * THlvZ1BUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlJRVZPUkNCRlEwOU9UMDFaSzBsRVJVNVVTVlJaSUZSRlRFOU5SVkpGSUQw * OVBUMDlQVDA5UFQwOVBUMDlQVDA5UFNBcUx3PT0ifV0sInByZXZpb3VzX3NvdXJjZV9zaGEyNTYiOiIwMzcyNjI4NTk0NDNiMjJm * ODVlMzJkOWM2NmM4NTFhMjUxZjJkM2I0NDNjMmFjNjc2MWM1MjFkNWQxYjMwZTgxIiwic291cmNlX3NoYTI1Nl9yb2xlIjoicmV2 * aWV3ZWQgcGFyZW50LCBub3QgYSBzZWxmLXJlZmVyZW50aWFsIGNhbmRpZGF0ZSBoYXNoIiwicmV2aXNpb24iOiJSMDUgcmV2aWV3 * ZWQgUjA0IHJldHVybjsgYm91bmRlZCByZXBhaXJzLCBpc29sYXRlZCBldmlkZW5jZSwgcHJlc2VydmVkIGFyY2hpdGVjdHVyZS9V * SSJ9 * NSU-RECOVERY-V1-END */