733ecd2681
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.
184 lines
6.8 KiB
JavaScript
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 ? '' : ''));
|