Skip to content

Commit 5122ccf

Browse files
committed
Remove XTM nonce read diagnostics
1 parent 3cf4319 commit 5122ccf

2 files changed

Lines changed: 24 additions & 243 deletions

File tree

lib/coins/core/factories.js

Lines changed: 22 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"use strict";
2-
const { types: utilTypes } = require("util");
32
const { arr2hex, calcErgReward, calcEthReward } = require("../helpers.js");
43
const COIN_PROFILE_SYMBOL = Symbol.for("nodejs-pool.coinProfile");
54
const XTM_T_MINING_HASH_OFFSET = 3;
@@ -12,7 +11,6 @@ const XTM_T_POW_DATA_OFFSET = XTM_T_POW_ALGO_OFFSET + 1;
1211
const XTM_T_POW_DATA_SIZE = 32;
1312
const XTM_T_RANDOMXT_POW_ALGO = 2;
1413
const XTM_T_POOL_RESERVED_OFFSET = XTM_T_POW_DATA_OFFSET;
15-
const ORIGINAL_BUFFER_READ_UINT32_BE = Buffer.prototype.readUInt32BE;
1614

1715
function cloneValue(value) {
1816
if (Array.isArray(value)) return value.slice();
@@ -812,163 +810,26 @@ const pow = {
812810
c29s: createCyclePowFactory("c29s", "c29s_packed_edges")
813811
};
814812

815-
function errorMessage(error) {
816-
return error && error.message ? error.message : String(error);
817-
}
818-
819-
function safeInspect(value) {
820-
if (typeof value === "function") return "[Function " + (value.name || "anonymous") + "]";
821-
if (typeof value === "bigint") return value.toString() + "n";
822-
if (typeof value === "undefined") return "undefined";
823-
try {
824-
return JSON.stringify(value);
825-
} catch (_error) {
826-
try {
827-
return String(value);
828-
} catch (_error2) {
829-
return "[uninspectable]";
830-
}
831-
}
832-
}
833-
834-
function safeGet(value, key) {
835-
try {
836-
return value ? value[key] : undefined;
837-
} catch (error) {
838-
return "[threw " + errorMessage(error) + "]";
839-
}
840-
}
841-
842-
function ctorName(value) {
843-
const ctor = safeGet(value, "constructor");
844-
return ctor && ctor.name ? ctor.name : typeof value;
845-
}
846-
847-
function protoCtorName(value) {
848-
try {
849-
const proto = Object.getPrototypeOf(value);
850-
return proto && proto.constructor && proto.constructor.name ? proto.constructor.name : safeInspect(proto);
851-
} catch (error) {
852-
return "[threw " + errorMessage(error) + "]";
853-
}
854-
}
855-
856-
function describeIndexedValues(value, offset) {
857-
const items = [];
858-
for (let index = offset; index < offset + 8; index++) {
859-
const byte = safeGet(value, index);
860-
items.push(index + ":" + typeof byte + ":" + safeInspect(byte) + ":integer=" + Number.isInteger(byte));
861-
}
862-
return items.join(",");
863-
}
864-
865-
function describeBufferBacking(value, offset) {
866-
if (!Buffer.isBuffer(value)) return "";
867-
try {
868-
const backing = Buffer.from(value.buffer, value.byteOffset, value.length);
869-
const view = new DataView(backing.buffer, backing.byteOffset, backing.byteLength);
870-
return " backingDataViewHi=" + view.getUint32(offset, false) +
871-
" backingDataViewLo=" + view.getUint32(offset + 4, false) +
872-
" backingBytes=" + Array.from(backing.subarray(offset, offset + 8)).join(",") +
873-
" backingHex=" + backing.toString("hex");
874-
} catch (error) {
875-
return " backing=[threw " + errorMessage(error) + "]";
876-
}
877-
}
878-
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-
904-
function describeLegacyNonceRead(value, offset) {
905-
return "receiver={" +
906-
"isBuffer:" + Buffer.isBuffer(value) +
907-
",isProxy:" + utilTypes.isProxy(value) +
908-
",instanceofBuffer:" + (value instanceof Buffer) +
909-
",ctor:" + ctorName(value) +
910-
",protoCtor:" + protoCtorName(value) +
911-
",length:" + safeInspect(safeGet(value, "length")) +
912-
",byteOffset:" + safeInspect(safeGet(value, "byteOffset")) +
913-
",byteLength:" + safeInspect(safeGet(value, "byteLength")) +
914-
",readUInt32BEOwn:" + Object.prototype.hasOwnProperty.call(value || {}, "readUInt32BE") +
915-
",readUInt32BEIsBufferProto:" + (safeGet(value, "readUInt32BE") === Buffer.prototype.readUInt32BE) +
916-
"} indexed=[" + describeIndexedValues(value, offset) + "]" +
917-
describeLegacyNonceReadMethods(value, offset) +
918-
describeBufferBacking(value, offset);
919-
}
920-
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-
937-
function readUInt64BufferBE(buf, offset = 0, label) {
938-
const prefix = "XTM legacy nonce read" + (label ? " " + label : "");
939-
if (!Buffer.isBuffer(buf)) {
940-
throw new TypeError(prefix + " expected Buffer; " + describeLegacyNonceRead(buf, offset));
941-
}
942-
let hiNumber;
943-
let loNumber;
944-
let hi;
945-
let lo;
946-
try {
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);
952-
} catch (error) {
953-
throw new RangeError(prefix + " failed: " + errorMessage(error) + "; " + describeLegacyNonceRead(buf, offset));
954-
}
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));
971-
}
813+
function readUInt64BufferBE(buf, offset = 0) {
814+
if (!Buffer.isBuffer(buf)) throw new TypeError("XTM nonce read expected Buffer");
815+
816+
/*
817+
* Do not use Buffer.readUInt32BE here.
818+
*
819+
* Production XTM-T submits on Node v24/V8 13.6 have twice observed
820+
* Buffer.readUInt32BE returning a fractional uint32 value such as
821+
* 2200978431.9999986 or 3447195903.9999986 while the same Buffer's
822+
* indexed bytes and DataView backing-store reads were the correct
823+
* integers. That made a valid block fail before it reached the daemon.
824+
*
825+
* DataView#getUint32 reads the Buffer's ArrayBuffer backing store
826+
* directly and matched the actual bytes in those incidents, so this
827+
* block-critical nonce conversion intentionally avoids Buffer's uint32
828+
* helper.
829+
*/
830+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
831+
const hi = BigInt(view.getUint32(offset, false));
832+
const lo = BigInt(view.getUint32(offset + 4, false));
972833
return ((hi << 32n) | lo).toString(10);
973834
}
974835

