@totemsdk/txpow 0.1.5 → 0.1.6
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/index.d.ts +3 -2
- package/dist/index.js +8 -1
- 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 +256 -14
- 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 +33 -0
- package/dist/workers/node.worker.js +47 -0
- package/package.json +6 -2
- package/src/wasm/miner.wasm +0 -0
package/README.md
CHANGED
|
@@ -69,6 +69,17 @@ const estimatedMs = estimateMiningCost(targetDifficulty, hashesPerSecond);
|
|
|
69
69
|
console.log(`Expected mining time: ${estimatedMs}ms`);
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
## Upstream Java source
|
|
73
|
+
|
|
74
|
+
This package is a TypeScript port of Minima's TxPoW envelope structures. Canonical upstream references:
|
|
75
|
+
|
|
76
|
+
- [`objects/TxPoW.java`](https://github.com/spartacusrex-minima/minima-core/blob/main/src/org/minima/objects/TxPoW.java) — full TxPoW envelope
|
|
77
|
+
- [`objects/TxBody.java`](https://github.com/spartacusrex-minima/minima-core/blob/main/src/org/minima/objects/TxBody.java) — body serialization
|
|
78
|
+
- [`objects/TxHeader.java`](https://github.com/spartacusrex-minima/minima-core/blob/main/src/org/minima/objects/TxHeader.java) — header serialization
|
|
79
|
+
- [`objects/Witness.java`](https://github.com/spartacusrex-minima/minima-core/blob/main/src/org/minima/objects/Witness.java) — witness/signature data
|
|
80
|
+
- [`objects/CoinProof.java`](https://github.com/spartacusrex-minima/minima-core/blob/main/src/org/minima/objects/CoinProof.java) — coin proof structure
|
|
81
|
+
- [`objects/ScriptProof.java`](https://github.com/spartacusrex-minima/minima-core/blob/main/src/org/minima/objects/ScriptProof.java) — script proof structure
|
|
82
|
+
|
|
72
83
|
## See also
|
|
73
84
|
|
|
74
85
|
- [`@totemsdk/tx-builder`](https://www.npmjs.com/package/@totemsdk/tx-builder) — construct `txBodyBytes` before mining
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
export { serializeTxHeader, serializeTxBody, serializeTxPoW, computeTxPoWId, type TxHeaderOptions, type TxBodyOptions, type TxPoWOptions, } from './serialization.js';
|
|
21
21
|
export { MAX_HASH, ZERO_HASH, TX_POW_MIN_DIFFICULTY, CASCADE_LEVELS, MAIN_NET_CHAIN_ID, } from './constants.js';
|
|
22
22
|
export { serializeMagic } from './magic.js';
|
|
23
|
-
export { mineTxPoW, isLessThan, type MineResult, type MineOptions, } from './mine.js';
|
|
23
|
+
export { mineTxPoW, mineTxPoWInProcess, isLessThan, setBrowserWorkerUrl, type MineResult, type MineOptions, } from './mine.js';
|
|
24
|
+
export { setWasmUrl, getBrowserWasmUrl, isWasmAvailable } from './mine-wasm.js';
|
|
24
25
|
export { fetchTxPowTarget, type TxPowParams } from './difficulty.js';
|
|
25
|
-
export { verifyProofOfWork, verifyTxPoWWork, type VerifyResult } from './verify.js';
|
|
26
|
+
export { verifyProofOfWork, verifyTxPoWWork, verifyTxPoWParts, type VerifyResult } from './verify.js';
|
|
26
27
|
export { calibrateHashRate, estimateMiningCost, type MiningEstimate, } from './calibrate.js';
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* if (!check.valid) drop(check.reason);
|
|
20
20
|
*/
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.estimateMiningCost = exports.calibrateHashRate = exports.verifyTxPoWWork = exports.verifyProofOfWork = exports.fetchTxPowTarget = exports.isLessThan = exports.mineTxPoW = exports.serializeMagic = exports.MAIN_NET_CHAIN_ID = exports.CASCADE_LEVELS = exports.TX_POW_MIN_DIFFICULTY = exports.ZERO_HASH = exports.MAX_HASH = exports.computeTxPoWId = exports.serializeTxPoW = exports.serializeTxBody = exports.serializeTxHeader = void 0;
|
|
22
|
+
exports.estimateMiningCost = exports.calibrateHashRate = exports.verifyTxPoWParts = exports.verifyTxPoWWork = exports.verifyProofOfWork = exports.fetchTxPowTarget = exports.isWasmAvailable = exports.getBrowserWasmUrl = exports.setWasmUrl = exports.setBrowserWorkerUrl = exports.isLessThan = exports.mineTxPoWInProcess = exports.mineTxPoW = exports.serializeMagic = exports.MAIN_NET_CHAIN_ID = exports.CASCADE_LEVELS = exports.TX_POW_MIN_DIFFICULTY = exports.ZERO_HASH = exports.MAX_HASH = exports.computeTxPoWId = exports.serializeTxPoW = exports.serializeTxBody = exports.serializeTxHeader = void 0;
|
|
23
23
|
var serialization_js_1 = require("./serialization.js");
|
|
24
24
|
Object.defineProperty(exports, "serializeTxHeader", { enumerable: true, get: function () { return serialization_js_1.serializeTxHeader; } });
|
|
25
25
|
Object.defineProperty(exports, "serializeTxBody", { enumerable: true, get: function () { return serialization_js_1.serializeTxBody; } });
|
|
@@ -35,12 +35,19 @@ var magic_js_1 = require("./magic.js");
|
|
|
35
35
|
Object.defineProperty(exports, "serializeMagic", { enumerable: true, get: function () { return magic_js_1.serializeMagic; } });
|
|
36
36
|
var mine_js_1 = require("./mine.js");
|
|
37
37
|
Object.defineProperty(exports, "mineTxPoW", { enumerable: true, get: function () { return mine_js_1.mineTxPoW; } });
|
|
38
|
+
Object.defineProperty(exports, "mineTxPoWInProcess", { enumerable: true, get: function () { return mine_js_1.mineTxPoWInProcess; } });
|
|
38
39
|
Object.defineProperty(exports, "isLessThan", { enumerable: true, get: function () { return mine_js_1.isLessThan; } });
|
|
40
|
+
Object.defineProperty(exports, "setBrowserWorkerUrl", { enumerable: true, get: function () { return mine_js_1.setBrowserWorkerUrl; } });
|
|
41
|
+
var mine_wasm_js_1 = require("./mine-wasm.js");
|
|
42
|
+
Object.defineProperty(exports, "setWasmUrl", { enumerable: true, get: function () { return mine_wasm_js_1.setWasmUrl; } });
|
|
43
|
+
Object.defineProperty(exports, "getBrowserWasmUrl", { enumerable: true, get: function () { return mine_wasm_js_1.getBrowserWasmUrl; } });
|
|
44
|
+
Object.defineProperty(exports, "isWasmAvailable", { enumerable: true, get: function () { return mine_wasm_js_1.isWasmAvailable; } });
|
|
39
45
|
var difficulty_js_1 = require("./difficulty.js");
|
|
40
46
|
Object.defineProperty(exports, "fetchTxPowTarget", { enumerable: true, get: function () { return difficulty_js_1.fetchTxPowTarget; } });
|
|
41
47
|
var verify_js_1 = require("./verify.js");
|
|
42
48
|
Object.defineProperty(exports, "verifyProofOfWork", { enumerable: true, get: function () { return verify_js_1.verifyProofOfWork; } });
|
|
43
49
|
Object.defineProperty(exports, "verifyTxPoWWork", { enumerable: true, get: function () { return verify_js_1.verifyTxPoWWork; } });
|
|
50
|
+
Object.defineProperty(exports, "verifyTxPoWParts", { enumerable: true, get: function () { return verify_js_1.verifyTxPoWParts; } });
|
|
44
51
|
var calibrate_js_1 = require("./calibrate.js");
|
|
45
52
|
Object.defineProperty(exports, "calibrateHashRate", { enumerable: true, get: function () { return calibrate_js_1.calibrateHashRate; } });
|
|
46
53
|
Object.defineProperty(exports, "estimateMiningCost", { enumerable: true, get: function () { return calibrate_js_1.estimateMiningCost; } });
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mine-wasm.ts — WASM mining loop loader and wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Loads the pre-compiled `miner.wasm` binary and provides `mineChunkWasm()`.
|
|
5
|
+
* Falls back gracefully when the binary is unavailable.
|
|
6
|
+
*
|
|
7
|
+
* ── Memory layout (must match miner.wat) ──
|
|
8
|
+
* [0 .. 31 ] — hash output buffer (host sha3_256 writes here)
|
|
9
|
+
* [32 .. 32+headerLen-1 ] — header buffer (set by caller before each call)
|
|
10
|
+
* [32+headerLen .. 32+headerLen+31] — target buffer (set once per mine)
|
|
11
|
+
*
|
|
12
|
+
* ── Runtime detection ──
|
|
13
|
+
* Node.js → reads dist/wasm/miner.wasm via fs.readFileSync
|
|
14
|
+
* Browser → fetches the URL supplied by setWasmUrl() (Task #114 supplies it)
|
|
15
|
+
*
|
|
16
|
+
* ── WASM binary compilation ──
|
|
17
|
+
* node scripts/compile-wasm.js (uses wabt npm package, no Emscripten needed)
|
|
18
|
+
* npm run build (postbuild copies miner.wasm → dist/wasm/)
|
|
19
|
+
*
|
|
20
|
+
* ── WASM module interface (miner.wat) ──
|
|
21
|
+
* import env.sha3_256(headerPtr, headerLen, outPtr) ← host provides via @noble/hashes
|
|
22
|
+
* export mine(headerPtr, headerLen, nonceOffset, nonceLen,
|
|
23
|
+
* targetPtr, startNonce, chunkSize) → i32
|
|
24
|
+
* export memory
|
|
25
|
+
*
|
|
26
|
+
* mine() returns the found nonce, or -1 (0xFFFFFFFF) if no nonce found in chunk.
|
|
27
|
+
*/
|
|
28
|
+
/** First byte of the header region (after the 32-byte hash output buffer). */
|
|
29
|
+
export declare const WASM_HEADER_OFFSET = 32;
|
|
30
|
+
/**
|
|
31
|
+
* Configure the URL from which the browser loads `miner.wasm`.
|
|
32
|
+
*
|
|
33
|
+
* Call this once at app startup in browser contexts. The extension build
|
|
34
|
+
* (Task #114) resolves this URL from the bundler's output.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* import { setWasmUrl } from '@totemsdk/txpow';
|
|
38
|
+
* setWasmUrl(browser.runtime.getURL('miner.wasm'));
|
|
39
|
+
*/
|
|
40
|
+
export declare function setWasmUrl(url: string): void;
|
|
41
|
+
/**
|
|
42
|
+
* Return the current browser WASM URL, or null if not yet configured.
|
|
43
|
+
* Used by the main thread to pass the URL into spawned browser Web Workers.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getBrowserWasmUrl(): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Returns true when the WASM binary is loaded and `mine` is ready to use.
|
|
48
|
+
* Cached after the first call; subsequent calls are synchronous (via memoised flag).
|
|
49
|
+
*/
|
|
50
|
+
export declare function isWasmAvailable(): Promise<boolean>;
|
|
51
|
+
/**
|
|
52
|
+
* Mine a chunk using the WASM binary.
|
|
53
|
+
*
|
|
54
|
+
* Copies `headerBuf` and `target` into WASM linear memory at the correct
|
|
55
|
+
* offsets, calls `mine()`, and returns the found nonce — or `null` if:
|
|
56
|
+
* - WASM binary is unavailable
|
|
57
|
+
* - No valid nonce was found within the specified chunk
|
|
58
|
+
*
|
|
59
|
+
* Grows WASM memory if needed (single-page default = 64 KB is ample for
|
|
60
|
+
* any realistic TxHeader, but growth is handled for safety).
|
|
61
|
+
*
|
|
62
|
+
* @param headerBuf Full TxHeader bytes for the current nonce encoding range.
|
|
63
|
+
* @param nonceOffset Byte offset of the nonce VALUE in the header (always 2).
|
|
64
|
+
* @param nonceLen Byte length of the nonce value (1 for 0–127, 2 for 128–32767, …).
|
|
65
|
+
* @param target 32-byte difficulty target (big-endian).
|
|
66
|
+
* @param startNonce First nonce value to try.
|
|
67
|
+
* @param chunkCount Number of consecutive nonce values to try.
|
|
68
|
+
*/
|
|
69
|
+
export declare function mineChunkWasm(headerBuf: Uint8Array, nonceOffset: number, nonceLen: number, target: Uint8Array, startNonce: bigint, chunkCount: bigint): Promise<bigint | null>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* mine-wasm.ts — WASM mining loop loader and wrapper.
|
|
4
|
+
*
|
|
5
|
+
* Loads the pre-compiled `miner.wasm` binary and provides `mineChunkWasm()`.
|
|
6
|
+
* Falls back gracefully when the binary is unavailable.
|
|
7
|
+
*
|
|
8
|
+
* ── Memory layout (must match miner.wat) ──
|
|
9
|
+
* [0 .. 31 ] — hash output buffer (host sha3_256 writes here)
|
|
10
|
+
* [32 .. 32+headerLen-1 ] — header buffer (set by caller before each call)
|
|
11
|
+
* [32+headerLen .. 32+headerLen+31] — target buffer (set once per mine)
|
|
12
|
+
*
|
|
13
|
+
* ── Runtime detection ──
|
|
14
|
+
* Node.js → reads dist/wasm/miner.wasm via fs.readFileSync
|
|
15
|
+
* Browser → fetches the URL supplied by setWasmUrl() (Task #114 supplies it)
|
|
16
|
+
*
|
|
17
|
+
* ── WASM binary compilation ──
|
|
18
|
+
* node scripts/compile-wasm.js (uses wabt npm package, no Emscripten needed)
|
|
19
|
+
* npm run build (postbuild copies miner.wasm → dist/wasm/)
|
|
20
|
+
*
|
|
21
|
+
* ── WASM module interface (miner.wat) ──
|
|
22
|
+
* import env.sha3_256(headerPtr, headerLen, outPtr) ← host provides via @noble/hashes
|
|
23
|
+
* export mine(headerPtr, headerLen, nonceOffset, nonceLen,
|
|
24
|
+
* targetPtr, startNonce, chunkSize) → i32
|
|
25
|
+
* export memory
|
|
26
|
+
*
|
|
27
|
+
* mine() returns the found nonce, or -1 (0xFFFFFFFF) if no nonce found in chunk.
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.WASM_HEADER_OFFSET = void 0;
|
|
31
|
+
exports.setWasmUrl = setWasmUrl;
|
|
32
|
+
exports.getBrowserWasmUrl = getBrowserWasmUrl;
|
|
33
|
+
exports.isWasmAvailable = isWasmAvailable;
|
|
34
|
+
exports.mineChunkWasm = mineChunkWasm;
|
|
35
|
+
const sha3_js_1 = require("@noble/hashes/sha3.js");
|
|
36
|
+
/** Byte offset in WASM linear memory where the hash output is written. */
|
|
37
|
+
const HASH_OUT_OFFSET = 0;
|
|
38
|
+
/** First byte of the header region (after the 32-byte hash output buffer). */
|
|
39
|
+
exports.WASM_HEADER_OFFSET = 32;
|
|
40
|
+
/**
|
|
41
|
+
* Sentinel returned by WASM mine() when no nonce is found in the chunk.
|
|
42
|
+
* The WAT uses `i32.const -1` (= 0xFFFFFFFF bit pattern).
|
|
43
|
+
* WebAssembly i32 return values are sign-extended to JavaScript Number,
|
|
44
|
+
* so 0xFFFFFFFF arrives as -1, NOT as 4294967295.
|
|
45
|
+
*/
|
|
46
|
+
const NOT_FOUND = -1;
|
|
47
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
// Browser WASM URL configuration
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
let _browserWasmUrl = null;
|
|
51
|
+
/**
|
|
52
|
+
* Configure the URL from which the browser loads `miner.wasm`.
|
|
53
|
+
*
|
|
54
|
+
* Call this once at app startup in browser contexts. The extension build
|
|
55
|
+
* (Task #114) resolves this URL from the bundler's output.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* import { setWasmUrl } from '@totemsdk/txpow';
|
|
59
|
+
* setWasmUrl(browser.runtime.getURL('miner.wasm'));
|
|
60
|
+
*/
|
|
61
|
+
function setWasmUrl(url) {
|
|
62
|
+
_browserWasmUrl = url;
|
|
63
|
+
_loadAttempted = false;
|
|
64
|
+
_instance = null;
|
|
65
|
+
_memory = null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Return the current browser WASM URL, or null if not yet configured.
|
|
69
|
+
* Used by the main thread to pass the URL into spawned browser Web Workers.
|
|
70
|
+
*/
|
|
71
|
+
function getBrowserWasmUrl() {
|
|
72
|
+
return _browserWasmUrl;
|
|
73
|
+
}
|
|
74
|
+
let _instance = null;
|
|
75
|
+
let _memory = null;
|
|
76
|
+
let _loadAttempted = false;
|
|
77
|
+
/** Build the WebAssembly import object, wiring sha3_256 from @noble/hashes. */
|
|
78
|
+
function buildImports() {
|
|
79
|
+
return {
|
|
80
|
+
env: {
|
|
81
|
+
/**
|
|
82
|
+
* sha3_256(headerPtr, headerLen, outPtr)
|
|
83
|
+
* Reads header bytes from WASM linear memory, computes SHA3-256,
|
|
84
|
+
* writes 32-byte digest back into WASM linear memory at outPtr.
|
|
85
|
+
*/
|
|
86
|
+
sha3_256(headerPtr, headerLen, outPtr) {
|
|
87
|
+
if (!_memory)
|
|
88
|
+
return;
|
|
89
|
+
const memView = new Uint8Array(_memory.buffer);
|
|
90
|
+
// Slice to get a clean copy unaffected by future mutations
|
|
91
|
+
const input = memView.slice(headerPtr, headerPtr + headerLen);
|
|
92
|
+
const hash = (0, sha3_js_1.sha3_256)(input);
|
|
93
|
+
memView.set(hash, outPtr);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Load the WASM binary once per process (idempotent).
|
|
100
|
+
* Returns the instantiated module, or null if unavailable.
|
|
101
|
+
*/
|
|
102
|
+
async function loadWasm() {
|
|
103
|
+
if (_loadAttempted)
|
|
104
|
+
return _instance;
|
|
105
|
+
_loadAttempted = true;
|
|
106
|
+
if (typeof WebAssembly === 'undefined')
|
|
107
|
+
return null;
|
|
108
|
+
try {
|
|
109
|
+
let wasmBytes;
|
|
110
|
+
if (typeof process !== 'undefined' && process.versions?.node) {
|
|
111
|
+
// ── Node.js path ──────────────────────────────────────────────────────
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
113
|
+
const fs = require('fs');
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
115
|
+
const path = require('path');
|
|
116
|
+
const wasmPath = path.join(__dirname, 'wasm', 'miner.wasm');
|
|
117
|
+
if (!fs.existsSync(wasmPath))
|
|
118
|
+
return null;
|
|
119
|
+
// Use new Uint8Array(nodeBuffer) — correct copy that respects
|
|
120
|
+
// Buffer byteOffset/byteLength, avoiding the shared pool pitfall
|
|
121
|
+
// of accessing .buffer directly on a pooled Node.js Buffer.
|
|
122
|
+
const nodeBuffer = fs.readFileSync(wasmPath);
|
|
123
|
+
wasmBytes = new Uint8Array(nodeBuffer);
|
|
124
|
+
}
|
|
125
|
+
else if (_browserWasmUrl !== null) {
|
|
126
|
+
// ── Browser path ──────────────────────────────────────────────────────
|
|
127
|
+
const resp = await globalThis.fetch(_browserWasmUrl);
|
|
128
|
+
if (!resp.ok)
|
|
129
|
+
return null;
|
|
130
|
+
const ab = await resp.arrayBuffer();
|
|
131
|
+
wasmBytes = new Uint8Array(ab);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// Browser context but no URL configured yet.
|
|
135
|
+
// Call setWasmUrl() to enable WASM in the browser (Task #114).
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
// Compile and instantiate with host imports
|
|
139
|
+
const buf = wasmBytes.buffer.slice(wasmBytes.byteOffset, wasmBytes.byteOffset + wasmBytes.byteLength);
|
|
140
|
+
const wasmModule = await WebAssembly.compile(buf);
|
|
141
|
+
const instance = await WebAssembly.instantiate(wasmModule, buildImports());
|
|
142
|
+
const exports = instance.exports;
|
|
143
|
+
if (typeof exports.mine !== 'function')
|
|
144
|
+
return null;
|
|
145
|
+
if (!(exports.memory instanceof WebAssembly.Memory))
|
|
146
|
+
return null;
|
|
147
|
+
_instance = instance;
|
|
148
|
+
_memory = exports.memory;
|
|
149
|
+
return _instance;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
156
|
+
// Public API
|
|
157
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
158
|
+
/**
|
|
159
|
+
* Returns true when the WASM binary is loaded and `mine` is ready to use.
|
|
160
|
+
* Cached after the first call; subsequent calls are synchronous (via memoised flag).
|
|
161
|
+
*/
|
|
162
|
+
async function isWasmAvailable() {
|
|
163
|
+
return (await loadWasm()) !== null;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Mine a chunk using the WASM binary.
|
|
167
|
+
*
|
|
168
|
+
* Copies `headerBuf` and `target` into WASM linear memory at the correct
|
|
169
|
+
* offsets, calls `mine()`, and returns the found nonce — or `null` if:
|
|
170
|
+
* - WASM binary is unavailable
|
|
171
|
+
* - No valid nonce was found within the specified chunk
|
|
172
|
+
*
|
|
173
|
+
* Grows WASM memory if needed (single-page default = 64 KB is ample for
|
|
174
|
+
* any realistic TxHeader, but growth is handled for safety).
|
|
175
|
+
*
|
|
176
|
+
* @param headerBuf Full TxHeader bytes for the current nonce encoding range.
|
|
177
|
+
* @param nonceOffset Byte offset of the nonce VALUE in the header (always 2).
|
|
178
|
+
* @param nonceLen Byte length of the nonce value (1 for 0–127, 2 for 128–32767, …).
|
|
179
|
+
* @param target 32-byte difficulty target (big-endian).
|
|
180
|
+
* @param startNonce First nonce value to try.
|
|
181
|
+
* @param chunkCount Number of consecutive nonce values to try.
|
|
182
|
+
*/
|
|
183
|
+
async function mineChunkWasm(headerBuf, nonceOffset, nonceLen, target, startNonce, chunkCount) {
|
|
184
|
+
const inst = await loadWasm();
|
|
185
|
+
if (!inst || !_memory)
|
|
186
|
+
return null;
|
|
187
|
+
// Renamed to wasmExports to avoid shadowing the CJS module-level `exports` object.
|
|
188
|
+
// If named `exports`, `exports.WASM_HEADER_OFFSET` would resolve to the WASM
|
|
189
|
+
// instance exports (which has no such property → undefined), making headerPtr=0.
|
|
190
|
+
const wasmExports = inst.exports;
|
|
191
|
+
// Memory layout: [0..31]=hash output, [32..32+headerLen-1]=header, [32+headerLen..32+headerLen+31]=target
|
|
192
|
+
const headerPtr = exports.WASM_HEADER_OFFSET;
|
|
193
|
+
const targetPtr = headerPtr + headerBuf.length;
|
|
194
|
+
const totalNeeded = targetPtr + 32;
|
|
195
|
+
// Grow memory if necessary
|
|
196
|
+
let memView = new Uint8Array(_memory.buffer);
|
|
197
|
+
if (memView.byteLength < totalNeeded) {
|
|
198
|
+
const pagesNeeded = Math.ceil((totalNeeded - memView.byteLength) / 65536) + 1;
|
|
199
|
+
try {
|
|
200
|
+
_memory.grow(pagesNeeded);
|
|
201
|
+
memView = new Uint8Array(_memory.buffer);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Write header and target into WASM memory
|
|
208
|
+
memView.set(headerBuf, headerPtr);
|
|
209
|
+
memView.set(target, targetPtr);
|
|
210
|
+
const result = wasmExports.mine(headerPtr, headerBuf.length, nonceOffset, nonceLen, targetPtr, Number(startNonce), Number(chunkCount));
|
|
211
|
+
// result === -1 → no nonce found (WASM i32 0xFFFFFFFF sign-extended to JS -1)
|
|
212
|
+
// result >= 0 → found nonce; use >>> 0 to treat as unsigned before BigInt
|
|
213
|
+
return result === NOT_FOUND ? null : BigInt(result >>> 0);
|
|
214
|
+
}
|
package/dist/mine.d.ts
CHANGED
|
@@ -12,7 +12,19 @@
|
|
|
12
12
|
* value bytes in place — no per-iteration allocation.
|
|
13
13
|
* 4. At each MiniNumber encoding boundary (nonce 128, 32768, 8388608, …) we
|
|
14
14
|
* re-allocate a new header buffer with the wider nonce field.
|
|
15
|
-
* 5. Between chunks we yield to the event loop so the
|
|
15
|
+
* 5. Between chunks we yield to the event loop so the calling thread is never blocked.
|
|
16
|
+
* 6. When the pre-compiled WASM binary is present (`dist/wasm/miner.wasm`), the
|
|
17
|
+
* inner loop runs in WASM for ~5-10x throughput. Falls back to pure JS otherwise.
|
|
18
|
+
* 7. In Node.js (main thread), mining runs inside a `worker_threads` Worker so the
|
|
19
|
+
* event loop is never blocked. In browser contexts the in-process loop is used
|
|
20
|
+
* until Task #114 wires up the Web Worker entry point.
|
|
21
|
+
*
|
|
22
|
+
* Control-flow invariant (IMPORTANT):
|
|
23
|
+
* WASM and JS are mutually exclusive per chunk — we never re-mine a chunk that
|
|
24
|
+
* WASM already scanned. When WASM returns null (nonce not found in chunk), we
|
|
25
|
+
* advance to the next chunk rather than falling back to JS for the same range.
|
|
26
|
+
* This means `source` is always accurate: 'wasm' when the binary was used for
|
|
27
|
+
* the winning chunk; 'js' when the JS path found the nonce.
|
|
16
28
|
*/
|
|
17
29
|
export interface MineResult {
|
|
18
30
|
/** Fully serialized TxHeader bytes with the winning nonce. */
|
|
@@ -21,7 +33,7 @@ export interface MineResult {
|
|
|
21
33
|
txpowId: Uint8Array;
|
|
22
34
|
nonce: bigint;
|
|
23
35
|
elapsedMs: number;
|
|
24
|
-
/** 'wasm' when the WASM binary
|
|
36
|
+
/** 'wasm' when the pre-compiled WASM binary found the nonce; 'js' otherwise. */
|
|
25
37
|
source: 'wasm' | 'js';
|
|
26
38
|
}
|
|
27
39
|
export interface MineOptions {
|
|
@@ -37,20 +49,59 @@ export interface MineOptions {
|
|
|
37
49
|
maxIterations?: number;
|
|
38
50
|
/** AbortSignal — rejects the Promise when aborted. */
|
|
39
51
|
signal?: AbortSignal;
|
|
52
|
+
/**
|
|
53
|
+
* Override the header timestamp (milliseconds since epoch).
|
|
54
|
+
* Defaults to Date.now(). Set a fixed value for deterministic testing.
|
|
55
|
+
*/
|
|
56
|
+
timeMilli?: bigint;
|
|
57
|
+
/**
|
|
58
|
+
* Force the pure-JS mining path even when `miner.wasm` is present.
|
|
59
|
+
* Useful for testing the JS fallback or comparing JS vs WASM performance.
|
|
60
|
+
*/
|
|
61
|
+
forceJs?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Internal: skip spawning a Node.js worker_threads Worker.
|
|
64
|
+
* Set by node.worker.ts to prevent recursive worker creation.
|
|
65
|
+
* Also forces in-process execution when set programmatically.
|
|
66
|
+
* @internal
|
|
67
|
+
*/
|
|
68
|
+
_skipWorker?: boolean;
|
|
40
69
|
}
|
|
41
70
|
/**
|
|
42
71
|
* Big-endian 256-bit comparison: true if a < b.
|
|
43
72
|
* Used to check: txpowId < mTxnDifficulty.
|
|
44
73
|
*/
|
|
45
74
|
export declare function isLessThan(a: Uint8Array, b: Uint8Array): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Configure the URL of the browser.worker.js bundle so that `mineTxPoW()`
|
|
77
|
+
* can spawn a Web Worker in browser contexts.
|
|
78
|
+
*
|
|
79
|
+
* Call this once at extension/dApp startup. The bundled worker file exposes
|
|
80
|
+
* the same message protocol as node.worker.ts.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* import { setBrowserWorkerUrl } from '@totemsdk/txpow';
|
|
84
|
+
* setBrowserWorkerUrl(browser.runtime.getURL('mine-worker.js'));
|
|
85
|
+
*/
|
|
86
|
+
export declare function setBrowserWorkerUrl(url: string): void;
|
|
87
|
+
/**
|
|
88
|
+
* Run the mining loop in the current thread/task.
|
|
89
|
+
* Called directly by the Node.js worker, and by the browser code path.
|
|
90
|
+
*/
|
|
91
|
+
export declare function mineTxPoWInProcess(txBodyBytes: Uint8Array, txnDifficulty: Uint8Array, options?: MineOptions): Promise<MineResult>;
|
|
46
92
|
/**
|
|
47
93
|
* Mine a TxPoW locally by iterating the header nonce until
|
|
48
94
|
* SHA3-256( TxHeader ) < txnDifficulty
|
|
49
95
|
*
|
|
96
|
+
* In Node.js (main thread), delegates to a `worker_threads` Worker so the
|
|
97
|
+
* event loop is never blocked. In browsers or Jest, runs in-process with
|
|
98
|
+
* periodic `setImmediate`/`setTimeout(0)` yields between chunks.
|
|
99
|
+
*
|
|
100
|
+
* Uses the pre-compiled `miner.wasm` binary for inner-loop throughput when
|
|
101
|
+
* available. Falls back to pure JS automatically when the binary is absent.
|
|
102
|
+
*
|
|
50
103
|
* @param txBodyBytes Pre-serialized TxBody bytes (from serializeTxBody).
|
|
51
104
|
* @param txnDifficulty 32-byte target. MUST be ≤ TX_POW_MIN_DIFFICULTY.
|
|
52
|
-
*
|
|
53
|
-
* at block level by TxPoWChecker.checkTxPoWSimple().
|
|
54
|
-
* @param options Chunk size, max iterations, abort signal.
|
|
105
|
+
* @param options Chunk size, max iterations, abort signal, timeMilli override.
|
|
55
106
|
*/
|
|
56
107
|
export declare function mineTxPoW(txBodyBytes: Uint8Array, txnDifficulty: Uint8Array, options?: MineOptions): Promise<MineResult>;
|
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
35
|
const sha3_js_1 = require("@noble/hashes/sha3.js");
|
|
22
36
|
const core_1 = 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
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -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;
|
|
@@ -80,27 +95,208 @@ function mineChunk(headerBuf, nonceValueLen, target, startNonce, endNonce) {
|
|
|
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;
|
|
295
|
+
const timeMilli = options?.timeMilli ?? BigInt(Date.now());
|
|
296
|
+
const forceJs = options?.forceJs ?? false;
|
|
101
297
|
const bodyHash = (0, sha3_js_1.sha3_256)(txBodyBytes);
|
|
102
|
-
const timeMilli = BigInt(Date.now());
|
|
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;
|
|
@@ -116,7 +312,19 @@ async function mineTxPoW(txBodyBytes, txnDifficulty, options) {
|
|
|
116
312
|
const nonceBytes = (0, core_1.writeMiniNumber)(nonce);
|
|
117
313
|
const nonceValueLen = nonceBytes.length - 2;
|
|
118
314
|
const headerBuf = (0, core_1.concat)(nonceBytes, headerTail);
|
|
119
|
-
|
|
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
329
|
const finalNonceBytes = (0, core_1.writeMiniNumber)(found);
|
|
122
330
|
const finalHeader = (0, core_1.concat)(finalNonceBytes, headerTail);
|
|
@@ -126,7 +334,7 @@ async function mineTxPoW(txBodyBytes, txnDifficulty, options) {
|
|
|
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/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;
|
package/dist/verify.js
CHANGED
|
@@ -5,20 +5,50 @@
|
|
|
5
5
|
* verifyProofOfWork checks that a given txpowId beats the difficulty target
|
|
6
6
|
* without re-mining. Used by relay nodes to reject under-work submissions.
|
|
7
7
|
*
|
|
8
|
-
* verifyTxPoWWork
|
|
9
|
-
*
|
|
10
|
-
* the
|
|
11
|
-
*
|
|
8
|
+
* verifyTxPoWWork receives raw TxPoW hex, parses the header/body split by
|
|
9
|
+
* verifying the embedded body-hash field, computes the canonical txpowId
|
|
10
|
+
* (SHA3-256 of the header only), extracts mTxnDifficulty from the TxBody,
|
|
11
|
+
* and checks txpowId < mTxnDifficulty. Falls back to the TX_POW_MIN_DIFFICULTY
|
|
12
|
+
* floor when parsing fails (malformed or non-standard TxPoW).
|
|
13
|
+
*
|
|
14
|
+
* verifyTxPoWParts is the low-level variant for callers who already hold
|
|
15
|
+
* pre-split header/body bytes.
|
|
16
|
+
*
|
|
17
|
+
* ── Wire format reference ─────────────────────────────────────────────────
|
|
18
|
+
* @totemsdk/core's writeHashToStream(hash) uses a 4-byte big-endian length
|
|
19
|
+
* prefix followed by the bytes: for a 32-byte hash → [0x00,0x00,0x00,0x20, ...32 bytes]
|
|
20
|
+
* = 36 bytes total. writeMiniData uses the same 4-byte prefix format.
|
|
21
|
+
*
|
|
22
|
+
* TxPoW wire format: TxHeader bytes | 0x01 (hasBody) | TxBody bytes
|
|
23
|
+
* TxPoW ID: SHA3-256(TxHeader bytes)
|
|
24
|
+
*
|
|
25
|
+
* TxHeader ends with:
|
|
26
|
+
* writeHashToStream(mCustomHash) — [0x00,0x00,0x00,0x20, ...32 zeros] 36 bytes
|
|
27
|
+
* writeHashToStream(mTxBodyHash) — [0x00,0x00,0x00,0x20, ...32 bytes] 36 bytes
|
|
28
|
+
*
|
|
29
|
+
* TxBody starts with:
|
|
30
|
+
* writeHashToStream(mPRNG) — [0x00,0x00,0x00,0x20, ...32 bytes] 36 bytes
|
|
31
|
+
* writeMiniData(mTxnDifficulty) — [0x00,0x00,0x00,len, ...len bytes] 4+len bytes
|
|
12
32
|
*/
|
|
13
33
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
34
|
exports.verifyProofOfWork = verifyProofOfWork;
|
|
35
|
+
exports.verifyTxPoWParts = verifyTxPoWParts;
|
|
15
36
|
exports.verifyTxPoWWork = verifyTxPoWWork;
|
|
37
|
+
const sha3_js_1 = require("@noble/hashes/sha3.js");
|
|
16
38
|
const mine_js_1 = require("./mine.js");
|
|
17
39
|
const serialization_js_1 = require("./serialization.js");
|
|
18
40
|
const constants_js_1 = require("./constants.js");
|
|
19
41
|
function toHex(bytes) {
|
|
20
42
|
return '0x' + Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
21
43
|
}
|
|
44
|
+
/** Read a big-endian 4-byte unsigned integer from bytes at offset. */
|
|
45
|
+
function readUint32BE(bytes, offset) {
|
|
46
|
+
return (((bytes[offset] << 24) |
|
|
47
|
+
(bytes[offset + 1] << 16) |
|
|
48
|
+
(bytes[offset + 2] << 8) |
|
|
49
|
+
bytes[offset + 3]) >>>
|
|
50
|
+
0);
|
|
51
|
+
}
|
|
22
52
|
/**
|
|
23
53
|
* Verify that a TxPoW ID beats the stated difficulty target.
|
|
24
54
|
*
|
|
@@ -52,15 +82,123 @@ function verifyProofOfWork(txpowId, mTxnDifficulty) {
|
|
|
52
82
|
reason: valid ? undefined : 'txpowId ≥ mTxnDifficulty',
|
|
53
83
|
};
|
|
54
84
|
}
|
|
85
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
86
|
+
// TxPoW header/body parser
|
|
87
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
88
|
+
/**
|
|
89
|
+
* Attempt to split raw TxPoW bytes into (headerBytes, bodyBytes).
|
|
90
|
+
*
|
|
91
|
+
* TxPoW wire format: TxHeader | 0x01 (hasBody byte) | TxBody
|
|
92
|
+
* TxPoW ID: SHA3-256(TxHeader)
|
|
93
|
+
*
|
|
94
|
+
* The header always ends with `writeHashToStream(sha3_256(TxBody))`.
|
|
95
|
+
* @totemsdk/core writeHashToStream uses a 4-byte big-endian length prefix:
|
|
96
|
+
* [0x00, 0x00, 0x00, 0x20, ...32 body-hash bytes] = 36 bytes
|
|
97
|
+
*
|
|
98
|
+
* We scan forward looking for a 0x01 hasBody byte where:
|
|
99
|
+
* - The preceding 36 bytes are [0,0,0,32] + 32-byte bodyHash
|
|
100
|
+
* - sha3_256(bodyBytes) == those 32 bytes
|
|
101
|
+
* When found the split is authenticated by the hash.
|
|
102
|
+
*
|
|
103
|
+
* Returns null when no valid split is found.
|
|
104
|
+
*/
|
|
105
|
+
function tryParseTxPoW(bytes) {
|
|
106
|
+
// writeHashToStream(32-byte hash) = 36 bytes
|
|
107
|
+
const HASH_FIELD_LEN = 36;
|
|
108
|
+
// Minimum body: PRNG (36 bytes) + mTxnDifficulty prefix+data (4+1 minimum)
|
|
109
|
+
const MIN_BODY = 41;
|
|
110
|
+
// Minimum header must end with txBodyHash field (36 bytes)
|
|
111
|
+
const MIN_HEADER = HASH_FIELD_LEN;
|
|
112
|
+
const MIN_TOTAL = MIN_HEADER + 1 + MIN_BODY;
|
|
113
|
+
if (bytes.length < MIN_TOTAL)
|
|
114
|
+
return null;
|
|
115
|
+
for (let i = MIN_HEADER; i <= bytes.length - MIN_BODY - 1; i++) {
|
|
116
|
+
if (bytes[i] !== 0x01)
|
|
117
|
+
continue;
|
|
118
|
+
const headerBytes = bytes.slice(0, i);
|
|
119
|
+
if (headerBytes.length < HASH_FIELD_LEN)
|
|
120
|
+
continue;
|
|
121
|
+
// Last 36 bytes of header = writeHashToStream(bodyHash)
|
|
122
|
+
// = [0x00, 0x00, 0x00, 0x20, ...32 bytes]
|
|
123
|
+
const hashFieldStart = headerBytes.length - HASH_FIELD_LEN;
|
|
124
|
+
if (headerBytes[hashFieldStart] !== 0x00 ||
|
|
125
|
+
headerBytes[hashFieldStart + 1] !== 0x00 ||
|
|
126
|
+
headerBytes[hashFieldStart + 2] !== 0x00 ||
|
|
127
|
+
headerBytes[hashFieldStart + 3] !== 0x20) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const embeddedBodyHash = headerBytes.slice(hashFieldStart + 4);
|
|
131
|
+
const bodyBytes = bytes.slice(i + 1);
|
|
132
|
+
const actualBodyHash = (0, sha3_js_1.sha3_256)(bodyBytes);
|
|
133
|
+
if (actualBodyHash.every((b, j) => b === embeddedBodyHash[j])) {
|
|
134
|
+
return { headerBytes, bodyBytes };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Extract mTxnDifficulty from TxBody bytes.
|
|
141
|
+
*
|
|
142
|
+
* TxBody wire format (using @totemsdk/core serialization):
|
|
143
|
+
* [0..3] [0x00,0x00,0x00,0x20] 4-byte big-endian length (32) for PRNG
|
|
144
|
+
* [4..35] prng bytes 32-byte random nonce
|
|
145
|
+
* [36..39] [0x00,0x00,0x00,len] 4-byte big-endian length for mTxnDifficulty
|
|
146
|
+
* [40..40+len-1] difficulty bytes len bytes (standard: 32)
|
|
147
|
+
*
|
|
148
|
+
* Returns null when the body is too short or length prefixes are invalid.
|
|
149
|
+
*/
|
|
150
|
+
function extractTxnDifficulty(bodyBytes) {
|
|
151
|
+
// PRNG field: 4-byte length prefix + 32 bytes = 36 bytes total
|
|
152
|
+
if (bodyBytes.length < 40)
|
|
153
|
+
return null;
|
|
154
|
+
// Length prefix for PRNG must be 0x00000020 (= 32)
|
|
155
|
+
if (bodyBytes[0] !== 0x00 ||
|
|
156
|
+
bodyBytes[1] !== 0x00 ||
|
|
157
|
+
bodyBytes[2] !== 0x00 ||
|
|
158
|
+
bodyBytes[3] !== 0x20) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
// After PRNG (36 bytes), the next 4 bytes are the length prefix for mTxnDifficulty
|
|
162
|
+
const diffLen = readUint32BE(bodyBytes, 36);
|
|
163
|
+
if (diffLen === 0 || 40 + diffLen > bodyBytes.length)
|
|
164
|
+
return null;
|
|
165
|
+
return bodyBytes.slice(40, 40 + diffLen);
|
|
166
|
+
}
|
|
167
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
168
|
+
// Public verification API
|
|
169
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
170
|
+
/**
|
|
171
|
+
* Verify a TxPoW from pre-split header and body bytes.
|
|
172
|
+
*
|
|
173
|
+
* Computes SHA3-256(headerBytes) as the txpowId, extracts mTxnDifficulty
|
|
174
|
+
* from the body, and checks txpowId < mTxnDifficulty. Falls back to
|
|
175
|
+
* TX_POW_MIN_DIFFICULTY when the body cannot be parsed.
|
|
176
|
+
*
|
|
177
|
+
* @param headerBytes Raw TxHeader bytes (SHA3-256 of these is the txpowId).
|
|
178
|
+
* @param bodyBytes Raw TxBody bytes (mTxnDifficulty extracted from here).
|
|
179
|
+
*/
|
|
180
|
+
function verifyTxPoWParts(headerBytes, bodyBytes) {
|
|
181
|
+
const txpowId = (0, serialization_js_1.computeTxPoWId)(headerBytes);
|
|
182
|
+
const difficulty = extractTxnDifficulty(bodyBytes) ?? constants_js_1.TX_POW_MIN_DIFFICULTY;
|
|
183
|
+
return verifyProofOfWork(txpowId, difficulty);
|
|
184
|
+
}
|
|
55
185
|
/**
|
|
56
186
|
* Relay-side work verification from raw TxPoW hex.
|
|
57
187
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
188
|
+
* Parses the TxPoW hex into header and body by locating the hasBody byte via
|
|
189
|
+
* body-hash verification. Computes the canonical TxPoW ID (SHA3-256 of the
|
|
190
|
+
* header only), extracts mTxnDifficulty from the TxBody, and verifies:
|
|
191
|
+
* txpowId < mTxnDifficulty
|
|
192
|
+
*
|
|
193
|
+
* Falls back to TX_POW_MIN_DIFFICULTY as a spam filter when:
|
|
194
|
+
* - The hex is invalid or malformed
|
|
195
|
+
* - No valid header/body split can be found (non-standard structure)
|
|
196
|
+
* - mTxnDifficulty cannot be extracted from the body
|
|
197
|
+
*
|
|
198
|
+
* PureMinima performs authoritative work verification on submission; this
|
|
199
|
+
* function is a first-pass relay-side filter.
|
|
61
200
|
*
|
|
62
|
-
*
|
|
63
|
-
* parser. PureMinima performs authoritative work verification on submission.
|
|
201
|
+
* @param txpowHex Hex-encoded serialized TxPoW (TxHeader | 0x01 | TxBody).
|
|
64
202
|
*/
|
|
65
203
|
function verifyTxPoWWork(txpowHex) {
|
|
66
204
|
if (typeof txpowHex !== 'string' || !/^[0-9a-fA-F]+$/.test(txpowHex)) {
|
|
@@ -80,6 +218,15 @@ function verifyTxPoWWork(txpowHex) {
|
|
|
80
218
|
};
|
|
81
219
|
}
|
|
82
220
|
const bytes = Uint8Array.from(Buffer.from(txpowHex, 'hex'));
|
|
221
|
+
// Attempt to parse the TxPoW into header + body using body-hash verification
|
|
222
|
+
const parsed = tryParseTxPoW(bytes);
|
|
223
|
+
if (parsed) {
|
|
224
|
+
return verifyTxPoWParts(parsed.headerBytes, parsed.bodyBytes);
|
|
225
|
+
}
|
|
226
|
+
// Fallback: use sha3_256 of the full bytes as a proxy id against the floor
|
|
83
227
|
const proxyId = (0, serialization_js_1.computeTxPoWId)(bytes);
|
|
84
|
-
return
|
|
228
|
+
return {
|
|
229
|
+
...verifyProofOfWork(proxyId, constants_js_1.TX_POW_MIN_DIFFICULTY),
|
|
230
|
+
reason: 'could not parse TxPoW structure — used proxy id against floor difficulty',
|
|
231
|
+
};
|
|
85
232
|
}
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser.worker.ts — Browser Web Worker entry point for TxPoW mining.
|
|
3
|
+
*
|
|
4
|
+
* This file is the Web Worker counterpart to node.worker.ts.
|
|
5
|
+
* It is designed to be bundled by the extension's build system (webpack / vite)
|
|
6
|
+
* as part of Task #114. It cannot be loaded directly from a CJS context.
|
|
7
|
+
*
|
|
8
|
+
* Usage (from the extension):
|
|
9
|
+
* import MinerWorker from '@totemsdk/txpow/dist/workers/browser.worker.js?worker';
|
|
10
|
+
* const worker = new MinerWorker();
|
|
11
|
+
* worker.postMessage({ txBodyBytes, txnDifficulty, wasmUrl, options });
|
|
12
|
+
* worker.onmessage = (e) => { const result: MineResult = e.data; };
|
|
13
|
+
*
|
|
14
|
+
* Communication protocol (mirrors node.worker.ts):
|
|
15
|
+
* Input (onmessage e.data): BrowserWorkerInput — serialized mining parameters
|
|
16
|
+
* Output (postMessage): BrowserWorkerOutput — serialized MineResult or error
|
|
17
|
+
*
|
|
18
|
+
* WASM in the worker:
|
|
19
|
+
* The browser Web Worker runs in its own JS context, so `setWasmUrl()` called
|
|
20
|
+
* on the main thread does NOT carry over. Pass the WASM URL in the input
|
|
21
|
+
* message (`wasmUrl` field) so the worker can call `setWasmUrl()` before
|
|
22
|
+
* starting the mining loop. Without `wasmUrl`, the worker mines in pure JS.
|
|
23
|
+
*
|
|
24
|
+
* See mine.ts for details on the BigInt string encoding and Uint8Array [] encoding.
|
|
25
|
+
*/
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* browser.worker.ts — Browser Web Worker entry point for TxPoW mining.
|
|
4
|
+
*
|
|
5
|
+
* This file is the Web Worker counterpart to node.worker.ts.
|
|
6
|
+
* It is designed to be bundled by the extension's build system (webpack / vite)
|
|
7
|
+
* as part of Task #114. It cannot be loaded directly from a CJS context.
|
|
8
|
+
*
|
|
9
|
+
* Usage (from the extension):
|
|
10
|
+
* import MinerWorker from '@totemsdk/txpow/dist/workers/browser.worker.js?worker';
|
|
11
|
+
* const worker = new MinerWorker();
|
|
12
|
+
* worker.postMessage({ txBodyBytes, txnDifficulty, wasmUrl, options });
|
|
13
|
+
* worker.onmessage = (e) => { const result: MineResult = e.data; };
|
|
14
|
+
*
|
|
15
|
+
* Communication protocol (mirrors node.worker.ts):
|
|
16
|
+
* Input (onmessage e.data): BrowserWorkerInput — serialized mining parameters
|
|
17
|
+
* Output (postMessage): BrowserWorkerOutput — serialized MineResult or error
|
|
18
|
+
*
|
|
19
|
+
* WASM in the worker:
|
|
20
|
+
* The browser Web Worker runs in its own JS context, so `setWasmUrl()` called
|
|
21
|
+
* on the main thread does NOT carry over. Pass the WASM URL in the input
|
|
22
|
+
* message (`wasmUrl` field) so the worker can call `setWasmUrl()` before
|
|
23
|
+
* starting the mining loop. Without `wasmUrl`, the worker mines in pure JS.
|
|
24
|
+
*
|
|
25
|
+
* See mine.ts for details on the BigInt string encoding and Uint8Array [] encoding.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
/// <reference lib="webworker" />
|
|
29
|
+
const mine_js_1 = require("../mine.js");
|
|
30
|
+
const mine_wasm_js_1 = require("../mine-wasm.js");
|
|
31
|
+
self.onmessage = async (e) => {
|
|
32
|
+
const input = e.data;
|
|
33
|
+
// Configure WASM inside this worker's JS context before mining starts.
|
|
34
|
+
// The main thread's setWasmUrl() call does not carry over to a Web Worker.
|
|
35
|
+
if (input.wasmUrl) {
|
|
36
|
+
(0, mine_wasm_js_1.setWasmUrl)(input.wasmUrl);
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const result = await (0, mine_js_1.mineTxPoWInProcess)(new Uint8Array(input.txBodyBytes), new Uint8Array(input.txnDifficulty), {
|
|
40
|
+
chunkSize: input.chunkSize,
|
|
41
|
+
maxIterations: input.maxIterations !== undefined
|
|
42
|
+
? Number(input.maxIterations)
|
|
43
|
+
: undefined,
|
|
44
|
+
timeMilli: input.timeMilli !== undefined ? BigInt(input.timeMilli) : undefined,
|
|
45
|
+
forceJs: input.forceJs,
|
|
46
|
+
_skipWorker: true,
|
|
47
|
+
});
|
|
48
|
+
const output = {
|
|
49
|
+
minedHeaderBytes: Array.from(result.minedHeaderBytes),
|
|
50
|
+
txpowId: Array.from(result.txpowId),
|
|
51
|
+
nonce: result.nonce.toString(),
|
|
52
|
+
elapsedMs: result.elapsedMs,
|
|
53
|
+
source: result.source,
|
|
54
|
+
};
|
|
55
|
+
self.postMessage(output);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const output = {
|
|
59
|
+
error: err instanceof Error ? err.message : String(err),
|
|
60
|
+
};
|
|
61
|
+
self.postMessage(output);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node.worker.ts — Node.js worker_threads entry point for TxPoW mining.
|
|
3
|
+
*
|
|
4
|
+
* This file is compiled to `dist/workers/node.worker.js` and spawned by
|
|
5
|
+
* `mineTxPoW()` in the main thread via `new Worker(workerPath, { workerData })`.
|
|
6
|
+
*
|
|
7
|
+
* Communication protocol:
|
|
8
|
+
* Input (workerData): NodeWorkerInput — serialized mining parameters
|
|
9
|
+
* Output (postMessage): NodeWorkerOutput — serialized MineResult or error
|
|
10
|
+
*
|
|
11
|
+
* BigInt values (nonce, timeMilli, maxIterations) are transmitted as strings
|
|
12
|
+
* to avoid structured-clone limitations on older Node.js versions.
|
|
13
|
+
* Uint8Arrays are transmitted as number[] for reliable structured-clone transfer.
|
|
14
|
+
*/
|
|
15
|
+
declare const workerData: any, parentPort: import("worker_threads").MessagePort | null;
|
|
16
|
+
declare const mineTxPoWInProcess: typeof import("../mine").mineTxPoWInProcess;
|
|
17
|
+
interface NodeWorkerInput {
|
|
18
|
+
txBodyBytes: number[];
|
|
19
|
+
txnDifficulty: number[];
|
|
20
|
+
chunkSize?: number;
|
|
21
|
+
maxIterations?: string;
|
|
22
|
+
timeMilli?: string;
|
|
23
|
+
forceJs?: boolean;
|
|
24
|
+
}
|
|
25
|
+
interface NodeWorkerOutput {
|
|
26
|
+
minedHeaderBytes?: number[];
|
|
27
|
+
txpowId?: number[];
|
|
28
|
+
nonce?: string;
|
|
29
|
+
elapsedMs?: number;
|
|
30
|
+
source?: 'wasm' | 'js';
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
declare function run(): Promise<void>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* node.worker.ts — Node.js worker_threads entry point for TxPoW mining.
|
|
4
|
+
*
|
|
5
|
+
* This file is compiled to `dist/workers/node.worker.js` and spawned by
|
|
6
|
+
* `mineTxPoW()` in the main thread via `new Worker(workerPath, { workerData })`.
|
|
7
|
+
*
|
|
8
|
+
* Communication protocol:
|
|
9
|
+
* Input (workerData): NodeWorkerInput — serialized mining parameters
|
|
10
|
+
* Output (postMessage): NodeWorkerOutput — serialized MineResult or error
|
|
11
|
+
*
|
|
12
|
+
* BigInt values (nonce, timeMilli, maxIterations) are transmitted as strings
|
|
13
|
+
* to avoid structured-clone limitations on older Node.js versions.
|
|
14
|
+
* Uint8Arrays are transmitted as number[] for reliable structured-clone transfer.
|
|
15
|
+
*/
|
|
16
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
17
|
+
const { workerData, parentPort } = require('worker_threads');
|
|
18
|
+
const { mineTxPoWInProcess } = require('../mine');
|
|
19
|
+
async function run() {
|
|
20
|
+
const input = workerData;
|
|
21
|
+
try {
|
|
22
|
+
const result = await mineTxPoWInProcess(new Uint8Array(input.txBodyBytes), new Uint8Array(input.txnDifficulty), {
|
|
23
|
+
chunkSize: input.chunkSize,
|
|
24
|
+
maxIterations: input.maxIterations !== undefined
|
|
25
|
+
? Number(input.maxIterations)
|
|
26
|
+
: undefined,
|
|
27
|
+
timeMilli: input.timeMilli !== undefined ? BigInt(input.timeMilli) : undefined,
|
|
28
|
+
forceJs: input.forceJs,
|
|
29
|
+
_skipWorker: true,
|
|
30
|
+
});
|
|
31
|
+
const output = {
|
|
32
|
+
minedHeaderBytes: Array.from(result.minedHeaderBytes),
|
|
33
|
+
txpowId: Array.from(result.txpowId),
|
|
34
|
+
nonce: result.nonce.toString(),
|
|
35
|
+
elapsedMs: result.elapsedMs,
|
|
36
|
+
source: result.source,
|
|
37
|
+
};
|
|
38
|
+
parentPort?.postMessage(output);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
const output = {
|
|
42
|
+
error: err instanceof Error ? err.message : String(err),
|
|
43
|
+
};
|
|
44
|
+
parentPort?.postMessage(output);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
run();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totemsdk/txpow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "TxPoW envelope serialization and proof-of-work mining for the Minima protocol",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
16
|
+
"src/wasm/miner.wasm",
|
|
16
17
|
"README.md",
|
|
17
18
|
"LICENSE"
|
|
18
19
|
],
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@totemsdk/core": "^1.0
|
|
21
|
+
"@totemsdk/core": "^1.1.0"
|
|
21
22
|
},
|
|
22
23
|
"peerDependencies": {
|
|
23
24
|
"@noble/hashes": "^2.0.0"
|
|
@@ -64,6 +65,9 @@
|
|
|
64
65
|
],
|
|
65
66
|
"scripts": {
|
|
66
67
|
"build": "tsc",
|
|
68
|
+
"postbuild": "node -e \"const fs=require('fs'),p=require('path');const src=p.join(__dirname,'src','wasm','miner.wasm');const dst=p.join(__dirname,'dist','wasm','miner.wasm');if(fs.existsSync(src)){fs.mkdirSync(p.dirname(dst),{recursive:true});fs.copyFileSync(src,dst);console.log('[@totemsdk/txpow] miner.wasm → dist/wasm/')}else{console.log('[@totemsdk/txpow] Note: miner.wasm not compiled — JS fallback only. Run npm run build:wasm with Emscripten to enable WASM acceleration.')}\"",
|
|
69
|
+
"compile:wasm": "node scripts/compile-wasm.js",
|
|
70
|
+
"build:wasm": "npm run compile:wasm && npm run build",
|
|
67
71
|
"clean": "rm -rf dist",
|
|
68
72
|
"test": "jest --passWithNoTests"
|
|
69
73
|
}
|
|
Binary file
|