initial commit of salvium-js
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
# salvium-js
|
||||
|
||||
JavaScript library for Salvium cryptocurrency - address validation, parsing, and cryptographic utilities.
|
||||
|
||||
## Features
|
||||
|
||||
- **Address Validation** - Validate all 18 Salvium address types
|
||||
- **Address Parsing** - Extract public keys, payment IDs, detect network/format/type
|
||||
- **Multi-Network Support** - Mainnet, Testnet, Stagenet
|
||||
- **Dual Format Support** - Legacy (CryptoNote) and CARROT addresses
|
||||
- **Base58 Encoding** - Monero-variant Base58 with checksums
|
||||
- **Keccak-256** - Pre-SHA3 Keccak hashing (cn_fast_hash)
|
||||
- **Signature Verification** - Verify message signatures (V1 and V2 formats)
|
||||
- **Zero Dependencies** - Pure JavaScript, works in browsers and Node.js
|
||||
|
||||
## Address Types Supported
|
||||
|
||||
| Network | Format | Standard | Integrated | Subaddress |
|
||||
|---------|--------|----------|------------|------------|
|
||||
| Mainnet | Legacy | SaLv... | SaLvi... | SaLvs... |
|
||||
| Mainnet | CARROT | SC1... | SC1i... | SC1s... |
|
||||
| Testnet | Legacy | SaLvT... | SaLvTi... | SaLvTs... |
|
||||
| Testnet | CARROT | SC1T... | SC1Ti... | SC1Ts... |
|
||||
| Stagenet | Legacy | SaLvS... | SaLvSi... | SaLvSs... |
|
||||
| Stagenet | CARROT | SC1S... | SC1Si... | SC1Ss... |
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install salvium-js
|
||||
```
|
||||
|
||||
Or include directly in browser:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import salvium from './salvium-js/src/index.js';
|
||||
</script>
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Validate an Address
|
||||
|
||||
```javascript
|
||||
import { isValidAddress, parseAddress } from 'salvium-js';
|
||||
|
||||
// Simple validation
|
||||
if (isValidAddress('SC1...')) {
|
||||
console.log('Valid address!');
|
||||
}
|
||||
|
||||
// Detailed parsing
|
||||
const info = parseAddress('SC1...');
|
||||
console.log(info);
|
||||
// {
|
||||
// valid: true,
|
||||
// network: 'mainnet',
|
||||
// format: 'carrot',
|
||||
// type: 'standard',
|
||||
// prefix: 'SC1',
|
||||
// spendPublicKey: Uint8Array(32),
|
||||
// viewPublicKey: Uint8Array(32),
|
||||
// paymentId: null,
|
||||
// error: null
|
||||
// }
|
||||
```
|
||||
|
||||
### Check Address Properties
|
||||
|
||||
```javascript
|
||||
import {
|
||||
isMainnet,
|
||||
isTestnet,
|
||||
isCarrot,
|
||||
isLegacy,
|
||||
isStandard,
|
||||
isIntegrated,
|
||||
isSubaddress
|
||||
} from 'salvium-js';
|
||||
|
||||
const addr = 'SC1...';
|
||||
|
||||
isMainnet(addr); // true
|
||||
isCarrot(addr); // true
|
||||
isStandard(addr); // true
|
||||
isIntegrated(addr); // false
|
||||
```
|
||||
|
||||
### Extract Keys
|
||||
|
||||
```javascript
|
||||
import { getSpendPublicKey, getViewPublicKey, bytesToHex } from 'salvium-js';
|
||||
|
||||
const spendKey = getSpendPublicKey('SC1...');
|
||||
const viewKey = getViewPublicKey('SC1...');
|
||||
|
||||
console.log('Spend Key:', bytesToHex(spendKey));
|
||||
console.log('View Key:', bytesToHex(viewKey));
|
||||
```
|
||||
|
||||
### Work with Integrated Addresses
|
||||
|
||||
```javascript
|
||||
import { toIntegratedAddress, toStandardAddress, getPaymentId, bytesToHex } from 'salvium-js';
|
||||
|
||||
// Create integrated address with payment ID
|
||||
const integrated = toIntegratedAddress('SC1...', 'deadbeef12345678');
|
||||
|
||||
// Extract payment ID
|
||||
const paymentId = getPaymentId(integrated);
|
||||
console.log('Payment ID:', bytesToHex(paymentId));
|
||||
|
||||
// Get standard address from integrated
|
||||
const standard = toStandardAddress(integrated);
|
||||
```
|
||||
|
||||
### Describe an Address
|
||||
|
||||
```javascript
|
||||
import { describeAddress } from 'salvium-js';
|
||||
|
||||
console.log(describeAddress('SC1...'));
|
||||
// "Mainnet CARROT standard"
|
||||
|
||||
console.log(describeAddress('SaLvi...'));
|
||||
// "Mainnet Legacy integrated (Payment ID: abcd1234...)"
|
||||
```
|
||||
|
||||
### Low-Level: Keccak-256
|
||||
|
||||
```javascript
|
||||
import { keccak256, keccak256Hex } from 'salvium-js';
|
||||
|
||||
const hash = keccak256('hello'); // Uint8Array(32)
|
||||
const hex = keccak256Hex('hello'); // "1c8aff950685..."
|
||||
```
|
||||
|
||||
### Low-Level: Base58
|
||||
|
||||
```javascript
|
||||
import { encode, decode, encodeAddress, decodeAddress } from 'salvium-js/base58';
|
||||
|
||||
const encoded = encode(new Uint8Array([1, 2, 3]));
|
||||
const decoded = decode(encoded);
|
||||
```
|
||||
|
||||
### Verify Message Signatures
|
||||
|
||||
```javascript
|
||||
import { verifySignature, parseSignature } from 'salvium-js';
|
||||
|
||||
// Verify a signature created with `sign` command in salvium-wallet-cli
|
||||
const result = verifySignature(
|
||||
'Hello, World!', // The original message
|
||||
'SC1...', // The signer's address
|
||||
'SigV2...' // The signature string
|
||||
);
|
||||
|
||||
console.log(result);
|
||||
// {
|
||||
// valid: true,
|
||||
// version: 2,
|
||||
// keyType: 'spend', // or 'view'
|
||||
// error: null
|
||||
// }
|
||||
|
||||
// Parse signature components
|
||||
const sig = parseSignature('SigV2...');
|
||||
console.log(sig);
|
||||
// {
|
||||
// valid: true,
|
||||
// version: 2,
|
||||
// c: Uint8Array(32),
|
||||
// r: Uint8Array(32),
|
||||
// signMask: 0
|
||||
// }
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Address Functions
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `parseAddress(addr)` | Parse address, returns detailed info object |
|
||||
| `isValidAddress(addr)` | Returns true if valid |
|
||||
| `isMainnet(addr)` | Check if mainnet address |
|
||||
| `isTestnet(addr)` | Check if testnet address |
|
||||
| `isStagenet(addr)` | Check if stagenet address |
|
||||
| `isCarrot(addr)` | Check if CARROT format |
|
||||
| `isLegacy(addr)` | Check if legacy CryptoNote format |
|
||||
| `isStandard(addr)` | Check if standard address |
|
||||
| `isIntegrated(addr)` | Check if integrated address |
|
||||
| `isSubaddress(addr)` | Check if subaddress |
|
||||
| `getSpendPublicKey(addr)` | Extract 32-byte spend public key |
|
||||
| `getViewPublicKey(addr)` | Extract 32-byte view public key |
|
||||
| `getPaymentId(addr)` | Extract 8-byte payment ID (integrated only) |
|
||||
| `toIntegratedAddress(addr, paymentId)` | Create integrated from standard |
|
||||
| `toStandardAddress(addr)` | Extract standard from integrated |
|
||||
| `describeAddress(addr)` | Human-readable description |
|
||||
|
||||
### Utility Functions
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `bytesToHex(bytes)` | Convert Uint8Array to hex string |
|
||||
| `hexToBytes(hex)` | Convert hex string to Uint8Array |
|
||||
| `keccak256(data)` | Keccak-256 hash, returns Uint8Array |
|
||||
| `keccak256Hex(data)` | Keccak-256 hash, returns hex string |
|
||||
|
||||
### Signature Functions
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `verifySignature(message, address, signature)` | Verify a message signature, returns result object |
|
||||
| `parseSignature(signature)` | Parse signature string into components |
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! Please read the Salvium source code for reference:
|
||||
https://github.com/salvium/salvium
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Re-export everything from src
|
||||
export * from './src/index.js';
|
||||
export { default } from './src/index.js';
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "salvium-js",
|
||||
"version": "0.1.0",
|
||||
"description": "JavaScript library for Salvium cryptocurrency - address validation, parsing, and signature verification",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.js",
|
||||
"./address": "./src/address.js",
|
||||
"./base58": "./src/base58.js",
|
||||
"./keccak": "./src/keccak.js",
|
||||
"./signature": "./src/signature.js",
|
||||
"./constants": "./src/constants.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node test/run.js"
|
||||
},
|
||||
"keywords": [
|
||||
"salvium",
|
||||
"cryptocurrency",
|
||||
"cryptonote",
|
||||
"carrot",
|
||||
"address",
|
||||
"validation",
|
||||
"base58",
|
||||
"keccak"
|
||||
],
|
||||
"author": "WhiskyMine",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/whiskymine/salvium-js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"browser": {
|
||||
"./src/keccak.js": "./src/keccak.browser.js"
|
||||
}
|
||||
}
|
||||
+417
@@ -0,0 +1,417 @@
|
||||
/**
|
||||
* Salvium Address Handling
|
||||
*
|
||||
* Supports all 18 address types across:
|
||||
* - 3 networks: mainnet, testnet, stagenet
|
||||
* - 2 formats: legacy (CryptoNote), carrot (CARROT)
|
||||
* - 3 types: standard, integrated, subaddress
|
||||
*/
|
||||
|
||||
import {
|
||||
NETWORK,
|
||||
ADDRESS_TYPE,
|
||||
ADDRESS_FORMAT,
|
||||
PREFIX_MAP,
|
||||
KEY_SIZE,
|
||||
PAYMENT_ID_SIZE,
|
||||
ADDRESS_DATA_SIZE,
|
||||
getPrefix
|
||||
} from './constants.js';
|
||||
import { decodeAddress, encodeAddress } from './base58.js';
|
||||
|
||||
/**
|
||||
* Result of parsing an address
|
||||
* @typedef {Object} ParsedAddress
|
||||
* @property {boolean} valid - Whether the address is valid
|
||||
* @property {string|null} network - Network type (mainnet, testnet, stagenet)
|
||||
* @property {string|null} format - Address format (legacy, carrot)
|
||||
* @property {string|null} type - Address type (standard, integrated, subaddress)
|
||||
* @property {string|null} prefix - Human-readable prefix (e.g., "SaLv", "SC1")
|
||||
* @property {Uint8Array|null} spendPublicKey - 32-byte public spend key
|
||||
* @property {Uint8Array|null} viewPublicKey - 32-byte public view key
|
||||
* @property {Uint8Array|null} paymentId - 8-byte payment ID (integrated addresses only)
|
||||
* @property {string|null} error - Error message if invalid
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse and validate a Salvium address
|
||||
* @param {string} address - The address string to parse
|
||||
* @returns {ParsedAddress} - Parsed address information
|
||||
*/
|
||||
export function parseAddress(address) {
|
||||
const result = {
|
||||
valid: false,
|
||||
network: null,
|
||||
format: null,
|
||||
type: null,
|
||||
prefix: null,
|
||||
spendPublicKey: null,
|
||||
viewPublicKey: null,
|
||||
paymentId: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
// Basic validation
|
||||
if (!address || typeof address !== 'string') {
|
||||
result.error = 'Address must be a non-empty string';
|
||||
return result;
|
||||
}
|
||||
|
||||
// Trim whitespace
|
||||
address = address.trim();
|
||||
|
||||
// Check length (rough bounds based on possible address types)
|
||||
// Standard: ~95-99 chars, Integrated: ~106-110 chars, Subaddress: ~95-99 chars
|
||||
// Allow wide range to accommodate all prefix sizes
|
||||
if (address.length < 90 || address.length > 150) {
|
||||
result.error = 'Invalid address length';
|
||||
return result;
|
||||
}
|
||||
|
||||
// Decode the address
|
||||
const decoded = decodeAddress(address);
|
||||
if (decoded === null) {
|
||||
result.error = 'Invalid Base58 encoding or checksum';
|
||||
return result;
|
||||
}
|
||||
|
||||
const { tag, data } = decoded;
|
||||
|
||||
// Look up the prefix
|
||||
const prefixInfo = PREFIX_MAP.get(tag);
|
||||
if (!prefixInfo) {
|
||||
result.error = `Unknown address prefix: 0x${tag.toString(16)}`;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.network = prefixInfo.network;
|
||||
result.format = prefixInfo.format;
|
||||
result.type = prefixInfo.type;
|
||||
result.prefix = prefixInfo.text;
|
||||
|
||||
// Validate data length based on address type
|
||||
const expectedDataSize = ADDRESS_DATA_SIZE[result.type];
|
||||
if (data.length !== expectedDataSize) {
|
||||
result.error = `Invalid data length: expected ${expectedDataSize} bytes, got ${data.length}`;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Extract public keys
|
||||
result.spendPublicKey = data.slice(0, KEY_SIZE);
|
||||
result.viewPublicKey = data.slice(KEY_SIZE, KEY_SIZE * 2);
|
||||
|
||||
// Extract payment ID for integrated addresses
|
||||
if (result.type === ADDRESS_TYPE.INTEGRATED) {
|
||||
result.paymentId = data.slice(KEY_SIZE * 2, KEY_SIZE * 2 + PAYMENT_ID_SIZE);
|
||||
}
|
||||
|
||||
result.valid = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a Salvium address
|
||||
* @param {string} address - The address string to validate
|
||||
* @returns {boolean} - True if valid, false otherwise
|
||||
*/
|
||||
export function isValidAddress(address) {
|
||||
return parseAddress(address).valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an address belongs to a specific network
|
||||
* @param {string} address - The address string
|
||||
* @param {string} network - Network to check (mainnet, testnet, stagenet)
|
||||
* @returns {boolean} - True if address belongs to the specified network
|
||||
*/
|
||||
export function isNetwork(address, network) {
|
||||
const parsed = parseAddress(address);
|
||||
return parsed.valid && parsed.network === network;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an address is a mainnet address
|
||||
* @param {string} address - The address string
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isMainnet(address) {
|
||||
return isNetwork(address, NETWORK.MAINNET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an address is a testnet address
|
||||
* @param {string} address - The address string
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isTestnet(address) {
|
||||
return isNetwork(address, NETWORK.TESTNET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an address is a stagenet address
|
||||
* @param {string} address - The address string
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isStagenet(address) {
|
||||
return isNetwork(address, NETWORK.STAGENET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an address uses the CARROT format
|
||||
* @param {string} address - The address string
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isCarrot(address) {
|
||||
const parsed = parseAddress(address);
|
||||
return parsed.valid && parsed.format === ADDRESS_FORMAT.CARROT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an address uses the legacy CryptoNote format
|
||||
* @param {string} address - The address string
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isLegacy(address) {
|
||||
const parsed = parseAddress(address);
|
||||
return parsed.valid && parsed.format === ADDRESS_FORMAT.LEGACY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an address is a standard address
|
||||
* @param {string} address - The address string
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isStandard(address) {
|
||||
const parsed = parseAddress(address);
|
||||
return parsed.valid && parsed.type === ADDRESS_TYPE.STANDARD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an address is an integrated address
|
||||
* @param {string} address - The address string
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isIntegrated(address) {
|
||||
const parsed = parseAddress(address);
|
||||
return parsed.valid && parsed.type === ADDRESS_TYPE.INTEGRATED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an address is a subaddress
|
||||
* @param {string} address - The address string
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSubaddress(address) {
|
||||
const parsed = parseAddress(address);
|
||||
return parsed.valid && parsed.type === ADDRESS_TYPE.SUBADDRESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the public spend key from an address
|
||||
* @param {string} address - The address string
|
||||
* @returns {Uint8Array|null} - 32-byte public spend key or null if invalid
|
||||
*/
|
||||
export function getSpendPublicKey(address) {
|
||||
const parsed = parseAddress(address);
|
||||
return parsed.valid ? parsed.spendPublicKey : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the public view key from an address
|
||||
* @param {string} address - The address string
|
||||
* @returns {Uint8Array|null} - 32-byte public view key or null if invalid
|
||||
*/
|
||||
export function getViewPublicKey(address) {
|
||||
const parsed = parseAddress(address);
|
||||
return parsed.valid ? parsed.viewPublicKey : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the payment ID from an integrated address
|
||||
* @param {string} address - The address string
|
||||
* @returns {Uint8Array|null} - 8-byte payment ID or null if not an integrated address
|
||||
*/
|
||||
export function getPaymentId(address) {
|
||||
const parsed = parseAddress(address);
|
||||
return parsed.valid && parsed.type === ADDRESS_TYPE.INTEGRATED ? parsed.paymentId : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an address from components
|
||||
* @param {Object} options - Address options
|
||||
* @param {string} options.network - Network type
|
||||
* @param {string} options.format - Address format
|
||||
* @param {string} options.type - Address type
|
||||
* @param {Uint8Array} options.spendPublicKey - 32-byte public spend key
|
||||
* @param {Uint8Array} options.viewPublicKey - 32-byte public view key
|
||||
* @param {Uint8Array} [options.paymentId] - 8-byte payment ID (for integrated addresses)
|
||||
* @returns {string|null} - Encoded address or null on error
|
||||
*/
|
||||
export function createAddress(options) {
|
||||
const { network, format, type, spendPublicKey, viewPublicKey, paymentId } = options;
|
||||
|
||||
// Validate keys
|
||||
if (!spendPublicKey || spendPublicKey.length !== KEY_SIZE) {
|
||||
return null;
|
||||
}
|
||||
if (!viewPublicKey || viewPublicKey.length !== KEY_SIZE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get prefix
|
||||
const prefix = getPrefix(network, format, type);
|
||||
if (prefix === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build data
|
||||
let data;
|
||||
if (type === ADDRESS_TYPE.INTEGRATED) {
|
||||
if (!paymentId || paymentId.length !== PAYMENT_ID_SIZE) {
|
||||
return null;
|
||||
}
|
||||
data = new Uint8Array(KEY_SIZE * 2 + PAYMENT_ID_SIZE);
|
||||
data.set(spendPublicKey, 0);
|
||||
data.set(viewPublicKey, KEY_SIZE);
|
||||
data.set(paymentId, KEY_SIZE * 2);
|
||||
} else {
|
||||
data = new Uint8Array(KEY_SIZE * 2);
|
||||
data.set(spendPublicKey, 0);
|
||||
data.set(viewPublicKey, KEY_SIZE);
|
||||
}
|
||||
|
||||
return encodeAddress(prefix, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function toIntegratedAddress(address, paymentId) {
|
||||
const parsed = parseAddress(address);
|
||||
|
||||
if (!parsed.valid || parsed.type !== ADDRESS_TYPE.STANDARD) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert hex string to bytes if needed
|
||||
if (typeof paymentId === 'string') {
|
||||
if (paymentId.length !== 16) {
|
||||
return null;
|
||||
}
|
||||
const bytes = new Uint8Array(8);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
bytes[i] = parseInt(paymentId.substr(i * 2, 2), 16);
|
||||
}
|
||||
paymentId = bytes;
|
||||
}
|
||||
|
||||
if (paymentId.length !== PAYMENT_ID_SIZE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createAddress({
|
||||
network: parsed.network,
|
||||
format: parsed.format,
|
||||
type: ADDRESS_TYPE.INTEGRATED,
|
||||
spendPublicKey: parsed.spendPublicKey,
|
||||
viewPublicKey: parsed.viewPublicKey,
|
||||
paymentId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the standard address from an integrated address
|
||||
* @param {string} address - Integrated address
|
||||
* @returns {string|null} - Standard address or null on error
|
||||
*/
|
||||
export function toStandardAddress(address) {
|
||||
const parsed = parseAddress(address);
|
||||
|
||||
if (!parsed.valid || parsed.type !== ADDRESS_TYPE.INTEGRATED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createAddress({
|
||||
network: parsed.network,
|
||||
format: parsed.format,
|
||||
type: ADDRESS_TYPE.STANDARD,
|
||||
spendPublicKey: parsed.spendPublicKey,
|
||||
viewPublicKey: parsed.viewPublicKey
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format address info as a human-readable string
|
||||
* @param {string} address - The address string
|
||||
* @returns {string} - Human-readable description
|
||||
*/
|
||||
export function describeAddress(address) {
|
||||
const parsed = parseAddress(address);
|
||||
|
||||
if (!parsed.valid) {
|
||||
return `Invalid address: ${parsed.error}`;
|
||||
}
|
||||
|
||||
const parts = [
|
||||
parsed.network.charAt(0).toUpperCase() + parsed.network.slice(1),
|
||||
parsed.format === ADDRESS_FORMAT.CARROT ? 'CARROT' : 'Legacy',
|
||||
parsed.type
|
||||
];
|
||||
|
||||
if (parsed.type === ADDRESS_TYPE.INTEGRATED) {
|
||||
const paymentIdHex = Array.from(parsed.paymentId)
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
parts.push(`(Payment ID: ${paymentIdHex})`);
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert bytes to hex string
|
||||
* @param {Uint8Array} bytes - Bytes to convert
|
||||
* @returns {string} - Hex string
|
||||
*/
|
||||
export function bytesToHex(bytes) {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex string to bytes
|
||||
* @param {string} hex - Hex string
|
||||
* @returns {Uint8Array} - Bytes
|
||||
*/
|
||||
export function hexToBytes(hex) {
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export default {
|
||||
parseAddress,
|
||||
isValidAddress,
|
||||
isNetwork,
|
||||
isMainnet,
|
||||
isTestnet,
|
||||
isStagenet,
|
||||
isCarrot,
|
||||
isLegacy,
|
||||
isStandard,
|
||||
isIntegrated,
|
||||
isSubaddress,
|
||||
getSpendPublicKey,
|
||||
getViewPublicKey,
|
||||
getPaymentId,
|
||||
createAddress,
|
||||
toIntegratedAddress,
|
||||
toStandardAddress,
|
||||
describeAddress,
|
||||
bytesToHex,
|
||||
hexToBytes
|
||||
};
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* Monero/Salvium Base58 Encoding/Decoding
|
||||
*
|
||||
* This is NOT the same as Bitcoin's Base58Check!
|
||||
* Monero uses a block-based encoding where:
|
||||
* - Data is split into 8-byte blocks
|
||||
* - Each 8-byte block encodes to exactly 11 Base58 characters
|
||||
* - Partial blocks use a size mapping table
|
||||
*/
|
||||
|
||||
import { BASE58_ALPHABET, BASE58_FULL_BLOCK_SIZE, BASE58_FULL_ENCODED_BLOCK_SIZE, BASE58_ENCODED_BLOCK_SIZES } from './constants.js';
|
||||
import { keccak256 } from './keccak.js';
|
||||
|
||||
// Build reverse alphabet lookup
|
||||
const ALPHABET_MAP = new Map();
|
||||
for (let i = 0; i < BASE58_ALPHABET.length; i++) {
|
||||
ALPHABET_MAP.set(BASE58_ALPHABET[i], i);
|
||||
}
|
||||
|
||||
// Build decoded block sizes lookup (reverse of encoded block sizes)
|
||||
const DECODED_BLOCK_SIZES = new Map();
|
||||
for (let i = 0; i < BASE58_ENCODED_BLOCK_SIZES.length; i++) {
|
||||
DECODED_BLOCK_SIZES.set(BASE58_ENCODED_BLOCK_SIZES[i], i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert bytes to big-endian uint64
|
||||
* @param {Uint8Array} data - Input bytes (up to 8 bytes)
|
||||
* @returns {BigInt} - Big-endian integer value
|
||||
*/
|
||||
function uint8BEToUint64(data) {
|
||||
let result = 0n;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
result = (result << 8n) | BigInt(data[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert uint64 to big-endian bytes
|
||||
* @param {BigInt} num - Number to convert
|
||||
* @param {number} size - Output size in bytes
|
||||
* @returns {Uint8Array} - Big-endian bytes
|
||||
*/
|
||||
function uint64ToUint8BE(num, size) {
|
||||
const result = new Uint8Array(size);
|
||||
for (let i = size - 1; i >= 0; i--) {
|
||||
result[i] = Number(num & 0xFFn);
|
||||
num >>= 8n;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a single block of data to Base58
|
||||
* @param {Uint8Array} block - Input block (1-8 bytes)
|
||||
* @returns {string} - Base58 encoded string
|
||||
*/
|
||||
function encodeBlock(block) {
|
||||
const encodedSize = BASE58_ENCODED_BLOCK_SIZES[block.length];
|
||||
const result = new Array(encodedSize).fill(BASE58_ALPHABET[0]);
|
||||
|
||||
let num = uint8BEToUint64(block);
|
||||
let i = encodedSize - 1;
|
||||
|
||||
while (num > 0n) {
|
||||
const remainder = Number(num % 58n);
|
||||
num = num / 58n;
|
||||
result[i] = BASE58_ALPHABET[remainder];
|
||||
i--;
|
||||
}
|
||||
|
||||
return result.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a single Base58 block to bytes
|
||||
* @param {string} block - Base58 encoded block
|
||||
* @returns {Uint8Array|null} - Decoded bytes or null on error
|
||||
*/
|
||||
function decodeBlock(block) {
|
||||
const decodedSize = DECODED_BLOCK_SIZES.get(block.length);
|
||||
if (decodedSize === undefined || decodedSize < 0) {
|
||||
return null; // Invalid block size
|
||||
}
|
||||
|
||||
if (decodedSize === 0) {
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
|
||||
let num = 0n;
|
||||
const base = 58n;
|
||||
|
||||
for (let i = 0; i < block.length; i++) {
|
||||
const digit = ALPHABET_MAP.get(block[i]);
|
||||
if (digit === undefined) {
|
||||
return null; // Invalid character
|
||||
}
|
||||
num = num * base + BigInt(digit);
|
||||
}
|
||||
|
||||
// Check for overflow
|
||||
if (decodedSize < BASE58_FULL_BLOCK_SIZE && num >= (1n << BigInt(8 * decodedSize))) {
|
||||
return null; // Overflow
|
||||
}
|
||||
|
||||
return uint64ToUint8BE(num, decodedSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode binary data to Base58 (Monero variant)
|
||||
* @param {Uint8Array|Array} data - Binary data to encode
|
||||
* @returns {string} - Base58 encoded string
|
||||
*/
|
||||
export function encode(data) {
|
||||
if (!(data instanceof Uint8Array)) {
|
||||
data = new Uint8Array(data);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const fullBlockCount = Math.floor(data.length / BASE58_FULL_BLOCK_SIZE);
|
||||
const lastBlockSize = data.length % BASE58_FULL_BLOCK_SIZE;
|
||||
|
||||
let result = '';
|
||||
|
||||
// Encode full blocks
|
||||
for (let i = 0; i < fullBlockCount; i++) {
|
||||
const block = data.slice(i * BASE58_FULL_BLOCK_SIZE, (i + 1) * BASE58_FULL_BLOCK_SIZE);
|
||||
result += encodeBlock(block);
|
||||
}
|
||||
|
||||
// Encode last partial block
|
||||
if (lastBlockSize > 0) {
|
||||
const block = data.slice(fullBlockCount * BASE58_FULL_BLOCK_SIZE);
|
||||
result += encodeBlock(block);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode Base58 string to binary data (Monero variant)
|
||||
* @param {string} encoded - Base58 encoded string
|
||||
* @returns {Uint8Array|null} - Decoded binary data or null on error
|
||||
*/
|
||||
export function decode(encoded) {
|
||||
if (encoded.length === 0) {
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
|
||||
const fullBlockCount = Math.floor(encoded.length / BASE58_FULL_ENCODED_BLOCK_SIZE);
|
||||
const lastBlockSize = encoded.length % BASE58_FULL_ENCODED_BLOCK_SIZE;
|
||||
const lastBlockDecodedSize = DECODED_BLOCK_SIZES.get(lastBlockSize);
|
||||
|
||||
if (lastBlockDecodedSize === undefined || lastBlockDecodedSize < 0) {
|
||||
return null; // Invalid encoded length
|
||||
}
|
||||
|
||||
const dataSize = fullBlockCount * BASE58_FULL_BLOCK_SIZE + lastBlockDecodedSize;
|
||||
const result = new Uint8Array(dataSize);
|
||||
|
||||
let offset = 0;
|
||||
|
||||
// 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;
|
||||
}
|
||||
result.set(decoded, offset);
|
||||
offset += BASE58_FULL_BLOCK_SIZE;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
result.set(decoded, offset);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a varint (variable-length integer)
|
||||
* @param {BigInt|number} value - Integer to encode
|
||||
* @returns {Uint8Array} - Varint encoded bytes
|
||||
*/
|
||||
export function encodeVarint(value) {
|
||||
value = BigInt(value);
|
||||
const bytes = [];
|
||||
|
||||
while (value >= 0x80n) {
|
||||
bytes.push(Number((value & 0x7Fn) | 0x80n));
|
||||
value >>= 7n;
|
||||
}
|
||||
bytes.push(Number(value));
|
||||
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function decodeVarint(data) {
|
||||
let value = 0n;
|
||||
let shift = 0n;
|
||||
let bytesRead = 0;
|
||||
|
||||
for (let i = 0; i < data.length && i < 10; i++) {
|
||||
const byte = BigInt(data[i]);
|
||||
value |= (byte & 0x7Fn) << shift;
|
||||
bytesRead++;
|
||||
|
||||
if ((byte & 0x80n) === 0n) {
|
||||
return { value, bytesRead };
|
||||
}
|
||||
|
||||
shift += 7n;
|
||||
}
|
||||
|
||||
return null; // Varint too long or incomplete
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode an address with tag/prefix and checksum
|
||||
* @param {BigInt|number} tag - Address prefix/tag
|
||||
* @param {Uint8Array} data - Address data (public keys)
|
||||
* @returns {string} - Base58 encoded address
|
||||
*/
|
||||
export function encodeAddress(tag, data) {
|
||||
// Build: [varint tag][data][4-byte checksum]
|
||||
const tagBytes = encodeVarint(tag);
|
||||
const combined = new Uint8Array(tagBytes.length + data.length);
|
||||
combined.set(tagBytes, 0);
|
||||
combined.set(data, tagBytes.length);
|
||||
|
||||
// Compute checksum (first 4 bytes of Keccak-256)
|
||||
const hash = keccak256(combined);
|
||||
const checksum = hash.slice(0, 4);
|
||||
|
||||
// Combine with checksum
|
||||
const withChecksum = new Uint8Array(combined.length + 4);
|
||||
withChecksum.set(combined, 0);
|
||||
withChecksum.set(checksum, combined.length);
|
||||
|
||||
return encode(withChecksum);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function decodeAddress(address) {
|
||||
const decoded = decode(address);
|
||||
if (decoded === null || decoded.length <= 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract checksum
|
||||
const checksum = decoded.slice(decoded.length - 4);
|
||||
const payload = decoded.slice(0, decoded.length - 4);
|
||||
|
||||
// Verify checksum
|
||||
const hash = keccak256(payload);
|
||||
const expectedChecksum = hash.slice(0, 4);
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if (checksum[i] !== expectedChecksum[i]) {
|
||||
return null; // Checksum mismatch
|
||||
}
|
||||
}
|
||||
|
||||
// Decode varint tag
|
||||
const varintResult = decodeVarint(payload);
|
||||
if (varintResult === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { value: tag, bytesRead } = varintResult;
|
||||
const data = payload.slice(bytesRead);
|
||||
|
||||
return { tag, data };
|
||||
}
|
||||
|
||||
export default {
|
||||
encode,
|
||||
decode,
|
||||
encodeVarint,
|
||||
decodeVarint,
|
||||
encodeAddress,
|
||||
decodeAddress
|
||||
};
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* Blake2b hash function
|
||||
* Pure JavaScript implementation based on RFC 7693
|
||||
*/
|
||||
|
||||
// IV constants
|
||||
const IV = new Uint32Array([
|
||||
0xf3bcc908, 0x6a09e667,
|
||||
0x84caa73b, 0xbb67ae85,
|
||||
0xfe94f82b, 0x3c6ef372,
|
||||
0x5f1d36f1, 0xa54ff53a,
|
||||
0xade682d1, 0x510e527f,
|
||||
0x2b3e6c1f, 0x9b05688c,
|
||||
0xfb41bd6b, 0x1f83d9ab,
|
||||
0x137e2179, 0x5be0cd19
|
||||
]);
|
||||
|
||||
// Sigma permutation table
|
||||
const SIGMA = [
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
[14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
|
||||
[11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4],
|
||||
[7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
|
||||
[9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13],
|
||||
[2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
|
||||
[12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11],
|
||||
[13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
|
||||
[6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5],
|
||||
[10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
[14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3]
|
||||
];
|
||||
|
||||
// 64-bit operations using two 32-bit values (lo, hi)
|
||||
// v is Uint32Array where v[2i] = low 32 bits, v[2i+1] = high 32 bits
|
||||
|
||||
function ADD64AA(v, a, b) {
|
||||
const o0 = v[a] + v[b];
|
||||
let o1 = v[a + 1] + v[b + 1];
|
||||
if (o0 >= 0x100000000) o1++;
|
||||
v[a] = o0 >>> 0;
|
||||
v[a + 1] = o1 >>> 0;
|
||||
}
|
||||
|
||||
function ADD64AC(v, a, b0, b1) {
|
||||
let o0 = v[a] + b0;
|
||||
let o1 = v[a + 1] + b1;
|
||||
if (o0 >= 0x100000000) o1++;
|
||||
v[a] = o0 >>> 0;
|
||||
v[a + 1] = o1 >>> 0;
|
||||
}
|
||||
|
||||
function XOR64(v, a, b) {
|
||||
v[a] ^= v[b];
|
||||
v[a + 1] ^= v[b + 1];
|
||||
}
|
||||
|
||||
function ROTR64(x, y, c) {
|
||||
// Rotate right by c bits, result in x
|
||||
const xl = x[0], xh = x[1];
|
||||
if (c < 32) {
|
||||
y[0] = (xl >>> c) | (xh << (32 - c));
|
||||
y[1] = (xh >>> c) | (xl << (32 - c));
|
||||
} else if (c === 32) {
|
||||
y[0] = xh;
|
||||
y[1] = xl;
|
||||
} else {
|
||||
const cc = c - 32;
|
||||
y[0] = (xh >>> cc) | (xl << (32 - cc));
|
||||
y[1] = (xl >>> cc) | (xh << (32 - cc));
|
||||
}
|
||||
}
|
||||
|
||||
// G mixing function
|
||||
function G(v, m, a, b, c, d, ix, iy) {
|
||||
const x0 = m[ix * 2], x1 = m[ix * 2 + 1];
|
||||
const y0 = m[iy * 2], y1 = m[iy * 2 + 1];
|
||||
const tmp = [0, 0];
|
||||
|
||||
// a = a + b + x
|
||||
ADD64AA(v, a * 2, b * 2);
|
||||
ADD64AC(v, a * 2, x0, x1);
|
||||
|
||||
// d = rotr64(d ^ a, 32)
|
||||
XOR64(v, d * 2, a * 2);
|
||||
tmp[0] = v[d * 2];
|
||||
tmp[1] = v[d * 2 + 1];
|
||||
v[d * 2] = tmp[1];
|
||||
v[d * 2 + 1] = tmp[0];
|
||||
|
||||
// c = c + d
|
||||
ADD64AA(v, c * 2, d * 2);
|
||||
|
||||
// b = rotr64(b ^ c, 24)
|
||||
XOR64(v, b * 2, c * 2);
|
||||
tmp[0] = v[b * 2];
|
||||
tmp[1] = v[b * 2 + 1];
|
||||
v[b * 2] = (tmp[0] >>> 24) | (tmp[1] << 8);
|
||||
v[b * 2 + 1] = (tmp[1] >>> 24) | (tmp[0] << 8);
|
||||
|
||||
// a = a + b + y
|
||||
ADD64AA(v, a * 2, b * 2);
|
||||
ADD64AC(v, a * 2, y0, y1);
|
||||
|
||||
// d = rotr64(d ^ a, 16)
|
||||
XOR64(v, d * 2, a * 2);
|
||||
tmp[0] = v[d * 2];
|
||||
tmp[1] = v[d * 2 + 1];
|
||||
v[d * 2] = (tmp[0] >>> 16) | (tmp[1] << 16);
|
||||
v[d * 2 + 1] = (tmp[1] >>> 16) | (tmp[0] << 16);
|
||||
|
||||
// c = c + d
|
||||
ADD64AA(v, c * 2, d * 2);
|
||||
|
||||
// b = rotr64(b ^ c, 63)
|
||||
XOR64(v, b * 2, c * 2);
|
||||
tmp[0] = v[b * 2];
|
||||
tmp[1] = v[b * 2 + 1];
|
||||
v[b * 2] = (tmp[1] >>> 31) | (tmp[0] << 1);
|
||||
v[b * 2 + 1] = (tmp[0] >>> 31) | (tmp[1] << 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blake2b context
|
||||
*/
|
||||
class Blake2bCtx {
|
||||
constructor(outlen, key) {
|
||||
this.h = new Uint32Array(16);
|
||||
this.b = new Uint8Array(128);
|
||||
this.c = 0; // pointer within buffer
|
||||
this.t = 0; // total bytes
|
||||
this.outlen = outlen;
|
||||
|
||||
// Initialize state with IV
|
||||
for (let i = 0; i < 16; i++) {
|
||||
this.h[i] = IV[i];
|
||||
}
|
||||
|
||||
// Mix in parameters: outlen, keylen
|
||||
const keylen = key ? key.length : 0;
|
||||
this.h[0] ^= 0x01010000 ^ (keylen << 8) ^ outlen;
|
||||
|
||||
// If keyed, process key block
|
||||
if (keylen > 0) {
|
||||
const block = new Uint8Array(128);
|
||||
for (let i = 0; i < keylen; i++) block[i] = key[i];
|
||||
this.update(block);
|
||||
}
|
||||
}
|
||||
|
||||
compress(last) {
|
||||
const v = new Uint32Array(32);
|
||||
const m = new Uint32Array(32);
|
||||
|
||||
// Initialize v[0..15] = h[0..7] (as 64-bit words)
|
||||
for (let i = 0; i < 16; i++) v[i] = this.h[i];
|
||||
|
||||
// Initialize v[16..31] = IV (as 64-bit words)
|
||||
for (let i = 0; i < 16; i++) v[16 + i] = IV[i];
|
||||
|
||||
// v[12] ^= t (low 64 bits of counter)
|
||||
v[24] ^= this.t >>> 0;
|
||||
v[25] ^= (this.t / 0x100000000) >>> 0;
|
||||
|
||||
// v[14] ^= 0xffffffffffffffff if last block
|
||||
if (last) {
|
||||
v[28] = ~v[28];
|
||||
v[29] = ~v[29];
|
||||
}
|
||||
|
||||
// Load message block as 16 64-bit words
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const off = i * 8;
|
||||
m[i * 2] = this.b[off] | (this.b[off + 1] << 8) | (this.b[off + 2] << 16) | (this.b[off + 3] << 24);
|
||||
m[i * 2 + 1] = this.b[off + 4] | (this.b[off + 5] << 8) | (this.b[off + 6] << 16) | (this.b[off + 7] << 24);
|
||||
}
|
||||
|
||||
// 12 rounds
|
||||
for (let round = 0; round < 12; round++) {
|
||||
const s = SIGMA[round];
|
||||
G(v, m, 0, 4, 8, 12, s[0], s[1]);
|
||||
G(v, m, 1, 5, 9, 13, s[2], s[3]);
|
||||
G(v, m, 2, 6, 10, 14, s[4], s[5]);
|
||||
G(v, m, 3, 7, 11, 15, s[6], s[7]);
|
||||
G(v, m, 0, 5, 10, 15, s[8], s[9]);
|
||||
G(v, m, 1, 6, 11, 12, s[10], s[11]);
|
||||
G(v, m, 2, 7, 8, 13, s[12], s[13]);
|
||||
G(v, m, 3, 4, 9, 14, s[14], s[15]);
|
||||
}
|
||||
|
||||
// h = h ^ v[0..7] ^ v[8..15]
|
||||
for (let i = 0; i < 16; i++) {
|
||||
this.h[i] ^= v[i] ^ v[16 + i];
|
||||
}
|
||||
}
|
||||
|
||||
update(input) {
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
if (this.c === 128) {
|
||||
this.t += this.c;
|
||||
this.compress(false);
|
||||
this.c = 0;
|
||||
}
|
||||
this.b[this.c++] = input[i];
|
||||
}
|
||||
}
|
||||
|
||||
final() {
|
||||
this.t += this.c;
|
||||
|
||||
// Pad with zeros
|
||||
while (this.c < 128) {
|
||||
this.b[this.c++] = 0;
|
||||
}
|
||||
|
||||
this.compress(true);
|
||||
|
||||
// Output
|
||||
const out = new Uint8Array(this.outlen);
|
||||
for (let i = 0; i < this.outlen; i++) {
|
||||
out[i] = (this.h[i >> 2] >> (8 * (i & 3))) & 0xff;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Blake2b hash
|
||||
* @param {Uint8Array} input - Input data
|
||||
* @param {number} outlen - Output length in bytes (1-64)
|
||||
* @param {Uint8Array} [key] - Optional key (up to 64 bytes)
|
||||
* @returns {Uint8Array} Hash output
|
||||
*/
|
||||
export function blake2b(input, outlen, key = null) {
|
||||
if (outlen < 1 || outlen > 64) throw new Error('Invalid output length');
|
||||
if (key && key.length > 64) throw new Error('Key too long');
|
||||
|
||||
const ctx = new Blake2bCtx(outlen, key);
|
||||
ctx.update(input);
|
||||
return ctx.final();
|
||||
}
|
||||
|
||||
/**
|
||||
* Blake2b hex output
|
||||
* @param {Uint8Array} input - Input data
|
||||
* @param {number} outlen - Output length in bytes
|
||||
* @param {Uint8Array} [key] - Optional key
|
||||
* @returns {string} Hex string
|
||||
*/
|
||||
export function blake2bHex(input, outlen, key = null) {
|
||||
const hash = blake2b(input, outlen, key);
|
||||
return Array.from(hash).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
export default { blake2b, blake2bHex };
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* CARROT Key Derivation Functions
|
||||
* Implements key derivation as per Salvium/Monero CARROT specification
|
||||
*/
|
||||
|
||||
import { blake2b } from './blake2b.js';
|
||||
import { hexToBytes, bytesToHex } from './address.js';
|
||||
|
||||
// Group order L for scalar reduction
|
||||
const L = (1n << 252n) + 27742317777372353535851937790883648493n;
|
||||
|
||||
// Create length-prefixed domain separator (matches Salvium SpFixedTranscript format)
|
||||
function makeDomainSep(str) {
|
||||
const strBytes = new TextEncoder().encode(str);
|
||||
const result = new Uint8Array(1 + strBytes.length);
|
||||
result[0] = strBytes.length; // Length prefix
|
||||
result.set(strBytes, 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Domain separators (from Salvium carrot_core/config.h)
|
||||
// Each is length-prefixed as per SpFixedTranscript format
|
||||
const DOMAIN_SEP = {
|
||||
PROVE_SPEND_KEY: makeDomainSep("Carrot prove-spend key"),
|
||||
VIEW_BALANCE_SECRET: makeDomainSep("Carrot view-balance secret"),
|
||||
GENERATE_IMAGE_KEY: makeDomainSep("Carrot generate-image key"),
|
||||
INCOMING_VIEW_KEY: makeDomainSep("Carrot incoming view key"),
|
||||
GENERATE_ADDRESS_SECRET: makeDomainSep("Carrot generate-address secret")
|
||||
};
|
||||
|
||||
/**
|
||||
* Reduce a 64-byte value modulo L (curve order)
|
||||
* This is sc_reduce from ref10
|
||||
* @param {Uint8Array} bytes - 64 bytes to reduce
|
||||
* @returns {Uint8Array} 32-byte scalar
|
||||
*/
|
||||
function scReduce(bytes) {
|
||||
// Convert 64 bytes to BigInt (little-endian)
|
||||
let n = 0n;
|
||||
for (let i = 63; i >= 0; i--) {
|
||||
n = (n << 8n) | BigInt(bytes[i]);
|
||||
}
|
||||
|
||||
// Reduce mod L
|
||||
n = n % L;
|
||||
|
||||
// Convert back to 32 bytes (little-endian)
|
||||
const result = new Uint8Array(32);
|
||||
for (let i = 0; i < 32; i++) {
|
||||
result[i] = Number(n & 0xffn);
|
||||
n = n >> 8n;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* H_32: 32-byte keyed hash
|
||||
* @param {Uint8Array} domainSep - Domain separator
|
||||
* @param {Uint8Array} key - 32-byte key
|
||||
* @returns {Uint8Array} 32-byte hash
|
||||
*/
|
||||
function deriveBytes32(domainSep, key) {
|
||||
return blake2b(domainSep, 32, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* H_n: Scalar derivation (hash to 64 bytes, then reduce mod L)
|
||||
* @param {Uint8Array} domainSep - Domain separator
|
||||
* @param {Uint8Array} key - 32-byte key
|
||||
* @returns {Uint8Array} 32-byte scalar
|
||||
*/
|
||||
function deriveScalar(domainSep, key) {
|
||||
const hash64 = blake2b(domainSep, 64, key);
|
||||
return scReduce(hash64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive view-balance secret from master secret
|
||||
* s_vb = H_32("Carrot view-balance secret", s_master)
|
||||
* @param {Uint8Array} masterSecret - 32-byte master secret (spend key)
|
||||
* @returns {Uint8Array} 32-byte view-balance secret
|
||||
*/
|
||||
export function makeViewBalanceSecret(masterSecret) {
|
||||
return deriveBytes32(DOMAIN_SEP.VIEW_BALANCE_SECRET, masterSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive view-incoming key from view-balance secret
|
||||
* k_vi = H_n("Carrot incoming view key", s_vb)
|
||||
* @param {Uint8Array} viewBalanceSecret - 32-byte view-balance secret
|
||||
* @returns {Uint8Array} 32-byte view-incoming key
|
||||
*/
|
||||
export function makeViewIncomingKey(viewBalanceSecret) {
|
||||
return deriveScalar(DOMAIN_SEP.INCOMING_VIEW_KEY, viewBalanceSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive prove-spend key from master secret
|
||||
* k_ps = H_n("Carrot prove-spend key", s_master)
|
||||
* @param {Uint8Array} masterSecret - 32-byte master secret
|
||||
* @returns {Uint8Array} 32-byte prove-spend key
|
||||
*/
|
||||
export function makeProveSpendKey(masterSecret) {
|
||||
return deriveScalar(DOMAIN_SEP.PROVE_SPEND_KEY, masterSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive generate-image key from view-balance secret
|
||||
* k_gi = H_n("Carrot generate-image key", s_vb)
|
||||
* @param {Uint8Array} viewBalanceSecret - 32-byte view-balance secret
|
||||
* @returns {Uint8Array} 32-byte generate-image key
|
||||
*/
|
||||
export function makeGenerateImageKey(viewBalanceSecret) {
|
||||
return deriveScalar(DOMAIN_SEP.GENERATE_IMAGE_KEY, viewBalanceSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive generate-address secret from view-balance secret
|
||||
* s_ga = H_32("Carrot generate-address secret", s_vb)
|
||||
* @param {Uint8Array} viewBalanceSecret - 32-byte view-balance secret
|
||||
* @returns {Uint8Array} 32-byte generate-address secret
|
||||
*/
|
||||
export function makeGenerateAddressSecret(viewBalanceSecret) {
|
||||
return deriveBytes32(DOMAIN_SEP.GENERATE_ADDRESS_SECRET, viewBalanceSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive all CARROT keys from master secret
|
||||
* @param {Uint8Array|string} masterSecret - 32-byte master secret or hex string
|
||||
* @returns {Object} All derived keys as hex strings
|
||||
*/
|
||||
export function deriveCarrotKeys(masterSecret) {
|
||||
// Convert hex string to bytes if needed
|
||||
if (typeof masterSecret === 'string') {
|
||||
masterSecret = hexToBytes(masterSecret);
|
||||
}
|
||||
|
||||
const viewBalanceSecret = makeViewBalanceSecret(masterSecret);
|
||||
const proveSpendKey = makeProveSpendKey(masterSecret);
|
||||
const viewIncomingKey = makeViewIncomingKey(viewBalanceSecret);
|
||||
const generateImageKey = makeGenerateImageKey(viewBalanceSecret);
|
||||
const generateAddressSecret = makeGenerateAddressSecret(viewBalanceSecret);
|
||||
|
||||
return {
|
||||
masterSecret: bytesToHex(masterSecret),
|
||||
proveSpendKey: bytesToHex(proveSpendKey),
|
||||
viewBalanceSecret: bytesToHex(viewBalanceSecret),
|
||||
generateImageKey: bytesToHex(generateImageKey),
|
||||
viewIncomingKey: bytesToHex(viewIncomingKey),
|
||||
generateAddressSecret: bytesToHex(generateAddressSecret)
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
makeViewBalanceSecret,
|
||||
makeViewIncomingKey,
|
||||
makeProveSpendKey,
|
||||
makeGenerateImageKey,
|
||||
makeGenerateAddressSecret,
|
||||
deriveCarrotKeys
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Salvium Network and Address Constants
|
||||
*
|
||||
* This module contains all address prefixes for Salvium across all network types
|
||||
* and address formats (Legacy CryptoNote and CARROT).
|
||||
*/
|
||||
|
||||
// Network types
|
||||
export const NETWORK = {
|
||||
MAINNET: 'mainnet',
|
||||
TESTNET: 'testnet',
|
||||
STAGENET: 'stagenet'
|
||||
};
|
||||
|
||||
// Address types
|
||||
export const ADDRESS_TYPE = {
|
||||
STANDARD: 'standard',
|
||||
INTEGRATED: 'integrated',
|
||||
SUBADDRESS: 'subaddress'
|
||||
};
|
||||
|
||||
// Address formats
|
||||
export const ADDRESS_FORMAT = {
|
||||
LEGACY: 'legacy', // CryptoNote style (SaLv...)
|
||||
CARROT: 'carrot' // CARROT style (SC1...)
|
||||
};
|
||||
|
||||
/**
|
||||
* Address prefixes organized by network, format, and type
|
||||
* Values are BigInt for handling large prefixes
|
||||
*/
|
||||
export const PREFIXES = {
|
||||
[NETWORK.MAINNET]: {
|
||||
[ADDRESS_FORMAT.LEGACY]: {
|
||||
[ADDRESS_TYPE.STANDARD]: { prefix: 0x3ef318n, text: 'SaLv' },
|
||||
[ADDRESS_TYPE.INTEGRATED]: { prefix: 0x55ef318n, text: 'SaLvi' },
|
||||
[ADDRESS_TYPE.SUBADDRESS]: { prefix: 0xf5ef318n, text: 'SaLvs' }
|
||||
},
|
||||
[ADDRESS_FORMAT.CARROT]: {
|
||||
[ADDRESS_TYPE.STANDARD]: { prefix: 0x180c96n, text: 'SC1' },
|
||||
[ADDRESS_TYPE.INTEGRATED]: { prefix: 0x2ccc96n, text: 'SC1i' },
|
||||
[ADDRESS_TYPE.SUBADDRESS]: { prefix: 0x314c96n, text: 'SC1s' }
|
||||
}
|
||||
},
|
||||
[NETWORK.TESTNET]: {
|
||||
[ADDRESS_FORMAT.LEGACY]: {
|
||||
[ADDRESS_TYPE.STANDARD]: { prefix: 0x15beb318n, text: 'SaLvT' },
|
||||
[ADDRESS_TYPE.INTEGRATED]: { prefix: 0xd055eb318n, text: 'SaLvTi' },
|
||||
[ADDRESS_TYPE.SUBADDRESS]: { prefix: 0xa59eb318n, text: 'SaLvTs' }
|
||||
},
|
||||
[ADDRESS_FORMAT.CARROT]: {
|
||||
[ADDRESS_TYPE.STANDARD]: { prefix: 0x254c96n, text: 'SC1T' },
|
||||
[ADDRESS_TYPE.INTEGRATED]: { prefix: 0x1ac50c96n, text: 'SC1Ti' },
|
||||
[ADDRESS_TYPE.SUBADDRESS]: { prefix: 0x3c54c96n, text: 'SC1Ts' }
|
||||
}
|
||||
},
|
||||
[NETWORK.STAGENET]: {
|
||||
[ADDRESS_FORMAT.LEGACY]: {
|
||||
[ADDRESS_TYPE.STANDARD]: { prefix: 0x149eb318n, text: 'SaLvS' },
|
||||
[ADDRESS_TYPE.INTEGRATED]: { prefix: 0xf343eb318n, text: 'SaLvSi' },
|
||||
[ADDRESS_TYPE.SUBADDRESS]: { prefix: 0x2d47eb318n, text: 'SaLvSs' }
|
||||
},
|
||||
[ADDRESS_FORMAT.CARROT]: {
|
||||
[ADDRESS_TYPE.STANDARD]: { prefix: 0x24cc96n, text: 'SC1S' },
|
||||
[ADDRESS_TYPE.INTEGRATED]: { prefix: 0x1a848c96n, text: 'SC1Si' },
|
||||
[ADDRESS_TYPE.SUBADDRESS]: { prefix: 0x384cc96n, text: 'SC1Ss' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a reverse lookup map from prefix value to address info
|
||||
*/
|
||||
export const PREFIX_MAP = new Map();
|
||||
|
||||
for (const [network, formats] of Object.entries(PREFIXES)) {
|
||||
for (const [format, types] of Object.entries(formats)) {
|
||||
for (const [type, info] of Object.entries(types)) {
|
||||
PREFIX_MAP.set(info.prefix, {
|
||||
network,
|
||||
format,
|
||||
type,
|
||||
text: info.text
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prefix info by prefix value
|
||||
* @param {BigInt} prefix - The prefix value
|
||||
* @returns {Object|null} - Address info or null if not found
|
||||
*/
|
||||
export function getPrefixInfo(prefix) {
|
||||
return PREFIX_MAP.get(prefix) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prefix for a specific address configuration
|
||||
* @param {string} network - Network type (mainnet, testnet, stagenet)
|
||||
* @param {string} format - Address format (legacy, carrot)
|
||||
* @param {string} type - Address type (standard, integrated, subaddress)
|
||||
* @returns {BigInt|null} - The prefix value or null if invalid
|
||||
*/
|
||||
export function getPrefix(network, format, type) {
|
||||
return PREFIXES[network]?.[format]?.[type]?.prefix || null;
|
||||
}
|
||||
|
||||
// Key sizes
|
||||
export const KEY_SIZE = 32; // 32 bytes = 256 bits
|
||||
export const CHECKSUM_SIZE = 4;
|
||||
export const PAYMENT_ID_SIZE = 8; // For integrated addresses
|
||||
|
||||
// Address data sizes (without prefix)
|
||||
export const ADDRESS_DATA_SIZE = {
|
||||
[ADDRESS_TYPE.STANDARD]: KEY_SIZE * 2, // spend_key + view_key = 64 bytes
|
||||
[ADDRESS_TYPE.INTEGRATED]: KEY_SIZE * 2 + PAYMENT_ID_SIZE, // + payment_id = 72 bytes
|
||||
[ADDRESS_TYPE.SUBADDRESS]: KEY_SIZE * 2 // spend_key + view_key = 64 bytes
|
||||
};
|
||||
|
||||
// Base58 alphabet (Monero variant)
|
||||
export const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||
|
||||
// Full block size for Base58 encoding (8 bytes -> 11 chars)
|
||||
export const BASE58_FULL_BLOCK_SIZE = 8;
|
||||
export const BASE58_FULL_ENCODED_BLOCK_SIZE = 11;
|
||||
|
||||
// Encoded block sizes for partial blocks
|
||||
export const BASE58_ENCODED_BLOCK_SIZES = [0, 2, 3, 5, 6, 7, 9, 10, 11];
|
||||
+714
@@ -0,0 +1,714 @@
|
||||
/**
|
||||
* Ed25519 Elliptic Curve Operations
|
||||
*
|
||||
* Direct port from Salvium/Monero ref10 logic using BigInt for correctness.
|
||||
* Field elements are represented as BigInt in range [0, p) where p = 2^255 - 19
|
||||
*/
|
||||
|
||||
// Prime field: p = 2^255 - 19
|
||||
const P = (1n << 255n) - 19n;
|
||||
|
||||
// Curve constant d = -121665/121666 mod p
|
||||
const D = 37095705934669439343138083508754565189542113879843219016388785533085940283555n;
|
||||
|
||||
// 2*d
|
||||
const D2 = (2n * D) % P;
|
||||
|
||||
// sqrt(-1) mod p
|
||||
const I = 19681161376707505956807079304988542015446066515923890162744021073123829784752n;
|
||||
|
||||
// Base point G (Ed25519 standard base point)
|
||||
// GY = 4/5 mod P, GX is the positive (even) square root satisfying the curve equation
|
||||
const GX = 15112221349535400772501151409588531511454012693041857206046113283949847762202n;
|
||||
const GY = 46316835694926478169428394003475163141307993866256225615783033603165251855960n;
|
||||
|
||||
// Generator T for CARROT/FCMP++ (from Salvium generators.cpp)
|
||||
// T = H_p(keccak("Monero Generator T"))
|
||||
// Encoded as: 966fc66b82cd56cf85eaec801c42845f5f408878d1561e00d3d7ded2794d094f
|
||||
const T_BYTES = new Uint8Array([
|
||||
0x96, 0x6f, 0xc6, 0x6b, 0x82, 0xcd, 0x56, 0xcf,
|
||||
0x85, 0xea, 0xec, 0x80, 0x1c, 0x42, 0x84, 0x5f,
|
||||
0x5f, 0x40, 0x88, 0x78, 0xd1, 0x56, 0x1e, 0x00,
|
||||
0xd3, 0xd7, 0xde, 0xd2, 0x79, 0x4d, 0x09, 0x4f
|
||||
]);
|
||||
|
||||
// Group order L
|
||||
const L = (1n << 252n) + 27742317777372353535851937790883648493n;
|
||||
|
||||
// Field operations
|
||||
function feAdd(a, b) {
|
||||
return (a + b) % P;
|
||||
}
|
||||
|
||||
function feSub(a, b) {
|
||||
return (a - b + P) % P;
|
||||
}
|
||||
|
||||
function feMul(a, b) {
|
||||
return (a * b) % P;
|
||||
}
|
||||
|
||||
function feSq(a) {
|
||||
return (a * a) % P;
|
||||
}
|
||||
|
||||
function feNeg(a) {
|
||||
return (P - a) % P;
|
||||
}
|
||||
|
||||
// Modular exponentiation
|
||||
function fePow(base, exp) {
|
||||
let result = 1n;
|
||||
base = base % P;
|
||||
while (exp > 0n) {
|
||||
if (exp & 1n) result = (result * base) % P;
|
||||
exp = exp >> 1n;
|
||||
base = (base * base) % P;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Modular inverse using Fermat's little theorem: a^(-1) = a^(p-2) mod p
|
||||
function feInv(a) {
|
||||
return fePow(a, P - 2n);
|
||||
}
|
||||
|
||||
// Square root mod p (for point decompression)
|
||||
// p ≡ 5 (mod 8), so sqrt(a) = a^((p+3)/8) or i*a^((p+3)/8)
|
||||
function feSqrt(a) {
|
||||
const exp = (P + 3n) / 8n;
|
||||
let r = fePow(a, exp);
|
||||
|
||||
// Check if r^2 = a
|
||||
if (feSq(r) === a) return r;
|
||||
|
||||
// Try i*r
|
||||
r = feMul(r, I);
|
||||
if (feSq(r) === a) return r;
|
||||
|
||||
return null; // No square root exists
|
||||
}
|
||||
|
||||
// Point in extended coordinates: (X:Y:Z:T) where x=X/Z, y=Y/Z, xy=T/Z
|
||||
// Represented as {X, Y, Z, T} with BigInt values
|
||||
|
||||
function pointZero() {
|
||||
return { X: 0n, Y: 1n, Z: 1n, T: 0n };
|
||||
}
|
||||
|
||||
function pointFromXY(x, y) {
|
||||
return { X: x, Y: y, Z: 1n, T: feMul(x, y) };
|
||||
}
|
||||
|
||||
function pointCopy(p) {
|
||||
return { X: p.X, Y: p.Y, Z: p.Z, T: p.T };
|
||||
}
|
||||
|
||||
// Point addition using unified formula (works for doubling too)
|
||||
// Based on https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html
|
||||
function pointAdd(p, q) {
|
||||
const A = feMul(feSub(p.Y, p.X), feSub(q.Y, q.X));
|
||||
const B = feMul(feAdd(p.Y, p.X), feAdd(q.Y, q.X));
|
||||
const C = feMul(feMul(p.T, q.T), D2);
|
||||
const D_ = feMul(p.Z, q.Z);
|
||||
const D2_ = feAdd(D_, D_);
|
||||
const E = feSub(B, A);
|
||||
const F = feSub(D2_, C);
|
||||
const G = feAdd(D2_, C);
|
||||
const H = feAdd(B, A);
|
||||
|
||||
return {
|
||||
X: feMul(E, F),
|
||||
Y: feMul(G, H),
|
||||
Z: feMul(F, G),
|
||||
T: feMul(E, H)
|
||||
};
|
||||
}
|
||||
|
||||
// Point doubling using ge_p2_dbl formula from Salvium ref10
|
||||
// This matches ge_p2_dbl.h exactly
|
||||
function pointDouble(p) {
|
||||
// Matching ge_p2_dbl.h exactly:
|
||||
// XX = X^2
|
||||
const XX = feSq(p.X);
|
||||
// YY = Y^2
|
||||
const YY = feSq(p.Y);
|
||||
// B = 2*Z^2 (fe_sq2 computes 2*a^2)
|
||||
const B = feMul(2n, feSq(p.Z));
|
||||
// A = X + Y
|
||||
const A = feAdd(p.X, p.Y);
|
||||
// AA = A^2 = (X+Y)^2
|
||||
const AA = feSq(A);
|
||||
// Y3 = YY + XX
|
||||
const Y3_p1p1 = feAdd(YY, XX);
|
||||
// Z3 = YY - XX
|
||||
const Z3_p1p1 = feSub(YY, XX);
|
||||
// X3 = AA - Y3 = (X+Y)^2 - (YY+XX) = 2XY
|
||||
const X3_p1p1 = feSub(AA, Y3_p1p1);
|
||||
// T3 = B - Z3 = 2Z^2 - (YY-XX)
|
||||
const T3_p1p1 = feSub(B, Z3_p1p1);
|
||||
|
||||
// Now convert from p1p1 to p3 (extended) using ge_p1p1_to_p3
|
||||
// r->X = p->X * p->T
|
||||
// r->Y = p->Y * p->Z
|
||||
// r->Z = p->Z * p->T
|
||||
// r->T = p->X * p->Y
|
||||
return {
|
||||
X: feMul(X3_p1p1, T3_p1p1),
|
||||
Y: feMul(Y3_p1p1, Z3_p1p1),
|
||||
Z: feMul(Z3_p1p1, T3_p1p1),
|
||||
T: feMul(X3_p1p1, Y3_p1p1)
|
||||
};
|
||||
}
|
||||
|
||||
// Unified add formula for comparison
|
||||
function pointAddUnified(p, q) {
|
||||
const A = feMul(feSub(p.Y, p.X), feSub(q.Y, q.X));
|
||||
const B = feMul(feAdd(p.Y, p.X), feAdd(q.Y, q.X));
|
||||
const C = feMul(feMul(p.T, q.T), D2);
|
||||
const D_ = feMul(p.Z, q.Z);
|
||||
const D2_ = feAdd(D_, D_);
|
||||
const E = feSub(B, A);
|
||||
const F = feSub(D2_, C);
|
||||
const G = feAdd(D2_, C);
|
||||
const H = feAdd(B, A);
|
||||
|
||||
return {
|
||||
X: feMul(E, F),
|
||||
Y: feMul(G, H),
|
||||
Z: feMul(F, G),
|
||||
T: feMul(E, H)
|
||||
};
|
||||
}
|
||||
|
||||
// Scalar multiplication using double-and-add
|
||||
function scalarMult(p, s) {
|
||||
let result = pointZero();
|
||||
let base = pointCopy(p);
|
||||
|
||||
// Process scalar bits from LSB to MSB
|
||||
while (s > 0n) {
|
||||
if (s & 1n) {
|
||||
result = pointAdd(result, base);
|
||||
}
|
||||
base = pointDouble(base);
|
||||
s = s >> 1n;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Compress point to 32 bytes
|
||||
function pointToBytes(p) {
|
||||
// Convert to affine: x = X/Z, y = Y/Z
|
||||
const zi = feInv(p.Z);
|
||||
const x = feMul(p.X, zi);
|
||||
const y = feMul(p.Y, zi);
|
||||
|
||||
// Encode y with sign of x in high bit
|
||||
const bytes = new Uint8Array(32);
|
||||
let yy = y;
|
||||
for (let i = 0; i < 32; i++) {
|
||||
bytes[i] = Number(yy & 0xffn);
|
||||
yy = yy >> 8n;
|
||||
}
|
||||
|
||||
// Set high bit if x is "negative" (odd)
|
||||
if (x & 1n) {
|
||||
bytes[31] |= 0x80;
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// Decompress 32 bytes to point
|
||||
function pointFromBytes(bytes) {
|
||||
// Extract y (clear high bit)
|
||||
let y = 0n;
|
||||
for (let i = 31; i >= 0; i--) {
|
||||
y = (y << 8n) | BigInt(bytes[i]);
|
||||
}
|
||||
const xSign = (y >> 255n) & 1n;
|
||||
y = y & ((1n << 255n) - 1n);
|
||||
|
||||
if (y >= P) return null;
|
||||
|
||||
// Recover x from curve equation: x^2 = (y^2 - 1) / (d*y^2 + 1)
|
||||
const y2 = feSq(y);
|
||||
const num = feSub(y2, 1n);
|
||||
const den = feAdd(feMul(D, y2), 1n);
|
||||
const denInv = feInv(den);
|
||||
const x2 = feMul(num, denInv);
|
||||
|
||||
let x = feSqrt(x2);
|
||||
if (x === null) return null;
|
||||
|
||||
// Adjust sign
|
||||
if ((x & 1n) !== xSign) {
|
||||
x = feNeg(x);
|
||||
}
|
||||
|
||||
return pointFromXY(x, y);
|
||||
}
|
||||
|
||||
// Convert scalar bytes (little-endian) to BigInt
|
||||
function scalarFromBytes(bytes) {
|
||||
let s = 0n;
|
||||
for (let i = bytes.length - 1; i >= 0; i--) {
|
||||
s = (s << 8n) | BigInt(bytes[i]);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// Public API
|
||||
|
||||
/**
|
||||
* Check if scalar is valid (< L)
|
||||
* @param {Uint8Array} s - 32-byte scalar
|
||||
* @returns {boolean} true if valid
|
||||
*/
|
||||
export function scalarCheck(s) {
|
||||
if (s.length !== 32) return false;
|
||||
const scalar = scalarFromBytes(s);
|
||||
return scalar < L;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if scalar is non-zero
|
||||
* @param {Uint8Array} s - 32-byte scalar
|
||||
* @returns {boolean} true if non-zero
|
||||
*/
|
||||
export function scalarIsNonzero(s) {
|
||||
const scalar = scalarFromBytes(s);
|
||||
return scalar !== 0n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scalar subtraction: r = a - b (mod L)
|
||||
* @param {Uint8Array} r - 32-byte output
|
||||
* @param {Uint8Array} a - 32-byte input
|
||||
* @param {Uint8Array} b - 32-byte input
|
||||
*/
|
||||
export function scalarSub(r, a, b) {
|
||||
const aVal = scalarFromBytes(a);
|
||||
const bVal = scalarFromBytes(b);
|
||||
let result = (aVal - bVal) % L;
|
||||
if (result < 0n) result += L;
|
||||
|
||||
// Convert back to bytes (little-endian)
|
||||
for (let i = 0; i < 32; i++) {
|
||||
r[i] = Number(result & 0xffn);
|
||||
result = result >> 8n;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scalar multiplication with base point: s * G
|
||||
* @param {Uint8Array} s - 32-byte scalar
|
||||
* @returns {Uint8Array} 32-byte compressed public key
|
||||
*/
|
||||
export function scalarMultBase(s) {
|
||||
const scalar = scalarFromBytes(s);
|
||||
const G = pointFromXY(GX, GY);
|
||||
const result = scalarMult(G, scalar);
|
||||
return pointToBytes(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scalar multiplication with arbitrary point: s * P
|
||||
* @param {Uint8Array} s - 32-byte scalar
|
||||
* @param {Uint8Array} P - 32-byte compressed point
|
||||
* @returns {Uint8Array|null} 32-byte compressed result, or null if P is invalid
|
||||
*/
|
||||
export function scalarMultPoint(s, P) {
|
||||
const scalar = scalarFromBytes(s);
|
||||
const point = pointFromBytes(P);
|
||||
if (!point) return null;
|
||||
const result = scalarMult(point, scalar);
|
||||
return pointToBytes(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the T generator point (used in CARROT)
|
||||
* @returns {Uint8Array} 32-byte compressed T point
|
||||
*/
|
||||
export function getGeneratorT() {
|
||||
return new Uint8Array(T_BYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the G generator point (standard Ed25519 base point)
|
||||
* @returns {Uint8Array} 32-byte compressed G point
|
||||
*/
|
||||
export function getGeneratorG() {
|
||||
const G = pointFromXY(GX, GY);
|
||||
return pointToBytes(G);
|
||||
}
|
||||
|
||||
/**
|
||||
* Point addition: P + Q
|
||||
* @param {Uint8Array} P - 32-byte compressed point
|
||||
* @param {Uint8Array} Q - 32-byte compressed point
|
||||
* @returns {Uint8Array|null} 32-byte compressed result, or null if invalid
|
||||
*/
|
||||
export function pointAddCompressed(P, Q) {
|
||||
const p = pointFromBytes(P);
|
||||
const q = pointFromBytes(Q);
|
||||
if (!p || !q) return null;
|
||||
const result = pointAdd(p, q);
|
||||
return pointToBytes(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute CARROT account spend public key: K_s = k_gi * G + k_ps * T
|
||||
* @param {Uint8Array} k_gi - 32-byte generate-image key
|
||||
* @param {Uint8Array} k_ps - 32-byte prove-spend key
|
||||
* @returns {Uint8Array} 32-byte compressed public key
|
||||
*/
|
||||
export function computeCarrotSpendPubkey(k_gi, k_ps) {
|
||||
const giScalar = scalarFromBytes(k_gi);
|
||||
const psScalar = scalarFromBytes(k_ps);
|
||||
|
||||
// Get generators
|
||||
const G = pointFromXY(GX, GY);
|
||||
const T = pointFromBytes(T_BYTES);
|
||||
if (!T) throw new Error('Failed to decode T generator');
|
||||
|
||||
// K_s = k_gi * G + k_ps * T
|
||||
const giG = scalarMult(G, giScalar);
|
||||
const psT = scalarMult(T, psScalar);
|
||||
const result = pointAdd(giG, psT);
|
||||
|
||||
return pointToBytes(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute CARROT account view public key: K_v = k_vi * K_s
|
||||
* Used for subaddress derivation, NOT for main address
|
||||
* @param {Uint8Array} k_vi - 32-byte view-incoming key
|
||||
* @param {Uint8Array} K_s - 32-byte spend public key
|
||||
* @returns {Uint8Array|null} 32-byte compressed view public key, or null if K_s invalid
|
||||
*/
|
||||
export function computeCarrotAccountViewPubkey(k_vi, K_s) {
|
||||
return scalarMultPoint(k_vi, K_s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute CARROT main address view public key: K_v = k_vi * G
|
||||
* This is what goes into the main address (j=0)
|
||||
* @param {Uint8Array} k_vi - 32-byte view-incoming key
|
||||
* @returns {Uint8Array} 32-byte compressed view public key
|
||||
*/
|
||||
export function computeCarrotMainAddressViewPubkey(k_vi) {
|
||||
return scalarMultBase(k_vi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress a public key (ge_frombytes_vartime equivalent)
|
||||
* @param {Uint8Array} pk - 32-byte compressed public key
|
||||
* @returns {Object|null} Point or null if invalid
|
||||
*/
|
||||
export { pointFromBytes };
|
||||
|
||||
/**
|
||||
* Compress a point to bytes (ge_tobytes equivalent)
|
||||
* @param {Object} p - Point {X, Y, Z, T}
|
||||
* @returns {Uint8Array} 32-byte compressed point
|
||||
*/
|
||||
export { pointToBytes };
|
||||
|
||||
/**
|
||||
* Double scalar multiplication: aP + bG
|
||||
* @param {Uint8Array} a - 32-byte scalar
|
||||
* @param {Object} P - Point
|
||||
* @param {Uint8Array} b - 32-byte scalar
|
||||
* @returns {Uint8Array} 32-byte compressed result
|
||||
*/
|
||||
export function doubleScalarMultBase(a, P, b) {
|
||||
const aScalar = scalarFromBytes(a);
|
||||
const bScalar = scalarFromBytes(b);
|
||||
const G = pointFromXY(GX, GY);
|
||||
|
||||
// Compute a*P + b*G
|
||||
const aP = scalarMult(P, aScalar);
|
||||
const bG = scalarMult(G, bScalar);
|
||||
const result = pointAdd(aP, bG);
|
||||
|
||||
return pointToBytes(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a point is the identity (neutral element)
|
||||
* @param {Uint8Array} p - 32-byte compressed point
|
||||
* @returns {boolean} true if identity
|
||||
*/
|
||||
export function isIdentity(p) {
|
||||
// Identity point encodes as y=1, x=0 -> bytes = [1, 0, 0, ..., 0]
|
||||
if (p[0] !== 1) return false;
|
||||
for (let i = 1; i < 32; i++) {
|
||||
if (p[i] !== 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Debug functions
|
||||
export function testDouble() {
|
||||
const G = pointFromXY(GX, GY);
|
||||
const G2 = pointAdd(G, G);
|
||||
return pointToBytes(G2);
|
||||
}
|
||||
|
||||
export function getBasePoint() {
|
||||
const G = pointFromXY(GX, GY);
|
||||
return pointToBytes(G);
|
||||
}
|
||||
|
||||
export function test2G() {
|
||||
const scalar = new Uint8Array(32);
|
||||
scalar[0] = 2;
|
||||
return scalarMultBase(scalar);
|
||||
}
|
||||
|
||||
// Debug: test G + identity = G
|
||||
export function testIdentity() {
|
||||
const G = pointFromXY(GX, GY);
|
||||
const identity = pointZero();
|
||||
const result = pointAdd(G, identity);
|
||||
return pointToBytes(result);
|
||||
}
|
||||
|
||||
// Debug: get affine coordinates of 2G
|
||||
export function get2GAffine() {
|
||||
const G = pointFromXY(GX, GY);
|
||||
const G2 = pointAdd(G, G);
|
||||
const zi = feInv(G2.Z);
|
||||
const x = feMul(G2.X, zi);
|
||||
const y = feMul(G2.Y, zi);
|
||||
return { x: x.toString(), y: y.toString() };
|
||||
}
|
||||
|
||||
// Debug: check if point (x,y) is on the curve: -x² + y² = 1 + d*x²*y²
|
||||
export function isOnCurve(x, y) {
|
||||
const x2 = feSq(x);
|
||||
const y2 = feSq(y);
|
||||
const lhs = feSub(y2, x2); // -x² + y² = y² - x²
|
||||
const rhs = feAdd(1n, feMul(D, feMul(x2, y2))); // 1 + d*x²*y²
|
||||
return lhs === rhs;
|
||||
}
|
||||
|
||||
// Debug: show curve equation values for G
|
||||
export function debugCurveEquation() {
|
||||
const x2 = feSq(GX);
|
||||
const y2 = feSq(GY);
|
||||
const lhs = feSub(y2, x2);
|
||||
const dxy2 = feMul(D, feMul(x2, y2));
|
||||
const rhs = feAdd(1n, dxy2);
|
||||
|
||||
return {
|
||||
GX: GX.toString(),
|
||||
GY: GY.toString(),
|
||||
D: D.toString(),
|
||||
P: P.toString(),
|
||||
x2: x2.toString(),
|
||||
y2: y2.toString(),
|
||||
lhs: lhs.toString(), // y² - x²
|
||||
dxy2: dxy2.toString(), // d*x²*y²
|
||||
rhs: rhs.toString(), // 1 + d*x²*y²
|
||||
match: lhs === rhs,
|
||||
diff: (lhs > rhs ? lhs - rhs : rhs - lhs).toString()
|
||||
};
|
||||
}
|
||||
|
||||
// Debug: verify G is on curve
|
||||
export function checkG() {
|
||||
return isOnCurve(GX, GY);
|
||||
}
|
||||
|
||||
// Debug: verify 2G is on curve
|
||||
export function check2G() {
|
||||
const G = pointFromXY(GX, GY);
|
||||
const G2 = pointAdd(G, G);
|
||||
const zi = feInv(G2.Z);
|
||||
const x = feMul(G2.X, zi);
|
||||
const y = feMul(G2.Y, zi);
|
||||
return isOnCurve(x, y);
|
||||
}
|
||||
|
||||
// Debug: compare doubling methods
|
||||
export function compare2GMethods() {
|
||||
const G = pointFromXY(GX, GY);
|
||||
|
||||
// Method 1: Using pointDouble (Salvium ge_p2_dbl)
|
||||
const G2_double = pointDouble(G);
|
||||
const zi1 = feInv(G2_double.Z);
|
||||
const x1 = feMul(G2_double.X, zi1);
|
||||
const y1 = feMul(G2_double.Y, zi1);
|
||||
|
||||
// Method 2: Using unified add formula
|
||||
const G2_add = pointAddUnified(G, G);
|
||||
const zi2 = feInv(G2_add.Z);
|
||||
const x2 = feMul(G2_add.X, zi2);
|
||||
const y2 = feMul(G2_add.Y, zi2);
|
||||
|
||||
return {
|
||||
doubleX: x1.toString(),
|
||||
doubleY: y1.toString(),
|
||||
addX: x2.toString(),
|
||||
addY: y2.toString(),
|
||||
match: x1 === x2 && y1 === y2,
|
||||
onCurve1: isOnCurve(x1, y1),
|
||||
onCurve2: isOnCurve(x2, y2)
|
||||
};
|
||||
}
|
||||
|
||||
// Debug: decode expected 2G from hex and show coordinates
|
||||
export function decodeExpected2G() {
|
||||
// Expected encoding: c9a3f86aae465f0e56513864510f3997561fa2c9e85ea21dc2292309f3cd6022
|
||||
const hex = 'c9a3f86aae465f0e56513864510f3997561fa2c9e85ea21dc2292309f3cd6022';
|
||||
const bytes = new Uint8Array(32);
|
||||
for (let i = 0; i < 32; i++) {
|
||||
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
||||
}
|
||||
|
||||
// Decode using our pointFromBytes
|
||||
const point = pointFromBytes(bytes);
|
||||
if (!point) return { error: 'Failed to decode point' };
|
||||
|
||||
const zi = feInv(point.Z);
|
||||
const x = feMul(point.X, zi);
|
||||
const y = feMul(point.Y, zi);
|
||||
|
||||
return {
|
||||
x: x.toString(),
|
||||
y: y.toString(),
|
||||
onCurve: isOnCurve(x, y)
|
||||
};
|
||||
}
|
||||
|
||||
// Debug: verify field operations are correct
|
||||
export function testFieldOps() {
|
||||
// Test that (a + b) mod p works
|
||||
const a = P - 1n; // largest value
|
||||
const b = 2n;
|
||||
const sum = feAdd(a, b); // should be 1
|
||||
|
||||
// Test subtraction
|
||||
const diff = feSub(2n, 5n); // should be p - 3
|
||||
|
||||
// Test multiplication
|
||||
const prod = feMul(GX, GY);
|
||||
|
||||
// Test identity point (0, 1) - must be on ANY Edwards curve
|
||||
const identityOnCurve = isOnCurve(0n, 1n);
|
||||
|
||||
// Verify y = 4/5 mod p for base point
|
||||
// 5 * GY mod P should equal 4
|
||||
const fiveTimesY = feMul(5n, GY);
|
||||
const yIsFourFifths = fiveTimesY === 4n;
|
||||
|
||||
return {
|
||||
addTest: sum === 1n,
|
||||
subTest: diff === P - 3n,
|
||||
prodNonZero: prod > 0n && prod < P,
|
||||
gxLessThanP: GX < P,
|
||||
gyLessThanP: GY < P,
|
||||
dLessThanP: D < P,
|
||||
identityOnCurve: identityOnCurve,
|
||||
yIsFourFifths: yIsFourFifths,
|
||||
fiveTimesY: fiveTimesY.toString()
|
||||
};
|
||||
}
|
||||
|
||||
// Convert 10-limb ref10 representation to BigInt
|
||||
// Limbs alternate between 26 and 25 bits: 26,25,26,25,26,25,26,25,26,25
|
||||
function limbsToFe(limbs) {
|
||||
const shifts = [0n, 26n, 51n, 77n, 102n, 128n, 153n, 179n, 204n, 230n];
|
||||
let result = 0n;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
result += BigInt(limbs[i]) << shifts[i];
|
||||
}
|
||||
// Reduce mod P and handle negatives
|
||||
result = ((result % P) + P) % P;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Debug: verify our D constant matches Salvium's d.h
|
||||
export function verifyDConstant() {
|
||||
// From Salvium d.h:
|
||||
const dLimbs = [-10913610, 13857413, -15372611, 6949391, 114729, -8787816, -6275908, -3247719, -18696448, -12055116];
|
||||
const dFromLimbs = limbsToFe(dLimbs);
|
||||
|
||||
// From Salvium d2.h (2*d):
|
||||
const d2Limbs = [-21827239, -5839606, -30745221, 13898782, 229458, 15978800, -12551817, -6495438, 29715968, 9444199];
|
||||
const d2FromLimbs = limbsToFe(d2Limbs);
|
||||
|
||||
return {
|
||||
ourD: D.toString(),
|
||||
salviumD: dFromLimbs.toString(),
|
||||
dMatch: D === dFromLimbs,
|
||||
ourD2: D2.toString(),
|
||||
salviumD2: d2FromLimbs.toString(),
|
||||
d2Match: D2 === d2FromLimbs
|
||||
};
|
||||
}
|
||||
|
||||
// Compute x from y for twisted Edwards curve: -x² + y² = 1 + d*x²*y²
|
||||
// x² = (y² - 1) / (1 + d*y²)
|
||||
export function computeXFromY(y) {
|
||||
const y2 = feSq(y);
|
||||
const num = feSub(y2, 1n); // y² - 1
|
||||
const den = feAdd(1n, feMul(D, y2)); // 1 + d*y²
|
||||
const x2 = feMul(num, feInv(den)); // (y² - 1) / (1 + d*y²)
|
||||
|
||||
// Get square root
|
||||
const x = feSqrt(x2);
|
||||
const xNeg = x ? feNeg(x) : null;
|
||||
|
||||
// Verify x² = x2
|
||||
const xSquaredCheck = x ? feSq(x) === x2 : null;
|
||||
|
||||
// Check if computed point is on curve
|
||||
const computedOnCurve = x ? isOnCurve(x, y) : null;
|
||||
const computedNegOnCurve = xNeg ? isOnCurve(xNeg, y) : null;
|
||||
|
||||
// Check if our GX satisfies x² = x2
|
||||
const gxSquared = feSq(GX);
|
||||
const gxSatisfiesX2 = gxSquared === x2;
|
||||
|
||||
return {
|
||||
x2: x2.toString(),
|
||||
x: x ? x.toString() : 'no sqrt exists',
|
||||
xNeg: xNeg ? xNeg.toString() : null,
|
||||
xSquaredCheck: xSquaredCheck,
|
||||
computedOnCurve: computedOnCurve,
|
||||
computedNegOnCurve: computedNegOnCurve,
|
||||
ourGX: GX.toString(),
|
||||
gxSquared: gxSquared.toString(),
|
||||
gxSatisfiesX2: gxSatisfiesX2,
|
||||
xMatchesGX: x === GX,
|
||||
xNegMatchesGX: xNeg === GX
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
scalarCheck,
|
||||
scalarIsNonzero,
|
||||
scalarSub,
|
||||
scalarMultBase,
|
||||
scalarMultPoint,
|
||||
pointFromBytes,
|
||||
pointToBytes,
|
||||
pointAddCompressed,
|
||||
doubleScalarMultBase,
|
||||
isIdentity,
|
||||
getGeneratorG,
|
||||
getGeneratorT,
|
||||
computeCarrotSpendPubkey,
|
||||
computeCarrotAccountViewPubkey,
|
||||
computeCarrotMainAddressViewPubkey,
|
||||
testDouble,
|
||||
getBasePoint,
|
||||
test2G,
|
||||
testIdentity,
|
||||
get2GAffine
|
||||
};
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* salvium-js - JavaScript library for Salvium cryptocurrency
|
||||
*
|
||||
* Features:
|
||||
* - Address validation and parsing for all 18 address types
|
||||
* - Support for Legacy (CryptoNote) and CARROT address formats
|
||||
* - Mainnet, Testnet, and Stagenet support
|
||||
* - Base58 encoding/decoding (Monero variant)
|
||||
* - Keccak-256 hashing
|
||||
* - Message signature verification (V1 and V2)
|
||||
*
|
||||
* @module salvium-js
|
||||
*/
|
||||
|
||||
// Re-export everything from submodules
|
||||
export * from './constants.js';
|
||||
export * from './keccak.js';
|
||||
export * from './base58.js';
|
||||
export * from './address.js';
|
||||
export * from './signature.js';
|
||||
export * from './blake2b.js';
|
||||
export * from './carrot.js';
|
||||
export {
|
||||
scalarMultBase,
|
||||
scalarMultPoint,
|
||||
pointAddCompressed,
|
||||
getGeneratorG,
|
||||
getGeneratorT,
|
||||
computeCarrotSpendPubkey,
|
||||
computeCarrotAccountViewPubkey,
|
||||
computeCarrotMainAddressViewPubkey,
|
||||
testDouble,
|
||||
getBasePoint,
|
||||
test2G,
|
||||
testIdentity,
|
||||
get2GAffine,
|
||||
isOnCurve,
|
||||
checkG,
|
||||
check2G,
|
||||
compare2GMethods,
|
||||
decodeExpected2G,
|
||||
testFieldOps,
|
||||
debugCurveEquation,
|
||||
verifyDConstant,
|
||||
computeXFromY
|
||||
} from './ed25519.js';
|
||||
|
||||
// Import named exports for combined API object
|
||||
import {
|
||||
NETWORK,
|
||||
ADDRESS_TYPE,
|
||||
ADDRESS_FORMAT,
|
||||
PREFIXES
|
||||
} from './constants.js';
|
||||
|
||||
import {
|
||||
keccak256,
|
||||
keccak256Hex,
|
||||
cnFastHash
|
||||
} from './keccak.js';
|
||||
|
||||
import {
|
||||
encode,
|
||||
decode,
|
||||
encodeAddress,
|
||||
decodeAddress
|
||||
} from './base58.js';
|
||||
|
||||
import {
|
||||
parseAddress,
|
||||
isValidAddress,
|
||||
isMainnet,
|
||||
isTestnet,
|
||||
isStagenet,
|
||||
isCarrot,
|
||||
isLegacy,
|
||||
isStandard,
|
||||
isIntegrated,
|
||||
isSubaddress,
|
||||
getSpendPublicKey,
|
||||
getViewPublicKey,
|
||||
getPaymentId,
|
||||
createAddress,
|
||||
toIntegratedAddress,
|
||||
toStandardAddress,
|
||||
describeAddress,
|
||||
bytesToHex,
|
||||
hexToBytes
|
||||
} from './address.js';
|
||||
|
||||
import {
|
||||
verifySignature,
|
||||
parseSignature
|
||||
} from './signature.js';
|
||||
|
||||
import {
|
||||
scalarMultBase,
|
||||
scalarMultPoint,
|
||||
pointAddCompressed,
|
||||
getGeneratorG,
|
||||
getGeneratorT,
|
||||
computeCarrotSpendPubkey,
|
||||
computeCarrotAccountViewPubkey,
|
||||
computeCarrotMainAddressViewPubkey
|
||||
} from './ed25519.js';
|
||||
|
||||
// Main API object
|
||||
const salvium = {
|
||||
// Constants
|
||||
NETWORK,
|
||||
ADDRESS_TYPE,
|
||||
ADDRESS_FORMAT,
|
||||
PREFIXES,
|
||||
|
||||
// Keccak
|
||||
keccak256,
|
||||
keccak256Hex,
|
||||
cnFastHash,
|
||||
|
||||
// Base58
|
||||
base58Encode: encode,
|
||||
base58Decode: decode,
|
||||
encodeAddress,
|
||||
decodeAddress,
|
||||
|
||||
// Address
|
||||
parseAddress,
|
||||
isValidAddress,
|
||||
isMainnet,
|
||||
isTestnet,
|
||||
isStagenet,
|
||||
isCarrot,
|
||||
isLegacy,
|
||||
isStandard,
|
||||
isIntegrated,
|
||||
isSubaddress,
|
||||
getSpendPublicKey,
|
||||
getViewPublicKey,
|
||||
getPaymentId,
|
||||
createAddress,
|
||||
toIntegratedAddress,
|
||||
toStandardAddress,
|
||||
describeAddress,
|
||||
bytesToHex,
|
||||
hexToBytes,
|
||||
|
||||
// Signatures
|
||||
verifySignature,
|
||||
parseSignature,
|
||||
|
||||
// Ed25519
|
||||
scalarMultBase,
|
||||
scalarMultPoint,
|
||||
pointAddCompressed,
|
||||
getGeneratorG,
|
||||
getGeneratorT,
|
||||
|
||||
// CARROT
|
||||
computeCarrotSpendPubkey,
|
||||
computeCarrotAccountViewPubkey,
|
||||
computeCarrotMainAddressViewPubkey
|
||||
};
|
||||
|
||||
export default salvium;
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Keccak-256 Hash Implementation
|
||||
*
|
||||
* Based on js-sha3 by Chen Yi-Cyuan (MIT License)
|
||||
* https://github.com/nicolo-ribaudo/js-sha3
|
||||
*
|
||||
* IMPORTANT: This is Keccak-256, NOT SHA3-256!
|
||||
* Monero/Salvium use the pre-NIST version with 0x01 padding.
|
||||
*/
|
||||
|
||||
const KECCAK_PADDING = [1, 256, 65536, 16777216];
|
||||
const SHIFT = [0, 8, 16, 24];
|
||||
const RC = [
|
||||
1, 0, 32898, 0, 32906, 2147483648, 2147516416, 2147483648,
|
||||
32907, 0, 2147483649, 0, 2147516545, 2147483648, 32777, 2147483648,
|
||||
138, 0, 136, 0, 2147516425, 0, 2147483658, 0,
|
||||
2147516555, 0, 139, 2147483648, 32905, 2147483648, 32771, 2147483648,
|
||||
32770, 2147483648, 128, 2147483648, 32778, 0, 2147483658, 2147483648,
|
||||
2147516545, 2147483648, 32896, 2147483648, 2147483649, 0, 2147516424, 2147483648
|
||||
];
|
||||
|
||||
function keccak(bits) {
|
||||
const outputBits = bits;
|
||||
const blockSize = (1600 - bits * 2) >> 5;
|
||||
const byteCount = blockSize << 2;
|
||||
const outputBytes = outputBits >> 3;
|
||||
const padding = KECCAK_PADDING;
|
||||
|
||||
return function(message) {
|
||||
let input;
|
||||
if (typeof message === 'string') {
|
||||
input = new TextEncoder().encode(message);
|
||||
} else if (message instanceof Uint8Array) {
|
||||
input = message;
|
||||
} else if (Array.isArray(message)) {
|
||||
input = new Uint8Array(message);
|
||||
} else {
|
||||
throw new Error('Invalid input type');
|
||||
}
|
||||
|
||||
const blocks = [];
|
||||
const state = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
state[i] = 0;
|
||||
}
|
||||
|
||||
let blockCount = blockSize;
|
||||
let byteCountInc = byteCount;
|
||||
let i, code, end = false, index = 0, block = 0;
|
||||
let start = 0, length = input.length;
|
||||
|
||||
for (i = 0; i < blockCount; i++) {
|
||||
blocks[i] = 0;
|
||||
}
|
||||
|
||||
while (!end) {
|
||||
block = 0;
|
||||
for (i = start; index < length && i < byteCountInc; ++index) {
|
||||
code = input[index];
|
||||
block |= code << SHIFT[i++ & 3];
|
||||
if (i === byteCountInc) {
|
||||
blocks[(i >> 2) - 1] = block;
|
||||
block = 0;
|
||||
} else if ((i & 3) === 0) {
|
||||
blocks[(i >> 2) - 1] = block;
|
||||
block = 0;
|
||||
}
|
||||
}
|
||||
start = i - byteCountInc;
|
||||
|
||||
if (index === length) {
|
||||
// Store remaining partial word before adding padding
|
||||
blocks[i >> 2] |= block;
|
||||
blocks[i >> 2] |= padding[i & 3];
|
||||
++index;
|
||||
}
|
||||
if (index > length && i < byteCountInc) {
|
||||
blocks[blockCount - 1] |= 0x80000000;
|
||||
end = true;
|
||||
}
|
||||
|
||||
for (i = 0; i < blockCount; i++) {
|
||||
state[i] ^= blocks[i];
|
||||
blocks[i] = 0;
|
||||
}
|
||||
f(state);
|
||||
}
|
||||
|
||||
// Output - use >>> for unsigned right shift to avoid sign extension issues
|
||||
const output = new Uint8Array(outputBytes);
|
||||
let outputIndex = 0;
|
||||
let stateIndex = 0;
|
||||
|
||||
while (outputIndex < outputBytes) {
|
||||
const s = state[stateIndex++];
|
||||
output[outputIndex++] = s & 0xff;
|
||||
if (outputIndex < outputBytes) output[outputIndex++] = (s >>> 8) & 0xff;
|
||||
if (outputIndex < outputBytes) output[outputIndex++] = (s >>> 16) & 0xff;
|
||||
if (outputIndex < outputBytes) output[outputIndex++] = (s >>> 24) & 0xff;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
}
|
||||
|
||||
function f(s) {
|
||||
let h, l, n, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9,
|
||||
b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15,
|
||||
b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29,
|
||||
b30, b31, b32, b33, b34, b35, b36, b37, b38, b39, b40, b41, b42, b43,
|
||||
b44, b45, b46, b47, b48, b49;
|
||||
|
||||
for (n = 0; n < 48; n += 2) {
|
||||
c0 = s[0] ^ s[10] ^ s[20] ^ s[30] ^ s[40];
|
||||
c1 = s[1] ^ s[11] ^ s[21] ^ s[31] ^ s[41];
|
||||
c2 = s[2] ^ s[12] ^ s[22] ^ s[32] ^ s[42];
|
||||
c3 = s[3] ^ s[13] ^ s[23] ^ s[33] ^ s[43];
|
||||
c4 = s[4] ^ s[14] ^ s[24] ^ s[34] ^ s[44];
|
||||
c5 = s[5] ^ s[15] ^ s[25] ^ s[35] ^ s[45];
|
||||
c6 = s[6] ^ s[16] ^ s[26] ^ s[36] ^ s[46];
|
||||
c7 = s[7] ^ s[17] ^ s[27] ^ s[37] ^ s[47];
|
||||
c8 = s[8] ^ s[18] ^ s[28] ^ s[38] ^ s[48];
|
||||
c9 = s[9] ^ s[19] ^ s[29] ^ s[39] ^ s[49];
|
||||
|
||||
h = c8 ^ ((c2 << 1) | (c3 >>> 31));
|
||||
l = c9 ^ ((c3 << 1) | (c2 >>> 31));
|
||||
s[0] ^= h; s[1] ^= l; s[10] ^= h; s[11] ^= l;
|
||||
s[20] ^= h; s[21] ^= l; s[30] ^= h; s[31] ^= l; s[40] ^= h; s[41] ^= l;
|
||||
|
||||
h = c0 ^ ((c4 << 1) | (c5 >>> 31));
|
||||
l = c1 ^ ((c5 << 1) | (c4 >>> 31));
|
||||
s[2] ^= h; s[3] ^= l; s[12] ^= h; s[13] ^= l;
|
||||
s[22] ^= h; s[23] ^= l; s[32] ^= h; s[33] ^= l; s[42] ^= h; s[43] ^= l;
|
||||
|
||||
h = c2 ^ ((c6 << 1) | (c7 >>> 31));
|
||||
l = c3 ^ ((c7 << 1) | (c6 >>> 31));
|
||||
s[4] ^= h; s[5] ^= l; s[14] ^= h; s[15] ^= l;
|
||||
s[24] ^= h; s[25] ^= l; s[34] ^= h; s[35] ^= l; s[44] ^= h; s[45] ^= l;
|
||||
|
||||
h = c4 ^ ((c8 << 1) | (c9 >>> 31));
|
||||
l = c5 ^ ((c9 << 1) | (c8 >>> 31));
|
||||
s[6] ^= h; s[7] ^= l; s[16] ^= h; s[17] ^= l;
|
||||
s[26] ^= h; s[27] ^= l; s[36] ^= h; s[37] ^= l; s[46] ^= h; s[47] ^= l;
|
||||
|
||||
h = c6 ^ ((c0 << 1) | (c1 >>> 31));
|
||||
l = c7 ^ ((c1 << 1) | (c0 >>> 31));
|
||||
s[8] ^= h; s[9] ^= l; s[18] ^= h; s[19] ^= l;
|
||||
s[28] ^= h; s[29] ^= l; s[38] ^= h; s[39] ^= l; s[48] ^= h; s[49] ^= l;
|
||||
|
||||
b0 = s[0]; b1 = s[1];
|
||||
b32 = (s[11] << 4) | (s[10] >>> 28); b33 = (s[10] << 4) | (s[11] >>> 28);
|
||||
b14 = (s[20] << 3) | (s[21] >>> 29); b15 = (s[21] << 3) | (s[20] >>> 29);
|
||||
b46 = (s[31] << 9) | (s[30] >>> 23); b47 = (s[30] << 9) | (s[31] >>> 23);
|
||||
b28 = (s[40] << 18) | (s[41] >>> 14); b29 = (s[41] << 18) | (s[40] >>> 14);
|
||||
b20 = (s[2] << 1) | (s[3] >>> 31); b21 = (s[3] << 1) | (s[2] >>> 31);
|
||||
b2 = (s[13] << 12) | (s[12] >>> 20); b3 = (s[12] << 12) | (s[13] >>> 20);
|
||||
b34 = (s[22] << 10) | (s[23] >>> 22); b35 = (s[23] << 10) | (s[22] >>> 22);
|
||||
b16 = (s[33] << 13) | (s[32] >>> 19); b17 = (s[32] << 13) | (s[33] >>> 19);
|
||||
b48 = (s[42] << 2) | (s[43] >>> 30); b49 = (s[43] << 2) | (s[42] >>> 30);
|
||||
b40 = (s[5] << 30) | (s[4] >>> 2); b41 = (s[4] << 30) | (s[5] >>> 2);
|
||||
b22 = (s[14] << 6) | (s[15] >>> 26); b23 = (s[15] << 6) | (s[14] >>> 26);
|
||||
b4 = (s[25] << 11) | (s[24] >>> 21); b5 = (s[24] << 11) | (s[25] >>> 21);
|
||||
b36 = (s[34] << 15) | (s[35] >>> 17); b37 = (s[35] << 15) | (s[34] >>> 17);
|
||||
b18 = (s[45] << 29) | (s[44] >>> 3); b19 = (s[44] << 29) | (s[45] >>> 3);
|
||||
b10 = (s[6] << 28) | (s[7] >>> 4); b11 = (s[7] << 28) | (s[6] >>> 4);
|
||||
b42 = (s[17] << 23) | (s[16] >>> 9); b43 = (s[16] << 23) | (s[17] >>> 9);
|
||||
b24 = (s[26] << 25) | (s[27] >>> 7); b25 = (s[27] << 25) | (s[26] >>> 7);
|
||||
b6 = (s[36] << 21) | (s[37] >>> 11); b7 = (s[37] << 21) | (s[36] >>> 11);
|
||||
b38 = (s[47] << 24) | (s[46] >>> 8); b39 = (s[46] << 24) | (s[47] >>> 8);
|
||||
b30 = (s[8] << 27) | (s[9] >>> 5); b31 = (s[9] << 27) | (s[8] >>> 5);
|
||||
b12 = (s[18] << 20) | (s[19] >>> 12); b13 = (s[19] << 20) | (s[18] >>> 12);
|
||||
b44 = (s[29] << 7) | (s[28] >>> 25); b45 = (s[28] << 7) | (s[29] >>> 25);
|
||||
b26 = (s[38] << 8) | (s[39] >>> 24); b27 = (s[39] << 8) | (s[38] >>> 24);
|
||||
b8 = (s[48] << 14) | (s[49] >>> 18); b9 = (s[49] << 14) | (s[48] >>> 18);
|
||||
|
||||
s[0] = b0 ^ (~b2 & b4); s[1] = b1 ^ (~b3 & b5);
|
||||
s[10] = b10 ^ (~b12 & b14); s[11] = b11 ^ (~b13 & b15);
|
||||
s[20] = b20 ^ (~b22 & b24); s[21] = b21 ^ (~b23 & b25);
|
||||
s[30] = b30 ^ (~b32 & b34); s[31] = b31 ^ (~b33 & b35);
|
||||
s[40] = b40 ^ (~b42 & b44); s[41] = b41 ^ (~b43 & b45);
|
||||
s[2] = b2 ^ (~b4 & b6); s[3] = b3 ^ (~b5 & b7);
|
||||
s[12] = b12 ^ (~b14 & b16); s[13] = b13 ^ (~b15 & b17);
|
||||
s[22] = b22 ^ (~b24 & b26); s[23] = b23 ^ (~b25 & b27);
|
||||
s[32] = b32 ^ (~b34 & b36); s[33] = b33 ^ (~b35 & b37);
|
||||
s[42] = b42 ^ (~b44 & b46); s[43] = b43 ^ (~b45 & b47);
|
||||
s[4] = b4 ^ (~b6 & b8); s[5] = b5 ^ (~b7 & b9);
|
||||
s[14] = b14 ^ (~b16 & b18); s[15] = b15 ^ (~b17 & b19);
|
||||
s[24] = b24 ^ (~b26 & b28); s[25] = b25 ^ (~b27 & b29);
|
||||
s[34] = b34 ^ (~b36 & b38); s[35] = b35 ^ (~b37 & b39);
|
||||
s[44] = b44 ^ (~b46 & b48); s[45] = b45 ^ (~b47 & b49);
|
||||
s[6] = b6 ^ (~b8 & b0); s[7] = b7 ^ (~b9 & b1);
|
||||
s[16] = b16 ^ (~b18 & b10); s[17] = b17 ^ (~b19 & b11);
|
||||
s[26] = b26 ^ (~b28 & b20); s[27] = b27 ^ (~b29 & b21);
|
||||
s[36] = b36 ^ (~b38 & b30); s[37] = b37 ^ (~b39 & b31);
|
||||
s[46] = b46 ^ (~b48 & b40); s[47] = b47 ^ (~b49 & b41);
|
||||
s[8] = b8 ^ (~b0 & b2); s[9] = b9 ^ (~b1 & b3);
|
||||
s[18] = b18 ^ (~b10 & b12); s[19] = b19 ^ (~b11 & b13);
|
||||
s[28] = b28 ^ (~b20 & b22); s[29] = b29 ^ (~b21 & b23);
|
||||
s[38] = b38 ^ (~b30 & b32); s[39] = b39 ^ (~b31 & b33);
|
||||
s[48] = b48 ^ (~b40 & b42); s[49] = b49 ^ (~b41 & b43);
|
||||
|
||||
s[0] ^= RC[n]; s[1] ^= RC[n + 1];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keccak-256 hash function
|
||||
* @param {Uint8Array|Array|string} input - Data to hash
|
||||
* @returns {Uint8Array} - 32-byte hash
|
||||
*/
|
||||
export const keccak256 = keccak(256);
|
||||
|
||||
/**
|
||||
* Compute Keccak-256 and return as hex string
|
||||
* @param {Uint8Array|Array|string} input - Data to hash
|
||||
* @returns {string} - 64-character hex string
|
||||
*/
|
||||
export function keccak256Hex(input) {
|
||||
const hash = keccak256(input);
|
||||
return Array.from(hash).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* cn_fast_hash - Salvium/Monero's name for Keccak-256
|
||||
* @param {Uint8Array|Array|string} input - Data to hash
|
||||
* @returns {Uint8Array} - 32-byte hash
|
||||
*/
|
||||
export const cnFastHash = keccak256;
|
||||
|
||||
export default { keccak256, keccak256Hex, cnFastHash };
|
||||
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* Salvium Message Signature Verification
|
||||
*
|
||||
* Supports both V1 (legacy) and V2 (domain-separated) signatures.
|
||||
*
|
||||
* Signature format: "SigV1" or "SigV2" + Base58(signature_bytes)
|
||||
* where signature_bytes = c (32 bytes) + r (32 bytes) + sign_mask (1 byte) = 65 bytes
|
||||
*
|
||||
* V1: hash = Keccak256(message)
|
||||
* V2: hash = Keccak256(domain_separator + spend_key + view_key + mode + varint(len) + message)
|
||||
*/
|
||||
|
||||
import { keccak256 } from './keccak.js';
|
||||
import { decode } from './base58.js';
|
||||
import { parseAddress, hexToBytes } from './address.js';
|
||||
import {
|
||||
scalarCheck,
|
||||
scalarIsNonzero,
|
||||
scalarSub,
|
||||
pointFromBytes,
|
||||
doubleScalarMultBase,
|
||||
isIdentity
|
||||
} from './ed25519.js';
|
||||
|
||||
// Domain separator for V2 signatures (includes null terminator)
|
||||
const HASH_KEY_MESSAGE_SIGNING = new TextEncoder().encode('MoneroMessageSignature\0');
|
||||
|
||||
/**
|
||||
* Encode a number as a varint (variable-length integer)
|
||||
* @param {number} n - Number to encode
|
||||
* @returns {Uint8Array} Varint bytes
|
||||
*/
|
||||
function encodeVarint(n) {
|
||||
const bytes = [];
|
||||
while (n >= 0x80) {
|
||||
bytes.push((n & 0x7f) | 0x80);
|
||||
n >>>= 7;
|
||||
}
|
||||
bytes.push(n & 0x7f);
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute V1 message hash (simple Keccak256)
|
||||
* @param {string} message - The message
|
||||
* @returns {Uint8Array} 32-byte hash
|
||||
*/
|
||||
function getMessageHashV1(message) {
|
||||
const messageBytes = new TextEncoder().encode(message);
|
||||
return keccak256(messageBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute V2 message hash with domain separation
|
||||
* @param {string} message - The message
|
||||
* @param {Uint8Array} spendKey - 32-byte spend public key
|
||||
* @param {Uint8Array} viewKey - 32-byte view public key
|
||||
* @param {number} mode - 0 for spend key, 1 for view key
|
||||
* @returns {Uint8Array} 32-byte hash
|
||||
*/
|
||||
function getMessageHashV2(message, spendKey, viewKey, mode) {
|
||||
const messageBytes = new TextEncoder().encode(message);
|
||||
const lenVarint = encodeVarint(messageBytes.length);
|
||||
|
||||
// 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);
|
||||
|
||||
let offset = 0;
|
||||
data.set(HASH_KEY_MESSAGE_SIGNING, offset);
|
||||
offset += HASH_KEY_MESSAGE_SIGNING.length;
|
||||
|
||||
data.set(spendKey, offset);
|
||||
offset += 32;
|
||||
|
||||
data.set(viewKey, offset);
|
||||
offset += 32;
|
||||
|
||||
data[offset++] = mode;
|
||||
|
||||
data.set(lenVarint, offset);
|
||||
offset += lenVarint.length;
|
||||
|
||||
data.set(messageBytes, offset);
|
||||
|
||||
return keccak256(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform Schnorr signature verification
|
||||
*
|
||||
* Verifies: R' = r*G + c*P, then checks hash(prefix || key || R') == c
|
||||
*
|
||||
* @param {Uint8Array} hash - 32-byte message hash
|
||||
* @param {Uint8Array} publicKey - 32-byte public key
|
||||
* @param {Uint8Array} sigC - 32-byte signature c component
|
||||
* @param {Uint8Array} sigR - 32-byte signature r component
|
||||
* @returns {boolean} true if signature is valid
|
||||
*/
|
||||
function checkSignature(hash, publicKey, sigC, sigR) {
|
||||
// Validate scalars
|
||||
if (!scalarCheck(sigC) || !scalarCheck(sigR) || !scalarIsNonzero(sigC)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decompress public key
|
||||
const P = pointFromBytes(publicKey);
|
||||
if (!P) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compute R' = c*P + r*G using double scalar multiplication
|
||||
const RBytes = doubleScalarMultBase(sigC, P, sigR);
|
||||
|
||||
// Check R' is not identity
|
||||
if (isIdentity(RBytes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recompute challenge: c' = H(hash || publicKey || R')
|
||||
const buf = new Uint8Array(32 + 32 + 32);
|
||||
buf.set(hash, 0);
|
||||
buf.set(publicKey, 32);
|
||||
buf.set(RBytes, 64);
|
||||
|
||||
const cPrime = keccak256(buf);
|
||||
|
||||
// Reduce c' mod L
|
||||
const cPrimeReduced = new Uint8Array(32);
|
||||
reduceScalar32(cPrimeReduced, cPrime);
|
||||
|
||||
// Check c' == c
|
||||
const diff = new Uint8Array(32);
|
||||
scalarSub(diff, cPrimeReduced, sigC);
|
||||
|
||||
return !scalarIsNonzero(diff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce a 32-byte value modulo L (simplified for hash output)
|
||||
* @param {Uint8Array} r - 32-byte output
|
||||
* @param {Uint8Array} x - 32-byte input
|
||||
*/
|
||||
function reduceScalar32(r, x) {
|
||||
// For a 32-byte input, just copy and rely on the fact that
|
||||
// Keccak output is uniformly distributed
|
||||
// Full reduction would require 64-byte input handling
|
||||
for (let i = 0; i < 32; i++) r[i] = x[i];
|
||||
|
||||
// Simple modular reduction for values that might exceed L
|
||||
const L = new Uint8Array([
|
||||
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,
|
||||
0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10
|
||||
]);
|
||||
|
||||
// Compare r with L and subtract if r >= L
|
||||
let borrow = 0;
|
||||
for (let i = 31; i >= 0; i--) {
|
||||
if (r[i] > L[i]) {
|
||||
// r > L, need to subtract
|
||||
borrow = 0;
|
||||
for (let j = 0; j < 32; j++) {
|
||||
const diff = r[j] - L[j] - borrow;
|
||||
if (diff < 0) {
|
||||
r[j] = diff + 256;
|
||||
borrow = 1;
|
||||
} else {
|
||||
r[j] = diff;
|
||||
borrow = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
} else if (r[i] < L[i]) {
|
||||
// r < L, no reduction needed
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a Salvium message signature
|
||||
*
|
||||
* @param {string} message - The original message that was signed
|
||||
* @param {string} address - Salvium address (to extract public keys)
|
||||
* @param {string} signature - The signature string (SigV1... or SigV2...)
|
||||
* @returns {Object} Result object with:
|
||||
* - valid: boolean - whether signature is valid
|
||||
* - version: number - signature version (1 or 2)
|
||||
* - keyType: string - 'spend' or 'view' (which key was used to sign)
|
||||
* - error: string|null - error message if invalid
|
||||
*/
|
||||
export function verifySignature(message, address, signature) {
|
||||
// Parse signature header
|
||||
const isV1 = signature.startsWith('SigV1');
|
||||
const isV2 = signature.startsWith('SigV2');
|
||||
|
||||
if (!isV1 && !isV2) {
|
||||
return { valid: false, version: 0, keyType: null, error: 'Invalid signature header (expected SigV1 or SigV2)' };
|
||||
}
|
||||
|
||||
const version = isV1 ? 1 : 2;
|
||||
const headerLen = 5; // "SigV1" or "SigV2"
|
||||
|
||||
// Decode signature from Base58
|
||||
let sigBytes;
|
||||
try {
|
||||
sigBytes = decode(signature.substring(headerLen));
|
||||
} catch (e) {
|
||||
return { valid: false, version, keyType: null, error: 'Failed to decode signature Base58' };
|
||||
}
|
||||
|
||||
// Signature should be 65 bytes (c: 32, r: 32, sign_mask: 1)
|
||||
if (sigBytes.length !== 65) {
|
||||
return { valid: false, version, keyType: null, error: `Invalid signature length: expected 65, got ${sigBytes.length}` };
|
||||
}
|
||||
|
||||
const sigC = sigBytes.slice(0, 32);
|
||||
const sigR = sigBytes.slice(32, 64);
|
||||
// sign_mask (byte 64) is not used for standard message verification
|
||||
|
||||
// Parse address to get public keys
|
||||
const addrInfo = parseAddress(address);
|
||||
if (!addrInfo.valid) {
|
||||
return { valid: false, version, keyType: null, error: `Invalid address: ${addrInfo.error}` };
|
||||
}
|
||||
|
||||
const spendKey = addrInfo.spendPublicKey;
|
||||
const viewKey = addrInfo.viewPublicKey;
|
||||
|
||||
// Try verification with spend key (mode 0)
|
||||
let hash;
|
||||
if (isV1) {
|
||||
hash = getMessageHashV1(message);
|
||||
} else {
|
||||
hash = getMessageHashV2(message, spendKey, viewKey, 0);
|
||||
}
|
||||
|
||||
if (checkSignature(hash, spendKey, sigC, sigR)) {
|
||||
return { valid: true, version, keyType: 'spend', error: null };
|
||||
}
|
||||
|
||||
// Try verification with view key (mode 1)
|
||||
if (isV2) {
|
||||
hash = getMessageHashV2(message, spendKey, viewKey, 1);
|
||||
}
|
||||
// For V1, the hash is just the message hash, same for both keys
|
||||
|
||||
if (checkSignature(hash, viewKey, sigC, sigR)) {
|
||||
return { valid: true, version, keyType: 'view', error: null };
|
||||
}
|
||||
|
||||
return { valid: false, version, keyType: null, error: 'Signature verification failed' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a signature string and extract its components
|
||||
*
|
||||
* @param {string} signature - The signature string
|
||||
* @returns {Object} Parsed signature with version, c, r, signMask
|
||||
*/
|
||||
export function parseSignature(signature) {
|
||||
const isV1 = signature.startsWith('SigV1');
|
||||
const isV2 = signature.startsWith('SigV2');
|
||||
|
||||
if (!isV1 && !isV2) {
|
||||
return { valid: false, error: 'Invalid signature header' };
|
||||
}
|
||||
|
||||
const version = isV1 ? 1 : 2;
|
||||
|
||||
try {
|
||||
const sigBytes = decode(signature.substring(5));
|
||||
if (sigBytes.length !== 65) {
|
||||
return { valid: false, error: 'Invalid signature length' };
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
version,
|
||||
c: sigBytes.slice(0, 32),
|
||||
r: sigBytes.slice(32, 64),
|
||||
signMask: sigBytes[64]
|
||||
};
|
||||
} catch (e) {
|
||||
return { valid: false, error: 'Failed to decode signature' };
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
verifySignature,
|
||||
parseSignature,
|
||||
getMessageHashV1,
|
||||
getMessageHashV2
|
||||
};
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* salvium-js Test Suite
|
||||
*
|
||||
* Run with: node test/run.js
|
||||
*/
|
||||
|
||||
import salvium, {
|
||||
keccak256,
|
||||
keccak256Hex,
|
||||
encode,
|
||||
decode,
|
||||
parseAddress,
|
||||
isValidAddress,
|
||||
isMainnet,
|
||||
isCarrot,
|
||||
isLegacy,
|
||||
describeAddress,
|
||||
bytesToHex,
|
||||
hexToBytes,
|
||||
NETWORK,
|
||||
ADDRESS_FORMAT,
|
||||
ADDRESS_TYPE
|
||||
} from '../src/index.js';
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
passed++;
|
||||
} catch (e) {
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` Error: ${e.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function assertEqual(actual, expected, msg = '') {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`${msg} Expected ${expected}, got ${actual}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertArrayEqual(actual, expected, msg = '') {
|
||||
if (actual.length !== expected.length) {
|
||||
throw new Error(`${msg} Length mismatch: ${actual.length} vs ${expected.length}`);
|
||||
}
|
||||
for (let i = 0; i < actual.length; i++) {
|
||||
if (actual[i] !== expected[i]) {
|
||||
throw new Error(`${msg} Mismatch at index ${i}: ${actual[i]} vs ${expected[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== salvium-js Test Suite ===\n');
|
||||
|
||||
// Keccak-256 Tests
|
||||
console.log('Keccak-256 Tests:');
|
||||
|
||||
test('Empty string hash', () => {
|
||||
// Known Keccak-256 hash of empty string
|
||||
const hash = keccak256Hex('');
|
||||
assertEqual(hash, 'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470');
|
||||
});
|
||||
|
||||
test('Simple string hash', () => {
|
||||
// Known Keccak-256 hash of "test"
|
||||
const hash = keccak256Hex('test');
|
||||
assertEqual(hash, '9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658');
|
||||
});
|
||||
|
||||
test('Bytes input', () => {
|
||||
const input = new Uint8Array([0x74, 0x65, 0x73, 0x74]); // "test"
|
||||
const hash = keccak256Hex(input);
|
||||
assertEqual(hash, '9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658');
|
||||
});
|
||||
|
||||
// Base58 Tests
|
||||
console.log('\nBase58 Tests:');
|
||||
|
||||
test('Encode empty', () => {
|
||||
const result = encode(new Uint8Array(0));
|
||||
assertEqual(result, '');
|
||||
});
|
||||
|
||||
test('Encode/decode roundtrip', () => {
|
||||
const original = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
const encoded = encode(original);
|
||||
const decoded = decode(encoded);
|
||||
assertArrayEqual(decoded, original);
|
||||
});
|
||||
|
||||
test('Encode/decode 64 bytes (key size)', () => {
|
||||
// Simulate public keys (64 bytes)
|
||||
const original = new Uint8Array(64);
|
||||
for (let i = 0; i < 64; i++) {
|
||||
original[i] = i;
|
||||
}
|
||||
const encoded = encode(original);
|
||||
const decoded = decode(encoded);
|
||||
assertArrayEqual(decoded, original);
|
||||
});
|
||||
|
||||
// Utility Tests
|
||||
console.log('\nUtility Tests:');
|
||||
|
||||
test('bytesToHex', () => {
|
||||
const bytes = new Uint8Array([0x00, 0x01, 0x0f, 0xff]);
|
||||
assertEqual(bytesToHex(bytes), '00010fff');
|
||||
});
|
||||
|
||||
test('hexToBytes', () => {
|
||||
const hex = '00010fff';
|
||||
const bytes = hexToBytes(hex);
|
||||
assertArrayEqual(bytes, new Uint8Array([0x00, 0x01, 0x0f, 0xff]));
|
||||
});
|
||||
|
||||
test('Hex roundtrip', () => {
|
||||
const original = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
|
||||
const hex = bytesToHex(original);
|
||||
const recovered = hexToBytes(hex);
|
||||
assertArrayEqual(recovered, original);
|
||||
});
|
||||
|
||||
// Address Validation Tests (with invalid addresses)
|
||||
console.log('\nAddress Validation Tests:');
|
||||
|
||||
test('Reject empty string', () => {
|
||||
assertEqual(isValidAddress(''), false);
|
||||
});
|
||||
|
||||
test('Reject too short', () => {
|
||||
assertEqual(isValidAddress('SaLv123'), false);
|
||||
});
|
||||
|
||||
test('Reject invalid characters', () => {
|
||||
// 0, O, I, l are not in Base58 alphabet
|
||||
assertEqual(isValidAddress('SaLv0OIl' + 'x'.repeat(90)), false);
|
||||
});
|
||||
|
||||
test('Reject invalid checksum', () => {
|
||||
// A valid-looking address with wrong checksum should fail
|
||||
const fakeAddr = '1'.repeat(95);
|
||||
assertEqual(isValidAddress(fakeAddr), false);
|
||||
});
|
||||
|
||||
// Constants Tests
|
||||
console.log('\nConstants Tests:');
|
||||
|
||||
test('Network constants exist', () => {
|
||||
assertEqual(NETWORK.MAINNET, 'mainnet');
|
||||
assertEqual(NETWORK.TESTNET, 'testnet');
|
||||
assertEqual(NETWORK.STAGENET, 'stagenet');
|
||||
});
|
||||
|
||||
test('Address type constants exist', () => {
|
||||
assertEqual(ADDRESS_TYPE.STANDARD, 'standard');
|
||||
assertEqual(ADDRESS_TYPE.INTEGRATED, 'integrated');
|
||||
assertEqual(ADDRESS_TYPE.SUBADDRESS, 'subaddress');
|
||||
});
|
||||
|
||||
test('Address format constants exist', () => {
|
||||
assertEqual(ADDRESS_FORMAT.LEGACY, 'legacy');
|
||||
assertEqual(ADDRESS_FORMAT.CARROT, 'carrot');
|
||||
});
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Summary ===');
|
||||
console.log(`Passed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
console.log('');
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Interactive test with real addresses
|
||||
console.log('=== Real Address Tests ===');
|
||||
console.log('To test with real Salvium addresses, run:');
|
||||
console.log(' node -e "import(\'./src/index.js\').then(s => console.log(s.parseAddress(\'YOUR_ADDRESS\')))"');
|
||||
console.log('');
|
||||
Reference in New Issue
Block a user