Skip to content

Commit f65e7c4

Browse files
committed
Fix XTM-T submit blob layout
1 parent 0b5c10d commit f65e7c4

3 files changed

Lines changed: 207 additions & 25 deletions

File tree

lib/coins/core/factories.js

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
"use strict";
22
const { arr2hex, calcErgReward, calcEthReward } = require("../helpers.js");
33
const COIN_PROFILE_SYMBOL = Symbol.for("nodejs-pool.coinProfile");
4+
const XTM_T_MINING_HASH_OFFSET = 3;
5+
const XTM_T_MINING_HASH_SIZE = 32;
6+
const XTM_T_NONCE_OFFSET = XTM_T_MINING_HASH_OFFSET + XTM_T_MINING_HASH_SIZE;
7+
const XTM_T_NONCE_SIZE = 8;
8+
const XTM_T_MINER_NONCE_OFFSET = XTM_T_NONCE_OFFSET + 4;
9+
const XTM_T_POW_ALGO_OFFSET = XTM_T_NONCE_OFFSET + XTM_T_NONCE_SIZE;
10+
const XTM_T_POW_DATA_OFFSET = XTM_T_POW_ALGO_OFFSET + 1;
11+
const XTM_T_POW_DATA_SIZE = 32;
12+
const XTM_T_RANDOMXT_POW_ALGO = 2;
413

514
function cloneValue(value) {
615
if (Array.isArray(value)) return value.slice();
716
if (value && typeof value === "object" && !Buffer.isBuffer(value)) return Object.assign({}, value);
817
return value;
918
}
1019

