Skip to content

Commit 3cf4319

Browse files
committed
Handle unsafe XTM legacy nonce reads
1 parent 32c2e4f commit 3cf4319

2 files changed

Lines changed: 162 additions & 25 deletions

File tree

lib/coins/core/factories.js

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"use strict";
2+
const { types: utilTypes } = require("util");
23
const { arr2hex, calcErgReward, calcEthReward } = require("../helpers.js");
34
const COIN_PROFILE_SYMBOL = Symbol.for("nodejs-pool.coinProfile");
45
const XTM_T_MINING_HASH_OFFSET = 3;
@@ -11,6 +12,7 @@ const XTM_T_POW_DATA_OFFSET = XTM_T_POW_ALGO_OFFSET + 1;
1112
const XTM_T_POW_DATA_SIZE = 32;
1213
const XTM_T_RANDOMXT_POW_ALGO = 2;
1314
const XTM_T_POOL_RESERVED_OFFSET = XTM_T_POW_DATA_OFFSET;
15+
const ORIGINAL_BUFFER_READ_UINT32_BE = Buffer.prototype.readUInt32BE;
1416

1517
function cloneValue(value) {
1618
if (Array.isArray(value)) return value.slice();
@@ -874,9 +876,35 @@ function describeBufferBacking(value, offset) {
874876
}
875877
}
876878

