Files
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

140 lines
5.2 KiB
JavaScript

#!/usr/bin/env bun
/**
* CARROT Self-Test
* Generates a CARROT output and verifies detection
* This tests the entire CARROT scanning pipeline end-to-end
*
* Usage:
* WALLET_SEED="your 25 word mnemonic" bun test/carrot-self-test.js
* MASTER_KEY="64-char-hex" bun test/carrot-self-test.js
*/
import { blake2b } from '../src/blake2b.js';
import { hexToBytes, bytesToHex } from '../src/address.js';
import { mnemonicToSeed } from '../src/mnemonic.js';
import { deriveKeys, deriveCarrotKeys } from '../src/carrot.js';
import { scalarMultBase } from '../src/crypto/index.js';
import {
x25519ScalarMult,
edwardsToMontgomeryU,
carrotEcdhKeyExchange,
computeCarrotViewTag,
makeInputContextCoinbase
} from '../src/carrot-scanning.js';
console.log('=== CARROT Self-Test ===\n');
// Get wallet seed from environment
if (!process.env.WALLET_SEED && !process.env.MASTER_KEY) {
console.error('ERROR: WALLET_SEED or MASTER_KEY environment variable required.\n');
console.log('Usage:');
console.log(' WALLET_SEED="your 25 word mnemonic" bun test/carrot-self-test.js');
console.log(' MASTER_KEY="64-char-hex" bun test/carrot-self-test.js');
process.exit(1);
}
// 1. Generate recipient wallet
console.log('1. Generating recipient wallet...');
let seedResult;
if (process.env.WALLET_SEED) {
const mnemonic = process.env.WALLET_SEED.trim();
seedResult = mnemonicToSeed(mnemonic, { language: 'auto' });
if (!seedResult.valid) {
console.error('Invalid mnemonic:', seedResult.error);
process.exit(1);
}
} else {
const masterKey = process.env.MASTER_KEY.trim();
if (masterKey.length !== 64) {
console.error('MASTER_KEY must be 64 hex characters');
process.exit(1);
}
seedResult = { seed: hexToBytes(masterKey), valid: true };
}
const cnKeys = deriveKeys(seedResult.seed);
const recipientCarrot = deriveCarrotKeys(cnKeys.spendSecretKey);
const k_vi = hexToBytes(recipientCarrot.viewIncomingKey);
const K_s = hexToBytes(recipientCarrot.accountSpendPubkey);
console.log(' Recipient k_vi:', recipientCarrot.viewIncomingKey);
console.log(' Recipient K_s:', recipientCarrot.accountSpendPubkey);
// 2. Sender generates ephemeral keypair for X25519
console.log('\n2. Sender generating ephemeral keypair...');
// Generate random ephemeral secret (in real code, use crypto.getRandomValues)
const d_e = new Uint8Array(32);
for (let i = 0; i < 32; i++) d_e[i] = Math.floor(Math.random() * 256);
d_e[0] &= 248; // Clear bits 0-2 for proper scalar
d_e[31] &= 127; // Clear bit 255
// D_e = d_e * G (in X25519 format)
// First compute on Edwards curve, then convert to Montgomery
const D_e_edwards = scalarMultBase(d_e);
const D_e = edwardsToMontgomeryU(D_e_edwards);
console.log(' Sender d_e (secret):', bytesToHex(d_e));
console.log(' Sender D_e (public):', bytesToHex(D_e));
// 3. Sender computes shared secret
console.log('\n3. Sender computing shared secret...');
// Convert recipient's K_s (Edwards) to X25519
const K_s_x25519 = edwardsToMontgomeryU(K_s);
console.log(' Recipient K_s (X25519):', bytesToHex(K_s_x25519));
// Sender computes s_sr = d_e * K_vi_G
// But wait - sender uses view pubkey, not spend pubkey
// k_vi * G in X25519 format
const K_vi_G = edwardsToMontgomeryU(hexToBytes(recipientCarrot.primaryAddressViewPubkey));
console.log(' Recipient K_vi*G (X25519):', bytesToHex(K_vi_G));
// Sender: s_sr = d_e * (k_vi * G)
const s_sr_sender = x25519ScalarMult(d_e, K_vi_G);
console.log(' Sender s_sr:', bytesToHex(s_sr_sender));
// 4. Generate one-time address Ko
console.log('\n4. Generating one-time address...');
// For simplicity, we'll use K_s directly as Ko (in real CARROT, there's more derivation)
const Ko = K_s;
console.log(' One-time address Ko:', bytesToHex(Ko));
// 5. Compute view tag (sender side)
console.log('\n5. Sender computing view tag...');
const blockHeight = 405000;
const inputContext = makeInputContextCoinbase(blockHeight);
const viewTagSender = computeCarrotViewTag(s_sr_sender, inputContext, Ko);
console.log(' Input context:', bytesToHex(inputContext));
console.log(' View tag (sender):', bytesToHex(viewTagSender));
// 6. Recipient scans the output
console.log('\n6. Recipient scanning output...');
// Recipient computes s_sr = k_vi * D_e
const s_sr_recipient = carrotEcdhKeyExchange(k_vi, D_e);
console.log(' Recipient s_sr:', bytesToHex(s_sr_recipient));
// Check if shared secrets match
const secretsMatch = bytesToHex(s_sr_sender) === bytesToHex(s_sr_recipient);
console.log(' Shared secrets match:', secretsMatch ? 'YES' : 'NO');
// Recipient computes expected view tag
const viewTagRecipient = computeCarrotViewTag(s_sr_recipient, inputContext, Ko);
console.log(' View tag (recipient):', bytesToHex(viewTagRecipient));
// Check if view tags match
const viewTagsMatch = bytesToHex(viewTagSender) === bytesToHex(viewTagRecipient);
console.log(' View tags match:', viewTagsMatch ? 'YES' : 'NO');
// 7. Final result
console.log('\n=== Results ===');
if (secretsMatch && viewTagsMatch) {
console.log('SUCCESS: CARROT ECDH and view tag computation working correctly!');
} else {
console.log('FAILURE: Something is wrong');
if (!secretsMatch) console.log(' - Shared secrets do not match');
if (!viewTagsMatch) console.log(' - View tags do not match');
}