2c128eb1c0
- Implement BP+ verification using @noble/curves for Ed25519 ops - MSM 256 points in ~420ms - mobile-friendly, no WASM needed - Add verifyBulletproofPlus, verifyBulletproofPlusBatch, verifyRangeProof - Add generator initialization with caching (Gi, Hi points) - 24 new tests including performance benchmarks - Update exports in index.js and package.json - Bump version to 0.3.0
774 lines
24 KiB
JavaScript
774 lines
24 KiB
JavaScript
/**
|
|
* Comprehensive Wallet Tests
|
|
*
|
|
* Tests wallet functionality in logical order:
|
|
* 1. Account keys
|
|
* 2. Address keys
|
|
* 3. View-only wallets
|
|
* 4. Key relationships
|
|
* 5. Subaddresses
|
|
* 6. Integrated addresses
|
|
* 7. Key images (preparation)
|
|
*/
|
|
|
|
import {
|
|
// Seed and key generation
|
|
generateSeed,
|
|
deriveKeys,
|
|
deriveCarrotKeys,
|
|
|
|
// Address creation
|
|
createAddress,
|
|
parseAddress,
|
|
|
|
// Subaddress functions
|
|
cnSubaddressSecretKey,
|
|
cnSubaddressSpendPublicKey,
|
|
cnSubaddress,
|
|
generateCNSubaddress,
|
|
carrotIndexExtensionGenerator,
|
|
carrotSubaddressScalar,
|
|
carrotSubaddress,
|
|
generateCarrotSubaddress,
|
|
|
|
// Integrated addresses
|
|
toIntegratedAddress,
|
|
toStandardAddress,
|
|
generateRandomPaymentId,
|
|
createIntegratedAddressWithRandomId,
|
|
|
|
// Ed25519 operations
|
|
scalarMultBase,
|
|
scalarMultPoint,
|
|
pointAddCompressed,
|
|
|
|
// Validation
|
|
isValidAddress,
|
|
isMainnet,
|
|
isSubaddress,
|
|
isIntegrated,
|
|
isStandard,
|
|
isCarrot,
|
|
isLegacy,
|
|
|
|
// Mnemonic
|
|
seedToMnemonic,
|
|
mnemonicToSeed,
|
|
|
|
// Utilities
|
|
bytesToHex,
|
|
hexToBytes,
|
|
keccak256,
|
|
|
|
// Constants
|
|
NETWORK,
|
|
ADDRESS_TYPE,
|
|
ADDRESS_FORMAT
|
|
} from '../src/index.js';
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` ✓ ${name}`);
|
|
passed++;
|
|
} catch (error) {
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` Error: ${error.message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
function assertEqual(actual, expected, message = '') {
|
|
if (actual !== expected) {
|
|
throw new Error(`${message} Expected "${expected}", got "${actual}"`);
|
|
}
|
|
}
|
|
|
|
function assertNotEqual(actual, expected, message = '') {
|
|
if (actual === expected) {
|
|
throw new Error(`${message} Values should not be equal: ${actual}`);
|
|
}
|
|
}
|
|
|
|
function assertTrue(value, message = '') {
|
|
if (!value) {
|
|
throw new Error(`${message} Expected true, got ${value}`);
|
|
}
|
|
}
|
|
|
|
function assertFalse(value, message = '') {
|
|
if (value) {
|
|
throw new Error(`${message} Expected false, got ${value}`);
|
|
}
|
|
}
|
|
|
|
function assertLength(value, length, message = '') {
|
|
if (value.length !== length) {
|
|
throw new Error(`${message} Expected length ${length}, got ${value.length}`);
|
|
}
|
|
}
|
|
|
|
function assertArrayEqual(a, b, message = '') {
|
|
if (a.length !== b.length) {
|
|
throw new Error(`${message} Length mismatch: ${a.length} vs ${b.length}`);
|
|
}
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (a[i] !== b[i]) {
|
|
throw new Error(`${message} Mismatch at index ${i}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate test wallet
|
|
const testSeed = generateSeed();
|
|
const testKeys = deriveKeys(testSeed);
|
|
const testCarrotKeys = deriveCarrotKeys(testSeed);
|
|
|
|
// ============================================================
|
|
// 1. ACCOUNT KEYS
|
|
// ============================================================
|
|
|
|
console.log('\n========================================');
|
|
console.log('1. ACCOUNT KEYS');
|
|
console.log('========================================');
|
|
|
|
test('Account 0 and Account 1 have different subaddress secrets', () => {
|
|
// In CryptoNote, accounts are distinguished by major index
|
|
const account0Secret = cnSubaddressSecretKey(testKeys.viewSecretKey, 0, 1);
|
|
const account1Secret = cnSubaddressSecretKey(testKeys.viewSecretKey, 1, 1);
|
|
|
|
assertNotEqual(bytesToHex(account0Secret), bytesToHex(account1Secret));
|
|
});
|
|
|
|
test('Account subaddress secrets are deterministic', () => {
|
|
const secret1 = cnSubaddressSecretKey(testKeys.viewSecretKey, 0, 1);
|
|
const secret2 = cnSubaddressSecretKey(testKeys.viewSecretKey, 0, 1);
|
|
|
|
assertEqual(bytesToHex(secret1), bytesToHex(secret2));
|
|
});
|
|
|
|
test('Different accounts produce different spend public keys', () => {
|
|
const account0Spend = cnSubaddressSpendPublicKey(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 1);
|
|
const account1Spend = cnSubaddressSpendPublicKey(testKeys.spendPublicKey, testKeys.viewSecretKey, 1, 1);
|
|
|
|
assertNotEqual(bytesToHex(account0Spend), bytesToHex(account1Spend));
|
|
});
|
|
|
|
test('Account 0 address 0 returns main spend key', () => {
|
|
const mainSpend = cnSubaddressSpendPublicKey(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 0);
|
|
|
|
assertEqual(bytesToHex(mainSpend), bytesToHex(testKeys.spendPublicKey));
|
|
});
|
|
|
|
test('Account generation is consistent across recovery', () => {
|
|
// Generate wallet, create account addresses, recover, verify same addresses
|
|
const seed = generateSeed();
|
|
const keys = deriveKeys(seed);
|
|
|
|
const account0Addr = cnSubaddress(keys.spendPublicKey, keys.viewSecretKey, 0, 1);
|
|
const account1Addr = cnSubaddress(keys.spendPublicKey, keys.viewSecretKey, 1, 0);
|
|
|
|
// Simulate recovery
|
|
const mnemonic = seedToMnemonic(seed, { language: 'english' });
|
|
const recovered = mnemonicToSeed(mnemonic, { language: 'english' });
|
|
const recoveredKeys = deriveKeys(recovered.seed);
|
|
|
|
const recoveredAccount0 = cnSubaddress(recoveredKeys.spendPublicKey, recoveredKeys.viewSecretKey, 0, 1);
|
|
const recoveredAccount1 = cnSubaddress(recoveredKeys.spendPublicKey, recoveredKeys.viewSecretKey, 1, 0);
|
|
|
|
assertEqual(bytesToHex(recoveredAccount0.spendPublicKey), bytesToHex(account0Addr.spendPublicKey));
|
|
assertEqual(bytesToHex(recoveredAccount1.spendPublicKey), bytesToHex(account1Addr.spendPublicKey));
|
|
});
|
|
|
|
test('CARROT accounts use different index generators', () => {
|
|
const s_ga = hexToBytes(testCarrotKeys.generateAddressSecret);
|
|
|
|
const gen0 = carrotIndexExtensionGenerator(s_ga, 0, 1);
|
|
const gen1 = carrotIndexExtensionGenerator(s_ga, 1, 1);
|
|
|
|
assertNotEqual(bytesToHex(gen0), bytesToHex(gen1));
|
|
});
|
|
|
|
// ============================================================
|
|
// 2. ADDRESS KEYS
|
|
// ============================================================
|
|
|
|
console.log('\n========================================');
|
|
console.log('2. ADDRESS KEYS');
|
|
console.log('========================================');
|
|
|
|
test('Each address has unique spend public key', () => {
|
|
const addr0 = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 0);
|
|
const addr1 = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 1);
|
|
const addr2 = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 2);
|
|
|
|
assertNotEqual(bytesToHex(addr0.spendPublicKey), bytesToHex(addr1.spendPublicKey));
|
|
assertNotEqual(bytesToHex(addr1.spendPublicKey), bytesToHex(addr2.spendPublicKey));
|
|
assertNotEqual(bytesToHex(addr0.spendPublicKey), bytesToHex(addr2.spendPublicKey));
|
|
});
|
|
|
|
test('Each address has unique view public key', () => {
|
|
const addr0 = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 0);
|
|
const addr1 = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 1);
|
|
const addr2 = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 2);
|
|
|
|
assertNotEqual(bytesToHex(addr0.viewPublicKey), bytesToHex(addr1.viewPublicKey));
|
|
assertNotEqual(bytesToHex(addr1.viewPublicKey), bytesToHex(addr2.viewPublicKey));
|
|
});
|
|
|
|
test('Address keys are 32 bytes each', () => {
|
|
const addr = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 5);
|
|
|
|
assertLength(addr.spendPublicKey, 32);
|
|
assertLength(addr.viewPublicKey, 32);
|
|
});
|
|
|
|
test('Address generation is deterministic', () => {
|
|
const addr1 = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 10);
|
|
const addr2 = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 10);
|
|
|
|
assertEqual(bytesToHex(addr1.spendPublicKey), bytesToHex(addr2.spendPublicKey));
|
|
assertEqual(bytesToHex(addr1.viewPublicKey), bytesToHex(addr2.viewPublicKey));
|
|
});
|
|
|
|
test('Address keys can create valid addresses', () => {
|
|
const addr = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 1);
|
|
|
|
const addressString = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.SUBADDRESS,
|
|
spendPublicKey: addr.spendPublicKey,
|
|
viewPublicKey: addr.viewPublicKey
|
|
});
|
|
|
|
assertTrue(isValidAddress(addressString));
|
|
assertTrue(isSubaddress(addressString));
|
|
});
|
|
|
|
test('CARROT address keys are derived correctly', () => {
|
|
const s_ga = hexToBytes(testCarrotKeys.generateAddressSecret);
|
|
|
|
const addr = carrotSubaddress(
|
|
testKeys.spendPublicKey,
|
|
testKeys.viewPublicKey,
|
|
s_ga,
|
|
0, 1
|
|
);
|
|
|
|
assertLength(addr.spendPublicKey, 32);
|
|
assertLength(addr.viewPublicKey, 32);
|
|
assertFalse(addr.isMainAddress);
|
|
});
|
|
|
|
// ============================================================
|
|
// 3. VIEW-ONLY WALLETS
|
|
// ============================================================
|
|
|
|
console.log('\n========================================');
|
|
console.log('3. VIEW-ONLY WALLETS');
|
|
console.log('========================================');
|
|
|
|
test('View secret key can derive view public key', () => {
|
|
const derivedViewPub = scalarMultBase(testKeys.viewSecretKey);
|
|
|
|
assertEqual(bytesToHex(derivedViewPub), bytesToHex(testKeys.viewPublicKey));
|
|
});
|
|
|
|
test('View-only wallet can verify it owns an address (via view key)', () => {
|
|
// A view-only wallet has: spendPublicKey, viewSecretKey
|
|
// It can verify ownership by checking if address was derived from its keys
|
|
|
|
const viewOnlyWallet = {
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewSecretKey: testKeys.viewSecretKey
|
|
// Note: does NOT have spendSecretKey
|
|
};
|
|
|
|
// Generate a subaddress
|
|
const subaddr = cnSubaddress(
|
|
viewOnlyWallet.spendPublicKey,
|
|
viewOnlyWallet.viewSecretKey,
|
|
0, 5
|
|
);
|
|
|
|
// Create address string
|
|
const addrString = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.SUBADDRESS,
|
|
spendPublicKey: subaddr.spendPublicKey,
|
|
viewPublicKey: subaddr.viewPublicKey
|
|
});
|
|
|
|
// View-only wallet can regenerate this address
|
|
const regenerated = cnSubaddress(
|
|
viewOnlyWallet.spendPublicKey,
|
|
viewOnlyWallet.viewSecretKey,
|
|
0, 5
|
|
);
|
|
|
|
const regeneratedString = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.SUBADDRESS,
|
|
spendPublicKey: regenerated.spendPublicKey,
|
|
viewPublicKey: regenerated.viewPublicKey
|
|
});
|
|
|
|
assertEqual(regeneratedString, addrString);
|
|
});
|
|
|
|
test('View-only wallet can scan subaddresses', () => {
|
|
// Simulate scanning first 10 addresses
|
|
const viewOnlyWallet = {
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewSecretKey: testKeys.viewSecretKey
|
|
};
|
|
|
|
const addresses = [];
|
|
for (let i = 0; i < 10; i++) {
|
|
const sub = cnSubaddress(
|
|
viewOnlyWallet.spendPublicKey,
|
|
viewOnlyWallet.viewSecretKey,
|
|
0, i
|
|
);
|
|
addresses.push(bytesToHex(sub.spendPublicKey));
|
|
}
|
|
|
|
// All should be unique
|
|
const unique = new Set(addresses);
|
|
assertEqual(unique.size, 10);
|
|
});
|
|
|
|
test('View-only cannot derive spend secret key', () => {
|
|
// This is a conceptual test - view-only means no spend secret
|
|
// We verify that viewSecretKey != spendSecretKey
|
|
assertNotEqual(
|
|
bytesToHex(testKeys.viewSecretKey),
|
|
bytesToHex(testKeys.spendSecretKey)
|
|
);
|
|
});
|
|
|
|
// ============================================================
|
|
// 4. KEY RELATIONSHIPS
|
|
// ============================================================
|
|
|
|
console.log('\n========================================');
|
|
console.log('4. KEY RELATIONSHIPS');
|
|
console.log('========================================');
|
|
|
|
test('spend_public = spend_secret * G', () => {
|
|
const derivedSpendPub = scalarMultBase(testKeys.spendSecretKey);
|
|
|
|
assertEqual(bytesToHex(derivedSpendPub), bytesToHex(testKeys.spendPublicKey));
|
|
});
|
|
|
|
test('view_public = view_secret * G', () => {
|
|
const derivedViewPub = scalarMultBase(testKeys.viewSecretKey);
|
|
|
|
assertEqual(bytesToHex(derivedViewPub), bytesToHex(testKeys.viewPublicKey));
|
|
});
|
|
|
|
test('view_secret = H(spend_secret) in CryptoNote', () => {
|
|
// In CryptoNote: view_secret = keccak256(spend_secret) mod L
|
|
const hash = keccak256(testKeys.spendSecretKey);
|
|
|
|
// The deriveKeys function reduces mod L, so we need to check
|
|
// that the hash (before reduction) when reduced equals viewSecretKey
|
|
// This is implicitly tested by the fact that deriveKeys works
|
|
|
|
// Verify view public key derivation
|
|
const viewPub = scalarMultBase(testKeys.viewSecretKey);
|
|
assertEqual(bytesToHex(viewPub), bytesToHex(testKeys.viewPublicKey));
|
|
});
|
|
|
|
test('Subaddress spend key: D = K_spend + m*G', () => {
|
|
// m = subaddress secret
|
|
const m = cnSubaddressSecretKey(testKeys.viewSecretKey, 0, 1);
|
|
|
|
// M = m * G
|
|
const M = scalarMultBase(m);
|
|
|
|
// D = K_spend + M
|
|
const D = pointAddCompressed(testKeys.spendPublicKey, M);
|
|
|
|
// Compare with cnSubaddressSpendPublicKey
|
|
const expected = cnSubaddressSpendPublicKey(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 1);
|
|
|
|
assertEqual(bytesToHex(D), bytesToHex(expected));
|
|
});
|
|
|
|
test('Subaddress view key: C = k_view * D', () => {
|
|
// D = subaddress spend public key
|
|
const D = cnSubaddressSpendPublicKey(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 1);
|
|
|
|
// C = k_view * D
|
|
const C = scalarMultPoint(testKeys.viewSecretKey, D);
|
|
|
|
// Compare with cnSubaddress
|
|
const sub = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 1);
|
|
|
|
assertEqual(bytesToHex(C), bytesToHex(sub.viewPublicKey));
|
|
});
|
|
|
|
test('CARROT: proveSpendKey derived from masterSecret', () => {
|
|
// k_ps = H_n("Carrot prove-spend key", s_master)
|
|
// This is tested implicitly by deriveCarrotKeys working
|
|
assertLength(testCarrotKeys.proveSpendKey, 64); // hex string = 64 chars
|
|
});
|
|
|
|
test('CARROT: viewBalanceSecret derived from masterSecret', () => {
|
|
// s_vb = H_32("Carrot view-balance secret", s_master)
|
|
assertLength(testCarrotKeys.viewBalanceSecret, 64);
|
|
});
|
|
|
|
test('CARROT: viewIncomingKey derived from viewBalanceSecret', () => {
|
|
// k_vi = H_n("Carrot incoming view key", s_vb)
|
|
assertLength(testCarrotKeys.viewIncomingKey, 64);
|
|
});
|
|
|
|
test('Key derivation is one-way (cannot derive secret from public)', () => {
|
|
// This is a mathematical property of EC - we just verify they're different
|
|
assertNotEqual(
|
|
bytesToHex(testKeys.spendSecretKey),
|
|
bytesToHex(testKeys.spendPublicKey)
|
|
);
|
|
assertNotEqual(
|
|
bytesToHex(testKeys.viewSecretKey),
|
|
bytesToHex(testKeys.viewPublicKey)
|
|
);
|
|
});
|
|
|
|
// ============================================================
|
|
// 5. SUBADDRESSES
|
|
// ============================================================
|
|
|
|
console.log('\n========================================');
|
|
console.log('5. SUBADDRESSES');
|
|
console.log('========================================');
|
|
|
|
test('Main address (0,0) returns original keys', () => {
|
|
const main = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 0);
|
|
|
|
assertEqual(bytesToHex(main.spendPublicKey), bytesToHex(testKeys.spendPublicKey));
|
|
});
|
|
|
|
test('Subaddresses are valid curve points', () => {
|
|
for (let i = 1; i <= 5; i++) {
|
|
const sub = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, i);
|
|
|
|
// Points should be non-zero
|
|
let hasNonZero = false;
|
|
for (let j = 0; j < 32; j++) {
|
|
if (sub.spendPublicKey[j] !== 0) hasNonZero = true;
|
|
}
|
|
assertTrue(hasNonZero, `Subaddress ${i} spend key should be non-zero`);
|
|
}
|
|
});
|
|
|
|
test('generateCNSubaddress produces valid address strings', () => {
|
|
const result = generateCNSubaddress({
|
|
network: NETWORK.MAINNET,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewSecretKey: testKeys.viewSecretKey,
|
|
major: 0,
|
|
minor: 1
|
|
});
|
|
|
|
assertTrue(isValidAddress(result.address));
|
|
assertTrue(isSubaddress(result.address));
|
|
assertTrue(isLegacy(result.address));
|
|
assertTrue(isMainnet(result.address));
|
|
});
|
|
|
|
test('generateCarrotSubaddress produces valid CARROT addresses', () => {
|
|
const result = generateCarrotSubaddress({
|
|
network: NETWORK.MAINNET,
|
|
accountSpendPubkey: testKeys.spendPublicKey,
|
|
accountViewPubkey: testKeys.viewPublicKey,
|
|
generateAddressSecret: hexToBytes(testCarrotKeys.generateAddressSecret),
|
|
major: 0,
|
|
minor: 1
|
|
});
|
|
|
|
assertTrue(isValidAddress(result.address));
|
|
assertTrue(isCarrot(result.address));
|
|
assertFalse(result.isMainAddress);
|
|
});
|
|
|
|
test('Subaddress index 0,0 vs 0,1 are different', () => {
|
|
const main = generateCNSubaddress({
|
|
network: NETWORK.MAINNET,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewSecretKey: testKeys.viewSecretKey,
|
|
major: 0,
|
|
minor: 0
|
|
});
|
|
|
|
const sub1 = generateCNSubaddress({
|
|
network: NETWORK.MAINNET,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewSecretKey: testKeys.viewSecretKey,
|
|
major: 0,
|
|
minor: 1
|
|
});
|
|
|
|
assertNotEqual(main.address, sub1.address);
|
|
});
|
|
|
|
test('Can generate many subaddresses efficiently', () => {
|
|
const addresses = new Set();
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
const sub = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, i);
|
|
addresses.add(bytesToHex(sub.spendPublicKey));
|
|
}
|
|
|
|
assertEqual(addresses.size, 100, 'All 100 subaddresses should be unique');
|
|
});
|
|
|
|
// ============================================================
|
|
// 6. INTEGRATED ADDRESSES
|
|
// ============================================================
|
|
|
|
console.log('\n========================================');
|
|
console.log('6. INTEGRATED ADDRESSES');
|
|
console.log('========================================');
|
|
|
|
test('Integrated address contains payment ID', () => {
|
|
const mainAddr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
|
|
const paymentId = 'abcdef0123456789';
|
|
const integrated = toIntegratedAddress(mainAddr, paymentId);
|
|
|
|
const parsed = parseAddress(integrated);
|
|
assertTrue(parsed.valid);
|
|
assertEqual(parsed.type, ADDRESS_TYPE.INTEGRATED);
|
|
assertEqual(bytesToHex(parsed.paymentId), paymentId);
|
|
});
|
|
|
|
test('Random payment IDs are unique', () => {
|
|
const ids = new Set();
|
|
for (let i = 0; i < 100; i++) {
|
|
const pid = generateRandomPaymentId();
|
|
ids.add(bytesToHex(pid));
|
|
}
|
|
assertEqual(ids.size, 100);
|
|
});
|
|
|
|
test('Payment ID is 8 bytes', () => {
|
|
const pid = generateRandomPaymentId();
|
|
assertLength(pid, 8);
|
|
});
|
|
|
|
test('Integrated address round-trips correctly', () => {
|
|
const mainAddr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
|
|
const integrated = toIntegratedAddress(mainAddr, 'deadbeef12345678');
|
|
const standard = toStandardAddress(integrated);
|
|
|
|
assertEqual(standard, mainAddr);
|
|
});
|
|
|
|
test('createIntegratedAddressWithRandomId returns all components', () => {
|
|
const mainAddr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
|
|
const result = createIntegratedAddressWithRandomId(mainAddr);
|
|
|
|
assertTrue(result !== null);
|
|
assertTrue(isIntegrated(result.address));
|
|
assertLength(result.paymentId, 8);
|
|
assertLength(result.paymentIdHex, 16);
|
|
});
|
|
|
|
test('Integrated addresses preserve network', () => {
|
|
// Mainnet
|
|
const mainnetAddr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
const mainnetIntegrated = toIntegratedAddress(mainnetAddr, 'abcdef0123456789');
|
|
assertTrue(isMainnet(mainnetIntegrated));
|
|
|
|
// Testnet
|
|
const testnetAddr = createAddress({
|
|
network: NETWORK.TESTNET,
|
|
format: ADDRESS_FORMAT.LEGACY,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
const testnetIntegrated = toIntegratedAddress(testnetAddr, 'abcdef0123456789');
|
|
const parsed = parseAddress(testnetIntegrated);
|
|
assertEqual(parsed.network, NETWORK.TESTNET);
|
|
});
|
|
|
|
test('CARROT integrated addresses work', () => {
|
|
const carrotAddr = createAddress({
|
|
network: NETWORK.MAINNET,
|
|
format: ADDRESS_FORMAT.CARROT,
|
|
type: ADDRESS_TYPE.STANDARD,
|
|
spendPublicKey: testKeys.spendPublicKey,
|
|
viewPublicKey: testKeys.viewPublicKey
|
|
});
|
|
|
|
const integrated = toIntegratedAddress(carrotAddr, 'fedcba9876543210');
|
|
|
|
assertTrue(isValidAddress(integrated));
|
|
assertTrue(isCarrot(integrated));
|
|
assertTrue(isIntegrated(integrated));
|
|
});
|
|
|
|
// ============================================================
|
|
// 7. KEY IMAGES (Preparation)
|
|
// ============================================================
|
|
|
|
console.log('\n========================================');
|
|
console.log('7. KEY IMAGES (Preparation)');
|
|
console.log('========================================');
|
|
|
|
test('Key image requires spend secret key', () => {
|
|
// Key image I = x * H_p(P) where x is spend secret, P is output public key
|
|
// We verify we have the necessary components
|
|
assertLength(testKeys.spendSecretKey, 32);
|
|
assertTrue(testKeys.spendSecretKey instanceof Uint8Array);
|
|
});
|
|
|
|
test('CARROT generateImageKey is derived correctly', () => {
|
|
// k_gi = H_n("Carrot generate-image key", s_vb)
|
|
assertLength(testCarrotKeys.generateImageKey, 64); // hex
|
|
|
|
// It should be different from other keys
|
|
assertNotEqual(testCarrotKeys.generateImageKey, testCarrotKeys.proveSpendKey);
|
|
assertNotEqual(testCarrotKeys.generateImageKey, testCarrotKeys.viewIncomingKey);
|
|
});
|
|
|
|
test('Each subaddress can have unique key images', () => {
|
|
// Key images are per-output, but subaddress derivation affects them
|
|
// Different subaddresses have different spend keys
|
|
const sub1 = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 1);
|
|
const sub2 = cnSubaddress(testKeys.spendPublicKey, testKeys.viewSecretKey, 0, 2);
|
|
|
|
// Different spend public keys mean different key image bases
|
|
assertNotEqual(bytesToHex(sub1.spendPublicKey), bytesToHex(sub2.spendPublicKey));
|
|
});
|
|
|
|
test('Key material is suitable for key image generation', () => {
|
|
// For key image generation, we need:
|
|
// 1. Spend secret key (scalar)
|
|
// 2. Output public key (point)
|
|
// 3. Hash-to-point function
|
|
|
|
// Verify spend secret is valid scalar (< L)
|
|
const L = (1n << 252n) + 27742317777372353535851937790883648493n;
|
|
let n = 0n;
|
|
for (let i = 31; i >= 0; i--) {
|
|
n = (n << 8n) | BigInt(testKeys.spendSecretKey[i]);
|
|
}
|
|
assertTrue(n < L, 'Spend secret should be < L');
|
|
});
|
|
|
|
// ============================================================
|
|
// RECOVERY VERIFICATION
|
|
// ============================================================
|
|
|
|
console.log('\n========================================');
|
|
console.log('RECOVERY VERIFICATION');
|
|
console.log('========================================');
|
|
|
|
test('Full wallet recovery produces identical addresses for all indices', () => {
|
|
const seed = generateSeed();
|
|
const keys = deriveKeys(seed);
|
|
|
|
// Generate addresses at various indices
|
|
const originalAddresses = [];
|
|
for (let major = 0; major < 3; major++) {
|
|
for (let minor = 0; minor < 5; minor++) {
|
|
const sub = cnSubaddress(keys.spendPublicKey, keys.viewSecretKey, major, minor);
|
|
originalAddresses.push({
|
|
major, minor,
|
|
spend: bytesToHex(sub.spendPublicKey),
|
|
view: bytesToHex(sub.viewPublicKey)
|
|
});
|
|
}
|
|
}
|
|
|
|
// Recover wallet
|
|
const mnemonic = seedToMnemonic(seed, { language: 'english' });
|
|
const recovered = mnemonicToSeed(mnemonic, { language: 'english' });
|
|
const recoveredKeys = deriveKeys(recovered.seed);
|
|
|
|
// Verify all addresses match
|
|
for (const orig of originalAddresses) {
|
|
const recov = cnSubaddress(
|
|
recoveredKeys.spendPublicKey,
|
|
recoveredKeys.viewSecretKey,
|
|
orig.major,
|
|
orig.minor
|
|
);
|
|
assertEqual(bytesToHex(recov.spendPublicKey), orig.spend,
|
|
`Account ${orig.major} Address ${orig.minor} spend key mismatch`);
|
|
assertEqual(bytesToHex(recov.viewPublicKey), orig.view,
|
|
`Account ${orig.major} Address ${orig.minor} view key mismatch`);
|
|
}
|
|
});
|
|
|
|
test('CARROT wallet recovery produces identical CARROT keys', () => {
|
|
const seed = generateSeed();
|
|
const carrotKeys = deriveCarrotKeys(seed);
|
|
|
|
// Recover
|
|
const mnemonic = seedToMnemonic(seed, { language: 'english' });
|
|
const recovered = mnemonicToSeed(mnemonic, { language: 'english' });
|
|
const recoveredCarrot = deriveCarrotKeys(recovered.seed);
|
|
|
|
assertEqual(recoveredCarrot.proveSpendKey, carrotKeys.proveSpendKey);
|
|
assertEqual(recoveredCarrot.viewBalanceSecret, carrotKeys.viewBalanceSecret);
|
|
assertEqual(recoveredCarrot.generateImageKey, carrotKeys.generateImageKey);
|
|
assertEqual(recoveredCarrot.viewIncomingKey, carrotKeys.viewIncomingKey);
|
|
assertEqual(recoveredCarrot.generateAddressSecret, carrotKeys.generateAddressSecret);
|
|
});
|
|
|
|
// ============================================================
|
|
// Summary
|
|
// ============================================================
|
|
|
|
console.log('\n========================================');
|
|
console.log('WALLET TEST SUMMARY');
|
|
console.log('========================================');
|
|
console.log(`Passed: ${passed}`);
|
|
console.log(`Failed: ${failed}`);
|
|
console.log(`Total: ${passed + failed}`);
|
|
|
|
if (failed > 0) {
|
|
console.log('\n⚠️ Some tests failed!');
|
|
process.exit(1);
|
|
} else {
|
|
console.log('\n✓ All wallet tests passed!');
|
|
}
|