20+
function cloneRpcTemplate(value) {
21+
return JSON.parse(JSON.stringify(value));
22+
}
23+
1124
function mergeSection(base, overrides) {
1225
const section = Object.assign({}, base);
1326
if (!overrides) return section;
@@ -463,9 +476,16 @@ function createXtmTRpc(overrides) {
463476
}
464477
const result = body.result;
465478
return ctx.callback({
466-
blocktemplate_blob: "00".repeat(3) + arr2hex(result.merge_mining_hash) + "00".repeat(8) + "02" + "00".repeat(32),
479+
blocktemplate_blob: "00".repeat(XTM_T_MINING_HASH_OFFSET)
480+
+ arr2hex(result.merge_mining_hash)
481+
+ "00".repeat(XTM_T_NONCE_SIZE)
482+
+ XTM_T_RANDOMXT_POW_ALGO.toString(16).padStart(2, "0")
483+
+ "00".repeat(XTM_T_POW_DATA_SIZE),
467484
seed_hash: arr2hex(result.vm_key),
468-
reserved_offset: 44,
485+
// Tari RandomXT's 76-byte mining blob reserves bytes 35..39 as
486+
// the high half of the big-endian u64 nonce. Keep pool
487+
// uniqueness there; pow_data must stay consensus data.
488+
reserved_offset: XTM_T_NONCE_OFFSET,
469489
difficulty: parseInt(result.miner_data.target_difficulty, 10),
470490
reward: parseInt(result.miner_data.reward, 10),
471491
height: parseInt(result.block.header.height, 10),
@@ -655,7 +675,7 @@ const blob = {
655675
return createBlob({
656676
nonceSize: 4,
657677
proofSize: 32,
658-
nonceOffset: 39,
678+
nonceOffset: XTM_T_MINER_NONCE_OFFSET,
659679
convert(ctx) {
660680
return Buffer.from(ctx.blobBuffer);
661681
},
@@ -1216,15 +1236,20 @@ function submitDeroBlock(ctx) {
12161236
}
12171237

12181238
function submitXtmRxBlock(ctx) {
1219-
ctx.blockTemplate.xtm_block.header.nonce = ctx.blockData.readUInt32BE(3 + 32 + 4).toString();
1220-
ctx.blockTemplate.xtm_block.header.pow.pow_data = [...ctx.blockData.slice(3 + 32 + 8 + 1)];
1221-
ctx.support.rpcPortDaemon(ctx.blockTemplate.port, "SubmitBlock", ctx.blockTemplate.xtm_block, ctx.replyFn, true);
1239+
const xtmBlock = cloneRpcTemplate(ctx.blockTemplate.xtm_block);
1240+
const powData = ctx.blockData.slice(XTM_T_POW_DATA_OFFSET);
1241+
1242+
xtmBlock.header.nonce = readUInt64BufferBE(ctx.blockData, XTM_T_NONCE_OFFSET);
1243+
xtmBlock.header.pow.pow_data = powData.every((byte) => byte === 0) ? [] : [...powData];
1244+
ctx.support.rpcPortDaemon(ctx.blockTemplate.port, "SubmitBlock", xtmBlock, ctx.replyFn, true);
12221245
}
12231246

12241247
function submitXtmCBlock(ctx) {
1225-
ctx.blockTemplate.xtm_block.header.nonce = readUInt64BufferBE(ctx.blockData, 0);
1226-
ctx.blockTemplate.xtm_block.header.pow.pow_data = ctx.job.c29_packed_edges;
1227-
ctx.support.rpcPortDaemon(ctx.blockTemplate.port, "SubmitBlock", ctx.blockTemplate.xtm_block, ctx.replyFn, true);
1248+
const xtmBlock = cloneRpcTemplate(ctx.blockTemplate.xtm_block);
1249+
1250+
xtmBlock.header.nonce = readUInt64BufferBE(ctx.blockData, 0);
1251+
xtmBlock.header.pow.pow_data = ctx.job.c29_packed_edges;
1252+
ctx.support.rpcPortDaemon(ctx.blockTemplate.port, "SubmitBlock", xtmBlock, ctx.replyFn, true);
12281253
}
12291254

12301255
function submitDualMainBlock(ctx) {

tests/pool/coin/basics.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,35 @@ test("BlockTemplate keeps main-template nonce layout stable across nextBlobHex c
150150
);
151151
});
152152

153+
test("BlockTemplate keeps XTM-T pool nonce in the Tari nonce prefix", () => {
154+
const coinFuncs = global.coinFuncs.__realCoinFuncs;
155+
const blob = Buffer.concat([
156+
Buffer.alloc(3),
157+
Buffer.alloc(32, 0x11),
158+
Buffer.alloc(8),
159+
Buffer.from([0x02]),
160+
Buffer.alloc(32)
161+
]);
162+
const blockTemplate = new coinFuncs.BlockTemplate({
163+
blocktemplate_blob: blob.toString("hex"),
164+
coin: "XTM-T",
165+
difficulty: 1,
166+
height: 302,
167+
port: 18146,
168+
reserved_offset: 35,
169+
reward: 1,
170+
seed_hash: "22".repeat(32),
171+
xtm_block: { header: { nonce: "0", pow: { pow_data: [] } } }
172+
});
173+
174+
const nextBlob = Buffer.from(blockTemplate.nextBlobHex(), "hex");
175+
176+
assert.equal(blockTemplate.extraNonce, 1);
177+
assert.equal(nextBlob.readUInt32BE(35), 1);
178+
assert.equal(nextBlob[43], 0x02);
179+
assert.equal(nextBlob.subarray(44).equals(Buffer.alloc(32)), true);
180+
});
181+
153182
test("BlockTemplate uses the SAL blob marker when daemon reserved offset is stale", () => {
154183
const coinFuncs = global.coinFuncs.__realCoinFuncs;
155184
const marker = Buffer.concat([Buffer.from([0x02, 17]), Buffer.alloc(17, 0)]);

tests/pool/coin/submitters.js

Lines changed: 144 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,22 @@ const {
1515

1616
const REAL_ETH_STYLE_PORT = 8645;
1717
const INSTANCE_ID_PID_MASK = (1 << 22) - 1;
18+
const XTM_T_MINING_HASH_OFFSET = 3;
19+
const XTM_T_MINING_HASH_SIZE = 32;
20+
const XTM_T_NONCE_OFFSET = XTM_T_MINING_HASH_OFFSET + XTM_T_MINING_HASH_SIZE;
21+
const XTM_T_NONCE_SIZE = 8;
22+
const XTM_T_MINER_NONCE_OFFSET = XTM_T_NONCE_OFFSET + 4;
23+
const XTM_T_POW_ALGO_OFFSET = XTM_T_NONCE_OFFSET + XTM_T_NONCE_SIZE;
24+
const XTM_T_POW_DATA_OFFSET = XTM_T_POW_ALGO_OFFSET + 1;
25+
const XTM_T_POW_DATA_SIZE = 32;
26+
const XTM_T_RANDOMXT_POW_ALGO = 2;
27+
const TARI_SOURCE_CANDIDATES = [
28+
process.env.POOL_TEST_TARI_SOURCE_DIR,
29+
process.env.TARI_SOURCE_DIR,
30+
"/usr/local/src/tari",
31+
"/usr/local/src/tari-src",
32+
process.env.HOME ? path.join(process.env.HOME, "tari") : ""
33+
].filter(Boolean);
1834

1935
function withPatchedPid(pid, fn) {
2036
const originalDescriptor = Object.getOwnPropertyDescriptor(process, "pid");
@@ -56,6 +72,42 @@ function createInstanceIdWord(poolId, pid) {
5672
}
5773
}
5874

75+
function createXtmTMiningBlob(miningHash, nonce, powData) {
76+
const powBytes = Buffer.alloc(XTM_T_POW_DATA_SIZE);
77+
if (powData) Buffer.from(powData).copy(powBytes, 0, 0, XTM_T_POW_DATA_SIZE);
78+
const blob = Buffer.concat([
79+
Buffer.alloc(XTM_T_MINING_HASH_OFFSET),
80+
miningHash,
81+
Buffer.alloc(XTM_T_NONCE_SIZE),
82+
Buffer.from([XTM_T_RANDOMXT_POW_ALGO]),
83+
powBytes
84+
]);
85+
86+
blob.writeBigUInt64BE(BigInt(nonce), XTM_T_NONCE_OFFSET);
87+
return blob;
88+
}
89+
90+
function reconstructXtmTMiningBlob(miningHash, submittedBlock) {
91+
const powData = submittedBlock.header.pow && submittedBlock.header.pow.pow_data
92+
? submittedBlock.header.pow.pow_data
93+
: [];
94+
return createXtmTMiningBlob(miningHash, BigInt(submittedBlock.header.nonce), powData);
95+
}
96+
97+
function findTariSourceFile(relativePath) {
98+
for (const root of TARI_SOURCE_CANDIDATES) {
99+
const candidate = path.join(root, relativePath);
100+
if (fs.existsSync(candidate)) return candidate;
101+
}
102+
return null;
103+
}
104+
105+
function readRustIntegerConst(source, name) {
106+
const match = source.match(new RegExp("\\bconst\\s+" + name + "\\s*:\\s*[^=]+\\s*=\\s*(\\d+)\\s*;"));
107+
assert.notEqual(match, null, "missing Tari source const " + name);
108+
return Number(match[1]);
109+
}
110+
59111
test.describe("pool coin helpers: submitters", { concurrency: false }, () => {
60112
test.beforeEach(() => {
61113
installTestGlobals();
@@ -377,11 +429,78 @@ test("erg mining.submit parser falls back to nonce suffix for standard submits",
377429
assert.equal(params.nonce, "5b3ec51d640c");
378430
});
379431

432+
test("xtm-t SubmitBlock payload roundtrips the miner's Tari RandomXT blob", () => {
433+
const coinFuncs = global.coinFuncs.__realCoinFuncs;
434+
const xtmTPool = coinFuncs.getPoolSettings("XTM-T");
435+
const miningHash = Buffer.from("31".repeat(XTM_T_MINING_HASH_SIZE), "hex");
436+
const xtmBlock = {
437+
header: {
438+
nonce: "0",
439+
pow: { pow_data: [7, 7, 7] }
440+
}
441+
};
442+
const blockTemplate = new coinFuncs.BlockTemplate({
443+
blocktemplate_blob: createXtmTMiningBlob(miningHash, 0n).toString("hex"),
444+
coin: "XTM-T",
445+
difficulty: 1,
446+
height: 271620,
447+
port: 18146,
448+
reserved_offset: XTM_T_NONCE_OFFSET,
449+
reward: 1,
450+
seed_hash: "22".repeat(32),
451+
xtm_block: xtmBlock
452+
});
453+
const calls = [];
454+
const minerBlob = Buffer.from(blockTemplate.nextBlobHex(), "hex");
455+
456+
minerBlob.writeUInt32BE(0x01020304, XTM_T_MINER_NONCE_OFFSET);
457+
458+
xtmTPool.submitBlockRpc.call(xtmTPool, {
459+
blockData: minerBlob,
460+
blockTemplate,
461+
replyFn() {},
462+
support: {
463+
rpcPortDaemon(port, method, params) {
464+
calls.push({ port, method, params });
465+
}
466+
}
467+
});
468+
469+
assert.equal(calls.length, 1);
470+
assert.equal(calls[0].port, 18146);
471+
assert.equal(calls[0].method, "SubmitBlock");
472+
assert.equal(calls[0].params.header.nonce, minerBlob.readBigUInt64BE(XTM_T_NONCE_OFFSET).toString(10));
473+
assert.deepEqual(calls[0].params.header.pow.pow_data, []);
474+
assert.equal(reconstructXtmTMiningBlob(miningHash, calls[0].params).equals(minerBlob), true);
475+
assert.notStrictEqual(calls[0].params, xtmBlock);
476+
assert.equal(xtmBlock.header.nonce, "0");
477+
assert.deepEqual(xtmBlock.header.pow.pow_data, [7, 7, 7]);
478+
});
479+
480+
test("xtm-t layout matches Tari source constants when source is available", (t) => {
481+
const innerPath = findTariSourceFile(path.join("applications", "minotari_node", "src", "xmrig_proxy", "inner.rs"));
482+
if (!innerPath) {
483+
t.skip("Tari source tree not available; set POOL_TEST_TARI_SOURCE_DIR to enable this contract check.");
484+
return;
485+
}
486+
487+
const innerSource = fs.readFileSync(innerPath, "utf8");
488+
assert.equal(readRustIntegerConst(innerSource, "TARI_BLOB_RESERVED_OFFSET"), XTM_T_NONCE_OFFSET);
489+
assert.equal(readRustIntegerConst(innerSource, "TARI_MINING_BLOB_SIZE"), XTM_T_POW_DATA_OFFSET + XTM_T_POW_DATA_SIZE);
490+
assert.equal(readRustIntegerConst(innerSource, "POW_ALGO_RANDOMXT"), XTM_T_RANDOMXT_POW_ALGO);
491+
492+
const helpersPath = findTariSourceFile(path.join("base_layer", "core", "src", "proof_of_work", "monero_rx", "helpers.rs"));
493+
if (!helpersPath) return;
494+
const helpersSource = fs.readFileSync(helpersPath, "utf8");
495+
assert.match(helpersSource, /nonce\.to_be_bytes\(\)/);
496+
assert.match(helpersSource, /pow\.to_bytes\(\)/);
497+
});
498+
380499
test("xtm submit and verify handlers preserve the pre-refactor special-case tari semantics", () => {
381500
const coinFuncs = global.coinFuncs.__realCoinFuncs;
382501
const xtmTPool = coinFuncs.getPoolSettings("XTM-T");
383502
const xtmCPool = coinFuncs.getPoolSettings("XTM-C");
384-
const blockDataRx = Buffer.alloc(48, 0);
503+
const blockDataRx = Buffer.alloc(76, 0);
385504
const blockDataC29 = Buffer.alloc(8, 0);
386505
const xtmRxCalls = [];
387506
const xtmC29Calls = [];
@@ -394,9 +513,22 @@ test("xtm submit and verify handlers preserve the pre-refactor special-case tari
394513
Buffer.from("aabbccdd", "hex")
395514
]);
396515
const job = { blob_type_num: 107 };
516+
const xtmRxBlock = {
517+
header: {
518+
nonce: "",
519+
pow: { pow_data: [99] }
520+
}
521+
};
522+
const xtmC29Block = {
523+
header: {
524+
nonce: "",
525+
pow: { pow_data: [88] }
526+
}
527+
};
397528

529+
blockDataRx.writeUInt32BE(7, 3 + 32);
398530
blockDataRx.writeUInt32BE(1234, 3 + 32 + 4);
399-
Buffer.from([9, 8, 7, 6]).copy(blockDataRx, 3 + 32 + 8 + 1);
531+
blockDataRx[3 + 32 + 8] = 2;
400532
blockDataC29.writeBigUInt64BE(15n, 0);
401533

402534
xtmTPool.resolveSubmittedBlockHash({
@@ -413,12 +545,7 @@ test("xtm submit and verify handlers preserve the pre-refactor special-case tari
413545
blockData: blockDataRx,
414546
blockTemplate: {
415547
port: 18146,
416-
xtm_block: {
417-
header: {
418-
nonce: "",
419-
pow: { pow_data: [] }
420-
}
421-
}
548+
xtm_block: xtmRxBlock
422549
},
423550
replyFn() {},
424551
support: {
@@ -498,12 +625,7 @@ test("xtm submit and verify handlers preserve the pre-refactor special-case tari
498625
blockData: blockDataC29,
499626
blockTemplate: {
500627
port: 18148,
501-
xtm_block: {
502-
header: {
503-
nonce: "",
504-
pow: { pow_data: [] }
505-
}
506-
}
628+
xtm_block: xtmC29Block
507629
},
508630
job,
509631
replyFn() {},
@@ -525,8 +647,11 @@ test("xtm submit and verify handlers preserve the pre-refactor special-case tari
525647
assert.equal(xtmRxCalls.length, 1);
526648
assert.equal(xtmRxCalls[0].port, 18146);
527649
assert.equal(xtmRxCalls[0].method, "SubmitBlock");
528-
assert.equal(xtmRxCalls[0].params.header.nonce, "1234");
529-
assert.deepEqual(xtmRxCalls[0].params.header.pow.pow_data, [9, 8, 7, 6]);
650+
assert.equal(xtmRxCalls[0].params.header.nonce, ((7n << 32n) | 1234n).toString(10));
651+
assert.deepEqual(xtmRxCalls[0].params.header.pow.pow_data, []);
652+
assert.notStrictEqual(xtmRxCalls[0].params, xtmRxBlock);
653+
assert.equal(xtmRxBlock.header.nonce, "");
654+
assert.deepEqual(xtmRxBlock.header.pow.pow_data, [99]);
530655

531656
assert.deepEqual(job.c29_packed_edges, [0xab, 0xcd]);
532657
assert.equal(verifyArgs[0], 77n);
@@ -540,5 +665,8 @@ test("xtm submit and verify handlers preserve the pre-refactor special-case tari
540665
assert.equal(xtmC29Calls[0].method, "SubmitBlock");
541666
assert.equal(xtmC29Calls[0].params.header.nonce, "15");
542667
assert.deepEqual(xtmC29Calls[0].params.header.pow.pow_data, [0xab, 0xcd]);
668+
assert.notStrictEqual(xtmC29Calls[0].params, xtmC29Block);
669+
assert.equal(xtmC29Block.header.nonce, "");
670+
assert.deepEqual(xtmC29Block.header.pow.pow_data, [88]);
543671
});
544672
});

0 commit comments

Comments
 (0)