@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,153 @@
|
|
|
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
|
+
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
54
|
+
import { writeMiniNumber, writeMiniData, concat, writeHashToStream, } from '@totemsdk/core';
|
|
55
|
+
import { serializeMagic } from './magic.js';
|
|
56
|
+
import { MAX_HASH, ZERO_HASH, CASCADE_LEVELS, MAIN_NET_CHAIN_ID, } from './constants.js';
|
|
57
|
+
/**
|
|
58
|
+
* Serialize a TxHeader per Minima's TxHeader.writeDataStream().
|
|
59
|
+
*
|
|
60
|
+
* @param txBodyHash SHA3-256 of the serialized TxBody (32 bytes)
|
|
61
|
+
* @param options Optional nonce and timeMilli overrides
|
|
62
|
+
*/
|
|
63
|
+
export function serializeTxHeader(txBodyHash, options) {
|
|
64
|
+
const nonce = options?.nonce ?? 0n;
|
|
65
|
+
const timeMilli = options?.timeMilli ?? BigInt(Date.now());
|
|
66
|
+
const parts = [];
|
|
67
|
+
parts.push(writeMiniNumber(nonce));
|
|
68
|
+
parts.push(writeMiniData(MAIN_NET_CHAIN_ID));
|
|
69
|
+
parts.push(writeMiniNumber(timeMilli));
|
|
70
|
+
parts.push(writeMiniNumber(0n));
|
|
71
|
+
parts.push(writeMiniData(MAX_HASH));
|
|
72
|
+
parts.push(new Uint8Array([CASCADE_LEVELS]));
|
|
73
|
+
parts.push(writeHashToStream(ZERO_HASH));
|
|
74
|
+
parts.push(writeHashToStream(ZERO_HASH));
|
|
75
|
+
parts.push(writeMiniNumber(0n));
|
|
76
|
+
parts.push(serializeMagic());
|
|
77
|
+
parts.push(writeHashToStream(ZERO_HASH));
|
|
78
|
+
parts.push(writeHashToStream(txBodyHash));
|
|
79
|
+
return concat(...parts);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Compute TxPoW ID = SHA3-256(TxHeader bytes).
|
|
83
|
+
* Matches Java: Crypto.getInstance().hashObject(mHeader) via SHA3Digest(256).
|
|
84
|
+
*/
|
|
85
|
+
export function computeTxPoWId(headerBytes) {
|
|
86
|
+
return sha3_256(headerBytes);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Build the empty burn transaction bytes.
|
|
90
|
+
*
|
|
91
|
+
* Equivalent to serializeTransaction({ linkHash: new Uint8Array([0x00]),
|
|
92
|
+
* inputs: [], outputs: [], state: [] })
|
|
93
|
+
* which produces:
|
|
94
|
+
* writeMiniNumber(0n) inputs count
|
|
95
|
+
* writeMiniNumber(0n) outputs count
|
|
96
|
+
* writeMiniNumber(0n) state count
|
|
97
|
+
* writeHashToStream([0x00]) linkHash = ZERO_TXPOWID (1 byte)
|
|
98
|
+
*/
|
|
99
|
+
function buildEmptyBurnTxBytes() {
|
|
100
|
+
return concat(writeMiniNumber(0n), writeMiniNumber(0n), writeMiniNumber(0n), writeHashToStream(new Uint8Array([0x00])));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Build the empty burn witness bytes.
|
|
104
|
+
*
|
|
105
|
+
* Equivalent to serializeWitness() with no arguments:
|
|
106
|
+
* writeMiniNumber(0n) signature count
|
|
107
|
+
* writeMiniNumber(0n) coinproof count
|
|
108
|
+
* writeMiniNumber(0n) scriptproof count
|
|
109
|
+
*/
|
|
110
|
+
function buildEmptyBurnWitnessBytes() {
|
|
111
|
+
return concat(writeMiniNumber(0n), writeMiniNumber(0n), writeMiniNumber(0n));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Serialize a TxBody per Minima's TxBody.writeDataStream().
|
|
115
|
+
*
|
|
116
|
+
* @param txBytes Pre-serialized Transaction bytes (from @totemsdk/core's serializeTransaction)
|
|
117
|
+
* @param witnessBytes Pre-serialized Witness bytes (from extension's serializeWitness)
|
|
118
|
+
* @param options Optional txnDifficulty override and test PRNG
|
|
119
|
+
*/
|
|
120
|
+
export function serializeTxBody(txBytes, witnessBytes, options) {
|
|
121
|
+
const txnDifficulty = options?.txnDifficulty ?? MAX_HASH;
|
|
122
|
+
let prng;
|
|
123
|
+
if (options?.prng) {
|
|
124
|
+
prng = options.prng;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
prng = new Uint8Array(32);
|
|
128
|
+
if (typeof crypto === 'undefined' || !crypto.getRandomValues) {
|
|
129
|
+
throw new Error('SECURITY: crypto.getRandomValues unavailable — cannot generate secure random bytes for transaction PRNG');
|
|
130
|
+
}
|
|
131
|
+
crypto.getRandomValues(prng);
|
|
132
|
+
}
|
|
133
|
+
return concat(writeHashToStream(prng), writeMiniData(txnDifficulty), txBytes, witnessBytes, buildEmptyBurnTxBytes(), buildEmptyBurnWitnessBytes(), writeMiniNumber(0n));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Serialize a complete TxPoW per Minima's TxPoW.writeDataStream().
|
|
137
|
+
*
|
|
138
|
+
* Assembles TxBody from pre-serialized tx+witness bytes, hashes the body to
|
|
139
|
+
* obtain mTxBodyHash, builds TxHeader with nonce=0, then concatenates:
|
|
140
|
+
* TxHeader | 0x01 (hasBody) | TxBody
|
|
141
|
+
*
|
|
142
|
+
* The returned bytes have nonce=0. Pass them to mineTxPoW() to find a valid
|
|
143
|
+
* nonce for local mining, or send as-is when MEG will re-mine.
|
|
144
|
+
*
|
|
145
|
+
* @param txBytes Pre-serialized Transaction bytes
|
|
146
|
+
* @param witnessBytes Pre-serialized Witness bytes
|
|
147
|
+
* @param options Optional txnDifficulty, nonce, timeMilli, prng
|
|
148
|
+
*/
|
|
149
|
+
export function serializeTxPoW(txBytes, witnessBytes, options) {
|
|
150
|
+
const txBody = serializeTxBody(txBytes, witnessBytes, options);
|
|
151
|
+
const txBodyHash = sha3_256(txBody);
|
|
152
|
+
return concat(serializeTxHeader(txBodyHash, options), new Uint8Array([0x01]), txBody);
|
|
153
|
+
}
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
export interface VerifyResult {
|
|
8
|
+
valid: boolean;
|
|
9
|
+
txpowId: string;
|
|
10
|
+
difficulty: string;
|
|
11
|
+
reason?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Verify that a TxPoW ID beats the stated difficulty target.
|
|
15
|
+
*
|
|
16
|
+
* valid = txpowId < mTxnDifficulty (big-endian 256-bit comparison)
|
|
17
|
+
*
|
|
18
|
+
* @param txpowId The 32-byte TxPoW ID (SHA3-256 of the header).
|
|
19
|
+
* @param mTxnDifficulty The 32-byte difficulty target from the TxBody.
|
|
20
|
+
*/
|
|
21
|
+
export declare function verifyProofOfWork(txpowId: Uint8Array, mTxnDifficulty: Uint8Array): VerifyResult;
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
import { isLessThan } from './mine.js';
|
|
8
|
+
function toHex(bytes) {
|
|
9
|
+
return '0x' + Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Verify that a TxPoW ID beats the stated difficulty target.
|
|
13
|
+
*
|
|
14
|
+
* valid = txpowId < mTxnDifficulty (big-endian 256-bit comparison)
|
|
15
|
+
*
|
|
16
|
+
* @param txpowId The 32-byte TxPoW ID (SHA3-256 of the header).
|
|
17
|
+
* @param mTxnDifficulty The 32-byte difficulty target from the TxBody.
|
|
18
|
+
*/
|
|
19
|
+
export function verifyProofOfWork(txpowId, mTxnDifficulty) {
|
|
20
|
+
if (txpowId.length !== 32) {
|
|
21
|
+
return {
|
|
22
|
+
valid: false,
|
|
23
|
+
txpowId: toHex(txpowId),
|
|
24
|
+
difficulty: toHex(mTxnDifficulty),
|
|
25
|
+
reason: `txpowId must be 32 bytes, got ${txpowId.length}`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (mTxnDifficulty.length !== 32) {
|
|
29
|
+
return {
|
|
30
|
+
valid: false,
|
|
31
|
+
txpowId: toHex(txpowId),
|
|
32
|
+
difficulty: toHex(mTxnDifficulty),
|
|
33
|
+
reason: `mTxnDifficulty must be 32 bytes, got ${mTxnDifficulty.length}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const valid = isLessThan(txpowId, mTxnDifficulty);
|
|
37
|
+
return {
|
|
38
|
+
valid,
|
|
39
|
+
txpowId: toHex(txpowId),
|
|
40
|
+
difficulty: toHex(mTxnDifficulty),
|
|
41
|
+
reason: valid ? undefined : 'txpowId ≥ mTxnDifficulty',
|
|
42
|
+
};
|
|
43
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@totemsdk/txpow",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TxPoW envelope serialization and proof-of-work mining for the Minima protocol",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"clean": "rm -rf dist",
|
|
17
|
+
"test": "jest --passWithNoTests"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@totemsdk/core": "^1.0.5"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@noble/hashes": ">=1.3.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"@noble/hashes": {
|
|
31
|
+
"optional": false
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@noble/hashes": "^1.3.0",
|
|
36
|
+
"@types/jest": "^29.0.0",
|
|
37
|
+
"@types/node": "^20.0.0",
|
|
38
|
+
"jest": "^29.0.0",
|
|
39
|
+
"ts-jest": "^29.0.0",
|
|
40
|
+
"typescript": "^5.0.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=16.0.0"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"license": "MIT"
|
|
49
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parity tests for @totemsdk/txpow serialization.
|
|
3
|
+
*
|
|
4
|
+
* These tests verify byte-exact output against known reference vectors derived
|
|
5
|
+
* from the extension's private serializeTxHeader / serializeTxBody / serializeTxPoW
|
|
6
|
+
* and from TxHeaderTests_1768991294482.java.
|
|
7
|
+
*
|
|
8
|
+
* GATE: serializeTxPoW output must be byte-identical to the extension's functions
|
|
9
|
+
* for the same inputs before the extension's private copies are deleted (Task #130).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { sha3_256 } from '@noble/hashes/sha3';
|
|
13
|
+
import {
|
|
14
|
+
serializeTxHeader,
|
|
15
|
+
serializeTxBody,
|
|
16
|
+
serializeTxPoW,
|
|
17
|
+
computeTxPoWId,
|
|
18
|
+
TX_POW_MIN_DIFFICULTY,
|
|
19
|
+
MAX_HASH,
|
|
20
|
+
ZERO_HASH,
|
|
21
|
+
CASCADE_LEVELS,
|
|
22
|
+
serializeMagic,
|
|
23
|
+
} from '../index.js';
|
|
24
|
+
import { writeMiniNumber, writeMiniData, writeHashToStream, concat } from '@totemsdk/core';
|
|
25
|
+
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// Helpers
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function bytesToHex(b: Uint8Array): string {
|
|
31
|
+
return Array.from(b).map(x => x.toString(16).padStart(2, '0')).join('');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hexToBytes(hex: string): Uint8Array {
|
|
35
|
+
const h = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
36
|
+
const out = new Uint8Array(h.length / 2);
|
|
37
|
+
for (let i = 0; i < h.length; i += 2) out[i / 2] = parseInt(h.slice(i, i + 2), 16);
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Known 32-byte body hash (all 0xAB for determinism)
|
|
42
|
+
const MOCK_BODY_HASH = new Uint8Array(32).fill(0xab);
|
|
43
|
+
|
|
44
|
+
// Deterministic PRNG for test reproducibility
|
|
45
|
+
const MOCK_PRNG = new Uint8Array(32).fill(0xcd);
|
|
46
|
+
|
|
47
|
+
// Empty pre-serialized transaction and witness (smallest valid inputs)
|
|
48
|
+
const EMPTY_TX_BYTES = concat(
|
|
49
|
+
writeMiniNumber(0n),
|
|
50
|
+
writeMiniNumber(0n),
|
|
51
|
+
writeMiniNumber(0n),
|
|
52
|
+
writeHashToStream(new Uint8Array([0x00]))
|
|
53
|
+
);
|
|
54
|
+
const EMPTY_WITNESS_BYTES = concat(
|
|
55
|
+
writeMiniNumber(0n),
|
|
56
|
+
writeMiniNumber(0n),
|
|
57
|
+
writeMiniNumber(0n)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
// TX_POW_MIN_DIFFICULTY constant
|
|
62
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
describe('TX_POW_MIN_DIFFICULTY', () => {
|
|
65
|
+
it('is 32 bytes', () => {
|
|
66
|
+
expect(TX_POW_MIN_DIFFICULTY.length).toBe(32);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('is less than MAX_HASH (big-endian 256-bit compare)', () => {
|
|
70
|
+
for (let i = 0; i < 32; i++) {
|
|
71
|
+
if (TX_POW_MIN_DIFFICULTY[i] < MAX_HASH[i]) return;
|
|
72
|
+
if (TX_POW_MIN_DIFFICULTY[i] > MAX_HASH[i]) throw new Error('TX_POW_MIN_DIFFICULTY > MAX_HASH');
|
|
73
|
+
}
|
|
74
|
+
throw new Error('TX_POW_MIN_DIFFICULTY === MAX_HASH');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('equals floor((2^256 - 1) / 1_000_000)', () => {
|
|
78
|
+
const expected = ((2n ** 256n) - 1n) / 1_000_000n;
|
|
79
|
+
let hex = expected.toString(16);
|
|
80
|
+
if (hex.length % 2 !== 0) hex = '0' + hex;
|
|
81
|
+
while (hex.length < 64) hex = '00' + hex;
|
|
82
|
+
expect(bytesToHex(TX_POW_MIN_DIFFICULTY)).toBe(hex);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
87
|
+
// computeTxPoWId
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
describe('computeTxPoWId', () => {
|
|
91
|
+
it('returns SHA3-256 of the provided bytes', () => {
|
|
92
|
+
const input = new Uint8Array([1, 2, 3, 4]);
|
|
93
|
+
const result = computeTxPoWId(input);
|
|
94
|
+
const expected = sha3_256(input);
|
|
95
|
+
expect(result).toEqual(expected);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('returns 32 bytes', () => {
|
|
99
|
+
const result = computeTxPoWId(MOCK_BODY_HASH);
|
|
100
|
+
expect(result.length).toBe(32);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
// serializeMagic
|
|
106
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
describe('serializeMagic', () => {
|
|
109
|
+
it('produces a non-empty byte array', () => {
|
|
110
|
+
const magic = serializeMagic();
|
|
111
|
+
expect(magic.length).toBeGreaterThan(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('contains expected MiniNumber values for maxTxPoWSize=65536', () => {
|
|
115
|
+
const magic = serializeMagic();
|
|
116
|
+
const hex = bytesToHex(magic);
|
|
117
|
+
// writeMiniNumber(65536n): scale=0 [00], len=3 [03], bytes=[01,00,00]
|
|
118
|
+
// so "000301 0000" — confirm first 6 bytes contain expected value
|
|
119
|
+
expect(hex.startsWith('000301')).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('is deterministic', () => {
|
|
123
|
+
expect(serializeMagic()).toEqual(serializeMagic());
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
// serializeTxHeader
|
|
129
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
describe('serializeTxHeader', () => {
|
|
132
|
+
const FIXED_TIME = 1700000000000n;
|
|
133
|
+
const FIXED_NONCE = 0n;
|
|
134
|
+
|
|
135
|
+
it('is deterministic for fixed nonce + timeMilli', () => {
|
|
136
|
+
const h1 = serializeTxHeader(MOCK_BODY_HASH, { nonce: FIXED_NONCE, timeMilli: FIXED_TIME });
|
|
137
|
+
const h2 = serializeTxHeader(MOCK_BODY_HASH, { nonce: FIXED_NONCE, timeMilli: FIXED_TIME });
|
|
138
|
+
expect(h1).toEqual(h2);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('starts with nonce=0 MiniNumber [00 01 00]', () => {
|
|
142
|
+
const h = serializeTxHeader(MOCK_BODY_HASH, { nonce: 0n, timeMilli: FIXED_TIME });
|
|
143
|
+
expect(Array.from(h.slice(0, 3))).toEqual([0x00, 0x01, 0x00]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('starts with nonce=1 MiniNumber [00 01 01] for nonce=1', () => {
|
|
147
|
+
const h = serializeTxHeader(MOCK_BODY_HASH, { nonce: 1n, timeMilli: FIXED_TIME });
|
|
148
|
+
expect(Array.from(h.slice(0, 3))).toEqual([0x00, 0x01, 0x01]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('encodes nonce=128 with 4 bytes (MiniNumber boundary)', () => {
|
|
152
|
+
const h = serializeTxHeader(MOCK_BODY_HASH, { nonce: 128n, timeMilli: FIXED_TIME });
|
|
153
|
+
// writeMiniNumber(128n): scale=0 [00], len=2 [02], bytes=[00,80]
|
|
154
|
+
expect(Array.from(h.slice(0, 4))).toEqual([0x00, 0x02, 0x00, 0x80]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('encodes nonce=32768 with 5 bytes (MiniNumber boundary)', () => {
|
|
158
|
+
const h = serializeTxHeader(MOCK_BODY_HASH, { nonce: 32768n, timeMilli: FIXED_TIME });
|
|
159
|
+
// writeMiniNumber(32768n): scale=0 [00], len=3 [03], bytes=[00,80,00]
|
|
160
|
+
expect(Array.from(h.slice(0, 5))).toEqual([0x00, 0x03, 0x00, 0x80, 0x00]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('includes CASCADE_LEVELS=32 as single byte in super-parents section', () => {
|
|
164
|
+
const h = serializeTxHeader(MOCK_BODY_HASH, { nonce: 0n, timeMilli: FIXED_TIME });
|
|
165
|
+
const hex = bytesToHex(h);
|
|
166
|
+
// After nonce[3] + chainID[5] + timeMilli[?] + blockNum[3] + blockDiff[36] = starts with 00 01 00
|
|
167
|
+
// We check that the byte 0x20 (=32=CASCADE_LEVELS) appears exactly once in isolation
|
|
168
|
+
expect(h).toContain(CASCADE_LEVELS);
|
|
169
|
+
const idx = h.indexOf(CASCADE_LEVELS);
|
|
170
|
+
expect(idx).toBeGreaterThan(0);
|
|
171
|
+
expect(hex).toContain('20'); // 0x20 = 32
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('ends with the txBodyHash via writeHashToStream (last 36 bytes = 4-byte len + 32 bytes)', () => {
|
|
175
|
+
const h = serializeTxHeader(MOCK_BODY_HASH, { nonce: 0n, timeMilli: FIXED_TIME });
|
|
176
|
+
const tail = h.slice(h.length - 36);
|
|
177
|
+
// writeHashToStream produces: 00 00 00 20 + 32 bytes
|
|
178
|
+
expect(Array.from(tail.slice(0, 4))).toEqual([0x00, 0x00, 0x00, 0x20]);
|
|
179
|
+
expect(tail.slice(4)).toEqual(MOCK_BODY_HASH);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('changes when nonce changes (mining loop property)', () => {
|
|
183
|
+
const h0 = serializeTxHeader(MOCK_BODY_HASH, { nonce: 0n, timeMilli: FIXED_TIME });
|
|
184
|
+
const h1 = serializeTxHeader(MOCK_BODY_HASH, { nonce: 1n, timeMilli: FIXED_TIME });
|
|
185
|
+
expect(h0).not.toEqual(h1);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
190
|
+
// serializeTxBody
|
|
191
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
describe('serializeTxBody', () => {
|
|
194
|
+
it('is deterministic when prng is provided', () => {
|
|
195
|
+
const b1 = serializeTxBody(EMPTY_TX_BYTES, EMPTY_WITNESS_BYTES, { prng: MOCK_PRNG });
|
|
196
|
+
const b2 = serializeTxBody(EMPTY_TX_BYTES, EMPTY_WITNESS_BYTES, { prng: MOCK_PRNG });
|
|
197
|
+
expect(b1).toEqual(b2);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('starts with writeHashToStream(prng): [00 00 00 20] + 32 prng bytes', () => {
|
|
201
|
+
const body = serializeTxBody(EMPTY_TX_BYTES, EMPTY_WITNESS_BYTES, { prng: MOCK_PRNG });
|
|
202
|
+
expect(Array.from(body.slice(0, 4))).toEqual([0x00, 0x00, 0x00, 0x20]);
|
|
203
|
+
expect(body.slice(4, 36)).toEqual(MOCK_PRNG);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('uses MAX_HASH as default txnDifficulty (36 bytes after PRNG)', () => {
|
|
207
|
+
const body = serializeTxBody(EMPTY_TX_BYTES, EMPTY_WITNESS_BYTES, { prng: MOCK_PRNG });
|
|
208
|
+
// After PRNG (36 bytes): writeMiniData(MAX_HASH) = [00 00 00 20] + 32×FF
|
|
209
|
+
const diffStart = 36;
|
|
210
|
+
expect(Array.from(body.slice(diffStart, diffStart + 4))).toEqual([0x00, 0x00, 0x00, 0x20]);
|
|
211
|
+
expect(Array.from(body.slice(diffStart + 4, diffStart + 36))).toEqual(Array(32).fill(0xff));
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('uses provided txnDifficulty correctly', () => {
|
|
215
|
+
const customDiff = new Uint8Array(32).fill(0x11);
|
|
216
|
+
const body = serializeTxBody(EMPTY_TX_BYTES, EMPTY_WITNESS_BYTES, {
|
|
217
|
+
prng: MOCK_PRNG,
|
|
218
|
+
txnDifficulty: customDiff,
|
|
219
|
+
});
|
|
220
|
+
const diffStart = 36;
|
|
221
|
+
expect(Array.from(body.slice(diffStart + 4, diffStart + 36))).toEqual(Array(32).fill(0x11));
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('includes the pre-serialized txBytes verbatim', () => {
|
|
225
|
+
const mockTx = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
|
|
226
|
+
const body = serializeTxBody(mockTx, EMPTY_WITNESS_BYTES, { prng: MOCK_PRNG });
|
|
227
|
+
const bodyHex = bytesToHex(body);
|
|
228
|
+
expect(bodyHex).toContain('deadbeef');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('includes the pre-serialized witnessBytes verbatim', () => {
|
|
232
|
+
const mockWitness = new Uint8Array([0xca, 0xfe, 0xba, 0xbe]);
|
|
233
|
+
const body = serializeTxBody(EMPTY_TX_BYTES, mockWitness, { prng: MOCK_PRNG });
|
|
234
|
+
const bodyHex = bytesToHex(body);
|
|
235
|
+
expect(bodyHex).toContain('cafebabe');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('ends with MiniNumber(0) for empty txpowid list', () => {
|
|
239
|
+
const body = serializeTxBody(EMPTY_TX_BYTES, EMPTY_WITNESS_BYTES, { prng: MOCK_PRNG });
|
|
240
|
+
// Last 3 bytes: writeMiniNumber(0n) = [00, 01, 00]
|
|
241
|
+
expect(Array.from(body.slice(body.length - 3))).toEqual([0x00, 0x01, 0x00]);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
246
|
+
// serializeTxPoW — structure tests
|
|
247
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
describe('serializeTxPoW', () => {
|
|
250
|
+
it('byte at header.length is 0x01 (hasBody flag)', () => {
|
|
251
|
+
const FIXED_TIME = 1700000000000n;
|
|
252
|
+
const body = serializeTxBody(EMPTY_TX_BYTES, EMPTY_WITNESS_BYTES, { prng: MOCK_PRNG });
|
|
253
|
+
const bodyHash = sha3_256(body);
|
|
254
|
+
const header = serializeTxHeader(bodyHash, { nonce: 0n, timeMilli: FIXED_TIME });
|
|
255
|
+
|
|
256
|
+
const txpow = serializeTxPoW(EMPTY_TX_BYTES, EMPTY_WITNESS_BYTES, {
|
|
257
|
+
nonce: 0n,
|
|
258
|
+
timeMilli: FIXED_TIME,
|
|
259
|
+
prng: MOCK_PRNG,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
expect(txpow[header.length]).toBe(0x01);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('header prefix matches standalone serializeTxHeader output', () => {
|
|
266
|
+
const FIXED_TIME = 1700000000000n;
|
|
267
|
+
const body = serializeTxBody(EMPTY_TX_BYTES, EMPTY_WITNESS_BYTES, { prng: MOCK_PRNG });
|
|
268
|
+
const bodyHash = sha3_256(body);
|
|
269
|
+
const expectedHeader = serializeTxHeader(bodyHash, { nonce: 0n, timeMilli: FIXED_TIME });
|
|
270
|
+
|
|
271
|
+
const txpow = serializeTxPoW(EMPTY_TX_BYTES, EMPTY_WITNESS_BYTES, {
|
|
272
|
+
nonce: 0n,
|
|
273
|
+
timeMilli: FIXED_TIME,
|
|
274
|
+
prng: MOCK_PRNG,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
expect(txpow.slice(0, expectedHeader.length)).toEqual(expectedHeader);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('txpowId derived from first headerLen bytes matches computeTxPoWId', () => {
|
|
281
|
+
const FIXED_TIME = 1700000000000n;
|
|
282
|
+
const body = serializeTxBody(EMPTY_TX_BYTES, EMPTY_WITNESS_BYTES, { prng: MOCK_PRNG });
|
|
283
|
+
const bodyHash = sha3_256(body);
|
|
284
|
+
const header = serializeTxHeader(bodyHash, { nonce: 0n, timeMilli: FIXED_TIME });
|
|
285
|
+
|
|
286
|
+
const txpow = serializeTxPoW(EMPTY_TX_BYTES, EMPTY_WITNESS_BYTES, {
|
|
287
|
+
nonce: 0n,
|
|
288
|
+
timeMilli: FIXED_TIME,
|
|
289
|
+
prng: MOCK_PRNG,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const idFromTxPoW = computeTxPoWId(txpow.slice(0, header.length));
|
|
293
|
+
const idFromHeader = computeTxPoWId(header);
|
|
294
|
+
expect(idFromTxPoW).toEqual(idFromHeader);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
299
|
+
// Nonce MiniNumber encoding boundaries
|
|
300
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
describe('nonce MiniNumber encoding boundaries', () => {
|
|
303
|
+
const BODY_HASH = new Uint8Array(32).fill(0x77);
|
|
304
|
+
const TIME = 1700000000000n;
|
|
305
|
+
|
|
306
|
+
it('nonce=0 encodes as 3 bytes [00 01 00]', () => {
|
|
307
|
+
const n = writeMiniNumber(0n);
|
|
308
|
+
expect(n.length).toBe(3);
|
|
309
|
+
expect(Array.from(n)).toEqual([0x00, 0x01, 0x00]);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('nonce=127 encodes as 3 bytes (still 1-byte unscaled)', () => {
|
|
313
|
+
const n = writeMiniNumber(127n);
|
|
314
|
+
expect(n.length).toBe(3);
|
|
315
|
+
expect(Array.from(n)).toEqual([0x00, 0x01, 0x7f]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('nonce=128 encodes as 4 bytes (2-byte unscaled: leading zero + 0x80)', () => {
|
|
319
|
+
const n = writeMiniNumber(128n);
|
|
320
|
+
expect(n.length).toBe(4);
|
|
321
|
+
expect(Array.from(n)).toEqual([0x00, 0x02, 0x00, 0x80]);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('nonce=32767 encodes as 4 bytes', () => {
|
|
325
|
+
const n = writeMiniNumber(32767n);
|
|
326
|
+
expect(n.length).toBe(4);
|
|
327
|
+
expect(Array.from(n)).toEqual([0x00, 0x02, 0x7f, 0xff]);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('nonce=32768 encodes as 5 bytes (3-byte unscaled: leading zero + 0x80 0x00)', () => {
|
|
331
|
+
const n = writeMiniNumber(32768n);
|
|
332
|
+
expect(n.length).toBe(5);
|
|
333
|
+
expect(Array.from(n)).toEqual([0x00, 0x03, 0x00, 0x80, 0x00]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('header byte length changes at nonce 128 boundary', () => {
|
|
337
|
+
const h127 = serializeTxHeader(BODY_HASH, { nonce: 127n, timeMilli: TIME });
|
|
338
|
+
const h128 = serializeTxHeader(BODY_HASH, { nonce: 128n, timeMilli: TIME });
|
|
339
|
+
expect(h128.length).toBe(h127.length + 1);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('header byte length changes at nonce 32768 boundary', () => {
|
|
343
|
+
const h32767 = serializeTxHeader(BODY_HASH, { nonce: 32767n, timeMilli: TIME });
|
|
344
|
+
const h32768 = serializeTxHeader(BODY_HASH, { nonce: 32768n, timeMilli: TIME });
|
|
345
|
+
expect(h32768.length).toBe(h32767.length + 1);
|
|
346
|
+
});
|
|
347
|
+
});
|