Files
salvium-rs/debug-carrot-output.js
Matt Hess 7f01cafc62 Crypto layer consolidation: eliminate JS scalar ops, add cn_scan + RCT batch verify
Crypto Backend Refactoring
   ──────────────────────────
   - Delete src/ed25519.js — all scalar/point operations now route through
     the crypto provider (WASM/FFI/JSI backends only)
   - Remove duplicate JS BigInt implementations of scReduce32, scReduce64,
     scalarAdd, scalarMul from scanning.js, carrot.js, and carrot-scanning.js;
     delegate to Rust backend via crypto/index.js
   - Remove debug/test exports from index.js (randomPoint, testDouble,
     getBasePoint, isOnCurve, etc.) that were only used during development
   - Rebuild WASM binary (476KB → 487KB) with updated Rust crate

   Consolidated CryptoNote Scanner (cn_scan)
   ─────────────────────────────────────────
   - New Rust module crates/salvium-crypto/src/cn_scan.rs replaces 5-12
     individual FFI round-trips per output with a single native call:
     view tag check → derive subaddress pubkey → subaddress map lookup →
     amount decryption → commitment mask → key image generation
   - FFI wrapper salvium_cn_scan_output in ffi.rs + C header declaration
   - FFI backend scanCnOutput() with full subaddress map marshaling
     (32-byte key + u32 major/minor LE per entry) and JSON result parsing
   - JSI backend delegation via this.native.cnScanOutput()
   - wallet-sync.js _scanCNOutput() tries native path first when available
     (FFI/JSI), falls through to existing JS pipeline for WASM/JS backends
   - Change pub(crate) visibility on subaddress.rs cn_subaddress_secret_key
   - 8 Rust unit tests covering view tag, amount, commitment mask,
     subaddress matching, and key image generation
   - Verified identical results: WASM (JS fallback) and FFI (native cn_scan)
     produce same 964 outputs at same chain height; FFI is 3x faster sync
     (0.8s vs 2.5s) with 12x less heap (12MB vs 150MB)

   RCT Batch Signature Verification
   ─────────────────────────────────
   - New Rust module crates/salvium-crypto/src/rct_verify.rs — single-call
     verification of all ring signatures in a transaction (CLSAG + TCLSAG),
     avoiding N individual JS↔Rust boundary crossings
   - Computes pre-MLSAG message hash matching C++ get_pre_mlsag_hash
   - FFI export salvium_verify_rct_signatures with flat byte array interface
   - FFI backend verifyRctSignatures() method
   - JS backend stub returns null (validation.js handles JS fallback)
   - validation.js: 200+ lines of RCT verification logic including
     flattenKeyImages, packTclsagSigsFlat, packClsagSigsFlat helpers

   Transaction Expansion
   ─────────────────────
   - transaction.js: add expandTransaction() matching C++ expand_transaction_2
     (copies key images from prefix inputs into TCLSAG/CLSAG signature structs)
   - New test/expand-transaction.test.js (634 lines)
   - New test/rct-verify-testnet.test.js (430 lines)

   Mining Resilience
   ─────────────────
   - salvium-miner main.rs: retry get_info and get_block_template up to 5
     times with 2s delay for transient daemon errors
   - full-testnet.js mineTo(): retry miner up to 3 times with 3s delay,
     check for partial progress between attempts

   Testnet Tooling
   ───────────────
   - sync-only.js: CRYPTO_BACKEND env var for A/B testing (wasm vs ffi)
   - full-testnet.js: daemon URL update (node12.whiskymine.io)
   - Debug scripts for cn_scan development (debug-cn-scan/marshal/match/wasm)
   - Android .gitignore and build-bundle.sh for mobile builds
2026-02-14 17:02:35 +00:00

138 lines
4.8 KiB
JavaScript

