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

184 lines
6.8 KiB
JavaScript

#!/usr/bin/env bun
/**
* Try ALL permutations of inputs to deriveCarrotCommitmentMask
* to find which combination produces the correct commitment.
*/
import { setCryptoBackend, blake2b, commit, scalarMultBase, scalarMultPoint, pointAddCompressed } from '../src/crypto/index.js';
import { getCryptoBackend } from '../src/crypto/provider.js';
import { MemoryStorage } from '../src/wallet-store.js';
import { readFileSync } from 'fs';
await setCryptoBackend('wasm');
function hexToBytes(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
return bytes;
}
function bytesToHex(bytes) {
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}
const backend = getCryptoBackend();
const w = JSON.parse(readFileSync(`${process.env.HOME}/testnet-wallet/wallet-a.json`, 'utf-8'));
const storage = new MemoryStorage();
storage.load(JSON.parse(readFileSync(`${process.env.HOME}/testnet-wallet/wallet-a-sync.json`, 'utf-8')));
const allOutputs = await storage.getOutputs({ isSpent: false });
const target = allOutputs.find(o => o.isCarrot && o.commitment && o.txHash?.startsWith('d2ad187c'));
const amount = BigInt(target.amount);
const outPk = hexToBytes(target.commitment);
const ctx = hexToBytes(target.carrotSharedSecret); // s_sender_receiver_CTX
// Our accountSpendPubkey (CARROT K_s)
const Ks = hexToBytes(w.carrotKeys.accountSpendPubkey);
// CryptoNote spend pubkey (different from CARROT K_s!)
const cnSpendPub = hexToBytes(w.spendPublicKey);
// We don't have s_sr_unctx stored, but let's check if it was stored elsewhere
// The carrotSharedSecret should be the CONTEXTUALIZED one
function deriveAndCompare(label, key, Ks_param, enoteType) {
const domain = 'Carrot commitment mask';
const domainBytes = new TextEncoder().encode(domain);
const amountBytes = new Uint8Array(8);
let a = amount;
for (let i = 0; i < 8; i++) { amountBytes[i] = Number(a & 0xffn); a >>= 8n; }
const typeBytes = new Uint8Array([enoteType]);
const transcript = new Uint8Array(1 + domainBytes.length + 8 + 32 + 1);
let off = 0;
transcript[off++] = domainBytes.length;
transcript.set(domainBytes, off); off += domainBytes.length;
transcript.set(amountBytes, off); off += 8;
transcript.set(Ks_param, off); off += 32;
transcript.set(typeBytes, off);
const hash64 = blake2b(transcript, 64, key);
const mask = backend.scReduce64(hash64);
const c = commit(amount, mask);
const match = bytesToHex(c) === bytesToHex(outPk);
if (match) {
console.log(`*** MATCH *** ${label}: type=${enoteType}`);
console.log(` mask: ${bytesToHex(mask)}`);
}
return match;
}
console.log('Target: TX=' + target.txHash.slice(0,16) + ' idx=' + target.outputIndex);
console.log(`Amount: ${amount}`);
console.log(`OutPk: ${bytesToHex(outPk)}\n`);
console.log('=== Trying combinations ===');
// Standard CARROT K_s with CTX secret
for (const type of [0, 1, 2]) {
deriveAndCompare(`ctx + CARROT_Ks`, ctx, Ks, type);
}
// CryptoNote spend pubkey with CTX secret
for (const type of [0, 1, 2]) {
deriveAndCompare(`ctx + CN_SpendPub`, ctx, cnSpendPub, type);
}
// What if the CryptoNote VIEW pubkey is used?
const cnViewPub = hexToBytes(w.viewPublicKey);
for (const type of [0, 1, 2]) {
deriveAndCompare(`ctx + CN_ViewPub`, ctx, cnViewPub, type);
}
// What if the CARROT view pubkey is used?
const carrotViewPub = hexToBytes(w.carrotKeys.primaryAddressViewPubkey);
for (const type of [0, 1, 2]) {
deriveAndCompare(`ctx + CARROT_ViewPub`, ctx, carrotViewPub, type);
}
// What if there's an accountViewPubkey?
if (w.carrotKeys.accountViewPubkey) {
const avp = hexToBytes(w.carrotKeys.accountViewPubkey);
for (const type of [0, 1, 2]) {
deriveAndCompare(`ctx + AccountViewPub`, ctx, avp, type);
}
}
// What if the key for blake2b is wrong? Try unkeyed
for (const type of [0, 1, 2]) {
const domain = 'Carrot commitment mask';
const domainBytes = new TextEncoder().encode(domain);
const amountBytes = new Uint8Array(8);
let a = amount;
for (let i = 0; i < 8; i++) { amountBytes[i] = Number(a & 0xffn); a >>= 8n; }
const typeBytes = new Uint8Array([enoteType]);
const transcript = new Uint8Array(1 + domainBytes.length + 8 + 32 + 1);
let off = 0;
transcript[off++] = domainBytes.length;
transcript.set(domainBytes, off); off += domainBytes.length;
transcript.set(amountBytes, off); off += 8;
transcript.set(Ks, off); off += 32;
transcript.set(new Uint8Array([type]), off);
// Try unkeyed blake2b
const hash64 = blake2b(transcript, 64, null);
const mask = backend.scReduce64(hash64);
const c = commit(amount, mask);
if (bytesToHex(c) === bytesToHex(outPk)) {
console.log(`*** MATCH *** unkeyed + CARROT_Ks type=${type}`);
}
}
// What if the transcript order is different? (key, amount, type)
for (const type of [0, 1]) {
const domain = 'Carrot commitment mask';
const domainBytes = new TextEncoder().encode(domain);
const amountBytes = new Uint8Array(8);
let a = amount;
for (let i = 0; i < 8; i++) { amountBytes[i] = Number(a & 0xffn); a >>= 8n; }
// Try: [len][domain][K_s][amount][type]
const t1 = new Uint8Array(1 + domainBytes.length + 32 + 8 + 1);
let off = 0;
t1[off++] = domainBytes.length;
t1.set(domainBytes, off); off += domainBytes.length;
t1.set(Ks, off); off += 32;
t1.set(amountBytes, off); off += 8;
t1[off] = type;
const h1 = blake2b(t1, 64, ctx);
const m1 = backend.scReduce64(h1);
const c1 = commit(amount, m1);
if (bytesToHex(c1) === bytesToHex(outPk)) {
console.log(`*** MATCH *** reordered [Ks,amount,type] type=${type}`);
}
// Try: [len][domain][type][amount][K_s]
const t2 = new Uint8Array(1 + domainBytes.length + 1 + 8 + 32);
off = 0;
t2[off++] = domainBytes.length;
t2.set(domainBytes, off); off += domainBytes.length;
t2[off++] = type;
t2.set(amountBytes, off); off += 8;
t2.set(Ks, off);
const h2 = blake2b(t2, 64, ctx);
const m2 = backend.scReduce64(h2);
const c2 = commit(amount, m2);
if (bytesToHex(c2) === bytesToHex(outPk)) {
console.log(`*** MATCH *** reordered [type,amount,Ks] type=${type}`);
}
}
// What if the commit function args are swapped?
const mask0 = hexToBytes('803b135e5613cdf4905268b48e408213b336ac491d81b67ce1adaf8d6673d004');
const cSwapped = commit(mask0, new Uint8Array(8)); // WRONG but let's check
// Actually, try commit with args in different order
// Our commit(amount, mask) = mask*G + amount*H
// What if C++ does commit(mask, amount)?
const amountAsScalar = new Uint8Array(32);
let a = amount;
for (let i = 0; i < 32 && a > 0n; i++) { amountAsScalar[i] = Number(a & 0xffn); a >>= 8n; }
const cReversed = commit(mask0, amountAsScalar);
console.log(`\ncommit with swapped args: ${bytesToHex(cReversed)}`);
console.log(`Match: ${bytesToHex(cReversed) === bytesToHex(outPk)}`);
console.log('\nDone - no match found' + (false ? '' : ''));