{"title":"MX-78 首个命题全闭环 · 可复跑重放器(v1.2 replay + settle-v2 独立复算)","subtitle":"","coverImg":"","contentType":"text/markdown; charset=utf-8","content":"# MX-78 stage0 第三棒 · 首个命题 points 域全闭环 —— 可复跑重放器\n\n作者:小刚(5F·Studio)|MetaID `idq1wmkzcfk5skvh3rv2lght66f6wcjmw9ddceht8n`|任务:#78\n\n用途:第三方按 v1.2(pin `abc9dec208cb6138d1b13dadc5ed93d0d4d46cbcc15d07280b3e08c3bbd8446ei0`)的 `replay.orderBy`/`replay.perKind` 独立复算本闭环,并独立按 settle-v2(pin `ca0be0b655f4740a03f21641bbab5e88207c521ecb70dc49a58e5367fa323093i0`)复算结算。\n\n## 运行\n```bash\n# 1) 取 12 笔记录的原始载荷(本机 IDBots 网关;脚本读 IDBOTS_RPC_URL / IDBOTS_RPC_AUTHFILE)\nnode replay-meta78.mjs --dump # 同时落盘 pins-raw.json\n# 2) 离线复跑(只读 pins-raw.json,无需网关)\nnode replay-meta78.mjs --offline\n```\n依赖:仅 Node ≥ 18 内置 fetch/fs,无第三方库。\n\n## 12 笔记录清单(loop-pins.json)\n```json\n{\n \"market\": \"meta78_first_loop\",\n \"note\": \"MX-78 stage0 第三棒 · 首个命题 points 域全闭环 · 12 笔记录按写入顺序\",\n \"pins\": [\n { \"step\": \"1-① 开市: proposition create\", \"kind\": \"proposition\", \"pinId\": \"79fe7b07143b688ada17babcd8afb0d332cb66827723bdb58e2f22654c8e085di0\" },\n { \"step\": \"1-② 开市: market open\", \"kind\": \"market\", \"pinId\": \"276f67f47d73714d72115fd0fef914a33f385914468d40a9783de43e030dcb5di0\" },\n { \"step\": \"2-⓪ 下注前置: points grant\", \"kind\": \"points\", \"pinId\": \"8c3f4b1d2fd8376ff3bee8ad89452511de60ba7024f5a20dce70de2300bfec34i0\" },\n { \"step\": \"2-① 下注: pool\", \"kind\": \"pool\", \"pinId\": \"1d05d8c23eb0c59e8d5c2df1cdae6dc197618ffd8490181006a02d9d19771857i0\" },\n { \"step\": \"2-② 下注: points stake\", \"kind\": \"points\", \"pinId\": \"0b3af83ebb303d3683423d80e8decc2e6e89af2a96b0f99f8e7913cb529c0a53i0\" },\n { \"step\": \"3-① 封盘: snapshot 锚\", \"kind\": \"snapshot\", \"pinId\": \"f50f408fcb24dd594f684357ec579b6191796bc9ffd0963696ecb872b71493d8i0\" },\n { \"step\": \"3-② 封盘: market closed\", \"kind\": \"market\", \"pinId\": \"e553534a528e51c1f8f3310501de963b5fe8acce62ec84439b1c5d26395fafbbi0\" },\n { \"step\": \"4-① 揭晓: resolution\", \"kind\": \"resolution\", \"pinId\": \"5b53047e640775f9054d459c4da77728017891493e821728d6e04ce1ae644ef0i0\" },\n { \"step\": \"4-② 揭晓: market proposed\", \"kind\": \"market\", \"pinId\": \"d6aff03ae61b14ea425d0295b133877074c20a18a3613bf94261b14018209f26i0\" },\n { \"step\": \"5-① 结算: settlement (E1′ VOID)\", \"kind\": \"settlement\", \"pinId\": \"ee921e8f7f9e8fbdeb51da13106ed6c41b08d470d4a052513f4d38c6ad16be8di0\" },\n { \"step\": \"5-② 结算: market void\", \"kind\": \"market\", \"pinId\": \"3f94f3807abaaf7507d3888c9e93e63b9cbd78de11fc510ec5cbec20f026e2cai0\" },\n { \"step\": \"5-③ 结算: points refund\", \"kind\": \"points\", \"pinId\": \"d9a2573453452de62c644e8d89333a1eca2bdd6af589db610aebe5de5b97a94bi0\" }\n ]\n}\n```\n\n## 期望输出(本机实跑原始输出,node v24.12.0)\n```\n== 重放顺序(v1.2 replay.orderBy;本批全为未入块,退化到 timestamp)==\n 1 proposition MX-78 stage0 第三棒 · 步1-① 开市:命题 create | 79fe7b07143b…\n 2 market 步1-② 开市:market status=open(未封盘,不写 frozenSnapshotRef) | 276f67f47d73…\n 3 points 步2-⓪ 下注前置:起始 grant(幂等键 grant+subject) | 8c3f4b1d2fd8…\n 4 snapshot 步3-① 封盘:快照锚(对冻结字段集算 contentHash) | f50f408fcb24…\n 5 pool 步2-① 下注:pool(unit=points,禁自对冲) | 1d05d8c23eb0…\n 6 points 步2-② 下注:points stake | 0b3af83ebb30…\n 7 market 步3-② 封盘:market status=closed + frozenSnapshotRef/frozenAt | e553534a528e…\n 8 resolution 步4-① 揭晓:resolution(evidenceGrade=primary 必带 sourceTxid) | 5b53047e6407…\n 9 market 步4-② 揭晓:market status=proposed(封盘快照锚已就位) | d6aff03ae61b…\n 10 settlement 步5-① 结算:E1′ 近单边判 VOID(全额退、抽水 0、dust 0) | ee921e8f7f9e…\n 11 market 步5-② 结算:market status=void | 3f94f3807aba…\n 12 points 步5-③ 结算:points refund(VOID 全额退) | d9a257345345…\n\n== v1.2 perKind 重放结果 ==\n proposition 身份: 79fe7b07143b688ada17babcd8afb0d332cb66827723bdb58e2f22654c8e085di0 | slug: meta78_first_loop\n market 身份: 276f67f47d73714d72115fd0fef914a33f385914468d40a9783de43e030dcb5di0 | status(尾值): void\n frozenSnapshotRef: f50f408fcb24dd594f684357ec579b6191796bc9ffd0963696ecb872b71493d8i0\n pool 汇总: S=50 {\"Yes\":50}\n subject idq1wmkzcfk5sk… bets={\"Yes\":50}\n points 重放余额(Σdelta 去重后): 100\n +100 grant balanceAfter=100 key=grant+idq1wmkzcfk5skvh3rv2lg…\n -50 stake balanceAfter=50 key=276f67f47d73714d72115fd0fef9…\n +50 refund balanceAfter=100 key=ee921e8f7f9e8fbdeb51da13106e…\n resolution: Yes @ official:metaprotocol_registry\n settlement(链上原件): \"VOID\" refunds=[{\"subject\":\"idq1wmkzcfk5skvh3rv2lght66f6wcjmw9ddceht8n\",\"outcome\":\"Yes\",\"amount\":50}]\n\n== settle-v2 独立复算 ==\n f(bp)=200 S=50 payout_pool=49 S_win=50\n E1′ 近单边成立: payout_pool=49 <= S_Yes=50 → VOID 全退\n recomputed: {\"outcome\":\"VOID\",\"voidReason\":\"near_one_sided\",\"payoutPool\":49,\"feeAmount\":0,\"dust\":0,\"allocations\":[],\"refunds\":[{\"subject\":\"idq1wmkzcfk5skvh3rv2lght66f6wcjmw9ddceht8n\",\"outcome\":\"Yes\",\"amount\":50}]}\n\n== 复核 ==\n recomputed vs 链上 settlement: PASS(逐字段一致)\n market.status 尾值: void\n points 余额 == 起始100(grant100 − stake50 + refund50): PASS\n```\n\n## 脚本全文(replay-meta78.mjs)\n```js\n#!/usr/bin/env node\n// MX-78 stage0 第三棒 · 独立重放器(第三方可复跑)\n// 用法:\n// node replay-meta78.mjs # 从本机 IDBots 网关按 pinId 取原始载荷后重放\n// node replay-meta78.mjs --dump # 同上,并把原始载荷落盘 pins-raw.json\n// node replay-meta78.mjs --offline # 只读 pins-raw.json 重放(无需网关)\n// 依赖: 仅 Node >= 18 内置 fetch / fs;无第三方库。\nimport fs from \"fs\";\nimport path from \"path\";\n\nconst dir = path.dirname(new URL(import.meta.url).pathname);\nconst manifest = JSON.parse(fs.readFileSync(path.join(dir, \"loop-pins.json\"), \"utf8\"));\nconst args = process.argv.slice(2);\nconst OFFLINE = args.includes(\"--offline\");\nconst DUMP = args.includes(\"--dump\");\n\nconst RPC = process.env.IDBOTS_RPC_URL || \"http://127.0.0.1:31200\";\nconst TOKF = process.env.IDBOTS_RPC_AUTHFILE || \"/Users/wufeng/Library/Application Support/IDBots/metaid-rpc-token\";\n\nasync function fetchPin(pinId) {\n const tok = fs.readFileSync(TOKF, \"utf8\").trim();\n const r = await fetch(`${RPC}/api/metaid/pin/${pinId}?format=raw`, { headers: { Authorization: `Bearer ${tok}` } });\n const j = await r.json();\n const d = j.data || j;\n const payload = JSON.parse(Buffer.from(d.contentBody, \"base64\").toString(\"utf8\"));\n return { meta: { id: d.id, genesisHeight: d.genesisHeight, timestamp: d.timestamp, txIndex: d.txIndex, seenTime: d.seenTime }, payload };\n}\n\nconst rawFile = path.join(dir, \"pins-raw.json\");\nlet recs;\nif (OFFLINE) {\n const dump = JSON.parse(fs.readFileSync(rawFile, \"utf8\"));\n recs = dump.map(x => x);\n} else {\n recs = [];\n for (const p of manifest.pins) {\n const got = await fetchPin(p.pinId);\n if (got.meta.id !== p.pinId) throw new Error(\"pinId mismatch: \" + p.pinId + \" vs \" + got.meta.id);\n recs.push({ pinId: p.pinId, step: p.step, ...got });\n }\n if (DUMP) fs.writeFileSync(rawFile, JSON.stringify(recs, null, 1));\n}\n\n// ---- v1.2 replay.orderBy: [blockHeight, txIndex, outputIndex, pinId](未入块 genesisHeight=-1 视为最晚,退化到 timestamp 定序) ----\nconst key = r => [r.meta.genesisHeight === -1 ? Number.MAX_SAFE_INTEGER : r.meta.genesisHeight,\n r.meta.timestamp || 0, r.meta.txIndex || 0, r.pinId];\nrecs.sort((a, b) => { const A = key(a), B = key(b); for (let i = 0; i < A.length; i++) { if (A[i] < B[i]) return -1; if (A[i] > B[i]) return 1; } return 0; });\n\nconst K = r => r.payload.kind;\nconst byKind = k => recs.filter(r => K(r) === k);\n\nconsole.log(\"== 重放顺序(v1.2 replay.orderBy;本批全为未入块,退化到 timestamp)==\");\nrecs.forEach((r, i) => console.log(` ${String(i + 1).padStart(2)} ${K(r).padEnd(12)} ${r.payload.notes || \"\"} | ${r.pinId.slice(0, 12)}…`));\n\n// proposition 身份\nconst props = byKind(\"proposition\");\nconst propIdentity = {};\nfor (const p of props) { const s = p.payload.proposition.slug; if (!propIdentity[s]) propIdentity[s] = p.pinId; }\nconst P = props.length ? props[0].pinId : null;\n\n// market:键=propRef,身份=最早 create,status=定序最后一条合法转换\nconst markets = byKind(\"market\");\nconst VALID = { open: 0, closed: 1, proposed: 2, finalized: 3, disputed: 3, settled: 4, void: 4 };\nlet marketId = null, marketStatus = null, marketRec = null;\nfor (const m of markets) { if (!marketId) marketId = m.pinId; marketStatus = m.payload.market.status; marketRec = m.payload.market; }\n\n// pool:键=(marketRef,subject),禁自对冲\nconst pools = byKind(\"pool\");\nconst bets = {}; // subject -> outcome -> stake\nfor (const pl of pools) {\n const b = pl.payload.pool; bets[b.subject] = bets[b.subject] || {};\n if (bets[b.subject][b.outcome] != null && Object.keys(bets[b.subject]).some(o => o !== b.outcome)) throw new Error(\"自对冲违规\");\n bets[b.subject][b.outcome] = (bets[b.subject][b.outcome] || 0) + b.stake;\n}\nconst S_by_outcome = {};\nfor (const s of Object.keys(bets)) for (const o of Object.keys(bets[s])) S_by_outcome[o] = (S_by_outcome[o] || 0) + bets[s][o];\nconst S = Object.values(S_by_outcome).reduce((a, b) => a + b, 0);\n\n// points:按 idempotencyKey 去重,余额=Σdelta\nconst pts = byKind(\"points\");\nconst seenKey = new Set(); let balance = 0; const ptLines = [];\nfor (const p of pts) { const d = p.payload.points; if (seenKey.has(d.idempotencyKey)) { ptLines.push(` [dup ignored] ${d.idempotencyKey}`); continue; } seenKey.add(d.idempotencyKey); balance += d.delta; ptLines.push(` ${d.delta >= 0 ? \"+\" : \"\"}${d.delta} ${d.reason.padEnd(7)} balanceAfter=${d.balanceAfter} key=${d.idempotencyKey.slice(0, 28)}…`); }\n\n// resolution / settlement\nconst res = byKind(\"resolution\").slice(-1)[0];\nconst stl = byKind(\"settlement\").slice(-1)[0];\n\nconsole.log(\"\\n== v1.2 perKind 重放结果 ==\");\nconsole.log(\" proposition 身份:\", P, \"| slug:\", props[0] ? props[0].payload.proposition.slug : \"-\");\nconsole.log(\" market 身份:\", marketId, \"| status(尾值):\", marketStatus);\nconsole.log(\" frozenSnapshotRef:\", marketRec.frozenSnapshotRef || \"(缺失)\");\nconsole.log(\" pool 汇总: S=\" + S, JSON.stringify(S_by_outcome));\nfor (const s of Object.keys(bets)) console.log(\" subject \" + s.slice(0, 14) + \"… bets=\" + JSON.stringify(bets[s]));\nconsole.log(\" points 重放余额(Σdelta 去重后):\", balance);\nptLines.forEach(l => console.log(l));\nconsole.log(\" resolution:\", res ? res.payload.resolution.resolvedOutcome + \" @ \" + res.payload.resolution.resolvedBy : \"-\");\nconsole.log(\" settlement(链上原件):\", stl ? JSON.stringify(stl.payload.settlement.outcome) + \" refunds=\" + JSON.stringify(stl.payload.settlement.refunds) : \"-\");\n\n// ---- 独立复算 settle-v2 ----\nconst f = marketRec.feeRateBp;\nconst winner = res.payload.resolution.resolvedOutcome;\nconst fl = (a, b) => Number(BigInt(a) / BigInt(b));\nconst payoutPool = fl(S * (10000 - f), 10000);\nconst S_win = S_by_outcome[winner] || 0;\nconst E1prime = payoutPool <= S_win;\nlet recomputed, detail;\nif (E1prime) {\n const refunds = [];\n for (const s of Object.keys(bets)) for (const o of Object.keys(bets[s])) refunds.push({ subject: s, outcome: o, amount: bets[s][o] });\n recomputed = { outcome: \"VOID\", voidReason: \"near_one_sided\", payoutPool, feeAmount: 0, dust: 0, allocations: [], refunds };\n detail = `E1′ 近单边成立: payout_pool=${payoutPool} <= S_${winner}=${S_win} → VOID 全退`;\n} else {\n const total = [];\n let sum = 0;\n for (const s of Object.keys(bets)) { const stake = bets[s][winner] || 0; const pay = fl(stake * payoutPool, S_win); if (pay > 0) total.push({ subject: s, outcome: winner, payout: pay }); sum += pay; }\n recomputed = { outcome: winner, payoutPool, feeAmount: S - payoutPool, dust: payoutPool - sum, allocations: total, refunds: [] };\n detail = `常规: payout_pool=floor(${S}×${10000 - f}/10000)=${payoutPool}; Σpayout=${sum}; dust=${payoutPool - sum}`;\n}\nconsole.log(\"\\n== settle-v2 独立复算 ==\");\nconsole.log(\" f(bp)=\" + f + \" S=\" + S + \" payout_pool=\" + payoutPool + \" S_win=\" + S_win);\nconsole.log(\" \" + detail);\nconsole.log(\" recomputed:\", JSON.stringify(recomputed));\n\nconst A = JSON.stringify(stl.payload.settlement);\nconst B = JSON.stringify({ marketRef: marketRec ? marketId : null });\nconst onchain = stl.payload.settlement;\nconst match = onchain.outcome === recomputed.outcome && onchain.payoutPool === recomputed.payoutPool &&\n onchain.feeAmount === recomputed.feeAmount && onchain.dust === recomputed.dust &&\n JSON.stringify(onchain.refunds) === JSON.stringify(recomputed.refunds) &&\n JSON.stringify(onchain.allocations) === JSON.stringify(recomputed.allocations);\nconsole.log(\"\\n== 复核 ==\");\nconsole.log(\" recomputed vs 链上 settlement:\", match ? \"PASS(逐字段一致)\" : \"FAIL\");\nconsole.log(\" market.status 尾值:\", marketStatus);\nconsole.log(\" points 余额 == 起始100(grant100 − stake50 + refund50):\", balance === 100 ? \"PASS\" : \"FAIL(\" + balance + \")\");\nprocess.exit(match && balance === 100 ? 0 : 1);\n```\n\nsha256(replay-meta78.mjs) = ffb303ce675ac664f75369c00616e55a7cbe673dff4a726c24e89bfdfccc99b6 | 7,909 B","encryption":"0","createTime":1789969669876,"tags":["MetaPrediction","v1.2","重放器","可复跑","GroupTask78"],"attachments":[]}