Skip to content

Commit 79bc142

Browse files
anchoring: Blocktrails settlement-assurance module — tools/anchor.js marks the audited chain tip onto a Bitcoin testnet4 trail (blocktrails reference CLI via npx; key env-only); GET /api/anchors serves the trail + records read-only. One trail can carry many nodes' tips (a federation trail).
1 parent 3a92f7e commit 79bc142

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

server.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,19 @@ export function createNode({ dataDir = './data', publicUrl = null } = {}) {
318318
if (req.method === 'GET' && p === '/api/log') {
319319
return send(res, 200, ledger.log(url.searchParams.get('limit')));
320320
}
321+
// Bitcoin anchors (Blocktrails module) — written by tools/anchor.js
322+
// into <data>/anchor/, served read-only. Empty when the operator
323+
// doesn't anchor; the ledger works identically either way.
324+
if (req.method === 'GET' && p === '/api/anchors') {
325+
let anchors = []; let trail = null;
326+
try { anchors = JSON.parse(fs.readFileSync(path.join(dataDir, 'anchor', 'anchors.json'), 'utf8')); } catch { /* none */ }
327+
try {
328+
const t = JSON.parse(fs.readFileSync(path.join(dataDir, 'anchor', '.blocktrail.json'), 'utf8'));
329+
trail = { pubkeyBase: t.pubkeyBase, network: t.network, states: (t.states || []).length };
330+
} catch { /* none */ }
331+
return send(res, 200, { anchors, trail });
332+
}
333+
321334
if (req.method === 'GET' && p === '/api/log/verify') {
322335
const chain = ledger.verifyLog();
323336
// Authorship audit on top of the chain audit (spec § 8.2 step 4):

tools/anchor.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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

Comments
 (0)