// machete.mjs (quantum exposure report, C12): MOCK targets for the test, one process per kind, serving RECORDED responses from a // fixture file (fixturi/*.json: where each answer came from and when is written in the file's `_sursa`). Each mock is a real local // listener: it counts the connections it ACCEPTS and the requests it answers, and writes the counts to --contor after every event, // so the test can compare them with the connection journal of the report process (paza-retea.mjs). // // node machete.mjs --fel electrum|esplora|bitcoind|eth --fixtura --gata --contor [--port N] // [--gol] every request is answered with an error / empty body (negative control: not "no evidence") // [--mut] every connection is closed without an answer (negative control: a timeout is not "no evidence") // [--suprascrie f] a JSON whose `raspunsuri` are merged over the fixture's (negative control: a planted spend); for // Esplora, a value { "_status": N, "_corp": "text" } is served with that HTTP status and body; for // Electrum (features, tip, history, a header at a height) and Ethereum, a value // { "_eroare": { code, message } } is served as that JSON-RPC error // [--plafon-s N] the mock exits by itself after N seconds (default 600) // A script the fixture does not hold is answered as an index answers a script never used (Electrum: an empty history; Esplora: zero // counts and an empty page for /address/:a and /scripthash/:h) and is counted in `unknown`: the sibling scripts the report reads beside // each address were not recorded with the fixtures (README, Test). The tip (Electrum blockchain.headers.subscribe; Esplora // /blocks/tip/height, /block-height/:h, /block/:hash/header), the genesis block (Electrum server.features; Esplora /block-height/0), the // block at a checkpoint height (Electrum blockchain.block.header; Esplora /block-height/:h), the chain of a Bitcoin Core node // (getblockhash, getblockcount) and the state of an Ethereum account at the head (eth_getProof, eth_getCode at the head number) are // served from the fixture, or from --suprascrie; a height or a method the fixture does not hold is answered with an error. // Listens on 127.0.0.1 only. Writes { port, pid } to --gata when listening. Nothing here reaches any network. // A mock never outlives the process that started it: it checks every second that its parent is still alive (process.ppid changes when // the parent dies on POSIX, where the orphan is adopted; on Windows the parent's pid is probed) and exits when it is not, and it exits in // any case at the time cap; a test killed in the middle leaves no listener behind. import fs from 'node:fs'; import net from 'node:net'; import http from 'node:http'; const arg = (k, d = null) => { const i = process.argv.indexOf(k); return i >= 0 ? process.argv[i + 1] : d; }; const are = (k) => process.argv.includes(k); const FEL = arg('--fel'); const FIX = arg('--fixtura'); const GATA = arg('--gata'); const CONTOR = arg('--contor'); const PORT = Number(arg('--port', '0')); if (!FEL || !FIX || !GATA || !CONTOR) { console.error('usage: --fel electrum|esplora|bitcoind|eth --fixtura f --gata f --contor f [--port N] [--gol] [--mut] [--suprascrie f]'); process.exit(2); } const fix = JSON.parse(fs.readFileSync(FIX, 'utf8')); const R = { ...fix.raspunsuri }; if (arg('--suprascrie')) Object.assign(R, JSON.parse(fs.readFileSync(arg('--suprascrie'), 'utf8')).raspunsuri || {}); const GOL = are('--gol'), MUT = are('--mut'); const PARINTE = process.ppid; const parinteViu = () => { if (process.ppid !== PARINTE) return false; try { process.kill(PARINTE, 0); return true; } catch (e) { return e.code !== 'ESRCH'; } }; setInterval(() => { if (!parinteViu()) process.exit(0); }, 1000).unref(); setTimeout(() => process.exit(0), Math.max(5, Number(arg('--plafon-s', '600'))) * 1000).unref(); const contor = { fel: FEL, connections: 0, requests: 0, unknown: [] }; const scrieContor = () => fs.writeFileSync(CONTOR, JSON.stringify(contor) + '\n'); scrieContor(); function raspunsElectrum(j) { if (GOL) return { jsonrpc: '2.0', id: j.id, error: { code: -32000, message: 'mock: no answer (--gol)' } }; if (j.method === 'server.version') return { jsonrpc: '2.0', id: j.id, result: ['mock-electrum/' + (fix._sursa ? fix._sursa.cand : '?'), '1.4'] }; if (j.method === 'server.features') { const k = 'server.features'; if (R[k] && R[k]._eroare) return { jsonrpc: '2.0', id: j.id, error: R[k]._eroare }; if (R[k]) return { jsonrpc: '2.0', id: j.id, result: R[k] }; contor.unknown.push(k); return { jsonrpc: '2.0', id: j.id, error: { code: -32601, message: 'mock: no features in the fixture' } }; } if (j.method === 'blockchain.scripthash.get_history') { const k = 'blockchain.scripthash.get_history:' + j.params[0]; if (R[k] && R[k]._eroare) return { jsonrpc: '2.0', id: j.id, error: R[k]._eroare }; if (R[k]) return { jsonrpc: '2.0', id: j.id, result: R[k] }; contor.unknown.push(k); return { jsonrpc: '2.0', id: j.id, result: [] }; } if (j.method === 'blockchain.headers.subscribe') { const k = 'blockchain.headers.subscribe'; if (R[k] && R[k]._eroare) return { jsonrpc: '2.0', id: j.id, error: R[k]._eroare }; if (R[k]) return { jsonrpc: '2.0', id: j.id, result: R[k] }; contor.unknown.push(k); return { jsonrpc: '2.0', id: j.id, error: { code: -32601, message: 'mock: no tip in the fixture' } }; } if (j.method === 'blockchain.transaction.get') { const k = 'blockchain.transaction.get:' + j.params[0]; if (R[k]) return { jsonrpc: '2.0', id: j.id, result: R[k] }; contor.unknown.push(k); return { jsonrpc: '2.0', id: j.id, error: { code: -32603, message: 'mock: unknown transaction' } }; } if (j.method === 'blockchain.block.header') { const k = 'blockchain.block.header:' + (j.params && j.params[0]); if (R[k] && R[k]._eroare) return { jsonrpc: '2.0', id: j.id, error: R[k]._eroare }; if (Object.hasOwn(R, k)) return { jsonrpc: '2.0', id: j.id, result: R[k] }; contor.unknown.push(k); return { jsonrpc: '2.0', id: j.id, error: { code: 1, message: 'mock: no header at this height in the fixture' } }; } return { jsonrpc: '2.0', id: j.id, error: { code: -32601, message: 'mock: unknown method' } }; } function raspunsBitcoind(j) { if (GOL) return { result: null, error: { code: -32603, message: 'mock: no answer (--gol)' }, id: j.id }; if (j.method === 'getblockhash') { const k = 'getblockhash:' + (j.params && j.params[0]); if (Object.hasOwn(R, k)) return { result: R[k], error: null, id: j.id }; contor.unknown.push(k); return { result: null, error: { code: -8, message: 'Block height out of range' }, id: j.id }; } if (j.method === 'getblockcount') { const k = 'getblockcount'; if (Object.hasOwn(R, k)) return { result: R[k], error: null, id: j.id }; contor.unknown.push(k); return { result: null, error: { code: -32601, message: 'mock: no block count in the fixture' }, id: j.id }; } if (j.method === 'scantxoutset') { const d = j.params && j.params[1] && j.params[1][0] ? String(j.params[1][0].desc || j.params[1][0]) : ''; const m = /^addr\(([^)]+)\)/.exec(d) || /^(raw\([0-9a-f]+\))/.exec(d); const k = 'scantxoutset:' + (m ? m[1] : d); if (R[k]) return { result: R[k], error: null, id: j.id }; contor.unknown.push(k); return { result: { success: true, unspents: [] }, error: null, id: j.id }; } if (j.method === 'getrawtransaction') { const k = 'getrawtransaction:' + j.params[0]; if (R[k]) return { result: R[k], error: null, id: j.id }; contor.unknown.push(k); return { result: null, error: { code: -5, message: 'No such mempool or blockchain transaction' }, id: j.id }; } return { result: null, error: { code: -32601, message: 'Method not found' }, id: j.id }; } function raspunsEth(j) { if (GOL) return { jsonrpc: '2.0', id: j.id, result: null }; const k = j.method + ':' + JSON.stringify(j.params || []); if (Object.hasOwn(R, k) && R[k] && typeof R[k] === 'object' && R[k]._eroare) return { jsonrpc: '2.0', id: j.id, error: R[k]._eroare }; if (Object.hasOwn(R, k)) return { jsonrpc: '2.0', id: j.id, result: R[k] }; contor.unknown.push(k); return { jsonrpc: '2.0', id: j.id, error: { code: -32000, message: 'mock: not in the fixture: ' + k.slice(0, 80) } }; } if (FEL === 'electrum') { const srv = net.createServer((sock) => { contor.connections++; scrieContor(); if (MUT) { sock.destroy(); return; } let buf = ''; sock.on('data', (d) => { buf += d.toString('utf8'); let i; while ((i = buf.indexOf('\n')) >= 0) { const line = buf.slice(0, i); buf = buf.slice(i + 1); if (!line.trim()) continue; contor.requests++; scrieContor(); let j; try { j = JSON.parse(line); } catch { sock.write(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'parse error' } }) + '\n'); continue; } sock.write(JSON.stringify(raspunsElectrum(j)) + '\n'); } }); sock.on('error', () => {}); }); srv.listen(PORT, '127.0.0.1', () => fs.writeFileSync(GATA, JSON.stringify({ port: srv.address().port, pid: process.pid }) + '\n')); } else { const srv = http.createServer((req, res) => { contor.requests++; scrieContor(); const parts = []; req.on('data', (d) => parts.push(d)); req.on('end', () => { const body = Buffer.concat(parts).toString('utf8'); if (MUT) { req.socket.destroy(); return; } if (FEL === 'esplora') { if (GOL) { res.writeHead(500, { 'content-type': 'text/plain' }); res.end(''); return; } const p = req.url.split('?')[0]; if (Object.hasOwn(R, p)) { const v = R[p]; if (v && typeof v === 'object' && Number.isInteger(v._status)) { res.writeHead(v._status, { 'content-type': 'text/plain' }); res.end(String(v._corp ?? '')); return; } res.writeHead(200, { 'content-type': typeof v === 'string' ? 'text/plain' : 'application/json' }); res.end(typeof v === 'string' ? v : JSON.stringify(v)); return; } if (/^\/(address|scripthash)\/[^/]+\/txs\/chain\/[0-9a-f]{64}$/.test(p)) { res.writeHead(200, { 'content-type': 'application/json' }); res.end('[]'); return; } // a script not in the fixture: as an index answers a script never used (zero counts, an empty page), counted in `unknown` const nefolosit = /^\/(address|scripthash)\/([^/]+)(\/txs)?$/.exec(p); if (nefolosit) { contor.unknown.push(p); scrieContor(); const zero = { funded_txo_count: 0, funded_txo_sum: 0, spent_txo_count: 0, spent_txo_sum: 0, tx_count: 0 }; res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify(nefolosit[3] ? [] : { [nefolosit[1]]: nefolosit[2], chain_stats: zero, mempool_stats: zero })); return; } contor.unknown.push(p); res.writeHead(404, { 'content-type': 'text/plain' }); res.end('not found (mock)'); return; } let j; try { j = JSON.parse(body); } catch { res.writeHead(400); res.end(''); return; } const r = FEL === 'bitcoind' ? raspunsBitcoind(j) : raspunsEth(j); res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify(r)); }); }); srv.on('connection', () => { contor.connections++; scrieContor(); }); srv.listen(PORT, '127.0.0.1', () => fs.writeFileSync(GATA, JSON.stringify({ port: srv.address().port, pid: process.pid }) + '\n')); }