● Add native Rust RandomX miner with IPC mode and JS mining scripts

Native miner achieves ~360 H/s/thread (vs ~10 H/s in WASM) using
  hardware AES-NI/AVX2 via direct RandomX C FFI. Includes multi-threaded
  mining with shared 2GB dataset, JSON-lines IPC protocol for parent
  process control, and JS-based mining scripts for testnet.
This commit is contained in:
Matt Hess
2026-02-01 17:50:07 +00:00
parent 3690068cad
commit 244bfb78a2
11 changed files with 3290 additions and 132 deletions
+8 -131
View File
@@ -362,12 +362,6 @@ export class WalletSync {
// Process blocks in batches using batch RPC for better performance
const BLOCK_BATCH_SIZE = 100; // Fetch 100 blocks per RPC call
// Debug: log first syncBatch call
if (this._syncBatchCount === undefined) this._syncBatchCount = 0;
this._syncBatchCount++;
if (this._syncBatchCount === 1) {
console.log(`[DEBUG] First _syncBatch call, headers count=${headers.length}`);
}
for (let i = 0; i < headers.length; i += BLOCK_BATCH_SIZE) {
if (this._stopRequested) break;
@@ -378,10 +372,6 @@ export class WalletSync {
// Batch fetch all blocks at once
const blocksResponse = await this.daemon.getBlocksByHeight(heights);
// Debug: log batch response status
if (this._syncBatchCount === 1 && i === 0) {
console.log(`[DEBUG] Batch fetch response: success=${blocksResponse.success}, has blocks=${!!blocksResponse.result?.blocks}, blocks count=${blocksResponse.result?.blocks?.length || 0}`);
}
if (!blocksResponse.success || !blocksResponse.result?.blocks) {
// Fallback to individual fetches if batch fails
@@ -457,20 +447,10 @@ export class WalletSync {
// blockData from get_blocks_by_height.bin contains block blob
// We need to parse it or use the included transactions
// Debug: log batch processing status
if (this._batchDebugCount === undefined) this._batchDebugCount = 0;
this._batchDebugCount++;
if (this._batchDebugCount <= 3) {
console.log(`[DEBUG] _processBlockFromBatch at height ${header.height}`);
console.log(`[DEBUG] blockData keys: ${blockData ? Object.keys(blockData).join(', ') : 'null'}`);
}
// If blockData has the structure we need, use it directly
// Otherwise fall back to individual fetch
if (!blockData || !blockData.block) {
if (this._batchDebugCount <= 3) {
console.log(`[DEBUG] Falling back to individual fetch for height ${header.height}`);
}
return this._processBlock(header);
}
@@ -543,9 +523,6 @@ export class WalletSync {
* @param {Object} header - Block header
*/
async _processBlock(header) {
// Debug: count blocks processed via individual fetch
if (this._individualBlockCount === undefined) this._individualBlockCount = 0;
this._individualBlockCount++;
// Get full block data - includes miner_tx and protocol_tx in JSON
const blockResponse = await this.daemon.getBlock({ height: header.height });
@@ -566,14 +543,6 @@ export class WalletSync {
}
}
// Debug: log first few blocks' miner_tx structure
if (this._individualBlockCount <= 3 && blockJson?.miner_tx) {
console.log(`[DEBUG] Block ${header.height} miner_tx keys: ${Object.keys(blockJson.miner_tx).join(', ')}`);
console.log(`[DEBUG] miner_tx.vout count: ${(blockJson.miner_tx.vout || []).length}`);
if (blockJson.miner_tx.vout?.[0]?.target) {
console.log(`[DEBUG] First vout target keys: ${Object.keys(blockJson.miner_tx.vout[0].target).join(', ')}`);
}
}
// Process miner_tx (coinbase - block reward)
if (blockJson?.miner_tx && block.miner_tx_hash) {
@@ -637,15 +606,6 @@ export class WalletSync {
async _processEmbeddedTransaction(txJson, txHash, header, options = {}) {
const { isMinerTx = false, isProtocolTx = false } = options;
// Debug: count embedded txs processed
if (this._embeddedTxCount === undefined) this._embeddedTxCount = 0;
this._embeddedTxCount++;
if (this._embeddedTxCount <= 3) {
console.log(`[DEBUG] Processing embedded tx at height ${header.height}, isMinerTx=${isMinerTx}, vout count=${(txJson.vout || []).length}`);
if (txJson.extra) {
console.log(`[DEBUG] extra field type: ${Array.isArray(txJson.extra) ? 'array' : typeof txJson.extra}, length=${txJson.extra.length || 0}`);
}
}
// Check if we already have this transaction
const existing = await this.storage.getTransaction(txHash);
@@ -659,11 +619,6 @@ export class WalletSync {
const txPubKey = extractTxPubKey(tx);
const paymentId = extractPaymentId(tx);
// Debug: log txPubKey extraction
if (this._embeddedTxCount <= 3) {
console.log(`[DEBUG] txPubKey extracted: ${txPubKey ? bytesToHex(txPubKey) : 'null'}`);
console.log(`[DEBUG] converted vout count: ${(tx.prefix?.vout || []).length}`);
}
// Determine transaction type
const txType = isMinerTx ? 'miner' : (isProtocolTx ? 'protocol' : this._getTxType(tx));
@@ -816,10 +771,17 @@ export class WalletSync {
viewTag
};
} else if (target?.key) {
// target.key can be a string (plain key) or object {key, asset_type, unlock_time}
// The latter appears in genesis/pre-tagged_key blocks
const keyVal = typeof target.key === 'object' ? target.key.key : target.key;
const assetType = typeof target.key === 'object' ? (target.key.asset_type || 'SAL') : 'SAL';
const unlockTime = typeof target.key === 'object' ? (target.key.unlock_time || 0) : 0;
return {
amount: this._safeBigInt(out.amount),
type: 0x02, // TXOUT_TYPE.KEY
key: hexToBytes(target.key)
key: hexToBytes(keyVal),
assetType,
unlockTime
};
}
return { amount: this._safeBigInt(out.amount), type: 0 };
@@ -880,12 +842,6 @@ export class WalletSync {
_convertExtraJson(extraArray) {
// Extra in JSON is just an array of bytes
if (!Array.isArray(extraArray) || extraArray.length === 0) {
// Debug: log if extra is empty or wrong format
if (this._extraDebugCount === undefined) this._extraDebugCount = 0;
this._extraDebugCount++;
if (this._extraDebugCount <= 3) {
console.log(`[DEBUG] _convertExtraJson: extraArray is ${Array.isArray(extraArray) ? 'array of length ' + extraArray.length : typeof extraArray}`);
}
return [];
}
@@ -893,12 +849,6 @@ export class WalletSync {
const extraBytes = new Uint8Array(extraArray);
const parsed = [];
// Debug: log first few bytes
if (this._extraDebugCount === undefined) this._extraDebugCount = 0;
this._extraDebugCount++;
if (this._extraDebugCount <= 3) {
console.log(`[DEBUG] _convertExtraJson: extraBytes length=${extraBytes.length}, first bytes: ${Array.from(extraBytes.slice(0, 5)).map(b => b.toString(16)).join(' ')}`);
}
let offset = 0;
while (offset < extraBytes.length) {
@@ -971,12 +921,6 @@ export class WalletSync {
const txHash = txData.tx_hash;
const txBlob = txData.as_hex;
// Debug: count regular txs processed
if (this._regularTxCount === undefined) this._regularTxCount = 0;
this._regularTxCount++;
if (this._regularTxCount <= 3) {
console.log(`[DEBUG] _processTransaction at height ${header.height}, tx ${txHash.slice(0, 16)}...`);
}
// Check if we already have this transaction
const existing = await this.storage.getTransaction(txHash);
@@ -990,10 +934,6 @@ export class WalletSync {
const txPubKey = extractTxPubKey(tx);
const paymentId = extractPaymentId(tx);
// Debug: log tx structure
if (this._regularTxCount <= 3) {
console.log(`[DEBUG] Regular tx: txPubKey=${txPubKey ? bytesToHex(txPubKey).slice(0, 16) + '...' : 'null'}, vout count=${(tx.prefix?.vout || []).length}`);
}
// Determine transaction type (Salvium-specific)
// For miner_tx and protocol_tx, use the type from the prefix
@@ -1095,24 +1035,6 @@ export class WalletSync {
const ownedOutputs = [];
const outputs = tx.prefix?.vout || tx.outputs || [];
// Debug: log first 3 calls unconditionally
if (this._scanOutputsCount === undefined) this._scanOutputsCount = 0;
this._scanOutputsCount++;
if (this._scanOutputsCount <= 3) {
console.log(`\n=== _scanOutputs call #${this._scanOutputsCount} at height ${header.height} ===`);
console.log(`txHash: ${txHash}`);
console.log(`txPubKey: ${txPubKey ? bytesToHex(txPubKey) : 'NULL'}`);
console.log(`outputs count: ${outputs.length}`);
console.log(`txType: ${txType}`);
if (outputs.length > 0) {
const firstOutput = outputs[0];
console.log(`first output type: ${firstOutput.type}`);
console.log(`first output keys: ${Object.keys(firstOutput).join(', ')}`);
const firstKey = this._extractOutputPubKey(firstOutput);
console.log(`first output pubkey: ${firstKey ? bytesToHex(firstKey).slice(0, 32) + '...' : 'NULL'}`);
}
console.log(`===\n`);
}
for (let i = 0; i < outputs.length; i++) {
const output = outputs[i];
@@ -1220,13 +1142,6 @@ export class WalletSync {
? hexToBytes(firstKi)
: firstKi;
inputContext = makeInputContext(firstKeyImage);
// Debug: show first key image
if (header.height >= 405588 && header.height <= 405590 && this._inputContextDebug === undefined) {
this._inputContextDebug = true;
console.log(`[INPUT CONTEXT DEBUG] height=${header.height} txHash=${txHash.slice(0,16)}...`);
console.log(` firstKeyImage: ${bytesToHex(firstKeyImage)}`);
console.log(` inputContext: ${bytesToHex(inputContext)}`);
}
} else {
// Coinbase transaction: input_context = 'C' || block_height
inputContext = makeInputContextCoinbase(header.height);
@@ -1269,15 +1184,6 @@ export class WalletSync {
encryptedAmount: tx.rct?.ecdhInfo?.[outputIndex]?.amount
};
// Debug CARROT scanning inputs
if (header.height >= 405585 && header.height <= 405595) {
console.log(`[DEBUG CARROT SCAN] height=${header.height} outputIndex=${outputIndex}`);
console.log(` key: ${outputForScan.key ? bytesToHex(outputForScan.key).slice(0,16) + '...' : 'NULL'}`);
console.log(` viewTag: ${outputForScan.viewTag}`);
console.log(` enoteEphemeralPubkey: ${outputForScan.enoteEphemeralPubkey ? (typeof outputForScan.enoteEphemeralPubkey === 'string' ? outputForScan.enoteEphemeralPubkey.slice(0,16) : bytesToHex(outputForScan.enoteEphemeralPubkey).slice(0,16)) + '...' : 'NULL'}`);
console.log(` amountCommitment: ${amountCommitment ? bytesToHex(amountCommitment).slice(0,16) + '...' : 'NULL'}`);
console.log(` carrotKeys: viewIncomingKey=${this.carrotKeys?.viewIncomingKey ? 'SET' : 'NULL'}, accountSpendPubkey=${this.carrotKeys?.accountSpendPubkey ? 'SET' : 'NULL'}`);
}
// Scan with CARROT algorithm
let result;
@@ -1370,33 +1276,21 @@ export class WalletSync {
* @private
*/
async _scanCNOutput(output, outputIndex, tx, txHash, txPubKey, header) {
// Debug: count CN scan attempts
if (this._cnScanCount === undefined) this._cnScanCount = 0;
this._cnScanCount++;
const outputPubKey = this._extractOutputPubKey(output);
if (!outputPubKey) {
if (this._cnScanCount <= 3) {
console.log(`[DEBUG] _scanCNOutput: no outputPubKey for output ${outputIndex}`);
}
return null;
}
// Compute key derivation: D = 8 * viewSecretKey * txPubKey
const derivation = generateKeyDerivation(txPubKey, this.keys.viewSecretKey);
if (!derivation) {
if (this._cnScanCount <= 3) {
console.log(`[DEBUG] _scanCNOutput: derivation failed`);
}
return null;
}
// Check view tag FIRST (if available) for fast rejection
if (output.viewTag !== undefined) {
const expectedViewTag = deriveViewTag(derivation, outputIndex);
if (this._cnScanCount <= 3) {
console.log(`[DEBUG] _scanCNOutput: viewTag check - output.viewTag=${output.viewTag}, expected=${expectedViewTag}`);
}
if (output.viewTag !== expectedViewTag) {
return null; // Not our output - skip expensive operations
}
@@ -1407,9 +1301,6 @@ export class WalletSync {
// This gives us the spend pubkey that was used to create this output
const derivedSpendPubKey = deriveSubaddressPublicKey(outputPubKey, derivation, outputIndex);
if (!derivedSpendPubKey) {
if (this._cnScanCount <= 3) {
console.log(`[DEBUG] _scanCNOutput: deriveSubaddressPublicKey failed`);
}
return null;
}
@@ -1421,12 +1312,6 @@ export class WalletSync {
subaddressIndex = this.subaddresses.get(derivedSpendPubKeyHex);
}
// Debug: log first few lookups
if (this._cnScanCount <= 3) {
console.log(`[DEBUG] _scanCNOutput: derived spend key=${derivedSpendPubKeyHex.slice(0, 16)}...`);
console.log(`[DEBUG] _scanCNOutput: subaddress map size=${this.subaddresses?.size || 0}`);
console.log(`[DEBUG] _scanCNOutput: found in map=${!!subaddressIndex}`);
}
if (!subaddressIndex) {
return null; // Not our output
@@ -1500,9 +1385,6 @@ export class WalletSync {
const spentOutputs = [];
const inputs = tx.prefix?.vin || tx.inputs || [];
// Debug: log first few checks
if (this._spentCheckCount === undefined) this._spentCheckCount = 0;
this._spentCheckCount++;
for (const input of inputs) {
// Parsed transactions use input.keyImage directly
@@ -1517,13 +1399,8 @@ export class WalletSync {
// Check if this key image belongs to one of our outputs
const output = await this.storage.getOutput(keyImage);
// Debug: log when we find a potential match or at specific heights
if (output || (header.height >= 270510 && header.height <= 270530)) {
console.log(`[DEBUG] _checkSpentOutputs height=${header.height} keyImage=${keyImage.slice(0,16)}... found=${!!output}`);
}
if (output && !output.isSpent) {
console.log(`[SPENT] Output spent at height ${header.height}: keyImage=${keyImage.slice(0,16)}... amount=${output.amount}`);
await this.storage.markOutputSpent(keyImage, txHash, header.height);
spentOutputs.push(output);
}