rwadurian/backend/mpc-system/services/service-party-app/electron/modules/address-derivation.ts

334 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as crypto from 'crypto';
import { bech32 } from 'bech32';
// =============================================================================
// 链配置
// =============================================================================
export interface ChainConfig {
name: string;
prefix: string;
coinType: number;
curve: 'secp256k1' | 'ed25519';
derivationPath: string;
}
export const CHAIN_CONFIGS: Record<string, ChainConfig> = {
kava: {
name: 'Kava',
prefix: 'kava',
coinType: 459,
curve: 'secp256k1',
derivationPath: "m/44'/459'/0'/0/0",
},
cosmos: {
name: 'Cosmos Hub',
prefix: 'cosmos',
coinType: 118,
curve: 'secp256k1',
derivationPath: "m/44'/118'/0'/0/0",
},
osmosis: {
name: 'Osmosis',
prefix: 'osmo',
coinType: 118,
curve: 'secp256k1',
derivationPath: "m/44'/118'/0'/0/0",
},
ethereum: {
name: 'Ethereum',
prefix: '0x',
coinType: 60,
curve: 'secp256k1',
derivationPath: "m/44'/60'/0'/0/0",
},
};
// =============================================================================
// 地址派生工具
// =============================================================================
/**
* 从公钥派生 Bech32 地址 (Cosmos 系列)
*
* 流程:
* 1. 公钥 → SHA256 → RIPEMD160 → 20字节地址
* 2. 20字节地址 → Bech32 编码
*/
export function deriveCosmosAddress(publicKeyHex: string, prefix: string): string {
// 移除可能的 0x 前缀
const cleanHex = publicKeyHex.startsWith('0x') ? publicKeyHex.slice(2) : publicKeyHex;
const publicKeyBytes = Buffer.from(cleanHex, 'hex');
// 对于 secp256k1需要压缩公钥 (33 bytes)
// 如果是未压缩公钥 (65 bytes),需要先压缩
let compressedKey: Buffer = publicKeyBytes;
if (publicKeyBytes.length === 65) {
compressedKey = compressSecp256k1PublicKey(publicKeyBytes);
} else if (publicKeyBytes.length === 64) {
// 没有前缀的未压缩公钥
const uncompressed = Buffer.concat([Buffer.from([0x04]), publicKeyBytes]);
compressedKey = compressSecp256k1PublicKey(uncompressed);
}
// SHA256 → RIPEMD160
const sha256Hash = crypto.createHash('sha256').update(compressedKey).digest();
const ripemd160Hash = crypto.createHash('ripemd160').update(sha256Hash).digest();
// Bech32 编码
const words = bech32.toWords(ripemd160Hash);
const address = bech32.encode(prefix, words);
return address;
}
/**
* 从公钥派生以太坊地址
*
* 流程:
* 1. 未压缩公钥 (去掉 04 前缀) → Keccak256 → 取后 20 字节
*/
export function deriveEthereumAddress(publicKeyHex: string): string {
const cleanHex = publicKeyHex.startsWith('0x') ? publicKeyHex.slice(2) : publicKeyHex;
const publicKeyBytes = Buffer.from(cleanHex, 'hex');
// 需要未压缩公钥的 x, y 坐标 (64 bytes)
let uncompressedKey: Buffer;
if (publicKeyBytes.length === 33) {
// 压缩公钥,需要解压
uncompressedKey = decompressSecp256k1PublicKey(publicKeyBytes);
} else if (publicKeyBytes.length === 65) {
// 未压缩公钥,去掉 04 前缀
uncompressedKey = publicKeyBytes.slice(1) as Buffer;
} else if (publicKeyBytes.length === 64) {
uncompressedKey = publicKeyBytes;
} else {
throw new Error(`Invalid public key length: ${publicKeyBytes.length}`);
}
// Keccak256 (使用 keccak256 而不是 sha3-256)
const { keccak_256 } = require('@noble/hashes/sha3');
const hash = keccak_256(uncompressedKey);
// 取后 20 字节
const addressBytes = hash.slice(-20);
const address = '0x' + Buffer.from(addressBytes).toString('hex');
return checksumAddress(address);
}
/**
* 从 Ed25519 公钥派生地址 (用于某些链)
*/
export function deriveEd25519Address(publicKeyHex: string, prefix: string): string {
const cleanHex = publicKeyHex.startsWith('0x') ? publicKeyHex.slice(2) : publicKeyHex;
const publicKeyBytes = Buffer.from(cleanHex, 'hex');
// SHA256 → RIPEMD160
const sha256Hash = crypto.createHash('sha256').update(publicKeyBytes).digest();
const ripemd160Hash = crypto.createHash('ripemd160').update(sha256Hash).digest();
// Bech32 编码
const words = bech32.toWords(ripemd160Hash);
const address = bech32.encode(prefix, words);
return address;
}
/**
* 压缩 secp256k1 公钥
*/
function compressSecp256k1PublicKey(uncompressed: Buffer): Buffer {
if (uncompressed.length !== 65 || uncompressed[0] !== 0x04) {
throw new Error('Invalid uncompressed public key');
}
const x = uncompressed.slice(1, 33);
const y = uncompressed.slice(33, 65);
// 判断 y 是奇数还是偶数
const prefix = y[31] % 2 === 0 ? 0x02 : 0x03;
return Buffer.concat([Buffer.from([prefix]), x]);
}
/**
* 解压缩 secp256k1 公钥
* 使用椭圆曲线数学: y² = x³ + 7 (mod p)
*/
function decompressSecp256k1PublicKey(compressed: Buffer): Buffer {
if (compressed.length !== 33) {
throw new Error('Invalid compressed public key');
}
// secp256k1 曲线参数
const p = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F');
const prefix = compressed[0];
const x = BigInt('0x' + compressed.slice(1).toString('hex'));
// 计算 y² = x³ + 7 (mod p)
const xCubed = modPow(x, 3n, p);
const ySquared = (xCubed + 7n) % p;
// 计算平方根 (p ≡ 3 mod 4, 所以 y = ySquared^((p+1)/4) mod p)
let y = modPow(ySquared, (p + 1n) / 4n, p);
// 根据前缀选择正确的 y 值
const isYOdd = y % 2n === 1n;
const shouldBeOdd = prefix === 0x03;
if (isYOdd !== shouldBeOdd) {
y = p - y;
}
// 转换为 Buffer (64 bytes: x || y)
const xBuffer = Buffer.from(x.toString(16).padStart(64, '0'), 'hex');
const yBuffer = Buffer.from(y.toString(16).padStart(64, '0'), 'hex');
return Buffer.concat([xBuffer, yBuffer]);
}
/**
* 模幂运算
*/
function modPow(base: bigint, exponent: bigint, modulus: bigint): bigint {
let result = 1n;
base = base % modulus;
while (exponent > 0n) {
if (exponent % 2n === 1n) {
result = (result * base) % modulus;
}
exponent = exponent / 2n;
base = (base * base) % modulus;
}
return result;
}
/**
* EIP-55 校验和地址
*/
function checksumAddress(address: string): string {
const { keccak_256 } = require('@noble/hashes/sha3');
const addr = address.toLowerCase().replace('0x', '');
const hash = Buffer.from(keccak_256(Buffer.from(addr, 'utf8'))).toString('hex');
let result = '0x';
for (let i = 0; i < addr.length; i++) {
if (parseInt(hash[i], 16) >= 8) {
result += addr[i].toUpperCase();
} else {
result += addr[i];
}
}
return result;
}
// =============================================================================
// 地址派生服务
// =============================================================================
export interface DerivedAddress {
chain: string;
chainName: string;
prefix: string;
address: string;
derivationPath: string;
publicKeyHex: string;
}
/**
* 地址派生服务
*
* 注意TSS keygen 生成的是聚合公钥,不是派生公钥。
* 对于需要不同链的地址,我们直接使用聚合公钥派生地址,
* 而不是像 HD 钱包那样从种子派生。
*
* 这意味着:
* - 所有链使用相同的公钥
* - 不同链的地址只是编码方式不同
* - 这是 TSS 钱包的标准做法
*/
export class AddressDerivationService {
/**
* 从 TSS 聚合公钥派生指定链的地址
*/
deriveAddress(publicKeyHex: string, chain: string): DerivedAddress {
const config = CHAIN_CONFIGS[chain];
if (!config) {
throw new Error(`Unsupported chain: ${chain}`);
}
let address: string;
if (chain === 'ethereum') {
address = deriveEthereumAddress(publicKeyHex);
} else if (config.curve === 'ed25519') {
address = deriveEd25519Address(publicKeyHex, config.prefix);
} else {
// Cosmos 系列 (kava, cosmos, osmosis 等)
address = deriveCosmosAddress(publicKeyHex, config.prefix);
}
return {
chain,
chainName: config.name,
prefix: config.prefix,
address,
derivationPath: config.derivationPath,
publicKeyHex,
};
}
/**
* 派生所有支持的链地址
*/
deriveAllAddresses(publicKeyHex: string): DerivedAddress[] {
const addresses: DerivedAddress[] = [];
for (const chain of Object.keys(CHAIN_CONFIGS)) {
try {
const derived = this.deriveAddress(publicKeyHex, chain);
addresses.push(derived);
} catch (err) {
console.error(`Failed to derive ${chain} address:`, err);
}
}
return addresses;
}
/**
* 验证地址格式
*/
validateAddress(address: string, chain: string): boolean {
const config = CHAIN_CONFIGS[chain];
if (!config) {
return false;
}
if (chain === 'ethereum') {
return /^0x[a-fA-F0-9]{40}$/.test(address);
}
try {
const decoded = bech32.decode(address);
return decoded.prefix === config.prefix;
} catch {
return false;
}
}
/**
* 获取支持的链列表
*/
getSupportedChains(): ChainConfig[] {
return Object.values(CHAIN_CONFIGS);
}
}
// 导出单例
export const addressDerivationService = new AddressDerivationService();