aster-signer.contract.test.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. /**
  2. * Contract Test: Aster Signer Interface
  3. *
  4. * Tests the IAsterSigner contract to ensure it correctly implements
  5. * EIP-191 (Ethereum Signed Message Standard) signing for Aster Network.
  6. */
  7. import { describe, test, expect, beforeEach } from '@jest/globals';
  8. import { Platform, SignResult, ErrorType } from '../../src/types/credential.js';
  9. // ============================================================================
  10. // Interface Contracts to Test
  11. // ============================================================================
  12. /**
  13. * Aster平台签名器接口
  14. */
  15. interface IAsterSigner {
  16. readonly platform: Platform.ASTER;
  17. /**
  18. * 对Aster网络交易进行签名
  19. * @param request Aster签名请求
  20. * @returns 签名结果,包含EIP-191格式签名
  21. */
  22. signTransaction(request: AsterSignRequest): Promise<AsterSignResponse>;
  23. /**
  24. * 对任意消息进行签名(EIP-191)
  25. * @param request 消息签名请求
  26. * @returns 签名结果
  27. */
  28. signMessage(request: AsterMessageSignRequest): Promise<AsterSignResponse>;
  29. /**
  30. * 验证Aster签名
  31. * @param request 验证请求
  32. * @returns 验证结果
  33. */
  34. verifySignature(request: AsterVerifyRequest): Promise<AsterVerifyResponse>;
  35. /**
  36. * 获取账户以太坊地址
  37. * @param accountId 账户ID
  38. * @returns 以太坊地址 (0x...)
  39. */
  40. getAddress(accountId: string): Promise<string>;
  41. /**
  42. * 批量签名交易
  43. * @param requests 批量签名请求
  44. * @returns 批量签名结果
  45. */
  46. signBatch(requests: AsterSignRequest[]): Promise<AsterSignResponse[]>;
  47. }
  48. // ============================================================================
  49. // Request/Response Types
  50. // ============================================================================
  51. interface AsterSignRequest {
  52. accountId: string;
  53. transaction: AsterTransaction;
  54. options?: AsterSignOptions;
  55. }
  56. interface AsterMessageSignRequest {
  57. accountId: string;
  58. message: string | Uint8Array;
  59. options?: AsterSignOptions;
  60. }
  61. interface AsterSignOptions {
  62. timeout?: number;
  63. chainId?: number;
  64. gasLimit?: string;
  65. gasPrice?: string;
  66. nonce?: number;
  67. }
  68. interface AsterTransaction {
  69. to: string;
  70. value?: string;
  71. data?: string;
  72. gasLimit?: string;
  73. gasPrice?: string;
  74. nonce?: number;
  75. chainId?: number;
  76. }
  77. interface AsterSignResponse extends SignResult {
  78. signature: string;
  79. algorithm: 'ecdsa-secp256k1' | 'eip-191';
  80. address: string;
  81. chainId?: number;
  82. txHash?: string;
  83. }
  84. interface AsterVerifyRequest {
  85. message: string | Uint8Array;
  86. signature: string;
  87. address: string;
  88. algorithm?: 'ecdsa-secp256k1' | 'eip-191';
  89. }
  90. interface AsterVerifyResponse {
  91. valid: boolean;
  92. algorithm: 'ecdsa-secp256k1' | 'eip-191';
  93. address: string;
  94. timestamp: Date;
  95. }
  96. // ============================================================================
  97. // Test Constants
  98. // ============================================================================
  99. const TEST_CREDENTIALS = {
  100. privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12',
  101. address: '0x742d35Cc6634C0532925a3b8D8002a66a30a1234'
  102. };
  103. const SAMPLE_TRANSACTIONS = {
  104. transfer: {
  105. to: '0x742d35Cc6634C0532925a3b8D8002a66a30a5678',
  106. value: '1000000000000000000', // 1 ETH
  107. gasLimit: '21000',
  108. gasPrice: '20000000000', // 20 Gwei
  109. chainId: 592 // Astar Network
  110. },
  111. contractCall: {
  112. to: '0x1234567890123456789012345678901234567890',
  113. data: '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d8002a66a30a567800000000000000000000000000000000000000000000000000000000000f4240',
  114. gasLimit: '60000',
  115. gasPrice: '25000000000',
  116. chainId: 592
  117. }
  118. };
  119. const SAMPLE_MESSAGES = {
  120. simple: 'Hello, Aster Network!',
  121. json: JSON.stringify({ action: 'verify', timestamp: Date.now() }),
  122. binary: new Uint8Array([72, 101, 108, 108, 111, 32, 65, 115, 116, 101, 114])
  123. };
  124. // ============================================================================
  125. // Mock Implementation for Testing
  126. // ============================================================================
  127. class MockAsterSigner implements IAsterSigner {
  128. readonly platform = Platform.ASTER;
  129. private accounts = new Map<string, any>();
  130. constructor() {
  131. // Add test account
  132. this.accounts.set('aster-test-001', {
  133. credentials: {
  134. type: 'aster',
  135. privateKey: TEST_CREDENTIALS.privateKey
  136. },
  137. address: TEST_CREDENTIALS.address
  138. });
  139. }
  140. async signTransaction(request: AsterSignRequest): Promise<AsterSignResponse> {
  141. const account = this.accounts.get(request.accountId);
  142. if (!account) {
  143. throw new Error(`Account ${request.accountId} not found`);
  144. }
  145. // Simulate ECDSA signing
  146. const signature = this.computeEcdsaSignature(
  147. this.serializeTransaction(request.transaction),
  148. account.credentials.privateKey
  149. );
  150. return {
  151. success: true,
  152. signature,
  153. algorithm: 'ecdsa-secp256k1',
  154. address: account.address,
  155. chainId: request.transaction.chainId,
  156. timestamp: new Date(),
  157. txHash: this.computeTransactionHash(request.transaction)
  158. };
  159. }
  160. async signMessage(request: AsterMessageSignRequest): Promise<AsterSignResponse> {
  161. const account = this.accounts.get(request.accountId);
  162. if (!account) {
  163. throw new Error(`Account ${request.accountId} not found`);
  164. }
  165. const message = typeof request.message === 'string'
  166. ? request.message
  167. : new TextDecoder().decode(request.message);
  168. // Simulate EIP-191 message signing
  169. const eip191Message = `\x19Ethereum Signed Message:\n${message.length}${message}`;
  170. const signature = this.computeEcdsaSignature(eip191Message, account.credentials.privateKey);
  171. return {
  172. success: true,
  173. signature,
  174. algorithm: 'eip-191',
  175. address: account.address,
  176. timestamp: new Date()
  177. };
  178. }
  179. async verifySignature(request: AsterVerifyRequest): Promise<AsterVerifyResponse> {
  180. // Simulate signature verification
  181. const isValid = this.verifyEcdsaSignature(
  182. request.message,
  183. request.signature,
  184. request.address
  185. );
  186. return {
  187. valid: isValid,
  188. algorithm: request.algorithm || 'eip-191',
  189. address: request.address,
  190. timestamp: new Date()
  191. };
  192. }
  193. async getAddress(accountId: string): Promise<string> {
  194. const account = this.accounts.get(accountId);
  195. if (!account) {
  196. throw new Error(`Account ${accountId} not found`);
  197. }
  198. return account.address;
  199. }
  200. async signBatch(requests: AsterSignRequest[]): Promise<AsterSignResponse[]> {
  201. return Promise.all(requests.map(req => this.signTransaction(req)));
  202. }
  203. private serializeTransaction(tx: AsterTransaction): string {
  204. return JSON.stringify({
  205. to: tx.to,
  206. value: tx.value || '0',
  207. data: tx.data || '0x',
  208. gasLimit: tx.gasLimit,
  209. gasPrice: tx.gasPrice,
  210. nonce: tx.nonce,
  211. chainId: tx.chainId
  212. });
  213. }
  214. private computeEcdsaSignature(message: string, privateKey: string): string {
  215. // Simulate ECDSA signature computation
  216. const hash = this.keccak256(message);
  217. return `0x${hash.substring(0, 130)}01`; // Mock signature with recovery id
  218. }
  219. private verifyEcdsaSignature(message: string | Uint8Array, signature: string, address: string): boolean {
  220. // Simulate signature verification
  221. return signature.length === 132 && address.length === 42 && signature.startsWith('0x');
  222. }
  223. private computeTransactionHash(tx: AsterTransaction): string {
  224. return this.keccak256(this.serializeTransaction(tx));
  225. }
  226. private keccak256(data: string): string {
  227. // Simulate Keccak256 hash
  228. return `0x${data.length.toString(16).padStart(64, '0')}${Date.now().toString(16).padStart(64, '0')}`;
  229. }
  230. }
  231. // ============================================================================
  232. // Contract Tests
  233. // ============================================================================
  234. describe('IAsterSigner Contract Tests', () => {
  235. let signer: IAsterSigner;
  236. beforeEach(() => {
  237. signer = new MockAsterSigner();
  238. });
  239. describe('Platform Identification', () => {
  240. test('should identify as ASTER platform', () => {
  241. expect(signer.platform).toBe(Platform.ASTER);
  242. });
  243. });
  244. describe('Transaction Signing', () => {
  245. test('should sign ETH transfer transaction', async () => {
  246. const request: AsterSignRequest = {
  247. accountId: 'aster-test-001',
  248. transaction: SAMPLE_TRANSACTIONS.transfer
  249. };
  250. const result = await signer.signTransaction(request);
  251. expect(result.success).toBe(true);
  252. expect(result.signature).toBeDefined();
  253. expect(result.signature).toMatch(/^0x[0-9a-fA-F]{130}$/);
  254. expect(result.algorithm).toBe('ecdsa-secp256k1');
  255. expect(result.address).toBe(TEST_CREDENTIALS.address);
  256. expect(result.chainId).toBe(592);
  257. expect(result.txHash).toBeDefined();
  258. });
  259. test('should sign contract call transaction', async () => {
  260. const request: AsterSignRequest = {
  261. accountId: 'aster-test-001',
  262. transaction: SAMPLE_TRANSACTIONS.contractCall
  263. };
  264. const result = await signer.signTransaction(request);
  265. expect(result.success).toBe(true);
  266. expect(result.signature).toBeDefined();
  267. expect(result.algorithm).toBe('ecdsa-secp256k1');
  268. expect(result.address).toBe(TEST_CREDENTIALS.address);
  269. });
  270. test('should fail for non-existent account', async () => {
  271. const request: AsterSignRequest = {
  272. accountId: 'non-existent-account',
  273. transaction: SAMPLE_TRANSACTIONS.transfer
  274. };
  275. await expect(signer.signTransaction(request)).rejects.toThrow('Account non-existent-account not found');
  276. });
  277. test('should handle transaction with options', async () => {
  278. const request: AsterSignRequest = {
  279. accountId: 'aster-test-001',
  280. transaction: SAMPLE_TRANSACTIONS.transfer,
  281. options: {
  282. timeout: 10000,
  283. chainId: 592,
  284. gasLimit: '25000'
  285. }
  286. };
  287. const result = await signer.signTransaction(request);
  288. expect(result.success).toBe(true);
  289. expect(result.chainId).toBe(592);
  290. });
  291. });
  292. describe('Message Signing (EIP-191)', () => {
  293. test('should sign string message', async () => {
  294. const request: AsterMessageSignRequest = {
  295. accountId: 'aster-test-001',
  296. message: SAMPLE_MESSAGES.simple
  297. };
  298. const result = await signer.signMessage(request);
  299. expect(result.success).toBe(true);
  300. expect(result.signature).toBeDefined();
  301. expect(result.algorithm).toBe('eip-191');
  302. expect(result.address).toBe(TEST_CREDENTIALS.address);
  303. });
  304. test('should sign JSON message', async () => {
  305. const request: AsterMessageSignRequest = {
  306. accountId: 'aster-test-001',
  307. message: SAMPLE_MESSAGES.json
  308. };
  309. const result = await signer.signMessage(request);
  310. expect(result.success).toBe(true);
  311. expect(result.algorithm).toBe('eip-191');
  312. });
  313. test('should sign binary message', async () => {
  314. const request: AsterMessageSignRequest = {
  315. accountId: 'aster-test-001',
  316. message: SAMPLE_MESSAGES.binary
  317. };
  318. const result = await signer.signMessage(request);
  319. expect(result.success).toBe(true);
  320. expect(result.algorithm).toBe('eip-191');
  321. });
  322. test('should fail for non-existent account', async () => {
  323. const request: AsterMessageSignRequest = {
  324. accountId: 'non-existent-account',
  325. message: SAMPLE_MESSAGES.simple
  326. };
  327. await expect(signer.signMessage(request)).rejects.toThrow('Account non-existent-account not found');
  328. });
  329. });
  330. describe('Signature Verification', () => {
  331. test('should verify valid signature', async () => {
  332. const request: AsterVerifyRequest = {
  333. message: SAMPLE_MESSAGES.simple,
  334. signature: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef121234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1201',
  335. address: TEST_CREDENTIALS.address
  336. };
  337. const result = await signer.verifySignature(request);
  338. expect(result.valid).toBe(true);
  339. expect(result.algorithm).toBe('eip-191');
  340. expect(result.address).toBe(TEST_CREDENTIALS.address);
  341. expect(result.timestamp).toBeInstanceOf(Date);
  342. });
  343. test('should reject invalid signature format', async () => {
  344. const request: AsterVerifyRequest = {
  345. message: SAMPLE_MESSAGES.simple,
  346. signature: 'invalid_signature',
  347. address: TEST_CREDENTIALS.address
  348. };
  349. const result = await signer.verifySignature(request);
  350. expect(result.valid).toBe(false);
  351. });
  352. test('should handle binary message verification', async () => {
  353. const request: AsterVerifyRequest = {
  354. message: SAMPLE_MESSAGES.binary,
  355. signature: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef121234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1201',
  356. address: TEST_CREDENTIALS.address,
  357. algorithm: 'eip-191'
  358. };
  359. const result = await signer.verifySignature(request);
  360. expect(result.algorithm).toBe('eip-191');
  361. });
  362. });
  363. describe('Address Management', () => {
  364. test('should return address for valid account', async () => {
  365. const address = await signer.getAddress('aster-test-001');
  366. expect(address).toBe(TEST_CREDENTIALS.address);
  367. expect(address).toMatch(/^0x[0-9a-fA-F]{40}$/);
  368. });
  369. test('should fail for non-existent account', async () => {
  370. await expect(signer.getAddress('non-existent-account')).rejects.toThrow('Account non-existent-account not found');
  371. });
  372. });
  373. describe('Batch Operations', () => {
  374. test('should handle batch transaction signing', async () => {
  375. const requests: AsterSignRequest[] = [
  376. {
  377. accountId: 'aster-test-001',
  378. transaction: SAMPLE_TRANSACTIONS.transfer
  379. },
  380. {
  381. accountId: 'aster-test-001',
  382. transaction: SAMPLE_TRANSACTIONS.contractCall
  383. }
  384. ];
  385. const results = await signer.signBatch(requests);
  386. expect(results).toHaveLength(2);
  387. expect(results[0].success).toBe(true);
  388. expect(results[1].success).toBe(true);
  389. expect(results[0].algorithm).toBe('ecdsa-secp256k1');
  390. expect(results[1].algorithm).toBe('ecdsa-secp256k1');
  391. });
  392. test('should handle empty batch', async () => {
  393. const results = await signer.signBatch([]);
  394. expect(results).toHaveLength(0);
  395. });
  396. });
  397. describe('Performance Requirements', () => {
  398. test('should complete transaction signing within 50ms', async () => {
  399. const request: AsterSignRequest = {
  400. accountId: 'aster-test-001',
  401. transaction: SAMPLE_TRANSACTIONS.transfer
  402. };
  403. const startTime = Date.now();
  404. const result = await signer.signTransaction(request);
  405. const duration = Date.now() - startTime;
  406. expect(result.success).toBe(true);
  407. expect(duration).toBeLessThan(50);
  408. });
  409. test('should complete message signing within 50ms', async () => {
  410. const request: AsterMessageSignRequest = {
  411. accountId: 'aster-test-001',
  412. message: SAMPLE_MESSAGES.simple
  413. };
  414. const startTime = Date.now();
  415. const result = await signer.signMessage(request);
  416. const duration = Date.now() - startTime;
  417. expect(result.success).toBe(true);
  418. expect(duration).toBeLessThan(50);
  419. });
  420. test('should handle concurrent requests', async () => {
  421. const requests = Array.from({ length: 10 }, () => ({
  422. accountId: 'aster-test-001',
  423. transaction: SAMPLE_TRANSACTIONS.transfer
  424. }));
  425. const startTime = Date.now();
  426. const results = await Promise.all(
  427. requests.map(req => signer.signTransaction(req))
  428. );
  429. const duration = Date.now() - startTime;
  430. expect(results).toHaveLength(10);
  431. expect(results.every(r => r.success)).toBe(true);
  432. expect(duration).toBeLessThan(200); // 10 concurrent requests < 200ms
  433. });
  434. });
  435. describe('Error Handling', () => {
  436. test('should handle malformed transaction gracefully', async () => {
  437. const request = {
  438. accountId: 'aster-test-001',
  439. transaction: {
  440. to: 'invalid_address',
  441. value: 'invalid_value'
  442. } as any
  443. };
  444. // Should not throw, but might return error in response
  445. try {
  446. const result = await signer.signTransaction(request);
  447. // If it succeeds, check success flag
  448. if (!result.success) {
  449. expect(result.error).toBeDefined();
  450. }
  451. } catch (error) {
  452. // If it throws, that's also acceptable
  453. expect(error).toBeInstanceOf(Error);
  454. }
  455. });
  456. test('should handle empty message', async () => {
  457. const request: AsterMessageSignRequest = {
  458. accountId: 'aster-test-001',
  459. message: ''
  460. };
  461. const result = await signer.signMessage(request);
  462. expect(result.success).toBe(true);
  463. expect(result.algorithm).toBe('eip-191');
  464. });
  465. test('should handle large message', async () => {
  466. const largeMessage = 'A'.repeat(10000); // 10KB message
  467. const request: AsterMessageSignRequest = {
  468. accountId: 'aster-test-001',
  469. message: largeMessage
  470. };
  471. const result = await signer.signMessage(request);
  472. expect(result.success).toBe(true);
  473. });
  474. });
  475. });
  476. // ============================================================================
  477. // Integration Hints for Implementation
  478. // ============================================================================
  479. /**
  480. * Implementation Notes:
  481. *
  482. * 1. ECDSA secp256k1 Algorithm:
  483. * - Use ethers.js or noble-secp256k1 library
  484. * - Generate recoverable signatures (v, r, s format)
  485. * - Support both transaction and message signing
  486. *
  487. * 2. EIP-191 Message Signing:
  488. * - Prefix: "\x19Ethereum Signed Message:\n{message_length}{message}"
  489. * - Use Keccak256 for hashing
  490. * - Return signature in 0x format
  491. *
  492. * 3. Transaction Serialization:
  493. * - RLP encoding for raw transactions
  494. * - Include chainId for EIP-155 protection
  495. * - Handle legacy and EIP-1559 transaction types
  496. *
  497. * 4. Performance Requirements:
  498. * - Individual signing: < 50ms
  499. * - Batch operations: optimize for concurrent requests
  500. * - Consider WebWorkers for CPU-intensive operations
  501. *
  502. * 5. Security Considerations:
  503. * - Validate transaction parameters
  504. * - Check gas limits and prices
  505. * - Implement replay protection
  506. * - Never log private keys
  507. *
  508. * 6. Aster Network Specifics:
  509. * - Chain ID: 592 (mainnet), 81 (testnet)
  510. * - Support for both EVM and WASM contracts
  511. * - Gas mechanics similar to Ethereum
  512. */