Files
salvium-rs/test/legacy-js/debug-full-carrot-scan.js
Matt Hess 733ecd2681 Migrate all JS tests to Rust: 9-crate workspace, 703 tests, 0 JS remaining
Add root Cargo workspace with 9 crates: salvium-crypto (extended),
  salvium-types, salvium-consensus, salvium-wallet, salvium-tx,
  salvium-rpc, salvium-miner (extended), salvium-cli, salvium-multisig.

  New modules: chain_state, block_weight, alt_chain, validation,
  offline signing, stake lifecycle, wallet sync/query/encryption/utxo,
  randomx utilities, and full multisig crate with CARROT support.

  Delete 188 JS test/helper/debug files; archive integration test
  scripts to test/legacy-js/ for live testnet use. Testnet integration
  tests (transfer, stake, burn, convert, sweep) remain as #[ignore]-
  gated Rust tests runnable with --ignored against a live daemon.
2026-02-17 23:09:35 +00:00

154 lines
5.3 KiB
JavaScript

#!/usr/bin/env bun
/**
* Debug full CARROT scan flow
*
* Usage:
* SPEND_SECRET="64-char-hex" TX_HASH="tx-hash" bun test/debug-full-carrot-scan.js
*
* Environment variables:
* SPEND_SECRET - 64-character hex spend secret key (required)
* TX_HASH - Transaction hash to scan (required)
* DAEMON_URL - Daemon RPC URL (default: http://seed01.salvium.io:19081)
*/
import { DaemonRPC } from '../src/rpc/daemon.js';
import { deriveKeys, deriveCarrotKeys } from '../src/carrot.js';
import { generateCarrotSubaddressMap } from '../src/subaddress.js';
import { hexToBytes, bytesToHex } from '../src/address.js';
import {
carrotEcdhKeyExchange,
computeCarrotViewTag,
makeInputContext,
makeCarrotSenderReceiverSecret,
recoverAddressSpendPubkey
} from '../src/carrot-scanning.js';
const SPEND_SECRET = process.env.SPEND_SECRET;
const TX_HASH = process.env.TX_HASH;
const DAEMON_URL = process.env.DAEMON_URL || 'http://seed01.salvium.io:19081';
if (!SPEND_SECRET || !TX_HASH) {
console.error('ERROR: SPEND_SECRET and TX_HASH environment variables required.\n');
console.log('Usage:');
console.log(' SPEND_SECRET="64-char-hex" TX_HASH="tx-hash" bun test/debug-full-carrot-scan.js');
process.exit(1);
}
console.log('=== Full CARROT Scan Debug ===\n');
// 1. Derive keys
const spendSecret = hexToBytes(SPEND_SECRET);
const cnKeys = deriveKeys(spendSecret);
const carrotKeys = deriveCarrotKeys(spendSecret);
console.log('1. Wallet Keys:');
console.log(' k_vi:', carrotKeys.viewIncomingKey);
console.log(' K_s:', carrotKeys.accountSpendPubkey);
// 2. Generate subaddress map
console.log('\n2. Generating subaddress map...');
const subaddressMap = generateCarrotSubaddressMap(
hexToBytes(carrotKeys.accountSpendPubkey),
hexToBytes(carrotKeys.accountViewPubkey),
hexToBytes(carrotKeys.generateAddressSecret),
50, 200
);
console.log(' Map size:', subaddressMap.size);
// Check if subaddress (0,1) is in the map
let found01 = null;
for (const [key, idx] of subaddressMap.entries()) {
if (idx.major === 0 && idx.minor === 1) {
found01 = key;
break;
}
}
console.log(' Subaddress (0,1) K_s:', found01 || 'NOT FOUND');
// 3. Fetch transaction
console.log('\n3. Fetching transaction...');
const daemon = new DaemonRPC({ url: DAEMON_URL });
const txResult = await daemon.getTransactions([TX_HASH], { decode_as_json: true });
const tx = JSON.parse(txResult.result.txs[0].as_json);
// Extract D_e from tx_extra
const extra = new Uint8Array(tx.extra);
let D_e = null;
if (extra[0] === 0x01) {
D_e = extra.slice(1, 33);
}
console.log(' D_e:', bytesToHex(D_e));
// Extract first key image
const firstKeyImage = tx.vin[0].key.k_image;
console.log(' First key image:', firstKeyImage);
// Build input context
const inputContext = makeInputContext(firstKeyImage);
console.log(' Input context:', bytesToHex(inputContext));
// 4. Process output 1 (the 10 SAL output)
console.log('\n4. Processing Output 1:');
const output = tx.vout[1];
const carrot = output.target.carrot_v1;
const Ko = hexToBytes(carrot.key);
const viewTagActual = hexToBytes(carrot.view_tag);
const commitment = tx.rct_signatures.outPk ? hexToBytes(tx.rct_signatures.outPk[1]) : null;
console.log(' Ko:', carrot.key);
console.log(' View tag:', carrot.view_tag);
console.log(' Commitment:', commitment ? bytesToHex(commitment) : 'NULL');
// 5. Compute shared secret
console.log('\n5. Computing shared secret:');
const k_vi = hexToBytes(carrotKeys.viewIncomingKey);
const s_sr_unctx = carrotEcdhKeyExchange(k_vi, D_e);
console.log(' s_sr_unctx:', bytesToHex(s_sr_unctx));
// 6. Compute view tag
console.log('\n6. Verifying view tag:');
const viewTagExpected = computeCarrotViewTag(s_sr_unctx, inputContext, Ko);
console.log(' Expected:', bytesToHex(viewTagExpected));
console.log(' Actual:', carrot.view_tag);
console.log(' Match:', bytesToHex(viewTagExpected) === carrot.view_tag ? 'YES ✓' : 'NO ✗');
if (bytesToHex(viewTagExpected) !== carrot.view_tag) {
console.log('\n VIEW TAG MISMATCH - stopping here');
process.exit(1);
}
// 7. Compute contextualized shared secret
console.log('\n7. Computing contextualized shared secret:');
const s_sr_ctx = makeCarrotSenderReceiverSecret(s_sr_unctx, D_e, inputContext);
console.log(' s_sr_ctx:', bytesToHex(s_sr_ctx));
// 8. Recover address spend pubkey
console.log('\n8. Recovering address spend pubkey:');
const K_s_recovered = recoverAddressSpendPubkey(Ko, s_sr_ctx, commitment || new Uint8Array(32));
const K_s_recovered_hex = bytesToHex(K_s_recovered);
console.log(' Recovered K_s:', K_s_recovered_hex);
console.log(' Main address K_s:', carrotKeys.accountSpendPubkey);
console.log(' Match main:', K_s_recovered_hex === carrotKeys.accountSpendPubkey ? 'YES' : 'NO');
// 9. Check subaddress map
console.log('\n9. Checking subaddress map:');
const subaddrIdx = subaddressMap.get(K_s_recovered_hex);
if (subaddrIdx) {
console.log(' FOUND! Subaddress index:', subaddrIdx);
} else {
console.log(' NOT FOUND in subaddress map');
console.log(' Looking for similar keys...');
// Check first few entries
let count = 0;
for (const [key, idx] of subaddressMap.entries()) {
if (count < 5 || (idx.major === 0 && idx.minor <= 5)) {
console.log(` (${idx.major},${idx.minor}): ${key.slice(0, 32)}...`);
}
count++;
}
}
console.log('\n=== End Debug ===');