import { setCryptoBackend } from './src/crypto/index.js';
import { DaemonRPC } from './src/rpc/daemon.js';
import { scanCarrotOutput, makeInputContext, makeInputContextCoinbase } from './src/carrot-scanning.js';
import { Wallet } from './src/wallet.js';
import { hexToBytes, bytesToHex } from './src/address.js';
import { readFileSync } from 'fs';
import { commit as pedersonCommit } from './src/crypto/index.js';
await setCryptoBackend('wasm');
const daemon = new DaemonRPC({ url: 'http://node12.whiskymine.io:29081' });
const wj = JSON.parse(readFileSync(process.env.HOME + '/testnet-wallet/wallet.json'));
const wallet = Wallet.fromJSON({ ...wj, network: 'testnet' });
// Helper to convert to bytes
function toBytes(val) {
if (val instanceof Uint8Array) return val;
if (typeof val === 'string') return hexToBytes(val);
if (val && typeof val === 'object') return new Uint8Array(Object.values(val));
throw new Error('Cannot convert to bytes: ' + typeof val);
}
console.log('=== CARROT Keys ===');
const viewIncomingKey = toBytes(wallet.carrotKeys.viewIncomingKey);
const accountSpendPubkey = toBytes(wallet.carrotKeys.accountSpendPubkey);
console.log('viewIncomingKey:', bytesToHex(viewIncomingKey));
console.log('accountSpendPubkey:', bytesToHex(accountSpendPubkey));
// Fetch block 1110 (CARROT block)
const resp = await daemon.getBlock({ height: 1780 });
console.log('\n=== Raw Response ===');
console.log('resp.result.json exists:', !!resp.result?.json);
if (!resp.result?.json) {
console.log('No JSON in response');
console.log('resp:', JSON.stringify(resp, null, 2).slice(0, 500));
process.exit(1);
}
const blockJson = JSON.parse(resp.result.json);
const minerTx = blockJson.miner_tx;
console.log('\n=== Block 1110 Miner TX ===');
console.log('version:', minerTx.version);
console.log('type:', minerTx.type);
console.log('outputs:', minerTx.vout?.length);
// Check output structure
console.log('\n=== Output Structure ===');
const output = minerTx.vout[0];
console.log('output.target keys:', Object.keys(output.target || {}));
const carrotTarget = output.target?.carrot_v1;
if (!carrotTarget) {
console.log('No carrot_v1 target found!');
console.log('Output:', JSON.stringify(output, null, 2));
process.exit(1);
}
console.log('\n=== CARROT Output ===');
console.log('key:', carrotTarget.key);
console.log('asset_type:', carrotTarget.asset_type);
console.log('view_tag:', carrotTarget.view_tag);
// Parse extra bytes
const extraBytes = new Uint8Array(minerTx.extra);
console.log('\n=== Extra Bytes ===');
console.log('raw:', bytesToHex(extraBytes));
// Extract tx pubkey (D_e for CARROT coinbase)
let offset = 0;
if (extraBytes[offset] === 0x01) {
offset++;
const txPubKey = extraBytes.slice(offset, offset + 32);
offset += 32;
console.log('txPubKey (D_e):', bytesToHex(txPubKey));
// Build input context for coinbase
const inputContext = makeInputContextCoinbase(1110);
console.log('\n=== Input Context ===');
console.log('context:', bytesToHex(inputContext));
// Get view tag as 3-byte array
const viewTagHex = carrotTarget.view_tag;
const viewTag = new Uint8Array(3);
viewTag[0] = parseInt(viewTagHex.slice(0, 2), 16);
viewTag[1] = parseInt(viewTagHex.slice(2, 4), 16);
viewTag[2] = parseInt(viewTagHex.slice(4, 6), 16);
console.log('viewTag:', bytesToHex(viewTag));
// Compute zeroCommit for coinbase (scalar 1)
const clearAmount = BigInt(output.amount);
const scalarOne = new Uint8Array(32);
scalarOne[0] = 1;
const amountCommitment = pedersonCommit(clearAmount, scalarOne);
console.log('amountCommitment:', bytesToHex(amountCommitment));
// Build output object for scanning
const outputForScan = {
key: hexToBytes(carrotTarget.key),
viewTag: viewTag,
enoteEphemeralPubkey: txPubKey,
encryptedAmount: null // Clear amount for coinbase
};
console.log('\n=== Scanning ===');
console.log('outputForScan.key:', bytesToHex(outputForScan.key));
console.log('outputForScan.viewTag:', bytesToHex(outputForScan.viewTag));
console.log('outputForScan.enoteEphemeralPubkey:', bytesToHex(outputForScan.enoteEphemeralPubkey));
console.log('\nviewIncomingKey:', bytesToHex(viewIncomingKey));
console.log('accountSpendPubkey:', bytesToHex(accountSpendPubkey));
// Try scanning
try {
const result = scanCarrotOutput(
outputForScan,
viewIncomingKey,
accountSpendPubkey,
inputContext,
new Map(), // No subaddresses
amountCommitment
);
if (result) {
console.log('\n=== SCAN RESULT ===');
console.log('owned:', result.owned);
console.log('amount:', result.amount);
console.log('isMainAddress:', result.isMainAddress);
console.log('subaddressIndex:', result.subaddressIndex);
} else {
console.log('\nScan result: null (not ours)');
}
} catch (e) {
console.log('\nScan error:', e.message);
console.log(e.stack);
}
}