@totemsdk/txpow 0.1.0
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/calibrate.d.ts +35 -0
- package/dist/calibrate.js +64 -0
- package/dist/constants.d.ts +27 -0
- package/dist/constants.js +44 -0
- package/dist/difficulty.d.ts +20 -0
- package/dist/difficulty.js +55 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +26 -0
- package/dist/magic.d.ts +19 -0
- package/dist/magic.js +32 -0
- package/dist/mine.d.ts +56 -0
- package/dist/mine.js +139 -0
- package/dist/serialization.d.ts +106 -0
- package/dist/serialization.js +153 -0
- package/dist/verify.d.ts +21 -0
- package/dist/verify.js +43 -0
- package/package.json +49 -0
- package/src/__tests__/serialization.test.ts +347 -0
- package/src/calibrate.ts +86 -0
- package/src/constants.ts +49 -0
- package/src/difficulty.ts +69 -0
- package/src/index.ts +56 -0
- package/src/magic.ts +37 -0
- package/src/mine.ts +212 -0
- package/src/serialization.ts +236 -0
- package/src/verify.ts +57 -0
- package/src/wasm/miner.c +165 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* calibrate.ts — Hash rate measurement and mining cost estimation.
|
|
3
|
+
*
|
|
4
|
+
* calibrateHashRate() measures how many SHA3-256 hashes/sec the current
|
|
5
|
+
* environment can produce. Used by the extension UX to show a warning
|
|
6
|
+
* before starting a mine on slow devices.
|
|
7
|
+
*
|
|
8
|
+
* estimateMiningCost() converts a difficulty target + hash rate into an
|
|
9
|
+
* expected time and confidence label.
|
|
10
|
+
*/
|
|
11
|
+
export interface MiningEstimate {
|
|
12
|
+
expectedHashes: bigint;
|
|
13
|
+
expectedSeconds: number;
|
|
14
|
+
confidence: 'fast' | 'normal' | 'slow';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Run 100K trial SHA3-256 hashes and return the measured hash rate (hashes/sec).
|
|
18
|
+
*
|
|
19
|
+
* Call once per session and cache the result — the cost is ~50ms on a
|
|
20
|
+
* typical desktop and ~300ms on a mid-range mobile.
|
|
21
|
+
*/
|
|
22
|
+
export declare function calibrateHashRate(): Promise<number>;
|
|
23
|
+
/**
|
|
24
|
+
* Given a difficulty target and a measured hash rate, return the expected
|
|
25
|
+
* number of hashes, expected wall-clock time, and a confidence label.
|
|
26
|
+
*
|
|
27
|
+
* expectedHashes ≈ MAX_HASH / txnDifficulty
|
|
28
|
+
* (geometric distribution: each hash has P(valid) = txnDifficulty / MAX_HASH)
|
|
29
|
+
*
|
|
30
|
+
* Confidence labels:
|
|
31
|
+
* fast < 2 s
|
|
32
|
+
* normal 2 – 15 s
|
|
33
|
+
* slow > 15 s
|
|
34
|
+
*/
|
|
35
|
+
export declare function estimateMiningCost(txnDifficulty: Uint8Array, hashRatePerSec: number): MiningEstimate;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* calibrate.ts — Hash rate measurement and mining cost estimation.
|
|
3
|
+
*
|
|
4
|
+
* calibrateHashRate() measures how many SHA3-256 hashes/sec the current
|
|
5
|
+
* environment can produce. Used by the extension UX to show a warning
|
|
6
|
+
* before starting a mine on slow devices.
|
|
7
|
+
*
|
|
8
|
+
* estimateMiningCost() converts a difficulty target + hash rate into an
|
|
9
|
+
* expected time and confidence label.
|
|
10
|
+
*/
|
|
11
|
+
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
12
|
+
/**
|
|
13
|
+
* Run 100K trial SHA3-256 hashes and return the measured hash rate (hashes/sec).
|
|
14
|
+
*
|
|
15
|
+
* Call once per session and cache the result — the cost is ~50ms on a
|
|
16
|
+
* typical desktop and ~300ms on a mid-range mobile.
|
|
17
|
+
*/
|
|
18
|
+
export async function calibrateHashRate() {
|
|
19
|
+
const TRIAL_COUNT = 100000;
|
|
20
|
+
const data = new Uint8Array(150).fill(0xab);
|
|
21
|
+
const start = typeof performance !== 'undefined'
|
|
22
|
+
? performance.now()
|
|
23
|
+
: Date.now();
|
|
24
|
+
for (let i = 0; i < TRIAL_COUNT; i++) {
|
|
25
|
+
sha3_256(data);
|
|
26
|
+
}
|
|
27
|
+
const elapsed = (typeof performance !== 'undefined' ? performance.now() : Date.now()) - start;
|
|
28
|
+
return Math.round(TRIAL_COUNT / (elapsed / 1000));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Given a difficulty target and a measured hash rate, return the expected
|
|
32
|
+
* number of hashes, expected wall-clock time, and a confidence label.
|
|
33
|
+
*
|
|
34
|
+
* expectedHashes ≈ MAX_HASH / txnDifficulty
|
|
35
|
+
* (geometric distribution: each hash has P(valid) = txnDifficulty / MAX_HASH)
|
|
36
|
+
*
|
|
37
|
+
* Confidence labels:
|
|
38
|
+
* fast < 2 s
|
|
39
|
+
* normal 2 – 15 s
|
|
40
|
+
* slow > 15 s
|
|
41
|
+
*/
|
|
42
|
+
export function estimateMiningCost(txnDifficulty, hashRatePerSec) {
|
|
43
|
+
if (hashRatePerSec <= 0) {
|
|
44
|
+
throw new Error('hashRatePerSec must be positive');
|
|
45
|
+
}
|
|
46
|
+
const diffBigInt = txnDifficulty.reduce((acc, b) => (acc << 8n) | BigInt(b), 0n);
|
|
47
|
+
if (diffBigInt === 0n) {
|
|
48
|
+
return { expectedHashes: 0n, expectedSeconds: 0, confidence: 'fast' };
|
|
49
|
+
}
|
|
50
|
+
const MAX_HASH_BIGINT = (2n ** 256n) - 1n;
|
|
51
|
+
const expectedHashes = MAX_HASH_BIGINT / diffBigInt;
|
|
52
|
+
const expectedSeconds = Number(expectedHashes) / hashRatePerSec;
|
|
53
|
+
let confidence;
|
|
54
|
+
if (expectedSeconds < 2) {
|
|
55
|
+
confidence = 'fast';
|
|
56
|
+
}
|
|
57
|
+
else if (expectedSeconds < 15) {
|
|
58
|
+
confidence = 'normal';
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
confidence = 'slow';
|
|
62
|
+
}
|
|
63
|
+
return { expectedHashes, expectedSeconds, confidence };
|
|
64
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* constants.ts — TxPoW-level constants matching Minima Java protocol values.
|
|
3
|
+
*
|
|
4
|
+
* ZERO_HASH: 32-byte all-zeros — used for MMRRoot, CustomHash, super-parent slot.
|
|
5
|
+
* MAX_HASH: 32-byte all-0xFF — default mBlockDifficulty / mTxnDifficulty for
|
|
6
|
+
* MEG-side-mined paths. Rejected by checkTxPoWSimple() at block level.
|
|
7
|
+
* TX_POW_MIN_DIFFICULTY: Safe transaction difficulty target.
|
|
8
|
+
* ≈ MAX_HASH / 1,000,000 = Magic.getMinTxPowWork() floor.
|
|
9
|
+
* Local-mining paths MUST use a target ≤ this value.
|
|
10
|
+
* MAIN_NET_CHAIN_ID: 1-byte [0x00] — Java MiniData("0x00") = MAIN_NET chain ID.
|
|
11
|
+
* CASCADE_LEVELS: 32 — number of super-parent slots in a fresh TxPoW header.
|
|
12
|
+
*/
|
|
13
|
+
export declare const CASCADE_LEVELS = 32;
|
|
14
|
+
export declare const ZERO_HASH: Uint8Array<ArrayBuffer>;
|
|
15
|
+
export declare const MAX_HASH: Uint8Array;
|
|
16
|
+
/**
|
|
17
|
+
* TX_POW_MIN_DIFFICULTY = floor((2^256 - 1) / 1_000_000)
|
|
18
|
+
*
|
|
19
|
+
* This is the hardcoded floor constant matching Magic.getMinTxPowWork().
|
|
20
|
+
* Any locally mined TxPoW must have mTxnDifficulty ≤ this value to pass
|
|
21
|
+
* TxPoWChecker.checkTxPoWSimple() at block inclusion.
|
|
22
|
+
*
|
|
23
|
+
* Computed: (2n**256n - 1n) / 1_000_000n
|
|
24
|
+
* Hex: 0x000010C6F7A0B5ED8538AACDD46595F0C7AC73E0E9DBF12F70000000000000000
|
|
25
|
+
*/
|
|
26
|
+
export declare const TX_POW_MIN_DIFFICULTY: Uint8Array;
|
|
27
|
+
export declare const MAIN_NET_CHAIN_ID: Uint8Array<ArrayBuffer>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* constants.ts — TxPoW-level constants matching Minima Java protocol values.
|
|
3
|
+
*
|
|
4
|
+
* ZERO_HASH: 32-byte all-zeros — used for MMRRoot, CustomHash, super-parent slot.
|
|
5
|
+
* MAX_HASH: 32-byte all-0xFF — default mBlockDifficulty / mTxnDifficulty for
|
|
6
|
+
* MEG-side-mined paths. Rejected by checkTxPoWSimple() at block level.
|
|
7
|
+
* TX_POW_MIN_DIFFICULTY: Safe transaction difficulty target.
|
|
8
|
+
* ≈ MAX_HASH / 1,000,000 = Magic.getMinTxPowWork() floor.
|
|
9
|
+
* Local-mining paths MUST use a target ≤ this value.
|
|
10
|
+
* MAIN_NET_CHAIN_ID: 1-byte [0x00] — Java MiniData("0x00") = MAIN_NET chain ID.
|
|
11
|
+
* CASCADE_LEVELS: 32 — number of super-parent slots in a fresh TxPoW header.
|
|
12
|
+
*/
|
|
13
|
+
export const CASCADE_LEVELS = 32;
|
|
14
|
+
export const ZERO_HASH = new Uint8Array(32);
|
|
15
|
+
export const MAX_HASH = (() => {
|
|
16
|
+
const b = new Uint8Array(32);
|
|
17
|
+
b.fill(0xff);
|
|
18
|
+
return b;
|
|
19
|
+
})();
|
|
20
|
+
/**
|
|
21
|
+
* TX_POW_MIN_DIFFICULTY = floor((2^256 - 1) / 1_000_000)
|
|
22
|
+
*
|
|
23
|
+
* This is the hardcoded floor constant matching Magic.getMinTxPowWork().
|
|
24
|
+
* Any locally mined TxPoW must have mTxnDifficulty ≤ this value to pass
|
|
25
|
+
* TxPoWChecker.checkTxPoWSimple() at block inclusion.
|
|
26
|
+
*
|
|
27
|
+
* Computed: (2n**256n - 1n) / 1_000_000n
|
|
28
|
+
* Hex: 0x000010C6F7A0B5ED8538AACDD46595F0C7AC73E0E9DBF12F70000000000000000
|
|
29
|
+
*/
|
|
30
|
+
export const TX_POW_MIN_DIFFICULTY = (() => {
|
|
31
|
+
const maxHashBigInt = (2n ** 256n) - 1n;
|
|
32
|
+
const minDiffBigInt = maxHashBigInt / 1000000n;
|
|
33
|
+
let hex = minDiffBigInt.toString(16);
|
|
34
|
+
if (hex.length % 2 !== 0)
|
|
35
|
+
hex = '0' + hex;
|
|
36
|
+
while (hex.length < 64)
|
|
37
|
+
hex = '00' + hex;
|
|
38
|
+
const bytes = new Uint8Array(32);
|
|
39
|
+
for (let i = 0; i < 32; i++) {
|
|
40
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
41
|
+
}
|
|
42
|
+
return bytes;
|
|
43
|
+
})();
|
|
44
|
+
export const MAIN_NET_CHAIN_ID = new Uint8Array([0x00]);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* difficulty.ts — Fetch and manage the TxPoW difficulty target.
|
|
3
|
+
*
|
|
4
|
+
* The live value comes from `GET /v1/wallet/txpow-params` on the Axia API.
|
|
5
|
+
* On any error (timeout, network failure, invalid response) we fall back to
|
|
6
|
+
* the hardcoded TX_POW_MIN_DIFFICULTY constant, which is the protocol floor.
|
|
7
|
+
*/
|
|
8
|
+
export interface TxPowParams {
|
|
9
|
+
minTxPowWork: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Fetch the current minimum TxPoW work from the Axia API.
|
|
13
|
+
*
|
|
14
|
+
* Falls back to TX_POW_MIN_DIFFICULTY (the hardcoded protocol floor) on any
|
|
15
|
+
* error: network failure, timeout, or malformed response.
|
|
16
|
+
*
|
|
17
|
+
* @param axiaBaseUrl Base URL of the Axia API, e.g. "https://api.axia.to"
|
|
18
|
+
* @param timeoutMs Request timeout in ms (default: 3000)
|
|
19
|
+
*/
|
|
20
|
+
export declare function fetchTxPowTarget(axiaBaseUrl: string, timeoutMs?: number): Promise<Uint8Array>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* difficulty.ts — Fetch and manage the TxPoW difficulty target.
|
|
3
|
+
*
|
|
4
|
+
* The live value comes from `GET /v1/wallet/txpow-params` on the Axia API.
|
|
5
|
+
* On any error (timeout, network failure, invalid response) we fall back to
|
|
6
|
+
* the hardcoded TX_POW_MIN_DIFFICULTY constant, which is the protocol floor.
|
|
7
|
+
*/
|
|
8
|
+
import { TX_POW_MIN_DIFFICULTY } from './constants.js';
|
|
9
|
+
function hexToBytes(hex) {
|
|
10
|
+
const h = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
11
|
+
const out = new Uint8Array(h.length / 2);
|
|
12
|
+
for (let i = 0; i < h.length; i += 2) {
|
|
13
|
+
out[i / 2] = parseInt(h.slice(i, i + 2), 16);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Fetch the current minimum TxPoW work from the Axia API.
|
|
19
|
+
*
|
|
20
|
+
* Falls back to TX_POW_MIN_DIFFICULTY (the hardcoded protocol floor) on any
|
|
21
|
+
* error: network failure, timeout, or malformed response.
|
|
22
|
+
*
|
|
23
|
+
* @param axiaBaseUrl Base URL of the Axia API, e.g. "https://api.axia.to"
|
|
24
|
+
* @param timeoutMs Request timeout in ms (default: 3000)
|
|
25
|
+
*/
|
|
26
|
+
export async function fetchTxPowTarget(axiaBaseUrl, timeoutMs = 3000) {
|
|
27
|
+
try {
|
|
28
|
+
const base = axiaBaseUrl.replace(/\/$/, '');
|
|
29
|
+
const url = `${base}/v1/wallet/txpow-params`;
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
32
|
+
let response;
|
|
33
|
+
try {
|
|
34
|
+
response = await fetch(url, { signal: controller.signal });
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
}
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error(`HTTP ${response.status}`);
|
|
41
|
+
}
|
|
42
|
+
const data = (await response.json());
|
|
43
|
+
if (typeof data.minTxPowWork !== 'string') {
|
|
44
|
+
throw new Error('Missing minTxPowWork field');
|
|
45
|
+
}
|
|
46
|
+
const clean = data.minTxPowWork.replace('0x', '').replace(/\s/g, '');
|
|
47
|
+
if (clean.length !== 64) {
|
|
48
|
+
throw new Error(`Invalid minTxPowWork length: ${clean.length} (expected 64 hex chars)`);
|
|
49
|
+
}
|
|
50
|
+
return hexToBytes(clean);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return new Uint8Array(TX_POW_MIN_DIFFICULTY);
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @totemsdk/txpow
|
|
3
|
+
*
|
|
4
|
+
* TxPoW envelope serialization and proof-of-work mining for the Minima protocol.
|
|
5
|
+
*
|
|
6
|
+
* USAGE — MEG-side mining (byte-identical to extension current behaviour):
|
|
7
|
+
* const txpow = serializeTxPoW(txBytes, witnessBytes);
|
|
8
|
+
* // Submit to Axia: node re-mines with correct difficulty
|
|
9
|
+
*
|
|
10
|
+
* USAGE — Local mining:
|
|
11
|
+
* const target = await fetchTxPowTarget(axiaBaseUrl);
|
|
12
|
+
* const txBody = serializeTxBody(txBytes, witnessBytes, { txnDifficulty: target });
|
|
13
|
+
* const result = await mineTxPoW(txBody, target);
|
|
14
|
+
* const txpow = concat(result.minedHeaderBytes, new Uint8Array([0x01]), txBody);
|
|
15
|
+
*
|
|
16
|
+
* USAGE — Verify (relay nodes):
|
|
17
|
+
* const check = verifyProofOfWork(txpowId, mTxnDifficulty);
|
|
18
|
+
* if (!check.valid) drop(check.reason);
|
|
19
|
+
*/
|
|
20
|
+
export { serializeTxHeader, serializeTxBody, serializeTxPoW, computeTxPoWId, type TxHeaderOptions, type TxBodyOptions, type TxPoWOptions, } from './serialization.js';
|
|
21
|
+
export { MAX_HASH, ZERO_HASH, TX_POW_MIN_DIFFICULTY, CASCADE_LEVELS, MAIN_NET_CHAIN_ID, } from './constants.js';
|
|
22
|
+
export { serializeMagic } from './magic.js';
|
|
23
|
+
export { mineTxPoW, isLessThan, type MineResult, type MineOptions, } from './mine.js';
|
|
24
|
+
export { fetchTxPowTarget, type TxPowParams } from './difficulty.js';
|
|
25
|
+
export { verifyProofOfWork, type VerifyResult } from './verify.js';
|
|
26
|
+
export { calibrateHashRate, estimateMiningCost, type MiningEstimate, } from './calibrate.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @totemsdk/txpow
|
|
3
|
+
*
|
|
4
|
+
* TxPoW envelope serialization and proof-of-work mining for the Minima protocol.
|
|
5
|
+
*
|
|
6
|
+
* USAGE — MEG-side mining (byte-identical to extension current behaviour):
|
|
7
|
+
* const txpow = serializeTxPoW(txBytes, witnessBytes);
|
|
8
|
+
* // Submit to Axia: node re-mines with correct difficulty
|
|
9
|
+
*
|
|
10
|
+
* USAGE — Local mining:
|
|
11
|
+
* const target = await fetchTxPowTarget(axiaBaseUrl);
|
|
12
|
+
* const txBody = serializeTxBody(txBytes, witnessBytes, { txnDifficulty: target });
|
|
13
|
+
* const result = await mineTxPoW(txBody, target);
|
|
14
|
+
* const txpow = concat(result.minedHeaderBytes, new Uint8Array([0x01]), txBody);
|
|
15
|
+
*
|
|
16
|
+
* USAGE — Verify (relay nodes):
|
|
17
|
+
* const check = verifyProofOfWork(txpowId, mTxnDifficulty);
|
|
18
|
+
* if (!check.valid) drop(check.reason);
|
|
19
|
+
*/
|
|
20
|
+
export { serializeTxHeader, serializeTxBody, serializeTxPoW, computeTxPoWId, } from './serialization.js';
|
|
21
|
+
export { MAX_HASH, ZERO_HASH, TX_POW_MIN_DIFFICULTY, CASCADE_LEVELS, MAIN_NET_CHAIN_ID, } from './constants.js';
|
|
22
|
+
export { serializeMagic } from './magic.js';
|
|
23
|
+
export { mineTxPoW, isLessThan, } from './mine.js';
|
|
24
|
+
export { fetchTxPowTarget } from './difficulty.js';
|
|
25
|
+
export { verifyProofOfWork } from './verify.js';
|
|
26
|
+
export { calibrateHashRate, estimateMiningCost, } from './calibrate.js';
|
package/dist/magic.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* magic.ts — Serialize the Minima Magic struct.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the extension's MinimaTransactionBuilder.ts::serializeMagic().
|
|
5
|
+
* Matches Magic.writeDataStream() in Java exactly.
|
|
6
|
+
*
|
|
7
|
+
* Magic contains two copies of 4 fields (current + desired parameters):
|
|
8
|
+
* maxTxPoWSize (MiniNumber)
|
|
9
|
+
* maxKISSVMOps (MiniNumber)
|
|
10
|
+
* maxTxnPerBlock(MiniNumber)
|
|
11
|
+
* minTxPoWWork (MiniData — 32 bytes)
|
|
12
|
+
*
|
|
13
|
+
* Default values from Magic.java:
|
|
14
|
+
* maxTxPoWSize = 65536
|
|
15
|
+
* maxKISSVMOps = 1024
|
|
16
|
+
* maxTxnPerBlock = 256
|
|
17
|
+
* minTxPoWWork = MAX_HASH (all 0xFF, 32 bytes) — placeholder in fresh TxPoW
|
|
18
|
+
*/
|
|
19
|
+
export declare function serializeMagic(): Uint8Array;
|
package/dist/magic.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* magic.ts — Serialize the Minima Magic struct.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the extension's MinimaTransactionBuilder.ts::serializeMagic().
|
|
5
|
+
* Matches Magic.writeDataStream() in Java exactly.
|
|
6
|
+
*
|
|
7
|
+
* Magic contains two copies of 4 fields (current + desired parameters):
|
|
8
|
+
* maxTxPoWSize (MiniNumber)
|
|
9
|
+
* maxKISSVMOps (MiniNumber)
|
|
10
|
+
* maxTxnPerBlock(MiniNumber)
|
|
11
|
+
* minTxPoWWork (MiniData — 32 bytes)
|
|
12
|
+
*
|
|
13
|
+
* Default values from Magic.java:
|
|
14
|
+
* maxTxPoWSize = 65536
|
|
15
|
+
* maxKISSVMOps = 1024
|
|
16
|
+
* maxTxnPerBlock = 256
|
|
17
|
+
* minTxPoWWork = MAX_HASH (all 0xFF, 32 bytes) — placeholder in fresh TxPoW
|
|
18
|
+
*/
|
|
19
|
+
import { writeMiniNumber, writeMiniData, concat } from '@totemsdk/core';
|
|
20
|
+
import { MAX_HASH } from './constants.js';
|
|
21
|
+
export function serializeMagic() {
|
|
22
|
+
const parts = [];
|
|
23
|
+
parts.push(writeMiniNumber(65536n));
|
|
24
|
+
parts.push(writeMiniNumber(1024n));
|
|
25
|
+
parts.push(writeMiniNumber(256n));
|
|
26
|
+
parts.push(writeMiniData(MAX_HASH));
|
|
27
|
+
parts.push(writeMiniNumber(65536n));
|
|
28
|
+
parts.push(writeMiniNumber(1024n));
|
|
29
|
+
parts.push(writeMiniNumber(256n));
|
|
30
|
+
parts.push(writeMiniData(MAX_HASH));
|
|
31
|
+
return concat(...parts);
|
|
32
|
+
}
|
package/dist/mine.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mine.ts — Local TxPoW proof-of-work mining loop.
|
|
3
|
+
*
|
|
4
|
+
* Algorithm (from TxPoWChecker.java):
|
|
5
|
+
* txpowId = SHA3-256( serialize(TxHeader) )
|
|
6
|
+
* valid = txpowId < mTxnDifficulty (big-endian 256-bit comparison)
|
|
7
|
+
*
|
|
8
|
+
* Strategy:
|
|
9
|
+
* 1. The TxBody is fixed during mining — only mNonce in the header changes.
|
|
10
|
+
* 2. We precompute the "header tail" (everything after mNonce) once per mine.
|
|
11
|
+
* 3. Within each chunk (fixed nonce encoding length), we update only the nonce
|
|
12
|
+
* value bytes in place — no per-iteration allocation.
|
|
13
|
+
* 4. At each MiniNumber encoding boundary (nonce 128, 32768, 8388608, …) we
|
|
14
|
+
* re-allocate a new header buffer with the wider nonce field.
|
|
15
|
+
* 5. Between chunks we yield to the event loop so the caller thread isn't blocked.
|
|
16
|
+
*/
|
|
17
|
+
export interface MineResult {
|
|
18
|
+
/** Fully serialized TxHeader bytes with the winning nonce. */
|
|
19
|
+
minedHeaderBytes: Uint8Array;
|
|
20
|
+
/** SHA3-256(minedHeaderBytes) — the canonical TxPoW ID. */
|
|
21
|
+
txpowId: Uint8Array;
|
|
22
|
+
nonce: bigint;
|
|
23
|
+
elapsedMs: number;
|
|
24
|
+
/** 'wasm' when the WASM binary is present; 'js' otherwise. */
|
|
25
|
+
source: 'wasm' | 'js';
|
|
26
|
+
}
|
|
27
|
+
export interface MineOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Hash iterations per async yield (default: 10_000).
|
|
30
|
+
* Lower = more responsive UI; higher = slightly faster mining.
|
|
31
|
+
*/
|
|
32
|
+
chunkSize?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Hard cap on total iterations (default: unlimited).
|
|
35
|
+
* Throws if exhausted without finding a valid nonce.
|
|
36
|
+
*/
|
|
37
|
+
maxIterations?: number;
|
|
38
|
+
/** AbortSignal — rejects the Promise when aborted. */
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Big-endian 256-bit comparison: true if a < b.
|
|
43
|
+
* Used to check: txpowId < mTxnDifficulty.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isLessThan(a: Uint8Array, b: Uint8Array): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Mine a TxPoW locally by iterating the header nonce until
|
|
48
|
+
* SHA3-256( TxHeader ) < txnDifficulty
|
|
49
|
+
*
|
|
50
|
+
* @param txBodyBytes Pre-serialized TxBody bytes (from serializeTxBody).
|
|
51
|
+
* @param txnDifficulty 32-byte target. MUST be ≤ TX_POW_MIN_DIFFICULTY.
|
|
52
|
+
* Passing MAX_HASH here will loop forever and be rejected
|
|
53
|
+
* at block level by TxPoWChecker.checkTxPoWSimple().
|
|
54
|
+
* @param options Chunk size, max iterations, abort signal.
|
|
55
|
+
*/
|
|
56
|
+
export declare function mineTxPoW(txBodyBytes: Uint8Array, txnDifficulty: Uint8Array, options?: MineOptions): Promise<MineResult>;
|
package/dist/mine.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mine.ts — Local TxPoW proof-of-work mining loop.
|
|
3
|
+
*
|
|
4
|
+
* Algorithm (from TxPoWChecker.java):
|
|
5
|
+
* txpowId = SHA3-256( serialize(TxHeader) )
|
|
6
|
+
* valid = txpowId < mTxnDifficulty (big-endian 256-bit comparison)
|
|
7
|
+
*
|
|
8
|
+
* Strategy:
|
|
9
|
+
* 1. The TxBody is fixed during mining — only mNonce in the header changes.
|
|
10
|
+
* 2. We precompute the "header tail" (everything after mNonce) once per mine.
|
|
11
|
+
* 3. Within each chunk (fixed nonce encoding length), we update only the nonce
|
|
12
|
+
* value bytes in place — no per-iteration allocation.
|
|
13
|
+
* 4. At each MiniNumber encoding boundary (nonce 128, 32768, 8388608, …) we
|
|
14
|
+
* re-allocate a new header buffer with the wider nonce field.
|
|
15
|
+
* 5. Between chunks we yield to the event loop so the caller thread isn't blocked.
|
|
16
|
+
*/
|
|
17
|
+
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
18
|
+
import { writeMiniNumber, writeMiniData, writeHashToStream, concat, } from '@totemsdk/core';
|
|
19
|
+
import { serializeMagic } from './magic.js';
|
|
20
|
+
import { MAX_HASH, ZERO_HASH, CASCADE_LEVELS, MAIN_NET_CHAIN_ID } from './constants.js';
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// Internal helpers
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Big-endian 256-bit comparison: true if a < b.
|
|
26
|
+
* Used to check: txpowId < mTxnDifficulty.
|
|
27
|
+
*/
|
|
28
|
+
export function isLessThan(a, b) {
|
|
29
|
+
for (let i = 0; i < 32; i++) {
|
|
30
|
+
if (a[i] < b[i])
|
|
31
|
+
return true;
|
|
32
|
+
if (a[i] > b[i])
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Returns the first nonce value that would require more bytes in its
|
|
39
|
+
* MiniNumber encoding (i.e. the next boundary at 128, 32768, 8388608, …).
|
|
40
|
+
*
|
|
41
|
+
* Pattern: bigIntToByteArray(n) grows by 1 byte at 2^(8k−1) for k=1,2,3,…
|
|
42
|
+
* k=1: 2^7 = 128
|
|
43
|
+
* k=2: 2^15 = 32768
|
|
44
|
+
* k=3: 2^23 = 8388608
|
|
45
|
+
*/
|
|
46
|
+
function nextNonceBoundary(nonce) {
|
|
47
|
+
const nonceValueLen = writeMiniNumber(nonce).length - 2;
|
|
48
|
+
return 2n ** BigInt(8 * nonceValueLen - 1);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the "header tail" — the part of TxHeader that follows mNonce.
|
|
52
|
+
*
|
|
53
|
+
* This is computed once at the start of a mine and never changes during it.
|
|
54
|
+
*/
|
|
55
|
+
function buildHeaderTail(txBodyHash, timeMilli) {
|
|
56
|
+
return concat(writeMiniData(MAIN_NET_CHAIN_ID), writeMiniNumber(timeMilli), writeMiniNumber(0n), writeMiniData(MAX_HASH), new Uint8Array([CASCADE_LEVELS]), writeHashToStream(ZERO_HASH), writeHashToStream(ZERO_HASH), writeMiniNumber(0n), serializeMagic(), writeHashToStream(ZERO_HASH), writeHashToStream(txBodyHash));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Synchronously mine a chunk of nonces within a fixed encoding boundary.
|
|
60
|
+
*
|
|
61
|
+
* Mutates headerBuf in place — only the nonce value bytes at offset 2 change.
|
|
62
|
+
* Returns the found nonce, or null if not found in this chunk.
|
|
63
|
+
*/
|
|
64
|
+
function mineChunk(headerBuf, nonceValueLen, target, startNonce, endNonce) {
|
|
65
|
+
const nonceValueOffset = 2;
|
|
66
|
+
for (let nonce = startNonce; nonce < endNonce; nonce++) {
|
|
67
|
+
let n = nonce;
|
|
68
|
+
for (let i = nonceValueLen - 1; i >= 0; i--) {
|
|
69
|
+
headerBuf[nonceValueOffset + i] = Number(n & 0xffn);
|
|
70
|
+
n >>= 8n;
|
|
71
|
+
}
|
|
72
|
+
const hash = sha3_256(headerBuf);
|
|
73
|
+
if (isLessThan(hash, target))
|
|
74
|
+
return nonce;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
// Public API
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
/**
|
|
82
|
+
* Mine a TxPoW locally by iterating the header nonce until
|
|
83
|
+
* SHA3-256( TxHeader ) < txnDifficulty
|
|
84
|
+
*
|
|
85
|
+
* @param txBodyBytes Pre-serialized TxBody bytes (from serializeTxBody).
|
|
86
|
+
* @param txnDifficulty 32-byte target. MUST be ≤ TX_POW_MIN_DIFFICULTY.
|
|
87
|
+
* Passing MAX_HASH here will loop forever and be rejected
|
|
88
|
+
* at block level by TxPoWChecker.checkTxPoWSimple().
|
|
89
|
+
* @param options Chunk size, max iterations, abort signal.
|
|
90
|
+
*/
|
|
91
|
+
export async function mineTxPoW(txBodyBytes, txnDifficulty, options) {
|
|
92
|
+
const chunkSize = BigInt(options?.chunkSize ?? 10000);
|
|
93
|
+
const maxIterations = options?.maxIterations !== undefined
|
|
94
|
+
? BigInt(options.maxIterations)
|
|
95
|
+
: undefined;
|
|
96
|
+
const signal = options?.signal;
|
|
97
|
+
const bodyHash = sha3_256(txBodyBytes);
|
|
98
|
+
const timeMilli = BigInt(Date.now());
|
|
99
|
+
const headerTail = buildHeaderTail(bodyHash, timeMilli);
|
|
100
|
+
const startTime = Date.now();
|
|
101
|
+
let totalIterations = 0n;
|
|
102
|
+
let nonce = 0n;
|
|
103
|
+
while (true) {
|
|
104
|
+
if (signal?.aborted) {
|
|
105
|
+
throw new DOMException('Mining aborted', 'AbortError');
|
|
106
|
+
}
|
|
107
|
+
if (maxIterations !== undefined && totalIterations >= maxIterations) {
|
|
108
|
+
throw new Error(`Mining exhausted: ${totalIterations} iterations without finding a valid nonce`);
|
|
109
|
+
}
|
|
110
|
+
const boundary = nextNonceBoundary(nonce);
|
|
111
|
+
const chunkEnd = nonce + chunkSize < boundary ? nonce + chunkSize : boundary;
|
|
112
|
+
const nonceBytes = writeMiniNumber(nonce);
|
|
113
|
+
const nonceValueLen = nonceBytes.length - 2;
|
|
114
|
+
const headerBuf = concat(nonceBytes, headerTail);
|
|
115
|
+
const found = mineChunk(headerBuf, nonceValueLen, txnDifficulty, nonce, chunkEnd);
|
|
116
|
+
if (found !== null) {
|
|
117
|
+
const finalNonceBytes = writeMiniNumber(found);
|
|
118
|
+
const finalHeader = concat(finalNonceBytes, headerTail);
|
|
119
|
+
const txpowId = sha3_256(finalHeader);
|
|
120
|
+
return {
|
|
121
|
+
minedHeaderBytes: finalHeader,
|
|
122
|
+
txpowId,
|
|
123
|
+
nonce: found,
|
|
124
|
+
elapsedMs: Date.now() - startTime,
|
|
125
|
+
source: 'js',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
totalIterations += chunkEnd - nonce;
|
|
129
|
+
nonce = chunkEnd;
|
|
130
|
+
await new Promise(resolve => {
|
|
131
|
+
if (typeof setImmediate !== 'undefined') {
|
|
132
|
+
setImmediate(resolve);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
setTimeout(resolve, 0);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* serialization.ts — TxPoW envelope serialization.
|
|
3
|
+
*
|
|
4
|
+
* Ports serializeTxHeader / serializeTxBody / serializeTxPoW from
|
|
5
|
+
* packages/totem-extension/src/core/transaction/MinimaTransactionBuilder.ts
|
|
6
|
+
* into the standalone @totemsdk/txpow package.
|
|
7
|
+
*
|
|
8
|
+
* KEY DESIGN DECISIONS
|
|
9
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
10
|
+
* • serializeTxBody accepts PRE-SERIALIZED transaction bytes and witness bytes.
|
|
11
|
+
* Callers use @totemsdk/core's serializeTransaction() + their own witness
|
|
12
|
+
* serializer, then pass the byte arrays here. This keeps @totemsdk/txpow
|
|
13
|
+
* free of WotsSignatureData / InputScriptInfo / CoinProof dependencies.
|
|
14
|
+
*
|
|
15
|
+
* • txnDifficulty defaults to MAX_HASH (all 0xFF) to produce byte-identical
|
|
16
|
+
* output to the extension's current MEG-mined path. For locally mined TxPoWs
|
|
17
|
+
* the caller MUST pass a value ≤ TX_POW_MIN_DIFFICULTY, otherwise the
|
|
18
|
+
* transaction will fail TxPoWChecker.checkTxPoWSimple() at block inclusion.
|
|
19
|
+
*
|
|
20
|
+
* • nonce defaults to 0n (the mining loop updates it). timeMilli defaults to
|
|
21
|
+
* Date.now() at call time.
|
|
22
|
+
*
|
|
23
|
+
* WIRE FORMAT REFERENCE
|
|
24
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
25
|
+
* TxHeader.writeDataStream():
|
|
26
|
+
* 1. mNonce MiniNumber
|
|
27
|
+
* 2. mChainID MiniData (MAIN_NET = [0x00])
|
|
28
|
+
* 3. mTimeMilli MiniNumber
|
|
29
|
+
* 4. mBlockNumber MiniNumber (0 for fresh TxPoW)
|
|
30
|
+
* 5. mBlockDifficulty MiniData (MAX_HASH for fresh TxPoW)
|
|
31
|
+
* 6. Super Parents RLE: 1 byte count + writeHashToStream(parent)
|
|
32
|
+
* 7. mMMRRoot writeHashToStream (ZERO_HASH for fresh TxPoW)
|
|
33
|
+
* 8. mMMRTotal MiniNumber (0)
|
|
34
|
+
* 9. mMagic Magic.writeDataStream()
|
|
35
|
+
* 10. mCustomHash writeHashToStream (ZERO_HASH)
|
|
36
|
+
* 11. mTxBodyHash writeHashToStream
|
|
37
|
+
*
|
|
38
|
+
* TxBody.writeDataStream():
|
|
39
|
+
* 1. mPRNG writeHashToStream (32 random bytes)
|
|
40
|
+
* 2. mTxnDifficulty MiniData
|
|
41
|
+
* 3. mTransaction Transaction.writeDataStream() (pre-serialized)
|
|
42
|
+
* 4. mWitness Witness.writeDataStream() (pre-serialized)
|
|
43
|
+
* 5. mBurnTransaction Transaction.writeDataStream() (empty)
|
|
44
|
+
* 6. mBurnWitness Witness.writeDataStream() (empty)
|
|
45
|
+
* 7. mTxPowIDList MiniNumber(0)
|
|
46
|
+
*
|
|
47
|
+
* TxPoW.writeDataStream():
|
|
48
|
+
* TxHeader bytes + MiniByte(0x01=hasBody) + TxBody bytes
|
|
49
|
+
*
|
|
50
|
+
* TxPoW ID:
|
|
51
|
+
* SHA3-256( TxHeader bytes ) — Java: Crypto.getInstance().hashObject(mHeader)
|
|
52
|
+
*/
|
|
53
|
+
export interface TxHeaderOptions {
|
|
54
|
+
/** Proof-of-work nonce. Defaults to 0n. The mining loop iterates this. */
|
|
55
|
+
nonce?: bigint;
|
|
56
|
+
/** Block timestamp in milliseconds. Defaults to Date.now() at call time. */
|
|
57
|
+
timeMilli?: bigint;
|
|
58
|
+
}
|
|
59
|
+
export interface TxBodyOptions {
|
|
60
|
+
/**
|
|
61
|
+
* Transaction difficulty target (32-byte MiniData).
|
|
62
|
+
* • MEG-side-mined path: leave undefined → MAX_HASH (all 0xFF)
|
|
63
|
+
* • Locally mined path: MUST be ≤ TX_POW_MIN_DIFFICULTY, typically fetched
|
|
64
|
+
* via fetchTxPowTarget() from the same package.
|
|
65
|
+
* Setting MAX_HASH for locally mined TxPoWs will cause block-level rejection.
|
|
66
|
+
*/
|
|
67
|
+
txnDifficulty?: Uint8Array;
|
|
68
|
+
/** Override the 32-byte PRNG field. Useful for deterministic tests only. */
|
|
69
|
+
prng?: Uint8Array;
|
|
70
|
+
}
|
|
71
|
+
export type TxPoWOptions = TxHeaderOptions & TxBodyOptions;
|
|
72
|
+
/**
|
|
73
|
+
* Serialize a TxHeader per Minima's TxHeader.writeDataStream().
|
|
74
|
+
*
|
|
75
|
+
* @param txBodyHash SHA3-256 of the serialized TxBody (32 bytes)
|
|
76
|
+
* @param options Optional nonce and timeMilli overrides
|
|
77
|
+
*/
|
|
78
|
+
export declare function serializeTxHeader(txBodyHash: Uint8Array, options?: TxHeaderOptions): Uint8Array;
|
|
79
|
+
/**
|
|
80
|
+
* Compute TxPoW ID = SHA3-256(TxHeader bytes).
|
|
81
|
+
* Matches Java: Crypto.getInstance().hashObject(mHeader) via SHA3Digest(256).
|
|
82
|
+
*/
|
|
83
|
+
export declare function computeTxPoWId(headerBytes: Uint8Array): Uint8Array;
|
|
84
|
+
/**
|
|
85
|
+
* Serialize a TxBody per Minima's TxBody.writeDataStream().
|
|
86
|
+
*
|
|
87
|
+
* @param txBytes Pre-serialized Transaction bytes (from @totemsdk/core's serializeTransaction)
|
|
88
|
+
* @param witnessBytes Pre-serialized Witness bytes (from extension's serializeWitness)
|
|
89
|
+
* @param options Optional txnDifficulty override and test PRNG
|
|
90
|
+
*/
|
|
91
|
+
export declare function serializeTxBody(txBytes: Uint8Array, witnessBytes: Uint8Array, options?: TxBodyOptions): Uint8Array;
|
|
92
|
+
/**
|
|
93
|
+
* Serialize a complete TxPoW per Minima's TxPoW.writeDataStream().
|
|
94
|
+
*
|
|
95
|
+
* Assembles TxBody from pre-serialized tx+witness bytes, hashes the body to
|
|
96
|
+
* obtain mTxBodyHash, builds TxHeader with nonce=0, then concatenates:
|
|
97
|
+
* TxHeader | 0x01 (hasBody) | TxBody
|
|
98
|
+
*
|
|
99
|
+
* The returned bytes have nonce=0. Pass them to mineTxPoW() to find a valid
|
|
100
|
+
* nonce for local mining, or send as-is when MEG will re-mine.
|
|
101
|
+
*
|
|
102
|
+
* @param txBytes Pre-serialized Transaction bytes
|
|
103
|
+
* @param witnessBytes Pre-serialized Witness bytes
|
|
104
|
+
* @param options Optional txnDifficulty, nonce, timeMilli, prng
|
|
105
|
+
*/
|
|
106
|
+
export declare function serializeTxPoW(txBytes: Uint8Array, witnessBytes: Uint8Array, options?: TxPoWOptions): Uint8Array;
|