@totemsdk/txpow 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/calibrate.js +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +8 -1
- package/dist/magic.js +6 -6
- package/dist/mine-wasm.d.ts +69 -0
- package/dist/mine-wasm.js +214 -0
- package/dist/mine.d.ts +56 -5
- package/dist/mine.js +266 -24
- package/dist/serialization.js +21 -20
- package/dist/verify.d.ts +48 -9
- package/dist/verify.js +157 -10
- package/dist/wasm/miner.wasm +0 -0
- package/dist/workers/browser.worker.d.ts +26 -0
- package/dist/workers/browser.worker.js +63 -0
- package/dist/workers/node.worker.d.ts +15 -0
- package/dist/workers/node.worker.js +48 -0
- package/package.json +14 -21
package/dist/mine.js
CHANGED
|
@@ -13,15 +13,30 @@
|
|
|
13
13
|
* value bytes in place — no per-iteration allocation.
|
|
14
14
|
* 4. At each MiniNumber encoding boundary (nonce 128, 32768, 8388608, …) we
|
|
15
15
|
* re-allocate a new header buffer with the wider nonce field.
|
|
16
|
-
* 5. Between chunks we yield to the event loop so the
|
|
16
|
+
* 5. Between chunks we yield to the event loop so the calling thread is never blocked.
|
|
17
|
+
* 6. When the pre-compiled WASM binary is present (`dist/wasm/miner.wasm`), the
|
|
18
|
+
* inner loop runs in WASM for ~5-10x throughput. Falls back to pure JS otherwise.
|
|
19
|
+
* 7. In Node.js (main thread), mining runs inside a `worker_threads` Worker so the
|
|
20
|
+
* event loop is never blocked. In browser contexts the in-process loop is used
|
|
21
|
+
* until Task #114 wires up the Web Worker entry point.
|
|
22
|
+
*
|
|
23
|
+
* Control-flow invariant (IMPORTANT):
|
|
24
|
+
* WASM and JS are mutually exclusive per chunk — we never re-mine a chunk that
|
|
25
|
+
* WASM already scanned. When WASM returns null (nonce not found in chunk), we
|
|
26
|
+
* advance to the next chunk rather than falling back to JS for the same range.
|
|
27
|
+
* This means `source` is always accurate: 'wasm' when the binary was used for
|
|
28
|
+
* the winning chunk; 'js' when the JS path found the nonce.
|
|
17
29
|
*/
|
|
18
30
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
31
|
exports.isLessThan = isLessThan;
|
|
32
|
+
exports.setBrowserWorkerUrl = setBrowserWorkerUrl;
|
|
33
|
+
exports.mineTxPoWInProcess = mineTxPoWInProcess;
|
|
20
34
|
exports.mineTxPoW = mineTxPoW;
|
|
21
|
-
const sha3_js_1 = require("@noble/hashes/sha3.js");
|
|
22
35
|
const core_1 = require("@totemsdk/core");
|
|
36
|
+
const core_2 = require("@totemsdk/core");
|
|
23
37
|
const magic_js_1 = require("./magic.js");
|
|
24
38
|
const constants_js_1 = require("./constants.js");
|
|
39
|
+
const mine_wasm_js_1 = require("./mine-wasm.js");
|
|
25
40
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
41
|
// Internal helpers
|
|
27
42
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -48,7 +63,7 @@ function isLessThan(a, b) {
|
|
|
48
63
|
* k=3: 2^23 = 8388608
|
|
49
64
|
*/
|
|
50
65
|
function nextNonceBoundary(nonce) {
|
|
51
|
-
const nonceValueLen = (0,
|
|
66
|
+
const nonceValueLen = (0, core_2.writeMiniNumber)(nonce, 0).length - 2;
|
|
52
67
|
return 2n ** BigInt(8 * nonceValueLen - 1);
|
|
53
68
|
}
|
|
54
69
|
/**
|
|
@@ -57,7 +72,7 @@ function nextNonceBoundary(nonce) {
|
|
|
57
72
|
* This is computed once at the start of a mine and never changes during it.
|
|
58
73
|
*/
|
|
59
74
|
function buildHeaderTail(txBodyHash, timeMilli) {
|
|
60
|
-
return (0,
|
|
75
|
+
return (0, core_2.concat)((0, core_2.writeMiniData)(constants_js_1.MAIN_NET_CHAIN_ID), (0, core_2.writeMiniNumber)(timeMilli, 0), (0, core_2.writeMiniNumber)(0n, 0), (0, core_2.writeMiniData)(constants_js_1.MAX_HASH), new Uint8Array([constants_js_1.CASCADE_LEVELS]), (0, core_2.writeHashToStream)(constants_js_1.ZERO_HASH), (0, core_2.writeHashToStream)(constants_js_1.ZERO_HASH), (0, core_2.writeMiniNumber)(0n, 0), (0, magic_js_1.serializeMagic)(), (0, core_2.writeHashToStream)(constants_js_1.ZERO_HASH), (0, core_2.writeHashToStream)(txBodyHash));
|
|
61
76
|
}
|
|
62
77
|
/**
|
|
63
78
|
* Synchronously mine a chunk of nonces within a fixed encoding boundary.
|
|
@@ -65,7 +80,7 @@ function buildHeaderTail(txBodyHash, timeMilli) {
|
|
|
65
80
|
* Mutates headerBuf in place — only the nonce value bytes at offset 2 change.
|
|
66
81
|
* Returns the found nonce, or null if not found in this chunk.
|
|
67
82
|
*/
|
|
68
|
-
function
|
|
83
|
+
function mineChunkJs(headerBuf, nonceValueLen, target, startNonce, endNonce) {
|
|
69
84
|
const nonceValueOffset = 2;
|
|
70
85
|
for (let nonce = startNonce; nonce < endNonce; nonce++) {
|
|
71
86
|
let n = nonce;
|
|
@@ -73,34 +88,215 @@ function mineChunk(headerBuf, nonceValueLen, target, startNonce, endNonce) {
|
|
|
73
88
|
headerBuf[nonceValueOffset + i] = Number(n & 0xffn);
|
|
74
89
|
n >>= 8n;
|
|
75
90
|
}
|
|
76
|
-
const hash = (0,
|
|
91
|
+
const hash = (0, core_1.sha3_256)(headerBuf);
|
|
77
92
|
if (isLessThan(hash, target))
|
|
78
93
|
return nonce;
|
|
79
94
|
}
|
|
80
95
|
return null;
|
|
81
96
|
}
|
|
82
97
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
-
//
|
|
98
|
+
// Browser Web Worker dispatch (optional URL-based)
|
|
84
99
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
let _browserWorkerUrl = null;
|
|
85
101
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
102
|
+
* Configure the URL of the browser.worker.js bundle so that `mineTxPoW()`
|
|
103
|
+
* can spawn a Web Worker in browser contexts.
|
|
88
104
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
105
|
+
* Call this once at extension/dApp startup. The bundled worker file exposes
|
|
106
|
+
* the same message protocol as node.worker.ts.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* import { setBrowserWorkerUrl } from '@totemsdk/txpow';
|
|
110
|
+
* setBrowserWorkerUrl(browser.runtime.getURL('mine-worker.js'));
|
|
94
111
|
*/
|
|
95
|
-
|
|
112
|
+
function setBrowserWorkerUrl(url) {
|
|
113
|
+
_browserWorkerUrl = url;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Spawn a browser Web Worker to run the mining loop off the main thread.
|
|
117
|
+
* Requires a prior call to setBrowserWorkerUrl() to configure the worker URL.
|
|
118
|
+
*/
|
|
119
|
+
async function mineTxPoWViaBrowserWorker(txBodyBytes, txnDifficulty, options) {
|
|
120
|
+
if (!_browserWorkerUrl) {
|
|
121
|
+
throw new Error('Browser Web Worker URL not configured. Call setBrowserWorkerUrl() first.');
|
|
122
|
+
}
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
124
|
+
const WorkerClass = globalThis.Worker;
|
|
125
|
+
const worker = new WorkerClass(_browserWorkerUrl);
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
let settled = false;
|
|
128
|
+
const settle = (fn) => {
|
|
129
|
+
if (!settled) {
|
|
130
|
+
settled = true;
|
|
131
|
+
fn();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
if (options?.signal) {
|
|
135
|
+
const abortHandler = () => {
|
|
136
|
+
settle(() => reject(new DOMException('Mining aborted', 'AbortError')));
|
|
137
|
+
worker.terminate();
|
|
138
|
+
};
|
|
139
|
+
if (options.signal.aborted) {
|
|
140
|
+
abortHandler();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
options.signal.addEventListener('abort', abortHandler, { once: true });
|
|
144
|
+
}
|
|
145
|
+
worker.onmessage = (e) => {
|
|
146
|
+
const msg = e.data;
|
|
147
|
+
if (msg['error']) {
|
|
148
|
+
settle(() => reject(new Error(String(msg['error']))));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
settle(() => resolve({
|
|
152
|
+
minedHeaderBytes: Uint8Array.from(msg['minedHeaderBytes']),
|
|
153
|
+
txpowId: Uint8Array.from(msg['txpowId']),
|
|
154
|
+
nonce: BigInt(msg['nonce']),
|
|
155
|
+
elapsedMs: msg['elapsedMs'],
|
|
156
|
+
source: msg['source'],
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
worker.onerror = (e) => {
|
|
161
|
+
settle(() => reject(new Error(e.message)));
|
|
162
|
+
};
|
|
163
|
+
// Pass the WASM URL so the worker can call setWasmUrl() in its own JS context.
|
|
164
|
+
// Web Workers have isolated module state — the main thread's setWasmUrl() call
|
|
165
|
+
// does not carry over. Without this the worker mines in pure JS.
|
|
166
|
+
const wasmUrl = (0, mine_wasm_js_1.getBrowserWasmUrl)();
|
|
167
|
+
worker.postMessage({
|
|
168
|
+
txBodyBytes: Array.from(txBodyBytes),
|
|
169
|
+
txnDifficulty: Array.from(txnDifficulty),
|
|
170
|
+
wasmUrl: wasmUrl ?? undefined,
|
|
171
|
+
chunkSize: options?.chunkSize,
|
|
172
|
+
maxIterations: options?.maxIterations !== undefined
|
|
173
|
+
? options.maxIterations.toString()
|
|
174
|
+
: undefined,
|
|
175
|
+
timeMilli: options?.timeMilli !== undefined
|
|
176
|
+
? options.timeMilli.toString()
|
|
177
|
+
: undefined,
|
|
178
|
+
forceJs: options?.forceJs,
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
183
|
+
// Node.js worker_threads dispatch
|
|
184
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
185
|
+
/**
|
|
186
|
+
* Returns true when mining should be delegated to a Node.js worker_threads Worker.
|
|
187
|
+
*
|
|
188
|
+
* Conditions for using a worker:
|
|
189
|
+
* - Running in Node.js (process.versions.node is set)
|
|
190
|
+
* - Not already inside a worker thread (isMainThread === true)
|
|
191
|
+
* - _skipWorker option is not set
|
|
192
|
+
* - Not in a Jest test environment (ts-jest runs from source, dist/workers/*.js
|
|
193
|
+
* may not exist yet; Jest's own worker processes also have isMainThread=false
|
|
194
|
+
* which handles the common --maxWorkers case automatically)
|
|
195
|
+
*/
|
|
196
|
+
function shouldUseNodeWorker(options) {
|
|
197
|
+
if (options?._skipWorker)
|
|
198
|
+
return false;
|
|
199
|
+
if (typeof process === 'undefined' || !process.versions?.node)
|
|
200
|
+
return false;
|
|
201
|
+
// In Jest, skip worker spawning: source workers are not compiled .js files
|
|
202
|
+
// and Jest's own worker pool already sets isMainThread=false for most runners.
|
|
203
|
+
if (process.env.NODE_ENV === 'test' ||
|
|
204
|
+
process.env.JEST_WORKER_ID !== undefined) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
209
|
+
const wt = require('worker_threads');
|
|
210
|
+
return wt.isMainThread;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Dispatch mining to a Node.js `worker_threads` Worker.
|
|
218
|
+
* The worker runs `mineTxPoWInProcess` with `_skipWorker: true` to prevent
|
|
219
|
+
* recursive worker spawning.
|
|
220
|
+
*/
|
|
221
|
+
async function mineTxPoWViaNodeWorker(txBodyBytes, txnDifficulty, options) {
|
|
222
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
223
|
+
const { Worker } = require('worker_threads');
|
|
224
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
225
|
+
const { join } = require('path');
|
|
226
|
+
const workerPath = join(__dirname, 'workers', 'node.worker.js');
|
|
227
|
+
return new Promise((resolve, reject) => {
|
|
228
|
+
let settled = false;
|
|
229
|
+
const settle = (fn) => {
|
|
230
|
+
if (!settled) {
|
|
231
|
+
settled = true;
|
|
232
|
+
fn();
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const worker = new Worker(workerPath, {
|
|
236
|
+
workerData: {
|
|
237
|
+
txBodyBytes: Array.from(txBodyBytes),
|
|
238
|
+
txnDifficulty: Array.from(txnDifficulty),
|
|
239
|
+
chunkSize: options?.chunkSize,
|
|
240
|
+
maxIterations: options?.maxIterations !== undefined
|
|
241
|
+
? options.maxIterations.toString()
|
|
242
|
+
: undefined,
|
|
243
|
+
timeMilli: options?.timeMilli !== undefined
|
|
244
|
+
? options.timeMilli.toString()
|
|
245
|
+
: undefined,
|
|
246
|
+
forceJs: options?.forceJs,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
if (options?.signal) {
|
|
250
|
+
const abortHandler = () => {
|
|
251
|
+
settle(() => reject(new DOMException('Mining aborted', 'AbortError')));
|
|
252
|
+
worker.terminate().catch(() => { });
|
|
253
|
+
};
|
|
254
|
+
if (options.signal.aborted) {
|
|
255
|
+
abortHandler();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
options.signal.addEventListener('abort', abortHandler, { once: true });
|
|
259
|
+
}
|
|
260
|
+
worker.on('message', (msg) => {
|
|
261
|
+
if (msg['error']) {
|
|
262
|
+
settle(() => reject(new Error(String(msg['error']))));
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
settle(() => resolve({
|
|
266
|
+
minedHeaderBytes: Uint8Array.from(msg['minedHeaderBytes']),
|
|
267
|
+
txpowId: Uint8Array.from(msg['txpowId']),
|
|
268
|
+
nonce: BigInt(msg['nonce']),
|
|
269
|
+
elapsedMs: msg['elapsedMs'],
|
|
270
|
+
source: msg['source'],
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
worker.on('error', (err) => settle(() => reject(err)));
|
|
275
|
+
worker.on('exit', (code) => {
|
|
276
|
+
if (code !== 0) {
|
|
277
|
+
settle(() => reject(new Error(`Mining worker exited with non-zero code ${code}`)));
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
283
|
+
// In-process mining loop
|
|
284
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
285
|
+
/**
|
|
286
|
+
* Run the mining loop in the current thread/task.
|
|
287
|
+
* Called directly by the Node.js worker, and by the browser code path.
|
|
288
|
+
*/
|
|
289
|
+
async function mineTxPoWInProcess(txBodyBytes, txnDifficulty, options) {
|
|
96
290
|
const chunkSize = BigInt(options?.chunkSize ?? 10000);
|
|
97
291
|
const maxIterations = options?.maxIterations !== undefined
|
|
98
292
|
? BigInt(options.maxIterations)
|
|
99
293
|
: undefined;
|
|
100
294
|
const signal = options?.signal;
|
|
101
|
-
const
|
|
102
|
-
const
|
|
295
|
+
const timeMilli = options?.timeMilli ?? BigInt(Date.now());
|
|
296
|
+
const forceJs = options?.forceJs ?? false;
|
|
297
|
+
const bodyHash = (0, core_1.sha3_256)(txBodyBytes);
|
|
103
298
|
const headerTail = buildHeaderTail(bodyHash, timeMilli);
|
|
299
|
+
const useWasm = !forceJs && (await (0, mine_wasm_js_1.isWasmAvailable)());
|
|
104
300
|
const startTime = Date.now();
|
|
105
301
|
let totalIterations = 0n;
|
|
106
302
|
let nonce = 0n;
|
|
@@ -113,20 +309,32 @@ async function mineTxPoW(txBodyBytes, txnDifficulty, options) {
|
|
|
113
309
|
}
|
|
114
310
|
const boundary = nextNonceBoundary(nonce);
|
|
115
311
|
const chunkEnd = nonce + chunkSize < boundary ? nonce + chunkSize : boundary;
|
|
116
|
-
const nonceBytes = (0,
|
|
312
|
+
const nonceBytes = (0, core_2.writeMiniNumber)(nonce, 0);
|
|
117
313
|
const nonceValueLen = nonceBytes.length - 2;
|
|
118
|
-
const headerBuf = (0,
|
|
119
|
-
|
|
314
|
+
const headerBuf = (0, core_2.concat)(nonceBytes, headerTail);
|
|
315
|
+
// ── CRITICAL: WASM and JS are mutually exclusive per chunk ──────────────
|
|
316
|
+
// When WASM returns null (no nonce in chunk), the chunk has been fully
|
|
317
|
+
// scanned by WASM — we do NOT re-scan it with JS. Doing so would double
|
|
318
|
+
// work and could incorrectly set source='wasm' on a JS-found nonce.
|
|
319
|
+
let found;
|
|
320
|
+
if (useWasm) {
|
|
321
|
+
found = await (0, mine_wasm_js_1.mineChunkWasm)(headerBuf.slice(), // WASM gets its own copy to avoid aliasing
|
|
322
|
+
2, nonceValueLen, txnDifficulty, nonce, chunkEnd - nonce);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
found = mineChunkJs(headerBuf, nonceValueLen, txnDifficulty, nonce, chunkEnd);
|
|
326
|
+
}
|
|
327
|
+
// ── End mutually exclusive section ───────────────────────────────────────
|
|
120
328
|
if (found !== null) {
|
|
121
|
-
const finalNonceBytes = (0,
|
|
122
|
-
const finalHeader = (0,
|
|
123
|
-
const txpowId = (0,
|
|
329
|
+
const finalNonceBytes = (0, core_2.writeMiniNumber)(found, 0);
|
|
330
|
+
const finalHeader = (0, core_2.concat)(finalNonceBytes, headerTail);
|
|
331
|
+
const txpowId = (0, core_1.sha3_256)(finalHeader);
|
|
124
332
|
return {
|
|
125
333
|
minedHeaderBytes: finalHeader,
|
|
126
334
|
txpowId,
|
|
127
335
|
nonce: found,
|
|
128
336
|
elapsedMs: Date.now() - startTime,
|
|
129
|
-
source: 'js',
|
|
337
|
+
source: useWasm ? 'wasm' : 'js',
|
|
130
338
|
};
|
|
131
339
|
}
|
|
132
340
|
totalIterations += chunkEnd - nonce;
|
|
@@ -141,3 +349,37 @@ async function mineTxPoW(txBodyBytes, txnDifficulty, options) {
|
|
|
141
349
|
});
|
|
142
350
|
}
|
|
143
351
|
}
|
|
352
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
353
|
+
// Public API
|
|
354
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
355
|
+
/**
|
|
356
|
+
* Mine a TxPoW locally by iterating the header nonce until
|
|
357
|
+
* SHA3-256( TxHeader ) < txnDifficulty
|
|
358
|
+
*
|
|
359
|
+
* In Node.js (main thread), delegates to a `worker_threads` Worker so the
|
|
360
|
+
* event loop is never blocked. In browsers or Jest, runs in-process with
|
|
361
|
+
* periodic `setImmediate`/`setTimeout(0)` yields between chunks.
|
|
362
|
+
*
|
|
363
|
+
* Uses the pre-compiled `miner.wasm` binary for inner-loop throughput when
|
|
364
|
+
* available. Falls back to pure JS automatically when the binary is absent.
|
|
365
|
+
*
|
|
366
|
+
* @param txBodyBytes Pre-serialized TxBody bytes (from serializeTxBody).
|
|
367
|
+
* @param txnDifficulty 32-byte target. MUST be ≤ TX_POW_MIN_DIFFICULTY.
|
|
368
|
+
* @param options Chunk size, max iterations, abort signal, timeMilli override.
|
|
369
|
+
*/
|
|
370
|
+
async function mineTxPoW(txBodyBytes, txnDifficulty, options) {
|
|
371
|
+
// ── Node.js: spawn a worker_threads Worker (keeps event loop unblocked) ──
|
|
372
|
+
if (shouldUseNodeWorker(options)) {
|
|
373
|
+
return mineTxPoWViaNodeWorker(txBodyBytes, txnDifficulty, options);
|
|
374
|
+
}
|
|
375
|
+
// ── Browser: spawn a Web Worker when the URL has been configured ──────────
|
|
376
|
+
// The extension build (Task #114) calls setBrowserWorkerUrl() at startup.
|
|
377
|
+
// Without a configured URL, mine falls back to in-process (setImmediate loop).
|
|
378
|
+
if (!options?._skipWorker &&
|
|
379
|
+
typeof process === 'undefined' && // not Node.js
|
|
380
|
+
_browserWorkerUrl !== null) {
|
|
381
|
+
return mineTxPoWViaBrowserWorker(txBodyBytes, txnDifficulty, options);
|
|
382
|
+
}
|
|
383
|
+
// ── In-process fallback (browser without worker URL, or explicit skip) ────
|
|
384
|
+
return mineTxPoWInProcess(txBodyBytes, txnDifficulty, options);
|
|
385
|
+
}
|
package/dist/serialization.js
CHANGED
|
@@ -56,8 +56,8 @@ exports.serializeTxHeader = serializeTxHeader;
|
|
|
56
56
|
exports.computeTxPoWId = computeTxPoWId;
|
|
57
57
|
exports.serializeTxBody = serializeTxBody;
|
|
58
58
|
exports.serializeTxPoW = serializeTxPoW;
|
|
59
|
-
const sha3_js_1 = require("@noble/hashes/sha3.js");
|
|
60
59
|
const core_1 = require("@totemsdk/core");
|
|
60
|
+
const core_2 = require("@totemsdk/core");
|
|
61
61
|
const magic_js_1 = require("./magic.js");
|
|
62
62
|
const constants_js_1 = require("./constants.js");
|
|
63
63
|
/**
|
|
@@ -70,26 +70,26 @@ function serializeTxHeader(txBodyHash, options) {
|
|
|
70
70
|
const nonce = options?.nonce ?? 0n;
|
|
71
71
|
const timeMilli = options?.timeMilli ?? BigInt(Date.now());
|
|
72
72
|
const parts = [];
|
|
73
|
-
parts.push((0,
|
|
74
|
-
parts.push((0,
|
|
75
|
-
parts.push((0,
|
|
76
|
-
parts.push((0,
|
|
77
|
-
parts.push((0,
|
|
73
|
+
parts.push((0, core_2.writeMiniNumber)(nonce, 0));
|
|
74
|
+
parts.push((0, core_2.writeMiniData)(constants_js_1.MAIN_NET_CHAIN_ID));
|
|
75
|
+
parts.push((0, core_2.writeMiniNumber)(timeMilli, 0));
|
|
76
|
+
parts.push((0, core_2.writeMiniNumber)(0n, 0));
|
|
77
|
+
parts.push((0, core_2.writeMiniData)(constants_js_1.MAX_HASH));
|
|
78
78
|
parts.push(new Uint8Array([constants_js_1.CASCADE_LEVELS]));
|
|
79
|
-
parts.push((0,
|
|
80
|
-
parts.push((0,
|
|
81
|
-
parts.push((0,
|
|
79
|
+
parts.push((0, core_2.writeHashToStream)(constants_js_1.ZERO_HASH));
|
|
80
|
+
parts.push((0, core_2.writeHashToStream)(constants_js_1.ZERO_HASH));
|
|
81
|
+
parts.push((0, core_2.writeMiniNumber)(0n, 0));
|
|
82
82
|
parts.push((0, magic_js_1.serializeMagic)());
|
|
83
|
-
parts.push((0,
|
|
84
|
-
parts.push((0,
|
|
85
|
-
return (0,
|
|
83
|
+
parts.push((0, core_2.writeHashToStream)(constants_js_1.ZERO_HASH));
|
|
84
|
+
parts.push((0, core_2.writeHashToStream)(txBodyHash));
|
|
85
|
+
return (0, core_2.concat)(...parts);
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
88
|
* Compute TxPoW ID = SHA3-256(TxHeader bytes).
|
|
89
89
|
* Matches Java: Crypto.getInstance().hashObject(mHeader) via SHA3Digest(256).
|
|
90
90
|
*/
|
|
91
91
|
function computeTxPoWId(headerBytes) {
|
|
92
|
-
return (0,
|
|
92
|
+
return (0, core_1.sha3_256)(headerBytes);
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
95
|
* Build the empty burn transaction bytes.
|
|
@@ -103,7 +103,7 @@ function computeTxPoWId(headerBytes) {
|
|
|
103
103
|
* writeHashToStream([0x00]) linkHash = ZERO_TXPOWID (1 byte)
|
|
104
104
|
*/
|
|
105
105
|
function buildEmptyBurnTxBytes() {
|
|
106
|
-
return (0,
|
|
106
|
+
return (0, core_2.concat)((0, core_2.writeMiniNumber)(0n, 0), (0, core_2.writeMiniNumber)(0n, 0), (0, core_2.writeMiniNumber)(0n, 0), (0, core_2.writeHashToStream)(new Uint8Array([0x00])));
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
109
|
* Build the empty burn witness bytes.
|
|
@@ -114,7 +114,7 @@ function buildEmptyBurnTxBytes() {
|
|
|
114
114
|
* writeMiniNumber(0n) scriptproof count
|
|
115
115
|
*/
|
|
116
116
|
function buildEmptyBurnWitnessBytes() {
|
|
117
|
-
return (0,
|
|
117
|
+
return (0, core_2.concat)((0, core_2.writeMiniNumber)(0n, 0), (0, core_2.writeMiniNumber)(0n, 0), (0, core_2.writeMiniNumber)(0n, 0));
|
|
118
118
|
}
|
|
119
119
|
/**
|
|
120
120
|
* Serialize a TxBody per Minima's TxBody.writeDataStream().
|
|
@@ -130,13 +130,14 @@ function serializeTxBody(txBytes, witnessBytes, options) {
|
|
|
130
130
|
prng = options.prng;
|
|
131
131
|
}
|
|
132
132
|
else {
|
|
133
|
-
|
|
133
|
+
const fresh = new Uint8Array(32);
|
|
134
134
|
if (typeof crypto === 'undefined' || !crypto.getRandomValues) {
|
|
135
135
|
throw new Error('SECURITY: crypto.getRandomValues unavailable — cannot generate secure random bytes for transaction PRNG');
|
|
136
136
|
}
|
|
137
|
-
crypto.getRandomValues(
|
|
137
|
+
crypto.getRandomValues(fresh);
|
|
138
|
+
prng = fresh;
|
|
138
139
|
}
|
|
139
|
-
return (0,
|
|
140
|
+
return (0, core_2.concat)((0, core_2.writeHashToStream)(prng), (0, core_2.writeMiniData)(txnDifficulty), txBytes, witnessBytes, buildEmptyBurnTxBytes(), buildEmptyBurnWitnessBytes(), (0, core_2.writeMiniNumber)(0n, 0));
|
|
140
141
|
}
|
|
141
142
|
/**
|
|
142
143
|
* Serialize a complete TxPoW per Minima's TxPoW.writeDataStream().
|
|
@@ -154,6 +155,6 @@ function serializeTxBody(txBytes, witnessBytes, options) {
|
|
|
154
155
|
*/
|
|
155
156
|
function serializeTxPoW(txBytes, witnessBytes, options) {
|
|
156
157
|
const txBody = serializeTxBody(txBytes, witnessBytes, options);
|
|
157
|
-
const txBodyHash = (0,
|
|
158
|
-
return (0,
|
|
158
|
+
const txBodyHash = (0, core_1.sha3_256)(txBody);
|
|
159
|
+
return (0, core_2.concat)(serializeTxHeader(txBodyHash, options), new Uint8Array([0x01]), txBody);
|
|
159
160
|
}
|
package/dist/verify.d.ts
CHANGED
|
@@ -4,10 +4,30 @@
|
|
|
4
4
|
* verifyProofOfWork checks that a given txpowId beats the difficulty target
|
|
5
5
|
* without re-mining. Used by relay nodes to reject under-work submissions.
|
|
6
6
|
*
|
|
7
|
-
* verifyTxPoWWork
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
10
|
-
*
|
|
7
|
+
* verifyTxPoWWork receives raw TxPoW hex, parses the header/body split by
|
|
8
|
+
* verifying the embedded body-hash field, computes the canonical txpowId
|
|
9
|
+
* (SHA3-256 of the header only), extracts mTxnDifficulty from the TxBody,
|
|
10
|
+
* and checks txpowId < mTxnDifficulty. Falls back to the TX_POW_MIN_DIFFICULTY
|
|
11
|
+
* floor when parsing fails (malformed or non-standard TxPoW).
|
|
12
|
+
*
|
|
13
|
+
* verifyTxPoWParts is the low-level variant for callers who already hold
|
|
14
|
+
* pre-split header/body bytes.
|
|
15
|
+
*
|
|
16
|
+
* ── Wire format reference ─────────────────────────────────────────────────
|
|
17
|
+
* @totemsdk/core's writeHashToStream(hash) uses a 4-byte big-endian length
|
|
18
|
+
* prefix followed by the bytes: for a 32-byte hash → [0x00,0x00,0x00,0x20, ...32 bytes]
|
|
19
|
+
* = 36 bytes total. writeMiniData uses the same 4-byte prefix format.
|
|
20
|
+
*
|
|
21
|
+
* TxPoW wire format: TxHeader bytes | 0x01 (hasBody) | TxBody bytes
|
|
22
|
+
* TxPoW ID: SHA3-256(TxHeader bytes)
|
|
23
|
+
*
|
|
24
|
+
* TxHeader ends with:
|
|
25
|
+
* writeHashToStream(mCustomHash) — [0x00,0x00,0x00,0x20, ...32 zeros] 36 bytes
|
|
26
|
+
* writeHashToStream(mTxBodyHash) — [0x00,0x00,0x00,0x20, ...32 bytes] 36 bytes
|
|
27
|
+
*
|
|
28
|
+
* TxBody starts with:
|
|
29
|
+
* writeHashToStream(mPRNG) — [0x00,0x00,0x00,0x20, ...32 bytes] 36 bytes
|
|
30
|
+
* writeMiniData(mTxnDifficulty) — [0x00,0x00,0x00,len, ...len bytes] 4+len bytes
|
|
11
31
|
*/
|
|
12
32
|
export interface VerifyResult {
|
|
13
33
|
valid: boolean;
|
|
@@ -24,14 +44,33 @@ export interface VerifyResult {
|
|
|
24
44
|
* @param mTxnDifficulty The 32-byte difficulty target from the TxBody.
|
|
25
45
|
*/
|
|
26
46
|
export declare function verifyProofOfWork(txpowId: Uint8Array, mTxnDifficulty: Uint8Array): VerifyResult;
|
|
47
|
+
/**
|
|
48
|
+
* Verify a TxPoW from pre-split header and body bytes.
|
|
49
|
+
*
|
|
50
|
+
* Computes SHA3-256(headerBytes) as the txpowId, extracts mTxnDifficulty
|
|
51
|
+
* from the body, and checks txpowId < mTxnDifficulty. Falls back to
|
|
52
|
+
* TX_POW_MIN_DIFFICULTY when the body cannot be parsed.
|
|
53
|
+
*
|
|
54
|
+
* @param headerBytes Raw TxHeader bytes (SHA3-256 of these is the txpowId).
|
|
55
|
+
* @param bodyBytes Raw TxBody bytes (mTxnDifficulty extracted from here).
|
|
56
|
+
*/
|
|
57
|
+
export declare function verifyTxPoWParts(headerBytes: Uint8Array, bodyBytes: Uint8Array): VerifyResult;
|
|
27
58
|
/**
|
|
28
59
|
* Relay-side work verification from raw TxPoW hex.
|
|
29
60
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
61
|
+
* Parses the TxPoW hex into header and body by locating the hasBody byte via
|
|
62
|
+
* body-hash verification. Computes the canonical TxPoW ID (SHA3-256 of the
|
|
63
|
+
* header only), extracts mTxnDifficulty from the TxBody, and verifies:
|
|
64
|
+
* txpowId < mTxnDifficulty
|
|
65
|
+
*
|
|
66
|
+
* Falls back to TX_POW_MIN_DIFFICULTY as a spam filter when:
|
|
67
|
+
* - The hex is invalid or malformed
|
|
68
|
+
* - No valid header/body split can be found (non-standard structure)
|
|
69
|
+
* - mTxnDifficulty cannot be extracted from the body
|
|
70
|
+
*
|
|
71
|
+
* PureMinima performs authoritative work verification on submission; this
|
|
72
|
+
* function is a first-pass relay-side filter.
|
|
33
73
|
*
|
|
34
|
-
*
|
|
35
|
-
* parser. PureMinima performs authoritative work verification on submission.
|
|
74
|
+
* @param txpowHex Hex-encoded serialized TxPoW (TxHeader | 0x01 | TxBody).
|
|
36
75
|
*/
|
|
37
76
|
export declare function verifyTxPoWWork(txpowHex: string): VerifyResult;
|