@@ -1412,15 +1273,15 @@ function submitXtmRxBlock(ctx) {
14121273
const xtmBlock = cloneRpcTemplate(ctx.blockTemplate.xtm_block);
14131274
const powData = ctx.blockData.slice(XTM_T_POW_DATA_OFFSET);
14141275

1415-
xtmBlock.header.nonce = readUInt64BufferBE(ctx.blockData, XTM_T_NONCE_OFFSET, "XTM-T");
1276+
xtmBlock.header.nonce = readUInt64BufferBE(ctx.blockData, XTM_T_NONCE_OFFSET);
14161277
xtmBlock.header.pow.pow_data = powData.every((byte) => byte === 0) ? [] : [...powData];
14171278
ctx.support.rpcPortDaemon(ctx.blockTemplate.port, "SubmitBlock", xtmBlock, ctx.replyFn, true);
14181279
}
14191280

14201281
function submitXtmCBlock(ctx) {
14211282
const xtmBlock = cloneRpcTemplate(ctx.blockTemplate.xtm_block);
14221283

1423-
xtmBlock.header.nonce = readUInt64BufferBE(ctx.blockData, 0, "XTM-C");
1284+
xtmBlock.header.nonce = readUInt64BufferBE(ctx.blockData, 0);
14241285
xtmBlock.header.pow.pow_data = ctx.job.c29_packed_edges;
14251286
ctx.support.rpcPortDaemon(ctx.blockTemplate.port, "SubmitBlock", xtmBlock, ctx.replyFn, true);
14261287
}

