@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/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 is a convenience wrapper for relay nodes that receive raw
9
- * TxPoW hex: it hashes the full bytes as a proxy txpowId and checks against
10
- * the network minimum difficulty. PureMinima performs authoritative validation
11
- * on submission; this is a first-pass spam filter.
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 core_1 = require("@totemsdk/core");
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, core_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
- * Validates hex format, minimum length (spam filter), then computes
59
- * SHA3-256 of the serialized bytes as a proxy txpowId and checks it
60
- * against TX_POW_MIN_DIFFICULTY.
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
- * Note: full structural parsing of TxPoW headers is deferred to a future
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 verifyProofOfWork(proxyId, constants_js_1.TX_POW_MIN_DIFFICULTY);
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,15 @@
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
+ export {};
@@ -0,0 +1,48 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ /* eslint-disable @typescript-eslint/no-var-requires */
18
+ const { workerData, parentPort } = require('worker_threads');
19
+ const { mineTxPoWInProcess } = require('../mine');
20
+ async function run() {
21
+ const input = workerData;
22
+ try {
23
+ const result = await mineTxPoWInProcess(new Uint8Array(input.txBodyBytes), new Uint8Array(input.txnDifficulty), {
24
+ chunkSize: input.chunkSize,
25
+ maxIterations: input.maxIterations !== undefined
26
+ ? Number(input.maxIterations)
27
+ : undefined,
28
+ timeMilli: input.timeMilli !== undefined ? BigInt(input.timeMilli) : undefined,
29
+ forceJs: input.forceJs,
30
+ _skipWorker: true,
31
+ });
32
+ const output = {
33
+ minedHeaderBytes: Array.from(result.minedHeaderBytes),
34
+ txpowId: Array.from(result.txpowId),
35
+ nonce: result.nonce.toString(),
36
+ elapsedMs: result.elapsedMs,
37
+ source: result.source,
38
+ };
39
+ parentPort?.postMessage(output);
40
+ }
41
+ catch (err) {
42
+ const output = {
43
+ error: err instanceof Error ? err.message : String(err),
44
+ };
45
+ parentPort?.postMessage(output);
46
+ }
47
+ }
48
+ run();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totemsdk/txpow",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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",
@@ -17,23 +17,14 @@
17
17
  "LICENSE"
18
18
  ],
19
19
  "dependencies": {
20
- "@totemsdk/core": "^1.0.9"
21
- },
22
- "peerDependencies": {
23
- "@noble/hashes": "^2.0.0"
24
- },
25
- "peerDependenciesMeta": {
26
- "@noble/hashes": {
27
- "optional": false
28
- }
20
+ "@totemsdk/core": "^1.2.8"
29
21
  },
30
22
  "devDependencies": {
31
- "@noble/hashes": "^2.2.0",
32
- "@types/jest": "^29.0.0",
23
+ "@types/jest": "^30.0.0",
33
24
  "@types/node": "^20.0.0",
34
- "jest": "^29.0.0",
25
+ "jest": "^30.4.2",
35
26
  "ts-jest": "^29.0.0",
36
- "typescript": "^5.0.0"
27
+ "typescript": "^7.0.2"
37
28
  },
38
29
  "engines": {
39
30
  "node": ">=16.0.0"
@@ -43,14 +34,14 @@
43
34
  },
44
35
  "license": "MIT",
45
36
  "author": "Totem SDK",
46
- "homepage": "https://totemsdk.com",
37
+ "homepage": "https://totem.ing",
47
38
  "bugs": {
48
- "url": "https://github.com/MrGheek/axia-totem/issues"
39
+ "url": "https://github.com/totem-sdk/totem-sdk/issues"
49
40
  },
50
41
  "repository": {
51
42
  "type": "git",
52
- "url": "git+https://github.com/MrGheek/axia-totem.git",
53
- "directory": "packages/totem-sdk/packages/txpow"
43
+ "url": "git+https://github.com/totem-sdk/totem-sdk.git",
44
+ "directory": "packages/txpow"
54
45
  },
55
46
  "keywords": [
56
47
  "totem",
@@ -59,12 +50,14 @@
59
50
  "blockchain",
60
51
  "quantum-resistant",
61
52
  "wots",
62
- "kissvm",
63
53
  "utxo"
64
54
  ],
65
55
  "scripts": {
66
- "build": "tsc",
56
+ "build": "npm run build:wasm && tsc",
57
+ "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.')}\"",
58
+ "compile:wasm": "node scripts/compile-wasm.js",
59
+ "build:wasm": "npm run compile:wasm",
67
60
  "clean": "rm -rf dist",
68
- "test": "jest --passWithNoTests"
61
+ "test": "jest"
69
62
  }
70
63
  }