● 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:
Matt Hess
2026-01-24 20:56:24 +00:00
parent 56ab4ec3fe
commit ea09516981
9 changed files with 272 additions and 274 deletions
+39 -23
View File
@@ -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
View File
@@ -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 };
+8 -2
View File
@@ -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
View File
@@ -185,8 +185,7 @@ import {
import {
verifySignature,
parseSignature,
testEd25519
parseSignature
} from './signature.js';
import {
+63 -29
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;