#!/usr/bin/env node // Sign and submit a Nano forecast ladder entry. // node client/sign-and-submit.js --key <64 hex private key> --round 0 --forecasts '{"btc_usd":0.4,...}' // node client/sign-and-submit.js --seed <64 hex seed> [--index 0] --round 0 --forecasts-file f.json // Options: --url http://127.0.0.1:3002 --nonce --stake-hash <64 hex> --dry-run (print, do not POST) // --work-rpc http://127.0.0.1:7076 (ask a Nano node for work instead of computing it here) // --threads N (default: all CPUs; in-process work takes ~2^29 hashes, roughly 25-100 s on 4 cores) // Needs: npm i nanocurrency blakejs (run from the ladder dir, or install those two packages) 'use strict'; const { Worker, isMainThread, parentPort, workerData } = require('worker_threads'); const os = require('os'); const fs = require('fs'); const nc = require('nanocurrency'); const { blake2b } = require('blakejs'); const THRESHOLD = 'fffffff800000000'; if (!isMainThread) { // worker: search one slice of the nonce space nc.computeWork(workerData.hash, { workThreshold: THRESHOLD, workerIndex: workerData.index, workerCount: workerData.count }) .then(w => parentPort.postMessage(w)); return; } function canonical(v) { if (v === null || typeof v !== 'object') return JSON.stringify(v); if (Array.isArray(v)) return '[' + v.map(canonical).join(',') + ']'; return '{' + Object.keys(v).sort().map(k => JSON.stringify(k) + ':' + canonical(v[k])).join(',') + '}'; } const blake256 = s => Buffer.from(blake2b(Buffer.from(s, 'utf8'), undefined, 32)).toString('hex').toUpperCase(); function computeWorkLocal(hash, threads) { return new Promise((resolve, reject) => { const started = Date.now(), workers = []; const timer = setInterval(() => process.stderr.write(' working... ' + Math.round((Date.now() - started) / 1000) + ' s elapsed (expected ~' + Math.round(100 / threads) + '-' + Math.round(200 / threads) + ' s on ~5 MH/s/thread)\n'), 5000); const done = w => { clearInterval(timer); workers.forEach(x => x.terminate()); w ? resolve(w) : reject(new Error('no work found')); }; for (let i = 0; i < threads; i++) { const w = new Worker(__filename, { workerData: { hash, index: i, count: threads } }); w.on('message', done); w.on('error', reject); workers.push(w); } }); } async function computeWorkRpc(hash, url) { const r = await fetch(url, { method: 'POST', body: JSON.stringify({ action: 'work_generate', hash, difficulty: THRESHOLD }) }); const j = await r.json(); if (j.error) throw new Error('work_generate: ' + j.error); return j.work; } (async () => { const a = {}; const argv = process.argv.slice(2); for (let i = 0; i < argv.length; i++) if (argv[i].startsWith('--')) a[argv[i].slice(2)] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true; let sk = a.key; if (a.seed) sk = nc.deriveSecretKey(a.seed.toUpperCase(), Number(a.index || 0)); if (!sk || !/^[0-9a-fA-F]{64}$/.test(sk) || a.round === undefined || !(a.forecasts || a['forecasts-file'])) { console.error('usage: --key <64hex> | --seed <64hex> [--index N] --round --forecasts | --forecasts-file [--url U] [--nonce S] [--stake-hash H] [--work-rpc U] [--threads N] [--dry-run]'); process.exit(2); } sk = sk.toUpperCase(); const address = nc.deriveAddress(nc.derivePublicKey(sk), { useNanoPrefix: true }); const forecasts = JSON.parse(a.forecasts || fs.readFileSync(a['forecasts-file'], 'utf8')); const url = a.url || 'http://127.0.0.1:3002'; const nonce = a.nonce || Date.now().toString(36) + Math.random().toString(36).slice(2, 8); const fields = { round: Number(a.round), address, forecasts, nonce }; const canon = canonical(fields), hash = blake256(canon); console.error('address ' + address + '\ncanonical ' + canon + '\nmessage ' + hash); const threads = Number(a.threads || os.cpus().length); const t0 = Date.now(); const work = a['work-rpc'] ? await computeWorkRpc(hash, a['work-rpc']) : await computeWorkLocal(hash, threads); console.error('work ' + work + ' (' + Math.round((Date.now() - t0) / 1000) + ' s)'); const signature = nc.signBlock({ hash, secretKey: sk }); const body = { ...fields, work, signature }; if (a['stake-hash']) body.stake_hash = a['stake-hash']; console.log(JSON.stringify(body, null, 1)); if (a['dry-run']) return; const r = await fetch(url + '/v1/entries', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); const text = await r.text(); console.error('HTTP ' + r.status + ' ' + text); process.exit(r.ok ? 0 : 1); })().catch(e => { console.error(e.message); process.exit(1); });