|
| 1 | +// Anchor a node's chain tip to Bitcoin — the Blocktrails settlement-assurance |
| 2 | +// module (the first pluggable finality backend; docs/spec/ § 13). |
| 3 | +// |
| 4 | +// ANCHOR_KEY=<privkey hex> node tools/anchor.js [nodeUrl] [--data <dir>] |
| 5 | +// |
| 6 | +// State string (git-mark convention: literal key order, hashed as-is): |
| 7 | +// {"node":"<origin>","seq":<n>,"tip":"sha256:…"} |
| 8 | +// |
| 9 | +// Each anchor advances a Blocktrails trail: a chained BIP-341 TapTweak of the |
| 10 | +// anchor key by the state hash → a fresh P2TR address; the mark transaction |
| 11 | +// pays the trail's whole balance forward to it. The chain of spends IS the |
| 12 | +// anchor history, ordered and timestamped by Bitcoin. A node that later |
| 13 | +// presents a different history for an anchored (seq, tip) is refuted by its |
| 14 | +// own anchor. One trail can carry MANY nodes' tips (a federation trail) — |
| 15 | +// the state names the node. |
| 16 | +// |
| 17 | +// The trail file (.blocktrail.json) and the anchor record (anchors.json) |
| 18 | +// live in <data>/anchor/, which the server exposes read-only at |
| 19 | +// GET /api/anchors. The key is env-only and never written to disk here. |
| 20 | +// |
| 21 | +// Uses the `blocktrails` reference CLI (same author) via npx — the server |
| 22 | +// itself carries no Bitcoin code. |
| 23 | + |
| 24 | +import { execFileSync } from 'node:child_process'; |
| 25 | +import fs from 'node:fs'; |
| 26 | +import path from 'node:path'; |
| 27 | + |
| 28 | +const args = process.argv.slice(2); |
| 29 | +const dataFlag = args.indexOf('--data'); |
| 30 | +const dataDir = dataFlag >= 0 ? args[dataFlag + 1] : (process.env.DATA || './data'); |
| 31 | +const nodeUrl = (args.find((a) => a.startsWith('http')) || 'http://localhost:3480').replace(/\/$/, ''); |
| 32 | +const key = process.env.ANCHOR_KEY; |
| 33 | +if (!key || !/^[0-9a-f]{64}$/.test(key)) { |
| 34 | + console.error('ANCHOR_KEY (64-hex privkey) required in the environment'); |
| 35 | + process.exit(1); |
| 36 | +} |
| 37 | + |
| 38 | +const anchorDir = path.join(dataDir, 'anchor'); |
| 39 | +fs.mkdirSync(anchorDir, { recursive: true }); |
| 40 | +const trailFile = path.join(anchorDir, '.blocktrail.json'); |
| 41 | +const recordFile = path.join(anchorDir, 'anchors.json'); |
| 42 | + |
| 43 | +// ---- what to anchor: the node's audited tip ------------------------------- |
| 44 | +const verify = await (await fetch(`${nodeUrl}/api/log/verify`)).json(); |
| 45 | +if (!verify.valid) { |
| 46 | + console.error(`refusing to anchor an INVALID chain: ${JSON.stringify(verify)}`); |
| 47 | + process.exit(1); |
| 48 | +} |
| 49 | +const state = JSON.stringify({ node: nodeUrl, seq: verify.seq, tip: verify.tip }); |
| 50 | +console.log(`anchoring: ${state}`); |
| 51 | + |
| 52 | +// Skip if this exact (node, seq, tip) is already anchored. |
| 53 | +let records = []; |
| 54 | +try { records = JSON.parse(fs.readFileSync(recordFile, 'utf8')); } catch { /* first run */ } |
| 55 | +if (records.some((r) => r.state === state)) { |
| 56 | + console.log('already anchored — nothing to do'); |
| 57 | + process.exit(0); |
| 58 | +} |
| 59 | + |
| 60 | +// ---- mark it via the blocktrails reference CLI ---------------------------- |
| 61 | +let out; |
| 62 | +try { |
| 63 | + out = execFileSync('npx', ['-y', 'blocktrails@0.0.12', 'mark', state, |
| 64 | + '--key', key, '--file', trailFile, '--network', 'tbtc4'], |
| 65 | + { encoding: 'utf8', cwd: anchorDir, stdio: ['ignore', 'pipe', 'pipe'] }); |
| 66 | +} catch (err) { |
| 67 | + console.error('mark failed:'); |
| 68 | + console.error(String(err.stdout || '')); |
| 69 | + console.error(String(err.stderr || err.message)); |
| 70 | + process.exit(1); |
| 71 | +} |
| 72 | +console.log(out); |
| 73 | + |
| 74 | +const txid = (out.match(/txid[:\s]+([0-9a-f]{64})/i) || out.match(/([0-9a-f]{64})/) || [])[1] || null; |
| 75 | +const address = (out.match(/(tb1p[a-z0-9]+)/) || [])[1] || null; |
| 76 | + |
| 77 | +records.push({ |
| 78 | + state, node: nodeUrl, seq: verify.seq, tip: verify.tip, |
| 79 | + address, txid, network: 'tbtc4', |
| 80 | + explorer: txid ? `https://mempool.guide/testnet4/tx/${txid}` : null, |
| 81 | + at: new Date().toISOString(), |
| 82 | +}); |
| 83 | +fs.writeFileSync(recordFile, JSON.stringify(records, null, 2)); |
| 84 | +console.log(`recorded → ${recordFile}`); |
| 85 | +if (txid) console.log(`explorer: https://mempool.guide/testnet4/tx/${txid}`); |
0 commit comments