tests/pool/coin/submitters.js

Lines changed: 2 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -488,87 +488,21 @@ 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 submit uses backing bytes when Buffer uint32 reads are unsafe", () => {
492-
const coinFuncs = global.coinFuncs.__realCoinFuncs;
493-
const xtmTPool = coinFuncs.getPoolSettings("XTM-T");
494-
const miningHash = Buffer.from("31".repeat(XTM_T_MINING_HASH_SIZE), "hex");
495-
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;
500-
const spoofedBlockData = new Proxy(blockData, {
501-
get(target, prop) {
502-
if (prop === "slice") return target.slice.bind(target);
503-
if (prop === String(XTM_T_MINER_NONCE_OFFSET)) return 130.99999999999991;
504-
return Reflect.get(target, prop, target);
505-
}
506-
});
507-
508-
Buffer.from("83304400", "hex").copy(blockData, XTM_T_MINER_NONCE_OFFSET);
509-
assert.equal(Buffer.isBuffer(spoofedBlockData), true);
510-
assert.equal(Number.isInteger(spoofedBlockData.readUInt32BE(XTM_T_MINER_NONCE_OFFSET)), false);
511-
console.warn = function captureWarn(message) {
512-
warnings.push(message);
513-
};
514-
515-
try {
516-
xtmTPool.submitBlockRpc.call(xtmTPool, {
517-
blockData: spoofedBlockData,
518-
blockTemplate: {
519-
port: 18146,
520-
reserved_offset: XTM_T_POOL_RESERVED_OFFSET,
521-
xtm_block: { header: { nonce: "0", pow: { pow_data: [] } } }
522-
},
523-
replyFn() {},
524-
support: {
525-
rpcPortDaemon(port, method, params) {
526-
calls.push({ port, method, params });
527-
}
528-
}
529-
});
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-
549491
test("xtm-t legacy nonce submit does not depend on Buffer readUInt32BE", () => {
550492
const coinFuncs = global.coinFuncs.__realCoinFuncs;
551493
const xtmTPool = coinFuncs.getPoolSettings("XTM-T");
552494
const miningHash = Buffer.from("31".repeat(XTM_T_MINING_HASH_SIZE), "hex");
553495
const blockData = createXtmTMiningBlob(miningHash, 0n, [4, 5, 6]);
554496
const originalReadUInt32BE = Buffer.prototype.readUInt32BE;
555-
const originalConsoleWarn = console.warn;
556497
const calls = [];
557-
const warnings = [];
558-
const emails = global.support.emails;
559-
let returnedBadNonceRead = false;
560498

561499
Buffer.from("83304400", "hex").copy(blockData, XTM_T_MINER_NONCE_OFFSET);
562500
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;
501+
if (this === blockData && (offset === XTM_T_NONCE_OFFSET || offset === XTM_T_MINER_NONCE_OFFSET)) {
502+
throw new Error("Buffer.readUInt32BE must not be used for XTM-T nonce submit");
566503
}
567504
return originalReadUInt32BE.call(this, offset, ...args);
568505
};
569-
console.warn = function captureWarn(message) {
570-
warnings.push(message);
571-
};
572506

573507
try {
574508
xtmTPool.submitBlockRpc.call(xtmTPool, {
@@ -587,24 +521,10 @@ test("xtm-t legacy nonce submit does not depend on Buffer readUInt32BE", () => {
587521
});
588522
} finally {
589523
Buffer.prototype.readUInt32BE = originalReadUInt32BE;
590-
console.warn = originalConsoleWarn;
591524
}
592525

593526
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));
596527
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/);
608528
});
609529

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

0 commit comments

Comments
 (0)