123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614 |
- /**
- * Contract Test: Aster Signer Interface
- *
- * Tests the IAsterSigner contract to ensure it correctly implements
- * EIP-191 (Ethereum Signed Message Standard) signing for Aster Network.
- */
- import { describe, test, expect, beforeEach } from '@jest/globals';
- import { Platform, SignResult, ErrorType } from '../../src/types/credential.js';
- // ============================================================================
- // Interface Contracts to Test
- // ============================================================================
- /**
- * Aster平台签名器接口
- */
- interface IAsterSigner {
- readonly platform: Platform.ASTER;
- /**
- * 对Aster网络交易进行签名
- * @param request Aster签名请求
- * @returns 签名结果,包含EIP-191格式签名
- */
- signTransaction(request: AsterSignRequest): Promise<AsterSignResponse>;
- /**
- * 对任意消息进行签名(EIP-191)
- * @param request 消息签名请求
- * @returns 签名结果
- */
- signMessage(request: AsterMessageSignRequest): Promise<AsterSignResponse>;
- /**
- * 验证Aster签名
- * @param request 验证请求
- * @returns 验证结果
- */
- verifySignature(request: AsterVerifyRequest): Promise<AsterVerifyResponse>;
- /**
- * 获取账户以太坊地址
- * @param accountId 账户ID
- * @returns 以太坊地址 (0x...)
- */
- getAddress(accountId: string): Promise<string>;
- /**
- * 批量签名交易
- * @param requests 批量签名请求
- * @returns 批量签名结果
- */
- signBatch(requests: AsterSignRequest[]): Promise<AsterSignResponse[]>;
- }
- // ============================================================================
- // Request/Response Types
- // ============================================================================
- interface AsterSignRequest {
- accountId: string;
- transaction: AsterTransaction;
- options?: AsterSignOptions;
- }
- interface AsterMessageSignRequest {
- accountId: string;
- message: string | Uint8Array;
- options?: AsterSignOptions;
- }
- interface AsterSignOptions {
- timeout?: number;
- chainId?: number;
- gasLimit?: string;
- gasPrice?: string;
- nonce?: number;
- }
- interface AsterTransaction {
- to: string;
- value?: string;
- data?: string;
- gasLimit?: string;
- gasPrice?: string;
- nonce?: number;
- chainId?: number;
- }
- interface AsterSignResponse extends SignResult {
- signature: string;
- algorithm: 'ecdsa-secp256k1' | 'eip-191';
- address: string;
- chainId?: number;
- txHash?: string;
- }
- interface AsterVerifyRequest {
- message: string | Uint8Array;
- signature: string;
- address: string;
- algorithm?: 'ecdsa-secp256k1' | 'eip-191';
- }
- interface AsterVerifyResponse {
- valid: boolean;
- algorithm: 'ecdsa-secp256k1' | 'eip-191';
- address: string;
- timestamp: Date;
- }
- // ============================================================================
- // Test Constants
- // ============================================================================
- const TEST_CREDENTIALS = {
- privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12',
- address: '0x742d35Cc6634C0532925a3b8D8002a66a30a1234'
- };
- const SAMPLE_TRANSACTIONS = {
- transfer: {
- to: '0x742d35Cc6634C0532925a3b8D8002a66a30a5678',
- value: '1000000000000000000', // 1 ETH
- gasLimit: '21000',
- gasPrice: '20000000000', // 20 Gwei
- chainId: 592 // Astar Network
- },
- contractCall: {
- to: '0x1234567890123456789012345678901234567890',
- data: '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d8002a66a30a567800000000000000000000000000000000000000000000000000000000000f4240',
- gasLimit: '60000',
- gasPrice: '25000000000',
- chainId: 592
- }
- };
- const SAMPLE_MESSAGES = {
- simple: 'Hello, Aster Network!',
- json: JSON.stringify({ action: 'verify', timestamp: Date.now() }),
- binary: new Uint8Array([72, 101, 108, 108, 111, 32, 65, 115, 116, 101, 114])
- };
- // ============================================================================
- // Mock Implementation for Testing
- // ============================================================================
- class MockAsterSigner implements IAsterSigner {
- readonly platform = Platform.ASTER;
- private accounts = new Map<string, any>();
- constructor() {
- // Add test account
- this.accounts.set('aster-test-001', {
- credentials: {
- type: 'aster',
- privateKey: TEST_CREDENTIALS.privateKey
- },
- address: TEST_CREDENTIALS.address
- });
- }
- async signTransaction(request: AsterSignRequest): Promise<AsterSignResponse> {
- const account = this.accounts.get(request.accountId);
- if (!account) {
- throw new Error(`Account ${request.accountId} not found`);
- }
- // Simulate ECDSA signing
- const signature = this.computeEcdsaSignature(
- this.serializeTransaction(request.transaction),
- account.credentials.privateKey
- );
- return {
- success: true,
- signature,
- algorithm: 'ecdsa-secp256k1',
- address: account.address,
- chainId: request.transaction.chainId,
- timestamp: new Date(),
- txHash: this.computeTransactionHash(request.transaction)
- };
- }
- async signMessage(request: AsterMessageSignRequest): Promise<AsterSignResponse> {
- const account = this.accounts.get(request.accountId);
- if (!account) {
- throw new Error(`Account ${request.accountId} not found`);
- }
- const message = typeof request.message === 'string'
- ? request.message
- : new TextDecoder().decode(request.message);
- // Simulate EIP-191 message signing
- const eip191Message = `\x19Ethereum Signed Message:\n${message.length}${message}`;
- const signature = this.computeEcdsaSignature(eip191Message, account.credentials.privateKey);
- return {
- success: true,
- signature,
- algorithm: 'eip-191',
- address: account.address,
- timestamp: new Date()
- };
- }
- async verifySignature(request: AsterVerifyRequest): Promise<AsterVerifyResponse> {
- // Simulate signature verification
- const isValid = this.verifyEcdsaSignature(
- request.message,
- request.signature,
- request.address
- );
- return {
- valid: isValid,
- algorithm: request.algorithm || 'eip-191',
- address: request.address,
- timestamp: new Date()
- };
- }
- async getAddress(accountId: string): Promise<string> {
- const account = this.accounts.get(accountId);
- if (!account) {
- throw new Error(`Account ${accountId} not found`);
- }
- return account.address;
- }
- async signBatch(requests: AsterSignRequest[]): Promise<AsterSignResponse[]> {
- return Promise.all(requests.map(req => this.signTransaction(req)));
- }
- private serializeTransaction(tx: AsterTransaction): string {
- return JSON.stringify({
- to: tx.to,
- value: tx.value || '0',
- data: tx.data || '0x',
- gasLimit: tx.gasLimit,
- gasPrice: tx.gasPrice,
- nonce: tx.nonce,
- chainId: tx.chainId
- });
- }
- private computeEcdsaSignature(message: string, privateKey: string): string {
- // Simulate ECDSA signature computation
- const hash = this.keccak256(message);
- return `0x${hash.substring(0, 130)}01`; // Mock signature with recovery id
- }
- private verifyEcdsaSignature(message: string | Uint8Array, signature: string, address: string): boolean {
- // Simulate signature verification
- return signature.length === 132 && address.length === 42 && signature.startsWith('0x');
- }
- private computeTransactionHash(tx: AsterTransaction): string {
- return this.keccak256(this.serializeTransaction(tx));
- }
- private keccak256(data: string): string {
- // Simulate Keccak256 hash
- return `0x${data.length.toString(16).padStart(64, '0')}${Date.now().toString(16).padStart(64, '0')}`;
- }
- }
- // ============================================================================
- // Contract Tests
- // ============================================================================
- describe('IAsterSigner Contract Tests', () => {
- let signer: IAsterSigner;
- beforeEach(() => {
- signer = new MockAsterSigner();
- });
- describe('Platform Identification', () => {
- test('should identify as ASTER platform', () => {
- expect(signer.platform).toBe(Platform.ASTER);
- });
- });
- describe('Transaction Signing', () => {
- test('should sign ETH transfer transaction', async () => {
- const request: AsterSignRequest = {
- accountId: 'aster-test-001',
- transaction: SAMPLE_TRANSACTIONS.transfer
- };
- const result = await signer.signTransaction(request);
- expect(result.success).toBe(true);
- expect(result.signature).toBeDefined();
- expect(result.signature).toMatch(/^0x[0-9a-fA-F]{130}$/);
- expect(result.algorithm).toBe('ecdsa-secp256k1');
- expect(result.address).toBe(TEST_CREDENTIALS.address);
- expect(result.chainId).toBe(592);
- expect(result.txHash).toBeDefined();
- });
- test('should sign contract call transaction', async () => {
- const request: AsterSignRequest = {
- accountId: 'aster-test-001',
- transaction: SAMPLE_TRANSACTIONS.contractCall
- };
- const result = await signer.signTransaction(request);
- expect(result.success).toBe(true);
- expect(result.signature).toBeDefined();
- expect(result.algorithm).toBe('ecdsa-secp256k1');
- expect(result.address).toBe(TEST_CREDENTIALS.address);
- });
- test('should fail for non-existent account', async () => {
- const request: AsterSignRequest = {
- accountId: 'non-existent-account',
- transaction: SAMPLE_TRANSACTIONS.transfer
- };
- await expect(signer.signTransaction(request)).rejects.toThrow('Account non-existent-account not found');
- });
- test('should handle transaction with options', async () => {
- const request: AsterSignRequest = {
- accountId: 'aster-test-001',
- transaction: SAMPLE_TRANSACTIONS.transfer,
- options: {
- timeout: 10000,
- chainId: 592,
- gasLimit: '25000'
- }
- };
- const result = await signer.signTransaction(request);
- expect(result.success).toBe(true);
- expect(result.chainId).toBe(592);
- });
- });
- describe('Message Signing (EIP-191)', () => {
- test('should sign string message', async () => {
- const request: AsterMessageSignRequest = {
- accountId: 'aster-test-001',
- message: SAMPLE_MESSAGES.simple
- };
- const result = await signer.signMessage(request);
- expect(result.success).toBe(true);
- expect(result.signature).toBeDefined();
- expect(result.algorithm).toBe('eip-191');
- expect(result.address).toBe(TEST_CREDENTIALS.address);
- });
- test('should sign JSON message', async () => {
- const request: AsterMessageSignRequest = {
- accountId: 'aster-test-001',
- message: SAMPLE_MESSAGES.json
- };
- const result = await signer.signMessage(request);
- expect(result.success).toBe(true);
- expect(result.algorithm).toBe('eip-191');
- });
- test('should sign binary message', async () => {
- const request: AsterMessageSignRequest = {
- accountId: 'aster-test-001',
- message: SAMPLE_MESSAGES.binary
- };
- const result = await signer.signMessage(request);
- expect(result.success).toBe(true);
- expect(result.algorithm).toBe('eip-191');
- });
- test('should fail for non-existent account', async () => {
- const request: AsterMessageSignRequest = {
- accountId: 'non-existent-account',
- message: SAMPLE_MESSAGES.simple
- };
- await expect(signer.signMessage(request)).rejects.toThrow('Account non-existent-account not found');
- });
- });
- describe('Signature Verification', () => {
- test('should verify valid signature', async () => {
- const request: AsterVerifyRequest = {
- message: SAMPLE_MESSAGES.simple,
- signature: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef121234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1201',
- address: TEST_CREDENTIALS.address
- };
- const result = await signer.verifySignature(request);
- expect(result.valid).toBe(true);
- expect(result.algorithm).toBe('eip-191');
- expect(result.address).toBe(TEST_CREDENTIALS.address);
- expect(result.timestamp).toBeInstanceOf(Date);
- });
- test('should reject invalid signature format', async () => {
- const request: AsterVerifyRequest = {
- message: SAMPLE_MESSAGES.simple,
- signature: 'invalid_signature',
- address: TEST_CREDENTIALS.address
- };
- const result = await signer.verifySignature(request);
- expect(result.valid).toBe(false);
- });
- test('should handle binary message verification', async () => {
- const request: AsterVerifyRequest = {
- message: SAMPLE_MESSAGES.binary,
- signature: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef121234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1201',
- address: TEST_CREDENTIALS.address,
- algorithm: 'eip-191'
- };
- const result = await signer.verifySignature(request);
- expect(result.algorithm).toBe('eip-191');
- });
- });
- describe('Address Management', () => {
- test('should return address for valid account', async () => {
- const address = await signer.getAddress('aster-test-001');
- expect(address).toBe(TEST_CREDENTIALS.address);
- expect(address).toMatch(/^0x[0-9a-fA-F]{40}$/);
- });
- test('should fail for non-existent account', async () => {
- await expect(signer.getAddress('non-existent-account')).rejects.toThrow('Account non-existent-account not found');
- });
- });
- describe('Batch Operations', () => {
- test('should handle batch transaction signing', async () => {
- const requests: AsterSignRequest[] = [
- {
- accountId: 'aster-test-001',
- transaction: SAMPLE_TRANSACTIONS.transfer
- },
- {
- accountId: 'aster-test-001',
- transaction: SAMPLE_TRANSACTIONS.contractCall
- }
- ];
- const results = await signer.signBatch(requests);
- expect(results).toHaveLength(2);
- expect(results[0].success).toBe(true);
- expect(results[1].success).toBe(true);
- expect(results[0].algorithm).toBe('ecdsa-secp256k1');
- expect(results[1].algorithm).toBe('ecdsa-secp256k1');
- });
- test('should handle empty batch', async () => {
- const results = await signer.signBatch([]);
- expect(results).toHaveLength(0);
- });
- });
- describe('Performance Requirements', () => {
- test('should complete transaction signing within 50ms', async () => {
- const request: AsterSignRequest = {
- accountId: 'aster-test-001',
- transaction: SAMPLE_TRANSACTIONS.transfer
- };
- const startTime = Date.now();
- const result = await signer.signTransaction(request);
- const duration = Date.now() - startTime;
- expect(result.success).toBe(true);
- expect(duration).toBeLessThan(50);
- });
- test('should complete message signing within 50ms', async () => {
- const request: AsterMessageSignRequest = {
- accountId: 'aster-test-001',
- message: SAMPLE_MESSAGES.simple
- };
- const startTime = Date.now();
- const result = await signer.signMessage(request);
- const duration = Date.now() - startTime;
- expect(result.success).toBe(true);
- expect(duration).toBeLessThan(50);
- });
- test('should handle concurrent requests', async () => {
- const requests = Array.from({ length: 10 }, () => ({
- accountId: 'aster-test-001',
- transaction: SAMPLE_TRANSACTIONS.transfer
- }));
- const startTime = Date.now();
- const results = await Promise.all(
- requests.map(req => signer.signTransaction(req))
- );
- const duration = Date.now() - startTime;
- expect(results).toHaveLength(10);
- expect(results.every(r => r.success)).toBe(true);
- expect(duration).toBeLessThan(200); // 10 concurrent requests < 200ms
- });
- });
- describe('Error Handling', () => {
- test('should handle malformed transaction gracefully', async () => {
- const request = {
- accountId: 'aster-test-001',
- transaction: {
- to: 'invalid_address',
- value: 'invalid_value'
- } as any
- };
- // Should not throw, but might return error in response
- try {
- const result = await signer.signTransaction(request);
- // If it succeeds, check success flag
- if (!result.success) {
- expect(result.error).toBeDefined();
- }
- } catch (error) {
- // If it throws, that's also acceptable
- expect(error).toBeInstanceOf(Error);
- }
- });
- test('should handle empty message', async () => {
- const request: AsterMessageSignRequest = {
- accountId: 'aster-test-001',
- message: ''
- };
- const result = await signer.signMessage(request);
- expect(result.success).toBe(true);
- expect(result.algorithm).toBe('eip-191');
- });
- test('should handle large message', async () => {
- const largeMessage = 'A'.repeat(10000); // 10KB message
- const request: AsterMessageSignRequest = {
- accountId: 'aster-test-001',
- message: largeMessage
- };
- const result = await signer.signMessage(request);
- expect(result.success).toBe(true);
- });
- });
- });
- // ============================================================================
- // Integration Hints for Implementation
- // ============================================================================
- /**
- * Implementation Notes:
- *
- * 1. ECDSA secp256k1 Algorithm:
- * - Use ethers.js or noble-secp256k1 library
- * - Generate recoverable signatures (v, r, s format)
- * - Support both transaction and message signing
- *
- * 2. EIP-191 Message Signing:
- * - Prefix: "\x19Ethereum Signed Message:\n{message_length}{message}"
- * - Use Keccak256 for hashing
- * - Return signature in 0x format
- *
- * 3. Transaction Serialization:
- * - RLP encoding for raw transactions
- * - Include chainId for EIP-155 protection
- * - Handle legacy and EIP-1559 transaction types
- *
- * 4. Performance Requirements:
- * - Individual signing: < 50ms
- * - Batch operations: optimize for concurrent requests
- * - Consider WebWorkers for CPU-intensive operations
- *
- * 5. Security Considerations:
- * - Validate transaction parameters
- * - Check gas limits and prices
- * - Implement replay protection
- * - Never log private keys
- *
- * 6. Aster Network Specifics:
- * - Chain ID: 592 (mainnet), 81 (testnet)
- * - Support for both EVM and WASM contracts
- * - Gas mechanics similar to Ethereum
- */
|