879+
function readUInt32IndexedBE(value, offset) {
880+
return value[offset] * 0x1000000 +
881+
value[offset + 1] * 0x10000 +
882+
value[offset + 2] * 0x100 +
883+
value[offset + 3];
884+
}
885+
886+
function describeLegacyNonceReadMethods(value, offset) {
887+
let indexedHi = "[unread]";
888+
let indexedLo = "[unread]";
889+
try {
890+
indexedHi = readUInt32IndexedBE(value, offset);
891+
indexedLo = readUInt32IndexedBE(value, offset + 4);
892+
} catch (error) {
893+
indexedHi = indexedLo = "[threw " + errorMessage(error) + "]";
894+
}
895+
return " methods={" +
896+
"readUInt32BEIsOriginal:" + (safeGet(value, "readUInt32BE") === ORIGINAL_BUFFER_READ_UINT32_BE) +
897+
",bufferProtoReadUInt32BEIsOriginal:" + (Buffer.prototype.readUInt32BE === ORIGINAL_BUFFER_READ_UINT32_BE) +
898+
",readUInt32BEName:" + safeInspect(safeGet(safeGet(value, "readUInt32BE"), "name")) +
899+
",indexedCalcHi:" + indexedHi +
900+
",indexedCalcLo:" + indexedLo +
901+
"}";
902+
}
903+
877904
function describeLegacyNonceRead(value, offset) {
878905
return "receiver={" +
879906
"isBuffer:" + Buffer.isBuffer(value) +
907+
",isProxy:" + utilTypes.isProxy(value) +
880908
",instanceofBuffer:" + (value instanceof Buffer) +
881909
",ctor:" + ctorName(value) +
882910
",protoCtor:" + protoCtorName(value) +
@@ -886,24 +914,61 @@ function describeLegacyNonceRead(value, offset) {
886914
",readUInt32BEOwn:" + Object.prototype.hasOwnProperty.call(value || {}, "readUInt32BE") +
887915
",readUInt32BEIsBufferProto:" + (safeGet(value, "readUInt32BE") === Buffer.prototype.readUInt32BE) +
888916
"} indexed=[" + describeIndexedValues(value, offset) + "]" +
917+
describeLegacyNonceReadMethods(value, offset) +
889918
describeBufferBacking(value, offset);
890919
}
891920

921+
function reportLegacyNonceReadDiagnostic(prefix, report) {
922+
console.warn(report);
923+
try {
924+
if (!global.support || typeof global.support.sendAdminFyi !== "function") return;
925+
global.support.sendAdminFyi(
926+
"coins:xtm-legacy-nonce-read:" + prefix,
927+
"FYI: XTM legacy nonce read mismatch",
928+
"The pool server: " + (global.config && global.config.hostname ? global.config.hostname : "unknown") +
929+
" saw an XTM legacy nonce Buffer/DataView mismatch.\n" + report,
930+
{ cooldownMs: 60 * 60 * 1000 }
931+
);
932+
} catch (_error) {
933+
// Diagnostics must never block a valid block submit.
934+
}
935+
}
936+
892937
function readUInt64BufferBE(buf, offset = 0, label) {
893938
const prefix = "XTM legacy nonce read" + (label ? " " + label : "");
939+
if (!Buffer.isBuffer(buf)) {
940+
throw new TypeError(prefix + " expected Buffer; " + describeLegacyNonceRead(buf, offset));
941+
}
894942
let hiNumber;
895943
let loNumber;
944+
let hi;
945+
let lo;
896946
try {
897-
hiNumber = buf.readUInt32BE(offset);
898-
loNumber = buf.readUInt32BE(offset + 4);
947+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
948+
hiNumber = view.getUint32(offset, false);
949+
loNumber = view.getUint32(offset + 4, false);
950+
hi = BigInt(hiNumber);
951+
lo = BigInt(loNumber);
899952
} catch (error) {
900953
throw new RangeError(prefix + " failed: " + errorMessage(error) + "; " + describeLegacyNonceRead(buf, offset));
901954
}
902-
if (!Number.isInteger(hiNumber) || !Number.isInteger(loNumber)) {
903-
throw new RangeError(prefix + " returned non-integer uint32 hi=" + hiNumber + " lo=" + loNumber + "; " + describeLegacyNonceRead(buf, offset));
955+
try {
956+
const legacyHi1 = buf.readUInt32BE(offset);
957+
const legacyLo1 = buf.readUInt32BE(offset + 4);
958+
const legacyHi2 = buf.readUInt32BE(offset);
959+
const legacyLo2 = buf.readUInt32BE(offset + 4);
960+
if (legacyHi1 !== hiNumber || legacyLo1 !== loNumber ||
961+
legacyHi2 !== hiNumber || legacyLo2 !== loNumber ||
962+
!Number.isInteger(legacyHi1) || !Number.isInteger(legacyLo1) ||
963+
!Number.isInteger(legacyHi2) || !Number.isInteger(legacyLo2)) {
964+
reportLegacyNonceReadDiagnostic(prefix, prefix + " mismatch" +
965+
" legacyHi1=" + legacyHi1 + " legacyLo1=" + legacyLo1 +
966+
" legacyHi2=" + legacyHi2 + " legacyLo2=" + legacyLo2 +
967+
" dataViewHi=" + hiNumber + " dataViewLo=" + loNumber + "; " + describeLegacyNonceRead(buf, offset));
968+
}
969+
} catch (error) {
970+
reportLegacyNonceReadDiagnostic(prefix, prefix + " legacy check failed: " + errorMessage(error) + "; " + describeLegacyNonceRead(buf, offset));
904971
}
905-
const hi = BigInt(hiNumber);
906-
const lo = BigInt(loNumber);
907972
return ((hi << 32n) | lo).toString(10);
908973
}
909974

tests/pool/coin/submitters.js

Lines changed: 91 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -488,27 +488,31 @@ test("xtm-t SubmitBlock payload roundtrips the miner's Tari RandomXT blob", () =
488488
assert.deepEqual(xtmBlock.header.pow.pow_data, [7, 7, 7]);
489489
});
490490

491-
test("xtm-t legacy nonce read reports non-integer indexed values", () => {
491+
test("xtm-t legacy nonce submit uses backing bytes when Buffer uint32 reads are unsafe", () => {
492492
const coinFuncs = global.coinFuncs.__realCoinFuncs;
493493
const xtmTPool = coinFuncs.getPoolSettings("XTM-T");
494494
const miningHash = Buffer.from("31".repeat(XTM_T_MINING_HASH_SIZE), "hex");
495495
const blockData = createXtmTMiningBlob(miningHash, 0n, [4, 5, 6]);
496+
const calls = [];
497+
const originalConsoleWarn = console.warn;
498+
const warnings = [];
499+
const emails = global.support.emails;
496500
const spoofedBlockData = new Proxy(blockData, {
497501
get(target, prop) {
498502
if (prop === "slice") return target.slice.bind(target);
499-
if (prop === String(XTM_T_MINER_NONCE_OFFSET)) return 0xa5;
500-
if (prop === String(XTM_T_MINER_NONCE_OFFSET + 1)) return 0x1e;
501-
if (prop === String(XTM_T_MINER_NONCE_OFFSET + 2)) return 0x01;
502-
if (prop === String(XTM_T_MINER_NONCE_OFFSET + 3)) return 255.99999856948853;
503+
if (prop === String(XTM_T_MINER_NONCE_OFFSET)) return 130.99999999999991;
503504
return Reflect.get(target, prop, target);
504505
}
505506
});
506507

507-
Buffer.from("a51e0200", "hex").copy(blockData, XTM_T_MINER_NONCE_OFFSET);
508+
Buffer.from("83304400", "hex").copy(blockData, XTM_T_MINER_NONCE_OFFSET);
508509
assert.equal(Buffer.isBuffer(spoofedBlockData), true);
509510
assert.equal(Number.isInteger(spoofedBlockData.readUInt32BE(XTM_T_MINER_NONCE_OFFSET)), false);
511+
console.warn = function captureWarn(message) {
512+
warnings.push(message);
513+
};
510514

511-
assert.throws(function submitSpoofedXtmBlob() {
515+
try {
512516
xtmTPool.submitBlockRpc.call(xtmTPool, {
513517
blockData: spoofedBlockData,
514518
blockTemplate: {
@@ -518,21 +522,89 @@ test("xtm-t legacy nonce read reports non-integer indexed values", () => {
518522
},
519523
replyFn() {},
520524
support: {
521-
rpcPortDaemon() {
522-
throw new Error("non-integer legacy nonce read must fail before submit");
525+
rpcPortDaemon(port, method, params) {
526+
calls.push({ port, method, params });
523527
}
524528
}
525529
});
526-
}, function checkError(error) {
527-
assert.match(error.message, /XTM legacy nonce read XTM-T returned non-integer uint32/);
528-
assert.match(error.message, /lo=2770207231\.9999986/);
529-
assert.match(error.message, /isBuffer:true/);
530-
assert.match(error.message, /readUInt32BEIsBufferProto:true/);
531-
assert.match(error.message, /42:number:255\.99999856948853:integer=false/);
532-
assert.match(error.message, /backingDataViewLo=2770207232/);
533-
assert.match(error.message, /backingBytes=0,0,0,0,165,30,2,0/);
534-
return true;
535-
});
530+
} finally {
531+
console.warn = originalConsoleWarn;
532+
}
533+
534+
assert.equal(calls.length, 1);
535+
assert.equal(calls[0].port, 18146);
536+
assert.equal(calls[0].method, "SubmitBlock");
537+
assert.equal(calls[0].params.header.nonce, blockData.readBigUInt64BE(XTM_T_NONCE_OFFSET).toString(10));
538+
assert.deepEqual(calls[0].params.header.pow.pow_data, [...blockData.subarray(XTM_T_POW_DATA_OFFSET)]);
539+
assert.match(warnings.join("\n"), /isProxy:true/);
540+
assert.equal(emails.length, 1);
541+
assert.equal(emails[0].key, "coins:xtm-legacy-nonce-read:XTM legacy nonce read XTM-T");
542+
assert.equal(emails[0].subject, "FYI: XTM legacy nonce read mismatch");
543+
assert.match(emails[0].body, /isProxy:true/);
544+
assert.match(emails[0].body, /legacyLo1=2200978431\.9999986/);
545+
assert.match(emails[0].body, /legacyLo2=2200978431\.9999986/);
546+
assert.match(emails[0].body, /indexedCalcLo:2200978431\.9999986/);
547+
});
548+
549+
test("xtm-t legacy nonce submit does not depend on Buffer readUInt32BE", () => {
550+
const coinFuncs = global.coinFuncs.__realCoinFuncs;
551+
const xtmTPool = coinFuncs.getPoolSettings("XTM-T");
552+
const miningHash = Buffer.from("31".repeat(XTM_T_MINING_HASH_SIZE), "hex");
553+
const blockData = createXtmTMiningBlob(miningHash, 0n, [4, 5, 6]);
554+
const originalReadUInt32BE = Buffer.prototype.readUInt32BE;
555+
const originalConsoleWarn = console.warn;
556+
const calls = [];
557+
const warnings = [];
558+
const emails = global.support.emails;
559+
let returnedBadNonceRead = false;
560+
561+
Buffer.from("83304400", "hex").copy(blockData, XTM_T_MINER_NONCE_OFFSET);
562+
Buffer.prototype.readUInt32BE = function patchedReadUInt32BE(offset, ...args) {
563+
if (this === blockData && offset === XTM_T_MINER_NONCE_OFFSET && !returnedBadNonceRead) {
564+
returnedBadNonceRead = true;
565+
return 2200978431.9999986;
566+
}
567+
return originalReadUInt32BE.call(this, offset, ...args);
568+
};
569+
console.warn = function captureWarn(message) {
570+
warnings.push(message);
571+
};
572+
573+
try {
574+
xtmTPool.submitBlockRpc.call(xtmTPool, {
575+
blockData,
576+
blockTemplate: {
577+
port: 18146,
578+
reserved_offset: XTM_T_POOL_RESERVED_OFFSET,
579+
xtm_block: { header: { nonce: "0", pow: { pow_data: [] } } }
580+
},
581+
replyFn() {},
582+
support: {
583+
rpcPortDaemon(port, method, params) {
584+
calls.push({ port, method, params });
585+
}
586+
}
587+
});
588+
} finally {
589+
Buffer.prototype.readUInt32BE = originalReadUInt32BE;
590+
console.warn = originalConsoleWarn;
591+
}
592+
593+
assert.equal(calls.length, 1);
594+
assert.equal(returnedBadNonceRead, true);
595+
assert.equal(calls[0].params.header.nonce, blockData.readBigUInt64BE(XTM_T_NONCE_OFFSET).toString(10));
596+
assert.equal(calls[0].params.header.nonce, "2200978432");
597+
assert.match(warnings.join("\n"), /isProxy:false/);
598+
assert.match(warnings.join("\n"), /indexed=\[35:number:0:integer=true/);
599+
assert.equal(emails.length, 1);
600+
assert.equal(emails[0].key, "coins:xtm-legacy-nonce-read:XTM legacy nonce read XTM-T");
601+
assert.equal(emails[0].subject, "FYI: XTM legacy nonce read mismatch");
602+
assert.match(emails[0].body, /legacyLo1=2200978431\.9999986/);
603+
assert.match(emails[0].body, /legacyLo2=2200978432/);
604+
assert.match(emails[0].body, /isProxy:false/);
605+
assert.match(emails[0].body, /indexed=\[35:number:0:integer=true/);
606+
assert.match(emails[0].body, /indexedCalcLo:2200978432/);
607+
assert.match(emails[0].body, /readUInt32BEIsOriginal:false/);
536608
});
537609

538610
test("xtm-t SubmitBlock rejects a blob with a corrupted pow_algo byte", () => {

0 commit comments

Comments
 (0)