● Add proper error handling - eliminate silent failures
- Add ParseError class with context (offset, field, expected/actual) - transaction.js: parseRctSigPrunable throws instead of returning null - address.js: createAddress, toIntegratedAddress, toStandardAddress throw on invalid input - base58.js: decode functions throw descriptive errors - scanning.js: validate inputs and throw on failures - carrot-scanning.js: validate required fields - signature.js: remove debug console.log and testEd25519 - Update tests to expect throws for error cases
This commit is contained in:
+39
-23
@@ -74,9 +74,11 @@ export function parseAddress(address) {
|
||||
}
|
||||
|
||||
// Decode the address
|
||||
const decoded = decodeAddress(address);
|
||||
if (decoded === null) {
|
||||
result.error = 'Invalid Base58 encoding or checksum';
|
||||
let decoded;
|
||||
try {
|
||||
decoded = decodeAddress(address);
|
||||
} catch (e) {
|
||||
result.error = e.message;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -256,24 +258,33 @@ export function createAddress(options) {
|
||||
const { network, format, type, spendPublicKey, viewPublicKey, paymentId } = options;
|
||||
|
||||
// Validate keys
|
||||
if (!spendPublicKey || spendPublicKey.length !== KEY_SIZE) {
|
||||
return null;
|
||||
if (!spendPublicKey) {
|
||||
throw new Error('createAddress: spendPublicKey is required');
|
||||
}
|
||||
if (!viewPublicKey || viewPublicKey.length !== KEY_SIZE) {
|
||||
return null;
|
||||
if (spendPublicKey.length !== KEY_SIZE) {
|
||||
throw new Error(`createAddress: spendPublicKey must be ${KEY_SIZE} bytes, got ${spendPublicKey.length}`);
|
||||
}
|
||||
if (!viewPublicKey) {
|
||||
throw new Error('createAddress: viewPublicKey is required');
|
||||
}
|
||||
if (viewPublicKey.length !== KEY_SIZE) {
|
||||
throw new Error(`createAddress: viewPublicKey must be ${KEY_SIZE} bytes, got ${viewPublicKey.length}`);
|
||||
}
|
||||
|
||||
// Get prefix
|
||||
const prefix = getPrefix(network, format, type);
|
||||
if (prefix === null) {
|
||||
return null;
|
||||
throw new Error(`createAddress: invalid network/format/type combination: ${network}/${format}/${type}`);
|
||||
}
|
||||
|
||||
// Build data
|
||||
let data;
|
||||
if (type === ADDRESS_TYPE.INTEGRATED) {
|
||||
if (!paymentId || paymentId.length !== PAYMENT_ID_SIZE) {
|
||||
return null;
|
||||
if (!paymentId) {
|
||||
throw new Error('createAddress: paymentId is required for integrated addresses');
|
||||
}
|
||||
if (paymentId.length !== PAYMENT_ID_SIZE) {
|
||||
throw new Error(`createAddress: paymentId must be ${PAYMENT_ID_SIZE} bytes, got ${paymentId.length}`);
|
||||
}
|
||||
data = new Uint8Array(KEY_SIZE * 2 + PAYMENT_ID_SIZE);
|
||||
data.set(spendPublicKey, 0);
|
||||
@@ -292,19 +303,23 @@ export function createAddress(options) {
|
||||
* Convert a standard address to an integrated address by adding a payment ID
|
||||
* @param {string} address - Standard address
|
||||
* @param {Uint8Array|string} paymentId - 8-byte payment ID (or 16-char hex string)
|
||||
* @returns {string|null} - Integrated address or null on error
|
||||
* @returns {string} - Integrated address
|
||||
* @throws {Error} If address is invalid or not a standard address
|
||||
*/
|
||||
export function toIntegratedAddress(address, paymentId) {
|
||||
const parsed = parseAddress(address);
|
||||
|
||||
if (!parsed.valid || parsed.type !== ADDRESS_TYPE.STANDARD) {
|
||||
return null;
|
||||
if (!parsed.valid) {
|
||||
throw new Error(`toIntegratedAddress: invalid address - ${parsed.error}`);
|
||||
}
|
||||
if (parsed.type !== ADDRESS_TYPE.STANDARD) {
|
||||
throw new Error(`toIntegratedAddress: address must be a standard address, got ${parsed.type}`);
|
||||
}
|
||||
|
||||
// Convert hex string to bytes if needed
|
||||
if (typeof paymentId === 'string') {
|
||||
if (paymentId.length !== 16) {
|
||||
return null;
|
||||
throw new Error(`toIntegratedAddress: payment ID hex string must be 16 characters, got ${paymentId.length}`);
|
||||
}
|
||||
const bytes = new Uint8Array(8);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
@@ -314,7 +329,7 @@ export function toIntegratedAddress(address, paymentId) {
|
||||
}
|
||||
|
||||
if (paymentId.length !== PAYMENT_ID_SIZE) {
|
||||
return null;
|
||||
throw new Error(`toIntegratedAddress: payment ID must be ${PAYMENT_ID_SIZE} bytes, got ${paymentId.length}`);
|
||||
}
|
||||
|
||||
return createAddress({
|
||||
@@ -330,13 +345,17 @@ export function toIntegratedAddress(address, paymentId) {
|
||||
/**
|
||||
* Extract the standard address from an integrated address
|
||||
* @param {string} address - Integrated address
|
||||
* @returns {string|null} - Standard address or null on error
|
||||
* @returns {string} - Standard address
|
||||
* @throws {Error} If address is invalid or not an integrated address
|
||||
*/
|
||||
export function toStandardAddress(address) {
|
||||
const parsed = parseAddress(address);
|
||||
|
||||
if (!parsed.valid || parsed.type !== ADDRESS_TYPE.INTEGRATED) {
|
||||
return null;
|
||||
if (!parsed.valid) {
|
||||
throw new Error(`toStandardAddress: invalid address - ${parsed.error}`);
|
||||
}
|
||||
if (parsed.type !== ADDRESS_TYPE.INTEGRATED) {
|
||||
throw new Error(`toStandardAddress: address must be an integrated address, got ${parsed.type}`);
|
||||
}
|
||||
|
||||
return createAddress({
|
||||
@@ -486,16 +505,13 @@ export function generateRandomPaymentId() {
|
||||
/**
|
||||
* Create an integrated address with a random payment ID
|
||||
* @param {string} address - Standard address
|
||||
* @returns {Object} { address, paymentId } - Integrated address and its payment ID
|
||||
* @returns {Object} { address, paymentId, paymentIdHex } - Integrated address and its payment ID
|
||||
* @throws {Error} If address is invalid or not a standard address
|
||||
*/
|
||||
export function createIntegratedAddressWithRandomId(address) {
|
||||
const paymentId = genPaymentId();
|
||||
const integratedAddress = toIntegratedAddress(address, paymentId);
|
||||
|
||||
if (!integratedAddress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
address: integratedAddress,
|
||||
paymentId,
|
||||
|
||||
+25
-27
@@ -76,12 +76,14 @@ function encodeBlock(block) {
|
||||
/**
|
||||
* Decode a single Base58 block to bytes
|
||||
* @param {string} block - Base58 encoded block
|
||||
* @returns {Uint8Array|null} - Decoded bytes or null on error
|
||||
* @param {number} blockIndex - Block index for error messages
|
||||
* @returns {Uint8Array} - Decoded bytes
|
||||
* @throws {Error} If block is invalid
|
||||
*/
|
||||
function decodeBlock(block) {
|
||||
function decodeBlock(block, blockIndex = 0) {
|
||||
const decodedSize = DECODED_BLOCK_SIZES.get(block.length);
|
||||
if (decodedSize === undefined || decodedSize < 0) {
|
||||
return null; // Invalid block size
|
||||
throw new Error(`Base58 decode: invalid block size ${block.length} at block ${blockIndex}`);
|
||||
}
|
||||
|
||||
if (decodedSize === 0) {
|
||||
@@ -94,14 +96,14 @@ function decodeBlock(block) {
|
||||
for (let i = 0; i < block.length; i++) {
|
||||
const digit = ALPHABET_MAP.get(block[i]);
|
||||
if (digit === undefined) {
|
||||
return null; // Invalid character
|
||||
throw new Error(`Base58 decode: invalid character '${block[i]}' at position ${i} in block ${blockIndex}`);
|
||||
}
|
||||
num = num * base + BigInt(digit);
|
||||
}
|
||||
|
||||
// Check for overflow
|
||||
if (decodedSize < BASE58_FULL_BLOCK_SIZE && num >= (1n << BigInt(8 * decodedSize))) {
|
||||
return null; // Overflow
|
||||
throw new Error(`Base58 decode: numeric overflow in block ${blockIndex}`);
|
||||
}
|
||||
|
||||
return uint64ToUint8BE(num, decodedSize);
|
||||
@@ -144,7 +146,8 @@ export function encode(data) {
|
||||
/**
|
||||
* Decode Base58 string to binary data (CryptoNote variant)
|
||||
* @param {string} encoded - Base58 encoded string
|
||||
* @returns {Uint8Array|null} - Decoded binary data or null on error
|
||||
* @returns {Uint8Array} - Decoded binary data
|
||||
* @throws {Error} If string is not valid Base58
|
||||
*/
|
||||
export function decode(encoded) {
|
||||
if (encoded.length === 0) {
|
||||
@@ -156,7 +159,7 @@ export function decode(encoded) {
|
||||
const lastBlockDecodedSize = DECODED_BLOCK_SIZES.get(lastBlockSize);
|
||||
|
||||
if (lastBlockDecodedSize === undefined || lastBlockDecodedSize < 0) {
|
||||
return null; // Invalid encoded length
|
||||
throw new Error(`Base58 decode: invalid encoded length ${encoded.length} (last block size ${lastBlockSize} is invalid)`);
|
||||
}
|
||||
|
||||
const dataSize = fullBlockCount * BASE58_FULL_BLOCK_SIZE + lastBlockDecodedSize;
|
||||
@@ -167,10 +170,7 @@ export function decode(encoded) {
|
||||
// Decode full blocks
|
||||
for (let i = 0; i < fullBlockCount; i++) {
|
||||
const block = encoded.slice(i * BASE58_FULL_ENCODED_BLOCK_SIZE, (i + 1) * BASE58_FULL_ENCODED_BLOCK_SIZE);
|
||||
const decoded = decodeBlock(block);
|
||||
if (decoded === null) {
|
||||
return null;
|
||||
}
|
||||
const decoded = decodeBlock(block, i);
|
||||
result.set(decoded, offset);
|
||||
offset += BASE58_FULL_BLOCK_SIZE;
|
||||
}
|
||||
@@ -178,10 +178,7 @@ export function decode(encoded) {
|
||||
// Decode last partial block
|
||||
if (lastBlockSize > 0) {
|
||||
const block = encoded.slice(fullBlockCount * BASE58_FULL_ENCODED_BLOCK_SIZE);
|
||||
const decoded = decodeBlock(block);
|
||||
if (decoded === null) {
|
||||
return null;
|
||||
}
|
||||
const decoded = decodeBlock(block, fullBlockCount);
|
||||
result.set(decoded, offset);
|
||||
}
|
||||
|
||||
@@ -209,9 +206,14 @@ export function encodeVarint(value) {
|
||||
/**
|
||||
* Decode a varint from the start of data
|
||||
* @param {Uint8Array} data - Data containing varint
|
||||
* @returns {{value: BigInt, bytesRead: number}|null} - Decoded value and bytes consumed
|
||||
* @returns {{value: BigInt, bytesRead: number}} - Decoded value and bytes consumed
|
||||
* @throws {Error} If varint is invalid or incomplete
|
||||
*/
|
||||
export function decodeVarint(data) {
|
||||
if (!data || data.length === 0) {
|
||||
throw new Error('decodeVarint: empty data');
|
||||
}
|
||||
|
||||
let value = 0n;
|
||||
let shift = 0n;
|
||||
let bytesRead = 0;
|
||||
@@ -228,7 +230,7 @@ export function decodeVarint(data) {
|
||||
shift += 7n;
|
||||
}
|
||||
|
||||
return null; // Varint too long or incomplete
|
||||
throw new Error(`decodeVarint: varint incomplete or too long (read ${bytesRead} bytes without termination)`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,12 +261,13 @@ export function encodeAddress(tag, data) {
|
||||
/**
|
||||
* Decode an address, verifying checksum and extracting tag and data
|
||||
* @param {string} address - Base58 encoded address
|
||||
* @returns {{tag: BigInt, data: Uint8Array}|null} - Decoded tag and data, or null on error
|
||||
* @returns {{tag: BigInt, data: Uint8Array}} - Decoded tag and data
|
||||
* @throws {Error} If address is invalid or checksum fails
|
||||
*/
|
||||
export function decodeAddress(address) {
|
||||
const decoded = decode(address);
|
||||
if (decoded === null || decoded.length <= 4) {
|
||||
return null;
|
||||
if (decoded.length <= 4) {
|
||||
throw new Error(`decodeAddress: address too short (${decoded.length} bytes, need >4)`);
|
||||
}
|
||||
|
||||
// Extract checksum
|
||||
@@ -277,17 +280,12 @@ export function decodeAddress(address) {
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if (checksum[i] !== expectedChecksum[i]) {
|
||||
return null; // Checksum mismatch
|
||||
throw new Error('decodeAddress: checksum mismatch - address may be corrupted or invalid');
|
||||
}
|
||||
}
|
||||
|
||||
// Decode varint tag
|
||||
const varintResult = decodeVarint(payload);
|
||||
if (varintResult === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { value: tag, bytesRead } = varintResult;
|
||||
const { value: tag, bytesRead } = decodeVarint(payload);
|
||||
const data = payload.slice(bytesRead);
|
||||
|
||||
return { tag, data };
|
||||
|
||||
@@ -627,8 +627,14 @@ export function scanCarrotOutput(output, viewIncomingKey, accountSpendPubkey, in
|
||||
console.log(` accountSpendPubkey: ${accountSpendPubkey ? 'SET' : 'NULL'}`);
|
||||
}
|
||||
|
||||
if (!onetimeAddress || !viewTag || !enoteEphemeralPubkey) {
|
||||
return null; // Missing required CARROT fields
|
||||
if (!onetimeAddress) {
|
||||
throw new Error('scanCarrotOutput: onetimeAddress is required');
|
||||
}
|
||||
if (!viewTag) {
|
||||
throw new Error('scanCarrotOutput: viewTag is required');
|
||||
}
|
||||
if (!enoteEphemeralPubkey) {
|
||||
throw new Error('scanCarrotOutput: enoteEphemeralPubkey is required');
|
||||
}
|
||||
|
||||
// 1. Compute uncontextualized shared secret using X25519
|
||||
|
||||
+1
-2
@@ -185,8 +185,7 @@ import {
|
||||
|
||||
import {
|
||||
verifySignature,
|
||||
parseSignature,
|
||||
testEd25519
|
||||
parseSignature
|
||||
} from './signature.js';
|
||||
|
||||
import {
|
||||
|
||||
+63
-29
@@ -129,6 +129,14 @@ export function generateKeyDerivation(txPubKey, viewSecretKey) {
|
||||
viewSecretKey = hexToBytes(viewSecretKey);
|
||||
}
|
||||
|
||||
// Validate inputs
|
||||
if (!txPubKey || txPubKey.length !== 32) {
|
||||
throw new Error(`generateKeyDerivation: txPubKey must be 32 bytes, got ${txPubKey?.length ?? 'null'}`);
|
||||
}
|
||||
if (!viewSecretKey || viewSecretKey.length !== 32) {
|
||||
throw new Error(`generateKeyDerivation: viewSecretKey must be 32 bytes, got ${viewSecretKey?.length ?? 'null'}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Convert scalar to BigInt (little-endian)
|
||||
let scalar = 0n;
|
||||
@@ -147,10 +155,16 @@ export function generateKeyDerivation(txPubKey, viewSecretKey) {
|
||||
} catch (e) {
|
||||
// Fallback to original implementation
|
||||
const result = scalarMultPoint(viewSecretKey, txPubKey);
|
||||
if (!result) return null;
|
||||
if (!result) {
|
||||
throw new Error(`generateKeyDerivation: point multiplication failed - ${e.message}`);
|
||||
}
|
||||
const eight = new Uint8Array(32);
|
||||
eight[0] = 8;
|
||||
return scalarMultPoint(eight, result);
|
||||
const cofactorResult = scalarMultPoint(eight, result);
|
||||
if (!cofactorResult) {
|
||||
throw new Error('generateKeyDerivation: cofactor multiplication failed');
|
||||
}
|
||||
return cofactorResult;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,23 +275,29 @@ export function deriveSubaddressPublicKey(outputKey, derivation, outputIndex) {
|
||||
}
|
||||
|
||||
// Validate inputs
|
||||
if (!outputKey || !(outputKey instanceof Uint8Array) || outputKey.length !== 32) {
|
||||
return null;
|
||||
if (!outputKey || !(outputKey instanceof Uint8Array)) {
|
||||
throw new Error('deriveSubaddressPublicKey: outputKey must be a Uint8Array');
|
||||
}
|
||||
if (!derivation || !(derivation instanceof Uint8Array) || derivation.length !== 32) {
|
||||
return null;
|
||||
if (outputKey.length !== 32) {
|
||||
throw new Error(`deriveSubaddressPublicKey: outputKey must be 32 bytes, got ${outputKey.length}`);
|
||||
}
|
||||
if (!derivation || !(derivation instanceof Uint8Array)) {
|
||||
throw new Error('deriveSubaddressPublicKey: derivation must be a Uint8Array');
|
||||
}
|
||||
if (derivation.length !== 32) {
|
||||
throw new Error(`deriveSubaddressPublicKey: derivation must be 32 bytes, got ${derivation.length}`);
|
||||
}
|
||||
|
||||
// scalar = H_s(derivation || output_index)
|
||||
const scalar = derivationToScalar(derivation, outputIndex);
|
||||
if (!scalar) {
|
||||
return null;
|
||||
throw new Error('deriveSubaddressPublicKey: derivationToScalar failed');
|
||||
}
|
||||
|
||||
// scalar * G
|
||||
const scalarG = scalarMultBase(scalar);
|
||||
if (!scalarG || scalarG.length !== 32) {
|
||||
return null;
|
||||
throw new Error('deriveSubaddressPublicKey: scalarMultBase failed');
|
||||
}
|
||||
|
||||
// Negate the point (subtract instead of add)
|
||||
@@ -521,27 +541,30 @@ export function checkSubaddressOwnership(outputPubKey, txPubKey, viewSecretKey,
|
||||
outputPubKey = hexToBytes(outputPubKey);
|
||||
}
|
||||
|
||||
// Compute key derivation
|
||||
const derivation = generateKeyDerivation(txPubKey, viewSecretKey);
|
||||
if (!derivation) return null;
|
||||
try {
|
||||
// Compute key derivation
|
||||
const derivation = generateKeyDerivation(txPubKey, viewSecretKey);
|
||||
|
||||
// For subaddress, we compute: derived = outputKey - scalar*G
|
||||
// Then check if derived matches any known subaddress spend key
|
||||
const derivedSpendKey = deriveSubaddressPublicKey(outputPubKey, derivation, outputIndex);
|
||||
if (!derivedSpendKey) return null;
|
||||
// For subaddress, we compute: derived = outputKey - scalar*G
|
||||
// Then check if derived matches any known subaddress spend key
|
||||
const derivedSpendKey = deriveSubaddressPublicKey(outputPubKey, derivation, outputIndex);
|
||||
|
||||
const derivedHex = bytesToHex(derivedSpendKey);
|
||||
const subaddressInfo = subaddressSpendKeys.get(derivedHex);
|
||||
const derivedHex = bytesToHex(derivedSpendKey);
|
||||
const subaddressInfo = subaddressSpendKeys.get(derivedHex);
|
||||
|
||||
if (subaddressInfo) {
|
||||
return {
|
||||
major: subaddressInfo.major,
|
||||
minor: subaddressInfo.minor,
|
||||
derivation
|
||||
};
|
||||
if (subaddressInfo) {
|
||||
return {
|
||||
major: subaddressInfo.major,
|
||||
minor: subaddressInfo.minor,
|
||||
derivation
|
||||
};
|
||||
}
|
||||
|
||||
return null; // Not our subaddress
|
||||
} catch (e) {
|
||||
// Crypto operation failed - this output isn't valid for our keys
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -559,9 +582,14 @@ export function checkSubaddressOwnership(outputPubKey, txPubKey, viewSecretKey,
|
||||
* @returns {Object|null} Scan result with derivation, amount, etc.
|
||||
*/
|
||||
export function scanOutput(output, txPubKey, viewSecretKey, spendPubKey, outputIndex) {
|
||||
// Compute key derivation
|
||||
const derivation = generateKeyDerivation(txPubKey, viewSecretKey);
|
||||
if (!derivation) return null;
|
||||
let derivation;
|
||||
try {
|
||||
// Compute key derivation
|
||||
derivation = generateKeyDerivation(txPubKey, viewSecretKey);
|
||||
} catch (e) {
|
||||
// Key derivation failed - cannot be our output
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check view tag first (if available) for optimization
|
||||
if (output.view_tag !== undefined) {
|
||||
@@ -572,7 +600,13 @@ export function scanOutput(output, txPubKey, viewSecretKey, spendPubKey, outputI
|
||||
}
|
||||
|
||||
// Derive expected output public key
|
||||
const expectedPubKey = derivePublicKey(derivation, outputIndex, spendPubKey);
|
||||
let expectedPubKey;
|
||||
try {
|
||||
expectedPubKey = derivePublicKey(derivation, outputIndex, spendPubKey);
|
||||
} catch (e) {
|
||||
// Key derivation failed - cannot be our output
|
||||
return null;
|
||||
}
|
||||
if (!expectedPubKey) return null;
|
||||
|
||||
// Compare with actual output key
|
||||
|
||||
+3
-124
@@ -12,15 +12,14 @@
|
||||
|
||||
import { keccak256 } from './keccak.js';
|
||||
import { decode } from './base58.js';
|
||||
import { parseAddress, hexToBytes } from './address.js';
|
||||
import { parseAddress } from './address.js';
|
||||
import {
|
||||
scalarCheck,
|
||||
scalarIsNonzero,
|
||||
scalarSub,
|
||||
pointFromBytes,
|
||||
doubleScalarMultBase,
|
||||
isIdentity,
|
||||
scalarMultBase
|
||||
isIdentity
|
||||
} from './ed25519.js';
|
||||
|
||||
// Domain separator for V2 signatures (includes null terminator)
|
||||
@@ -60,22 +59,9 @@ function getMessageHashV1(message) {
|
||||
* @returns {Uint8Array} 32-byte hash
|
||||
*/
|
||||
function getMessageHashV2(message, spendKey, viewKey, mode) {
|
||||
// Debug helper
|
||||
const bytesToHex = (bytes) => Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
const messageBytes = new TextEncoder().encode(message);
|
||||
const lenVarint = encodeVarint(messageBytes.length);
|
||||
|
||||
console.log('=== V2 Hash Construction ===');
|
||||
console.log('Domain sep length:', HASH_KEY_MESSAGE_SIGNING.length);
|
||||
console.log('Domain sep:', bytesToHex(HASH_KEY_MESSAGE_SIGNING));
|
||||
console.log('Spend key:', bytesToHex(spendKey));
|
||||
console.log('View key:', bytesToHex(viewKey));
|
||||
console.log('Mode:', mode);
|
||||
console.log('Message length:', messageBytes.length);
|
||||
console.log('Len varint:', bytesToHex(lenVarint));
|
||||
console.log('Message bytes:', bytesToHex(messageBytes));
|
||||
|
||||
// Concatenate: domain_separator + spend_key + view_key + mode + len + message
|
||||
const totalLen = HASH_KEY_MESSAGE_SIGNING.length + 32 + 32 + 1 + lenVarint.length + messageBytes.length;
|
||||
const data = new Uint8Array(totalLen);
|
||||
@@ -97,11 +83,7 @@ function getMessageHashV2(message, spendKey, viewKey, mode) {
|
||||
|
||||
data.set(messageBytes, offset);
|
||||
|
||||
const hash = keccak256(data);
|
||||
console.log('Total data length:', data.length);
|
||||
console.log('V2 hash result:', bytesToHex(hash));
|
||||
|
||||
return hash;
|
||||
return keccak256(data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,37 +98,22 @@ function getMessageHashV2(message, spendKey, viewKey, mode) {
|
||||
* @returns {boolean} true if signature is valid
|
||||
*/
|
||||
function checkSignature(hash, publicKey, sigC, sigR) {
|
||||
// Import bytesToHex for debugging
|
||||
const bytesToHex = (bytes) => Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
console.log('=== checkSignature ===');
|
||||
console.log('hash (message hash):', bytesToHex(hash));
|
||||
console.log('publicKey:', bytesToHex(publicKey));
|
||||
console.log('sigC:', bytesToHex(sigC));
|
||||
console.log('sigR:', bytesToHex(sigR));
|
||||
|
||||
// Validate scalars
|
||||
if (!scalarCheck(sigC) || !scalarCheck(sigR) || !scalarIsNonzero(sigC)) {
|
||||
console.log('Scalar validation failed');
|
||||
return false;
|
||||
}
|
||||
console.log('Scalar validation passed');
|
||||
|
||||
// Decompress public key
|
||||
const P = pointFromBytes(publicKey);
|
||||
if (!P) {
|
||||
console.log('Failed to decompress public key');
|
||||
return false;
|
||||
}
|
||||
console.log('Public key decompressed successfully');
|
||||
|
||||
// Compute R' = c*P + r*G using double scalar multiplication
|
||||
const RBytes = doubleScalarMultBase(sigC, P, sigR);
|
||||
console.log('R\' computed:', bytesToHex(RBytes));
|
||||
|
||||
// Check R' is not identity
|
||||
if (isIdentity(RBytes)) {
|
||||
console.log('R\' is identity point');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -158,19 +125,14 @@ function checkSignature(hash, publicKey, sigC, sigR) {
|
||||
buf.set(RBytes, 64);
|
||||
|
||||
const cPrime = keccak256(buf);
|
||||
console.log('cPrime (raw hash) H(m||P||R):', bytesToHex(cPrime));
|
||||
|
||||
// Reduce c' mod L
|
||||
const cPrimeReduced = new Uint8Array(32);
|
||||
reduceScalar32(cPrimeReduced, cPrime);
|
||||
console.log('cPrime (reduced):', bytesToHex(cPrimeReduced));
|
||||
console.log('sigC (expected):', bytesToHex(sigC));
|
||||
|
||||
// Check c' == c
|
||||
const diff = new Uint8Array(32);
|
||||
scalarSub(diff, cPrimeReduced, sigC);
|
||||
console.log('diff:', bytesToHex(diff));
|
||||
console.log('diff nonzero:', scalarIsNonzero(diff));
|
||||
|
||||
return !scalarIsNonzero(diff);
|
||||
}
|
||||
@@ -212,89 +174,6 @@ function reduceScalar32(r, x) {
|
||||
* - keyType: string - 'spend' or 'view' (which key was used to sign)
|
||||
* - error: string|null - error message if invalid
|
||||
*/
|
||||
// Test function to verify Ed25519 and Keccak are working
|
||||
export function testEd25519() {
|
||||
const bytesToHex = (bytes) => Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
// Test 1: Verify 2*G (this is the key test - if this works, Ed25519 is correct)
|
||||
const two = new Uint8Array(32);
|
||||
two[0] = 2;
|
||||
const twoG = scalarMultBase(two);
|
||||
const twoGHex = bytesToHex(twoG);
|
||||
const twoGExpected = 'c9a3f86aae465f0e56513864510f3997561fa2c9e85ea21dc2292309f3cd6022';
|
||||
console.log('2*G =', twoGHex);
|
||||
console.log('2*G expected:', twoGExpected);
|
||||
console.log('2*G match:', twoGHex === twoGExpected);
|
||||
|
||||
// Test 2: Verify Keccak-256
|
||||
// Known test vector: Keccak256("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
|
||||
const emptyHash = keccak256(new Uint8Array(0));
|
||||
const emptyHashHex = bytesToHex(emptyHash);
|
||||
const emptyHashExpected = 'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470';
|
||||
console.log('Keccak256("") =', emptyHashHex);
|
||||
console.log('Expected:', emptyHashExpected);
|
||||
console.log('Keccak match:', emptyHashHex === emptyHashExpected);
|
||||
|
||||
// Test 3: Keccak of "test"
|
||||
const testHash = keccak256(new TextEncoder().encode('test'));
|
||||
const testHashHex = bytesToHex(testHash);
|
||||
// Keccak256("test") = 9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658
|
||||
const testHashExpected = '9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658';
|
||||
console.log('Keccak256("test") =', testHashHex);
|
||||
console.log('Expected:', testHashExpected);
|
||||
console.log('Keccak test match:', testHashHex === testHashExpected);
|
||||
|
||||
// Test 4: Point decompression roundtrip
|
||||
// Compress 2*G, decompress it, recompress, should match
|
||||
const twoGDecompressed = pointFromBytes(twoG);
|
||||
if (twoGDecompressed) {
|
||||
// Need to access pointToBytes - let me export it
|
||||
console.log('2*G decompression: success');
|
||||
} else {
|
||||
console.log('2*G decompression: FAILED');
|
||||
}
|
||||
|
||||
// Test 5: Test with the actual public key from the signature test
|
||||
const testPubKey = new Uint8Array([
|
||||
0x28, 0x97, 0x3e, 0x82, 0x1c, 0xc2, 0xf2, 0xde,
|
||||
0x5e, 0x68, 0x9a, 0xd6, 0x1c, 0x4d, 0xda, 0xd4,
|
||||
0x8a, 0x1b, 0xac, 0x77, 0xf1, 0x94, 0x43, 0x97,
|
||||
0xe0, 0x6e, 0x90, 0xf7, 0xd6, 0x5f, 0xda, 0xd1
|
||||
]);
|
||||
const pubKeyPoint = pointFromBytes(testPubKey);
|
||||
console.log('Public key decompression:', pubKeyPoint ? 'success' : 'FAILED');
|
||||
|
||||
// Test 6: Double scalar mult: verify 3*G + 5*G = 8*G
|
||||
const three = new Uint8Array(32); three[0] = 3;
|
||||
const five = new Uint8Array(32); five[0] = 5;
|
||||
const eight = new Uint8Array(32); eight[0] = 8;
|
||||
|
||||
const threeG = scalarMultBase(three);
|
||||
const threeGPoint = pointFromBytes(threeG);
|
||||
const result = doubleScalarMultBase(three, threeGPoint, five); // 3*(3G) + 5*G = 9G + 5G = 14G? No wait...
|
||||
|
||||
// Actually test: a*G + b*G = (a+b)*G using G as the point
|
||||
// doubleScalarMultBase(a, P, b) = a*P + b*G
|
||||
// If P = G, then a*G + b*G = (a+b)*G
|
||||
const oneScalar = new Uint8Array(32); oneScalar[0] = 1;
|
||||
const Gcompressed = scalarMultBase(oneScalar);
|
||||
const Gpoint = pointFromBytes(Gcompressed);
|
||||
|
||||
// 3*G + 5*G should equal 8*G
|
||||
const sumResult = doubleScalarMultBase(three, Gpoint, five);
|
||||
const eightG = scalarMultBase(eight);
|
||||
|
||||
console.log('3*G + 5*G =', bytesToHex(sumResult));
|
||||
console.log('8*G = ', bytesToHex(eightG));
|
||||
console.log('Double scalar mult match:', bytesToHex(sumResult) === bytesToHex(eightG));
|
||||
|
||||
return {
|
||||
ed25519OK: twoGHex === twoGExpected,
|
||||
keccakOK: emptyHashHex === emptyHashExpected && testHashHex === testHashExpected,
|
||||
doubleScalarOK: bytesToHex(sumResult) === bytesToHex(eightG)
|
||||
};
|
||||
}
|
||||
|
||||
export function verifySignature(message, address, signature) {
|
||||
// Parse signature header
|
||||
const isV1 = signature.startsWith('SigV1');
|
||||
|
||||
+90
-45
@@ -11,6 +11,36 @@
|
||||
*/
|
||||
|
||||
import { keccak256, keccak256Hex } from './keccak.js';
|
||||
|
||||
// =============================================================================
|
||||
// ERROR CLASSES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Error thrown when parsing fails
|
||||
* Provides detailed context about what went wrong and where
|
||||
*/
|
||||
export class ParseError extends Error {
|
||||
constructor(message, context = {}) {
|
||||
super(message);
|
||||
this.name = 'ParseError';
|
||||
this.offset = context.offset;
|
||||
this.field = context.field;
|
||||
this.expected = context.expected;
|
||||
this.actual = context.actual;
|
||||
this.dataLength = context.dataLength;
|
||||
}
|
||||
|
||||
toString() {
|
||||
let msg = `ParseError: ${this.message}`;
|
||||
if (this.field) msg += ` [field: ${this.field}]`;
|
||||
if (this.offset !== undefined) msg += ` [offset: ${this.offset}]`;
|
||||
if (this.dataLength !== undefined) msg += ` [dataLength: ${this.dataLength}]`;
|
||||
if (this.expected !== undefined) msg += ` [expected: ${this.expected}]`;
|
||||
if (this.actual !== undefined) msg += ` [actual: ${this.actual}]`;
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
import { scalarMultBase, scalarMultPoint, pointAddCompressed, getGeneratorG } from './ed25519.js';
|
||||
import { generateKeyDerivation, derivationToScalar, derivePublicKey, deriveSecretKey } from './scanning.js';
|
||||
import { hashToPoint, generateKeyImage } from './keyimage.js';
|
||||
@@ -4111,8 +4141,8 @@ function parseRingCtSignature(data, startOffset, inputCount, outputCount, mixin
|
||||
// Parse prunable data (bulletproofs + CLSAGs) with bounds checking
|
||||
// The prunable section follows the base section
|
||||
if (offset < data.length && type !== RCT_TYPE.Null) {
|
||||
const prunable = parseRctSigPrunable(data, offset, type, inputCount, outputCount, mixin);
|
||||
if (prunable) {
|
||||
try {
|
||||
const prunable = parseRctSigPrunable(data, offset, type, inputCount, outputCount, mixin);
|
||||
rct.bulletproofPlus = prunable.bulletproofPlus;
|
||||
rct.CLSAGs = prunable.CLSAGs;
|
||||
rct.TCLSAGs = prunable.TCLSAGs;
|
||||
@@ -4120,6 +4150,12 @@ function parseRingCtSignature(data, startOffset, inputCount, outputCount, mixin
|
||||
if (prunable._endOffset) {
|
||||
offset = prunable._endOffset;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof ParseError) {
|
||||
rct.prunable_parse_error = e.toString();
|
||||
} else {
|
||||
rct.prunable_parse_error = `Unexpected error parsing prunable: ${e.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4302,18 +4338,29 @@ function parseSalviumData(data, startOffset, full = true) {
|
||||
function parseRctSigPrunable(data, startOffset, type, inputCount, outputCount, mixin) {
|
||||
let offset = startOffset;
|
||||
|
||||
// Bounds check helper
|
||||
const canRead = (count) => offset + count <= data.length;
|
||||
|
||||
const readBytes = (count) => {
|
||||
if (!canRead(count)) return null;
|
||||
const readBytes = (count, fieldName) => {
|
||||
if (offset + count > data.length) {
|
||||
throw new ParseError(`Unexpected end of data reading ${fieldName}`, {
|
||||
field: fieldName,
|
||||
offset,
|
||||
expected: count,
|
||||
actual: data.length - offset,
|
||||
dataLength: data.length
|
||||
});
|
||||
}
|
||||
const result = data.slice(offset, offset + count);
|
||||
offset += count;
|
||||
return result;
|
||||
};
|
||||
|
||||
const readVarint = () => {
|
||||
if (offset >= data.length) return null;
|
||||
const readVarint = (fieldName) => {
|
||||
if (offset >= data.length) {
|
||||
throw new ParseError(`Unexpected end of data reading varint for ${fieldName}`, {
|
||||
field: fieldName,
|
||||
offset,
|
||||
dataLength: data.length
|
||||
});
|
||||
}
|
||||
const { value, bytesRead } = decodeVarint(data, offset);
|
||||
offset += bytesRead;
|
||||
return value;
|
||||
@@ -4323,36 +4370,44 @@ function parseRctSigPrunable(data, startOffset, type, inputCount, outputCount, m
|
||||
|
||||
// For BulletproofPlus types (6, 7, 8, 9), parse BulletproofPlus
|
||||
if (type >= RCT_TYPE.BulletproofPlus) {
|
||||
const nbp = readVarint();
|
||||
if (nbp === null || nbp > 1000) return null; // Sanity check
|
||||
const nbp = readVarint('bulletproofPlus count');
|
||||
if (nbp > 1000) {
|
||||
throw new ParseError('Invalid bulletproofPlus count', {
|
||||
field: 'bulletproofPlus count',
|
||||
offset,
|
||||
expected: '<=1000',
|
||||
actual: nbp
|
||||
});
|
||||
}
|
||||
|
||||
result.bulletproofPlus = [];
|
||||
for (let i = 0; i < nbp; i++) {
|
||||
const A = readBytes(32);
|
||||
const A1 = readBytes(32);
|
||||
const B = readBytes(32);
|
||||
const r1 = readBytes(32);
|
||||
const s1 = readBytes(32);
|
||||
const d1 = readBytes(32);
|
||||
|
||||
if (!d1) return null; // Ran out of data
|
||||
const A = readBytes(32, `bulletproofPlus[${i}].A`);
|
||||
const A1 = readBytes(32, `bulletproofPlus[${i}].A1`);
|
||||
const B = readBytes(32, `bulletproofPlus[${i}].B`);
|
||||
const r1 = readBytes(32, `bulletproofPlus[${i}].r1`);
|
||||
const s1 = readBytes(32, `bulletproofPlus[${i}].s1`);
|
||||
const d1 = readBytes(32, `bulletproofPlus[${i}].d1`);
|
||||
|
||||
// L array
|
||||
const Lcount = readVarint();
|
||||
if (Lcount === null || Lcount > 64) return null; // Sanity check (log2(2^64) = 64 max)
|
||||
const Lcount = readVarint(`bulletproofPlus[${i}].L count`);
|
||||
if (Lcount > 64) {
|
||||
throw new ParseError('Invalid L array count in bulletproofPlus', {
|
||||
field: `bulletproofPlus[${i}].L count`,
|
||||
offset,
|
||||
expected: '<=64',
|
||||
actual: Lcount
|
||||
});
|
||||
}
|
||||
const L = [];
|
||||
for (let j = 0; j < Lcount; j++) {
|
||||
const key = readBytes(32);
|
||||
if (!key) return null;
|
||||
L.push(key);
|
||||
L.push(readBytes(32, `bulletproofPlus[${i}].L[${j}]`));
|
||||
}
|
||||
|
||||
// R array (same size as L)
|
||||
const R = [];
|
||||
for (let j = 0; j < Lcount; j++) {
|
||||
const key = readBytes(32);
|
||||
if (!key) return null;
|
||||
R.push(key);
|
||||
R.push(readBytes(32, `bulletproofPlus[${i}].R[${j}]`));
|
||||
}
|
||||
|
||||
result.bulletproofPlus.push({ A, A1, B, r1, s1, d1, L, R });
|
||||
@@ -4370,22 +4425,17 @@ function parseRctSigPrunable(data, startOffset, type, inputCount, outputCount, m
|
||||
// sx array: mixin + 1 elements, NO size prefix
|
||||
const sx = [];
|
||||
for (let j = 0; j < ringSize; j++) {
|
||||
const key = readBytes(32);
|
||||
if (!key) return null;
|
||||
sx.push(key);
|
||||
sx.push(readBytes(32, `TCLSAG[${i}].sx[${j}]`));
|
||||
}
|
||||
|
||||
// sy array: mixin + 1 elements, NO size prefix
|
||||
const sy = [];
|
||||
for (let j = 0; j < ringSize; j++) {
|
||||
const key = readBytes(32);
|
||||
if (!key) return null;
|
||||
sy.push(key);
|
||||
sy.push(readBytes(32, `TCLSAG[${i}].sy[${j}]`));
|
||||
}
|
||||
|
||||
const c1 = readBytes(32);
|
||||
const D = readBytes(32);
|
||||
if (!D) return null;
|
||||
const c1 = readBytes(32, `TCLSAG[${i}].c1`);
|
||||
const D = readBytes(32, `TCLSAG[${i}].D`);
|
||||
|
||||
result.TCLSAGs.push({ sx, sy, c1, D });
|
||||
}
|
||||
@@ -4396,14 +4446,11 @@ function parseRctSigPrunable(data, startOffset, type, inputCount, outputCount, m
|
||||
// s array: mixin + 1 elements, NO size prefix
|
||||
const s = [];
|
||||
for (let j = 0; j < ringSize; j++) {
|
||||
const key = readBytes(32);
|
||||
if (!key) return null;
|
||||
s.push(key);
|
||||
s.push(readBytes(32, `CLSAG[${i}].s[${j}]`));
|
||||
}
|
||||
|
||||
const c1 = readBytes(32);
|
||||
const D = readBytes(32);
|
||||
if (!D) return null;
|
||||
const c1 = readBytes(32, `CLSAG[${i}].c1`);
|
||||
const D = readBytes(32, `CLSAG[${i}].D`);
|
||||
|
||||
result.CLSAGs.push({ s, c1, D });
|
||||
}
|
||||
@@ -4413,9 +4460,7 @@ function parseRctSigPrunable(data, startOffset, type, inputCount, outputCount, m
|
||||
if (type >= RCT_TYPE.BulletproofPlus) {
|
||||
result.pseudoOuts = [];
|
||||
for (let i = 0; i < inputCount; i++) {
|
||||
const key = readBytes(32);
|
||||
if (!key) return null;
|
||||
result.pseudoOuts.push(key);
|
||||
result.pseudoOuts.push(readBytes(32, `pseudoOuts[${i}]`));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-8
@@ -1181,14 +1181,20 @@ export class WalletSync {
|
||||
}
|
||||
|
||||
// Scan with CARROT algorithm
|
||||
const result = scanCarrotOutput(
|
||||
outputForScan,
|
||||
this.carrotKeys.viewIncomingKey,
|
||||
this.carrotKeys.accountSpendPubkey,
|
||||
inputContext,
|
||||
this.carrotSubaddresses,
|
||||
amountCommitment
|
||||
);
|
||||
let result;
|
||||
try {
|
||||
result = scanCarrotOutput(
|
||||
outputForScan,
|
||||
this.carrotKeys.viewIncomingKey,
|
||||
this.carrotKeys.accountSpendPubkey,
|
||||
inputContext,
|
||||
this.carrotSubaddresses,
|
||||
amountCommitment
|
||||
);
|
||||
} catch (e) {
|
||||
// Missing required fields in CARROT output - skip this output
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user