@totemsdk/txpow 0.1.3 → 0.1.4
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/LICENSE +21 -0
- package/README.md +3 -3
- package/dist/calibrate.js +8 -4
- package/dist/constants.js +8 -5
- package/dist/difficulty.js +6 -3
- package/dist/index.js +27 -7
- package/dist/magic.js +15 -12
- package/dist/mine.js +19 -15
- package/dist/serialization.js +34 -28
- package/dist/verify.js +14 -10
- package/package.json +26 -5
- package/src/__tests__/serialization.test.ts +0 -347
- package/src/calibrate.ts +0 -86
- package/src/constants.ts +0 -49
- package/src/difficulty.ts +0 -69
- package/src/index.ts +0 -56
- package/src/magic.ts +0 -37
- package/src/mine.ts +0 -212
- package/src/serialization.ts +0 -236
- package/src/verify.ts +0 -96
- package/src/wasm/miner.c +0 -165
package/src/mine.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
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
|
-
|
|
18
|
-
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
19
|
-
import {
|
|
20
|
-
writeMiniNumber,
|
|
21
|
-
writeMiniData,
|
|
22
|
-
writeHashToStream,
|
|
23
|
-
concat,
|
|
24
|
-
} from '@totemsdk/core';
|
|
25
|
-
import { serializeMagic } from './magic.js';
|
|
26
|
-
import { MAX_HASH, ZERO_HASH, CASCADE_LEVELS, MAIN_NET_CHAIN_ID } from './constants.js';
|
|
27
|
-
|
|
28
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
-
// Public types
|
|
30
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
export interface MineResult {
|
|
33
|
-
/** Fully serialized TxHeader bytes with the winning nonce. */
|
|
34
|
-
minedHeaderBytes: Uint8Array;
|
|
35
|
-
/** SHA3-256(minedHeaderBytes) — the canonical TxPoW ID. */
|
|
36
|
-
txpowId: Uint8Array;
|
|
37
|
-
nonce: bigint;
|
|
38
|
-
elapsedMs: number;
|
|
39
|
-
/** 'wasm' when the WASM binary is present; 'js' otherwise. */
|
|
40
|
-
source: 'wasm' | 'js';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface MineOptions {
|
|
44
|
-
/**
|
|
45
|
-
* Hash iterations per async yield (default: 10_000).
|
|
46
|
-
* Lower = more responsive UI; higher = slightly faster mining.
|
|
47
|
-
*/
|
|
48
|
-
chunkSize?: number;
|
|
49
|
-
/**
|
|
50
|
-
* Hard cap on total iterations (default: unlimited).
|
|
51
|
-
* Throws if exhausted without finding a valid nonce.
|
|
52
|
-
*/
|
|
53
|
-
maxIterations?: number;
|
|
54
|
-
/** AbortSignal — rejects the Promise when aborted. */
|
|
55
|
-
signal?: AbortSignal;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
-
// Internal helpers
|
|
60
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Big-endian 256-bit comparison: true if a < b.
|
|
64
|
-
* Used to check: txpowId < mTxnDifficulty.
|
|
65
|
-
*/
|
|
66
|
-
export function isLessThan(a: Uint8Array, b: Uint8Array): boolean {
|
|
67
|
-
for (let i = 0; i < 32; i++) {
|
|
68
|
-
if (a[i] < b[i]) return true;
|
|
69
|
-
if (a[i] > b[i]) return false;
|
|
70
|
-
}
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Returns the first nonce value that would require more bytes in its
|
|
76
|
-
* MiniNumber encoding (i.e. the next boundary at 128, 32768, 8388608, …).
|
|
77
|
-
*
|
|
78
|
-
* Pattern: bigIntToByteArray(n) grows by 1 byte at 2^(8k−1) for k=1,2,3,…
|
|
79
|
-
* k=1: 2^7 = 128
|
|
80
|
-
* k=2: 2^15 = 32768
|
|
81
|
-
* k=3: 2^23 = 8388608
|
|
82
|
-
*/
|
|
83
|
-
function nextNonceBoundary(nonce: bigint): bigint {
|
|
84
|
-
const nonceValueLen = writeMiniNumber(nonce).length - 2;
|
|
85
|
-
return 2n ** BigInt(8 * nonceValueLen - 1);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Build the "header tail" — the part of TxHeader that follows mNonce.
|
|
90
|
-
*
|
|
91
|
-
* This is computed once at the start of a mine and never changes during it.
|
|
92
|
-
*/
|
|
93
|
-
function buildHeaderTail(txBodyHash: Uint8Array, timeMilli: bigint): Uint8Array {
|
|
94
|
-
return concat(
|
|
95
|
-
writeMiniData(MAIN_NET_CHAIN_ID),
|
|
96
|
-
writeMiniNumber(timeMilli),
|
|
97
|
-
writeMiniNumber(0n),
|
|
98
|
-
writeMiniData(MAX_HASH),
|
|
99
|
-
new Uint8Array([CASCADE_LEVELS]),
|
|
100
|
-
writeHashToStream(ZERO_HASH),
|
|
101
|
-
writeHashToStream(ZERO_HASH),
|
|
102
|
-
writeMiniNumber(0n),
|
|
103
|
-
serializeMagic(),
|
|
104
|
-
writeHashToStream(ZERO_HASH),
|
|
105
|
-
writeHashToStream(txBodyHash)
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Synchronously mine a chunk of nonces within a fixed encoding boundary.
|
|
111
|
-
*
|
|
112
|
-
* Mutates headerBuf in place — only the nonce value bytes at offset 2 change.
|
|
113
|
-
* Returns the found nonce, or null if not found in this chunk.
|
|
114
|
-
*/
|
|
115
|
-
function mineChunk(
|
|
116
|
-
headerBuf: Uint8Array,
|
|
117
|
-
nonceValueLen: number,
|
|
118
|
-
target: Uint8Array,
|
|
119
|
-
startNonce: bigint,
|
|
120
|
-
endNonce: bigint
|
|
121
|
-
): bigint | null {
|
|
122
|
-
const nonceValueOffset = 2;
|
|
123
|
-
|
|
124
|
-
for (let nonce = startNonce; nonce < endNonce; nonce++) {
|
|
125
|
-
let n = nonce;
|
|
126
|
-
for (let i = nonceValueLen - 1; i >= 0; i--) {
|
|
127
|
-
headerBuf[nonceValueOffset + i] = Number(n & 0xffn);
|
|
128
|
-
n >>= 8n;
|
|
129
|
-
}
|
|
130
|
-
const hash = sha3_256(headerBuf);
|
|
131
|
-
if (isLessThan(hash, target)) return nonce;
|
|
132
|
-
}
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
137
|
-
// Public API
|
|
138
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Mine a TxPoW locally by iterating the header nonce until
|
|
142
|
-
* SHA3-256( TxHeader ) < txnDifficulty
|
|
143
|
-
*
|
|
144
|
-
* @param txBodyBytes Pre-serialized TxBody bytes (from serializeTxBody).
|
|
145
|
-
* @param txnDifficulty 32-byte target. MUST be ≤ TX_POW_MIN_DIFFICULTY.
|
|
146
|
-
* Passing MAX_HASH here will loop forever and be rejected
|
|
147
|
-
* at block level by TxPoWChecker.checkTxPoWSimple().
|
|
148
|
-
* @param options Chunk size, max iterations, abort signal.
|
|
149
|
-
*/
|
|
150
|
-
export async function mineTxPoW(
|
|
151
|
-
txBodyBytes: Uint8Array,
|
|
152
|
-
txnDifficulty: Uint8Array,
|
|
153
|
-
options?: MineOptions
|
|
154
|
-
): Promise<MineResult> {
|
|
155
|
-
const chunkSize = BigInt(options?.chunkSize ?? 10_000);
|
|
156
|
-
const maxIterations = options?.maxIterations !== undefined
|
|
157
|
-
? BigInt(options.maxIterations)
|
|
158
|
-
: undefined;
|
|
159
|
-
const signal = options?.signal;
|
|
160
|
-
|
|
161
|
-
const bodyHash = sha3_256(txBodyBytes);
|
|
162
|
-
const timeMilli = BigInt(Date.now());
|
|
163
|
-
const headerTail = buildHeaderTail(bodyHash, timeMilli);
|
|
164
|
-
|
|
165
|
-
const startTime = Date.now();
|
|
166
|
-
let totalIterations = 0n;
|
|
167
|
-
let nonce = 0n;
|
|
168
|
-
|
|
169
|
-
while (true) {
|
|
170
|
-
if (signal?.aborted) {
|
|
171
|
-
throw new DOMException('Mining aborted', 'AbortError');
|
|
172
|
-
}
|
|
173
|
-
if (maxIterations !== undefined && totalIterations >= maxIterations) {
|
|
174
|
-
throw new Error(
|
|
175
|
-
`Mining exhausted: ${totalIterations} iterations without finding a valid nonce`
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const boundary = nextNonceBoundary(nonce);
|
|
180
|
-
const chunkEnd = nonce + chunkSize < boundary ? nonce + chunkSize : boundary;
|
|
181
|
-
|
|
182
|
-
const nonceBytes = writeMiniNumber(nonce);
|
|
183
|
-
const nonceValueLen = nonceBytes.length - 2;
|
|
184
|
-
const headerBuf = concat(nonceBytes, headerTail);
|
|
185
|
-
|
|
186
|
-
const found = mineChunk(headerBuf, nonceValueLen, txnDifficulty, nonce, chunkEnd);
|
|
187
|
-
|
|
188
|
-
if (found !== null) {
|
|
189
|
-
const finalNonceBytes = writeMiniNumber(found);
|
|
190
|
-
const finalHeader = concat(finalNonceBytes, headerTail);
|
|
191
|
-
const txpowId = sha3_256(finalHeader);
|
|
192
|
-
return {
|
|
193
|
-
minedHeaderBytes: finalHeader,
|
|
194
|
-
txpowId,
|
|
195
|
-
nonce: found,
|
|
196
|
-
elapsedMs: Date.now() - startTime,
|
|
197
|
-
source: 'js',
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
totalIterations += chunkEnd - nonce;
|
|
202
|
-
nonce = chunkEnd;
|
|
203
|
-
|
|
204
|
-
await new Promise<void>(resolve => {
|
|
205
|
-
if (typeof setImmediate !== 'undefined') {
|
|
206
|
-
setImmediate(resolve);
|
|
207
|
-
} else {
|
|
208
|
-
setTimeout(resolve, 0);
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
}
|
package/src/serialization.ts
DELETED
|
@@ -1,236 +0,0 @@
|
|
|
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
|
-
|
|
54
|
-
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
55
|
-
import {
|
|
56
|
-
writeMiniNumber,
|
|
57
|
-
writeMiniData,
|
|
58
|
-
concat,
|
|
59
|
-
writeHashToStream,
|
|
60
|
-
} from '@totemsdk/core';
|
|
61
|
-
import { serializeMagic } from './magic.js';
|
|
62
|
-
import {
|
|
63
|
-
MAX_HASH,
|
|
64
|
-
ZERO_HASH,
|
|
65
|
-
CASCADE_LEVELS,
|
|
66
|
-
MAIN_NET_CHAIN_ID,
|
|
67
|
-
} from './constants.js';
|
|
68
|
-
|
|
69
|
-
export interface TxHeaderOptions {
|
|
70
|
-
/** Proof-of-work nonce. Defaults to 0n. The mining loop iterates this. */
|
|
71
|
-
nonce?: bigint;
|
|
72
|
-
/** Block timestamp in milliseconds. Defaults to Date.now() at call time. */
|
|
73
|
-
timeMilli?: bigint;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface TxBodyOptions {
|
|
77
|
-
/**
|
|
78
|
-
* Transaction difficulty target (32-byte MiniData).
|
|
79
|
-
* • MEG-side-mined path: leave undefined → MAX_HASH (all 0xFF)
|
|
80
|
-
* • Locally mined path: MUST be ≤ TX_POW_MIN_DIFFICULTY, typically fetched
|
|
81
|
-
* via fetchTxPowTarget() from the same package.
|
|
82
|
-
* Setting MAX_HASH for locally mined TxPoWs will cause block-level rejection.
|
|
83
|
-
*/
|
|
84
|
-
txnDifficulty?: Uint8Array;
|
|
85
|
-
/** Override the 32-byte PRNG field. Useful for deterministic tests only. */
|
|
86
|
-
prng?: Uint8Array;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export type TxPoWOptions = TxHeaderOptions & TxBodyOptions;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Serialize a TxHeader per Minima's TxHeader.writeDataStream().
|
|
93
|
-
*
|
|
94
|
-
* @param txBodyHash SHA3-256 of the serialized TxBody (32 bytes)
|
|
95
|
-
* @param options Optional nonce and timeMilli overrides
|
|
96
|
-
*/
|
|
97
|
-
export function serializeTxHeader(
|
|
98
|
-
txBodyHash: Uint8Array,
|
|
99
|
-
options?: TxHeaderOptions
|
|
100
|
-
): Uint8Array {
|
|
101
|
-
const nonce = options?.nonce ?? 0n;
|
|
102
|
-
const timeMilli = options?.timeMilli ?? BigInt(Date.now());
|
|
103
|
-
|
|
104
|
-
const parts: Uint8Array[] = [];
|
|
105
|
-
|
|
106
|
-
parts.push(writeMiniNumber(nonce));
|
|
107
|
-
parts.push(writeMiniData(MAIN_NET_CHAIN_ID));
|
|
108
|
-
parts.push(writeMiniNumber(timeMilli));
|
|
109
|
-
parts.push(writeMiniNumber(0n));
|
|
110
|
-
parts.push(writeMiniData(MAX_HASH));
|
|
111
|
-
|
|
112
|
-
parts.push(new Uint8Array([CASCADE_LEVELS]));
|
|
113
|
-
parts.push(writeHashToStream(ZERO_HASH));
|
|
114
|
-
|
|
115
|
-
parts.push(writeHashToStream(ZERO_HASH));
|
|
116
|
-
parts.push(writeMiniNumber(0n));
|
|
117
|
-
|
|
118
|
-
parts.push(serializeMagic());
|
|
119
|
-
|
|
120
|
-
parts.push(writeHashToStream(ZERO_HASH));
|
|
121
|
-
|
|
122
|
-
parts.push(writeHashToStream(txBodyHash));
|
|
123
|
-
|
|
124
|
-
return concat(...parts);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Compute TxPoW ID = SHA3-256(TxHeader bytes).
|
|
129
|
-
* Matches Java: Crypto.getInstance().hashObject(mHeader) via SHA3Digest(256).
|
|
130
|
-
*/
|
|
131
|
-
export function computeTxPoWId(headerBytes: Uint8Array): Uint8Array {
|
|
132
|
-
return sha3_256(headerBytes);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Build the empty burn transaction bytes.
|
|
137
|
-
*
|
|
138
|
-
* Equivalent to serializeTransaction({ linkHash: new Uint8Array([0x00]),
|
|
139
|
-
* inputs: [], outputs: [], state: [] })
|
|
140
|
-
* which produces:
|
|
141
|
-
* writeMiniNumber(0n) inputs count
|
|
142
|
-
* writeMiniNumber(0n) outputs count
|
|
143
|
-
* writeMiniNumber(0n) state count
|
|
144
|
-
* writeHashToStream([0x00]) linkHash = ZERO_TXPOWID (1 byte)
|
|
145
|
-
*/
|
|
146
|
-
function buildEmptyBurnTxBytes(): Uint8Array {
|
|
147
|
-
return concat(
|
|
148
|
-
writeMiniNumber(0n),
|
|
149
|
-
writeMiniNumber(0n),
|
|
150
|
-
writeMiniNumber(0n),
|
|
151
|
-
writeHashToStream(new Uint8Array([0x00]))
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Build the empty burn witness bytes.
|
|
157
|
-
*
|
|
158
|
-
* Equivalent to serializeWitness() with no arguments:
|
|
159
|
-
* writeMiniNumber(0n) signature count
|
|
160
|
-
* writeMiniNumber(0n) coinproof count
|
|
161
|
-
* writeMiniNumber(0n) scriptproof count
|
|
162
|
-
*/
|
|
163
|
-
function buildEmptyBurnWitnessBytes(): Uint8Array {
|
|
164
|
-
return concat(
|
|
165
|
-
writeMiniNumber(0n),
|
|
166
|
-
writeMiniNumber(0n),
|
|
167
|
-
writeMiniNumber(0n)
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Serialize a TxBody per Minima's TxBody.writeDataStream().
|
|
173
|
-
*
|
|
174
|
-
* @param txBytes Pre-serialized Transaction bytes (from @totemsdk/core's serializeTransaction)
|
|
175
|
-
* @param witnessBytes Pre-serialized Witness bytes (from extension's serializeWitness)
|
|
176
|
-
* @param options Optional txnDifficulty override and test PRNG
|
|
177
|
-
*/
|
|
178
|
-
export function serializeTxBody(
|
|
179
|
-
txBytes: Uint8Array,
|
|
180
|
-
witnessBytes: Uint8Array,
|
|
181
|
-
options?: TxBodyOptions
|
|
182
|
-
): Uint8Array {
|
|
183
|
-
const txnDifficulty = options?.txnDifficulty ?? MAX_HASH;
|
|
184
|
-
|
|
185
|
-
let prng: Uint8Array;
|
|
186
|
-
if (options?.prng) {
|
|
187
|
-
prng = options.prng;
|
|
188
|
-
} else {
|
|
189
|
-
prng = new Uint8Array(32);
|
|
190
|
-
if (typeof crypto === 'undefined' || !crypto.getRandomValues) {
|
|
191
|
-
throw new Error(
|
|
192
|
-
'SECURITY: crypto.getRandomValues unavailable — cannot generate secure random bytes for transaction PRNG'
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
crypto.getRandomValues(prng);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return concat(
|
|
199
|
-
writeHashToStream(prng),
|
|
200
|
-
writeMiniData(txnDifficulty),
|
|
201
|
-
txBytes,
|
|
202
|
-
witnessBytes,
|
|
203
|
-
buildEmptyBurnTxBytes(),
|
|
204
|
-
buildEmptyBurnWitnessBytes(),
|
|
205
|
-
writeMiniNumber(0n)
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Serialize a complete TxPoW per Minima's TxPoW.writeDataStream().
|
|
211
|
-
*
|
|
212
|
-
* Assembles TxBody from pre-serialized tx+witness bytes, hashes the body to
|
|
213
|
-
* obtain mTxBodyHash, builds TxHeader with nonce=0, then concatenates:
|
|
214
|
-
* TxHeader | 0x01 (hasBody) | TxBody
|
|
215
|
-
*
|
|
216
|
-
* The returned bytes have nonce=0. Pass them to mineTxPoW() to find a valid
|
|
217
|
-
* nonce for local mining, or send as-is when MEG will re-mine.
|
|
218
|
-
*
|
|
219
|
-
* @param txBytes Pre-serialized Transaction bytes
|
|
220
|
-
* @param witnessBytes Pre-serialized Witness bytes
|
|
221
|
-
* @param options Optional txnDifficulty, nonce, timeMilli, prng
|
|
222
|
-
*/
|
|
223
|
-
export function serializeTxPoW(
|
|
224
|
-
txBytes: Uint8Array,
|
|
225
|
-
witnessBytes: Uint8Array,
|
|
226
|
-
options?: TxPoWOptions
|
|
227
|
-
): Uint8Array {
|
|
228
|
-
const txBody = serializeTxBody(txBytes, witnessBytes, options);
|
|
229
|
-
const txBodyHash = sha3_256(txBody);
|
|
230
|
-
|
|
231
|
-
return concat(
|
|
232
|
-
serializeTxHeader(txBodyHash, options),
|
|
233
|
-
new Uint8Array([0x01]),
|
|
234
|
-
txBody
|
|
235
|
-
);
|
|
236
|
-
}
|
package/src/verify.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* verify.ts — TxPoW proof-of-work verification.
|
|
3
|
-
*
|
|
4
|
-
* verifyProofOfWork checks that a given txpowId beats the difficulty target
|
|
5
|
-
* without re-mining. Used by relay nodes to reject under-work submissions.
|
|
6
|
-
*
|
|
7
|
-
* verifyTxPoWWork is a convenience wrapper for relay nodes that receive raw
|
|
8
|
-
* TxPoW hex: it hashes the full bytes as a proxy txpowId and checks against
|
|
9
|
-
* the network minimum difficulty. PureMinima performs authoritative validation
|
|
10
|
-
* on submission; this is a first-pass spam filter.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { isLessThan } from './mine.js';
|
|
14
|
-
import { computeTxPoWId } from './serialization.js';
|
|
15
|
-
import { TX_POW_MIN_DIFFICULTY } from './constants.js';
|
|
16
|
-
|
|
17
|
-
export interface VerifyResult {
|
|
18
|
-
valid: boolean;
|
|
19
|
-
txpowId: string;
|
|
20
|
-
difficulty: string;
|
|
21
|
-
reason?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function toHex(bytes: Uint8Array): string {
|
|
25
|
-
return '0x' + Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Verify that a TxPoW ID beats the stated difficulty target.
|
|
30
|
-
*
|
|
31
|
-
* valid = txpowId < mTxnDifficulty (big-endian 256-bit comparison)
|
|
32
|
-
*
|
|
33
|
-
* @param txpowId The 32-byte TxPoW ID (SHA3-256 of the header).
|
|
34
|
-
* @param mTxnDifficulty The 32-byte difficulty target from the TxBody.
|
|
35
|
-
*/
|
|
36
|
-
export function verifyProofOfWork(
|
|
37
|
-
txpowId: Uint8Array,
|
|
38
|
-
mTxnDifficulty: Uint8Array
|
|
39
|
-
): VerifyResult {
|
|
40
|
-
if (txpowId.length !== 32) {
|
|
41
|
-
return {
|
|
42
|
-
valid: false,
|
|
43
|
-
txpowId: toHex(txpowId),
|
|
44
|
-
difficulty: toHex(mTxnDifficulty),
|
|
45
|
-
reason: `txpowId must be 32 bytes, got ${txpowId.length}`,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
if (mTxnDifficulty.length !== 32) {
|
|
49
|
-
return {
|
|
50
|
-
valid: false,
|
|
51
|
-
txpowId: toHex(txpowId),
|
|
52
|
-
difficulty: toHex(mTxnDifficulty),
|
|
53
|
-
reason: `mTxnDifficulty must be 32 bytes, got ${mTxnDifficulty.length}`,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const valid = isLessThan(txpowId, mTxnDifficulty);
|
|
58
|
-
return {
|
|
59
|
-
valid,
|
|
60
|
-
txpowId: toHex(txpowId),
|
|
61
|
-
difficulty: toHex(mTxnDifficulty),
|
|
62
|
-
reason: valid ? undefined : 'txpowId ≥ mTxnDifficulty',
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Relay-side work verification from raw TxPoW hex.
|
|
68
|
-
*
|
|
69
|
-
* Validates hex format, minimum length (spam filter), then computes
|
|
70
|
-
* SHA3-256 of the serialized bytes as a proxy txpowId and checks it
|
|
71
|
-
* against TX_POW_MIN_DIFFICULTY.
|
|
72
|
-
*
|
|
73
|
-
* Note: full structural parsing of TxPoW headers is deferred to a future
|
|
74
|
-
* parser. PureMinima performs authoritative work verification on submission.
|
|
75
|
-
*/
|
|
76
|
-
export function verifyTxPoWWork(txpowHex: string): VerifyResult {
|
|
77
|
-
if (typeof txpowHex !== 'string' || !/^[0-9a-fA-F]+$/.test(txpowHex)) {
|
|
78
|
-
return {
|
|
79
|
-
valid: false,
|
|
80
|
-
txpowId: '',
|
|
81
|
-
difficulty: toHex(TX_POW_MIN_DIFFICULTY),
|
|
82
|
-
reason: 'invalid hex encoding',
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
if (txpowHex.length < 200) {
|
|
86
|
-
return {
|
|
87
|
-
valid: false,
|
|
88
|
-
txpowId: '',
|
|
89
|
-
difficulty: toHex(TX_POW_MIN_DIFFICULTY),
|
|
90
|
-
reason: 'TxPoW too short (spam filter: minimum 100 bytes)',
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
const bytes = Uint8Array.from(Buffer.from(txpowHex, 'hex'));
|
|
94
|
-
const proxyId = computeTxPoWId(bytes);
|
|
95
|
-
return verifyProofOfWork(proxyId, TX_POW_MIN_DIFFICULTY);
|
|
96
|
-
}
|
package/src/wasm/miner.c
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* miner.c — SHA3-256 proof-of-work mining loop for WASM compilation.
|
|
3
|
-
*
|
|
4
|
-
* Compile (requires Emscripten):
|
|
5
|
-
* emcc -O3 -o miner.wasm miner.c \
|
|
6
|
-
* -s WASM=1 -s SIDE_MODULE=1 \
|
|
7
|
-
* -s EXPORTED_FUNCTIONS='["_mine","_malloc","_free"]' \
|
|
8
|
-
* --no-entry
|
|
9
|
-
*
|
|
10
|
-
* Or set EMSCRIPTEN env var and run: npm run build:wasm
|
|
11
|
-
*
|
|
12
|
-
* WASM interface:
|
|
13
|
-
* uint32_t mine(
|
|
14
|
-
* uint8_t* header, // header buffer in WASM linear memory
|
|
15
|
-
* int headerLen, // total header length
|
|
16
|
-
* int nonceOffset, // byte offset of nonce VALUE (= 2, after scale+len)
|
|
17
|
-
* int nonceLen, // byte length of nonce value (1, 2, or 3)
|
|
18
|
-
* uint8_t* target, // 32-byte target in WASM linear memory
|
|
19
|
-
* uint32_t startNonce, // first nonce to try
|
|
20
|
-
* uint32_t chunkSize // number of hashes to attempt
|
|
21
|
-
* )
|
|
22
|
-
* Returns: found nonce (0 = found at nonce 0, 0xFFFFFFFF = not found in chunk)
|
|
23
|
-
*
|
|
24
|
-
* SHA3-256 implementation: keccak-tiny (public domain, David Leon Gil, 2015)
|
|
25
|
-
* Inlined here so the WASM binary has no external dependencies.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
#include <stdint.h>
|
|
29
|
-
#include <string.h>
|
|
30
|
-
|
|
31
|
-
/* ── keccak-tiny ─────────────────────────────────────────────────────────── */
|
|
32
|
-
|
|
33
|
-
#define SHA3_256_RATE 136
|
|
34
|
-
#define SHA3_256_CAP 64
|
|
35
|
-
|
|
36
|
-
static const uint64_t KECCAK_ROUND_CONSTANTS[24] = {
|
|
37
|
-
0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808AULL,
|
|
38
|
-
0x8000000080008000ULL, 0x000000000000808BULL, 0x0000000080000001ULL,
|
|
39
|
-
0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008AULL,
|
|
40
|
-
0x0000000000000088ULL, 0x0000000080008009ULL, 0x000000008000000AULL,
|
|
41
|
-
0x000000008000808BULL, 0x800000000000008BULL, 0x8000000000008089ULL,
|
|
42
|
-
0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL,
|
|
43
|
-
0x000000000000800AULL, 0x800000008000000AULL, 0x8000000080008081ULL,
|
|
44
|
-
0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
static const int KECCAK_RHO[24] = {
|
|
48
|
-
1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15,
|
|
49
|
-
21, 8, 18, 2, 61, 56, 14
|
|
50
|
-
};
|
|
51
|
-
static const int KECCAK_PI[24] = {
|
|
52
|
-
10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12,
|
|
53
|
-
2, 20, 14, 22, 9, 6, 1
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
#define ROL64(a, b) (((a) << (b)) | ((a) >> (64 - (b))))
|
|
57
|
-
|
|
58
|
-
static void keccak_f(uint64_t* state) {
|
|
59
|
-
for (int round = 0; round < 24; round++) {
|
|
60
|
-
uint64_t C[5], D[5], tmp;
|
|
61
|
-
for (int x = 0; x < 5; x++)
|
|
62
|
-
C[x] = state[x] ^ state[x+5] ^ state[x+10] ^ state[x+15] ^ state[x+20];
|
|
63
|
-
for (int x = 0; x < 5; x++) {
|
|
64
|
-
D[x] = C[(x+4)%5] ^ ROL64(C[(x+1)%5], 1);
|
|
65
|
-
for (int y = 0; y < 5; y++) state[x + y*5] ^= D[x];
|
|
66
|
-
}
|
|
67
|
-
tmp = state[1];
|
|
68
|
-
for (int i = 0; i < 24; i++) {
|
|
69
|
-
int j = KECCAK_PI[i];
|
|
70
|
-
uint64_t t = state[j];
|
|
71
|
-
state[j] = ROL64(tmp, KECCAK_RHO[i]);
|
|
72
|
-
tmp = t;
|
|
73
|
-
}
|
|
74
|
-
for (int y = 0; y < 5; y++) {
|
|
75
|
-
uint64_t lane[5];
|
|
76
|
-
for (int x = 0; x < 5; x++) lane[x] = state[x + y*5];
|
|
77
|
-
for (int x = 0; x < 5; x++)
|
|
78
|
-
state[x + y*5] = lane[x] ^ (~lane[(x+1)%5] & lane[(x+2)%5]);
|
|
79
|
-
}
|
|
80
|
-
state[0] ^= KECCAK_ROUND_CONSTANTS[round];
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
static void sha3_256_hash(const uint8_t* msg, int msgLen, uint8_t* out) {
|
|
85
|
-
uint64_t state[25];
|
|
86
|
-
memset(state, 0, sizeof(state));
|
|
87
|
-
|
|
88
|
-
uint8_t buf[SHA3_256_RATE];
|
|
89
|
-
int absorbed = 0;
|
|
90
|
-
|
|
91
|
-
while (msgLen > 0) {
|
|
92
|
-
int take = msgLen < SHA3_256_RATE ? msgLen : SHA3_256_RATE;
|
|
93
|
-
if (absorbed + take < SHA3_256_RATE) {
|
|
94
|
-
memcpy(buf + absorbed, msg, take);
|
|
95
|
-
absorbed += take;
|
|
96
|
-
msgLen -= take;
|
|
97
|
-
msg += take;
|
|
98
|
-
} else {
|
|
99
|
-
memcpy(buf + absorbed, msg, SHA3_256_RATE - absorbed);
|
|
100
|
-
for (int i = 0; i < SHA3_256_RATE; i++)
|
|
101
|
-
((uint8_t*)state)[i] ^= buf[i];
|
|
102
|
-
keccak_f(state);
|
|
103
|
-
absorbed = 0;
|
|
104
|
-
msg += (SHA3_256_RATE - absorbed + take - (SHA3_256_RATE - absorbed));
|
|
105
|
-
msgLen -= take;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/* Padding: SHA-3 domain = 0x06, last byte 0x80 */
|
|
110
|
-
memset(buf + absorbed, 0, SHA3_256_RATE - absorbed);
|
|
111
|
-
buf[absorbed] = 0x06;
|
|
112
|
-
buf[SHA3_256_RATE - 1] ^= 0x80;
|
|
113
|
-
for (int i = 0; i < SHA3_256_RATE; i++)
|
|
114
|
-
((uint8_t*)state)[i] ^= buf[i];
|
|
115
|
-
keccak_f(state);
|
|
116
|
-
|
|
117
|
-
memcpy(out, state, 32);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/* ── Mining export ────────────────────────────────────────────────────────── */
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Mine a chunk: iterate nonce from startNonce to startNonce+chunkSize−1.
|
|
124
|
-
* Write the nonce big-endian into header[nonceOffset..nonceOffset+nonceLen).
|
|
125
|
-
* Hash the header with SHA3-256 and compare to target (32 bytes).
|
|
126
|
-
*
|
|
127
|
-
* Returns the found nonce, or 0xFFFFFFFF if not found in this chunk.
|
|
128
|
-
*
|
|
129
|
-
* NOTE: nonce value is written big-endian. For nonce 0: all zero bytes.
|
|
130
|
-
* The caller ensures startNonce..startNonce+chunkSize stay within
|
|
131
|
-
* the current MiniNumber encoding length boundary.
|
|
132
|
-
*/
|
|
133
|
-
__attribute__((used))
|
|
134
|
-
uint32_t mine(
|
|
135
|
-
uint8_t* header,
|
|
136
|
-
int headerLen,
|
|
137
|
-
int nonceOffset,
|
|
138
|
-
int nonceLen,
|
|
139
|
-
uint8_t* target,
|
|
140
|
-
uint32_t startNonce,
|
|
141
|
-
uint32_t chunkSize
|
|
142
|
-
) {
|
|
143
|
-
uint8_t hash[32];
|
|
144
|
-
|
|
145
|
-
for (uint32_t n = startNonce; n < startNonce + chunkSize; n++) {
|
|
146
|
-
/* Write nonce big-endian */
|
|
147
|
-
uint32_t tmp = n;
|
|
148
|
-
for (int i = nonceLen - 1; i >= 0; i--) {
|
|
149
|
-
header[nonceOffset + i] = (uint8_t)(tmp & 0xFF);
|
|
150
|
-
tmp >>= 8;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
sha3_256_hash(header, headerLen, hash);
|
|
154
|
-
|
|
155
|
-
/* Big-endian 256-bit compare: hash < target? */
|
|
156
|
-
int lt = 0;
|
|
157
|
-
for (int i = 0; i < 32; i++) {
|
|
158
|
-
if (hash[i] < target[i]) { lt = 1; break; }
|
|
159
|
-
if (hash[i] > target[i]) { lt = 0; break; }
|
|
160
|
-
}
|
|
161
|
-
if (lt) return n;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return 0xFFFFFFFFU;
|
|
165
|
-
}
|