@agirails/sdk 2.0.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +183 -0
  2. package/dist/ACTPClient.d.ts +52 -0
  3. package/dist/ACTPClient.d.ts.map +1 -0
  4. package/dist/ACTPClient.js +120 -0
  5. package/dist/ACTPClient.js.map +1 -0
  6. package/dist/abi/ACTPKernel.json +1340 -0
  7. package/dist/abi/ERC20.json +38 -0
  8. package/dist/abi/EscrowVault.json +64 -0
  9. package/dist/builders/DeliveryProofBuilder.d.ts +37 -0
  10. package/dist/builders/DeliveryProofBuilder.d.ts.map +1 -0
  11. package/dist/builders/DeliveryProofBuilder.js +165 -0
  12. package/dist/builders/DeliveryProofBuilder.js.map +1 -0
  13. package/dist/builders/QuoteBuilder.d.ts +68 -0
  14. package/dist/builders/QuoteBuilder.d.ts.map +1 -0
  15. package/dist/builders/QuoteBuilder.js +255 -0
  16. package/dist/builders/QuoteBuilder.js.map +1 -0
  17. package/dist/builders/index.d.ts +3 -0
  18. package/dist/builders/index.d.ts.map +1 -0
  19. package/dist/builders/index.js +10 -0
  20. package/dist/builders/index.js.map +1 -0
  21. package/dist/config/networks.d.ts +27 -0
  22. package/dist/config/networks.d.ts.map +1 -0
  23. package/dist/config/networks.js +103 -0
  24. package/dist/config/networks.js.map +1 -0
  25. package/dist/errors/index.d.ts +38 -0
  26. package/dist/errors/index.d.ts.map +1 -0
  27. package/dist/errors/index.js +87 -0
  28. package/dist/errors/index.js.map +1 -0
  29. package/dist/index.d.ts +19 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +68 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/protocol/ACTPKernel.d.ts +30 -0
  34. package/dist/protocol/ACTPKernel.d.ts.map +1 -0
  35. package/dist/protocol/ACTPKernel.js +261 -0
  36. package/dist/protocol/ACTPKernel.js.map +1 -0
  37. package/dist/protocol/EASHelper.d.ts +23 -0
  38. package/dist/protocol/EASHelper.d.ts.map +1 -0
  39. package/dist/protocol/EASHelper.js +106 -0
  40. package/dist/protocol/EASHelper.js.map +1 -0
  41. package/dist/protocol/EscrowVault.d.ts +24 -0
  42. package/dist/protocol/EscrowVault.d.ts.map +1 -0
  43. package/dist/protocol/EscrowVault.js +114 -0
  44. package/dist/protocol/EscrowVault.js.map +1 -0
  45. package/dist/protocol/EventMonitor.d.ts +18 -0
  46. package/dist/protocol/EventMonitor.d.ts.map +1 -0
  47. package/dist/protocol/EventMonitor.js +92 -0
  48. package/dist/protocol/EventMonitor.js.map +1 -0
  49. package/dist/protocol/MessageSigner.d.ts +23 -0
  50. package/dist/protocol/MessageSigner.d.ts.map +1 -0
  51. package/dist/protocol/MessageSigner.js +178 -0
  52. package/dist/protocol/MessageSigner.js.map +1 -0
  53. package/dist/protocol/ProofGenerator.d.ts +22 -0
  54. package/dist/protocol/ProofGenerator.d.ts.map +1 -0
  55. package/dist/protocol/ProofGenerator.js +64 -0
  56. package/dist/protocol/ProofGenerator.js.map +1 -0
  57. package/dist/protocol/QuoteBuilder.d.ts +2 -0
  58. package/dist/protocol/QuoteBuilder.d.ts.map +1 -0
  59. package/dist/protocol/QuoteBuilder.js +7 -0
  60. package/dist/protocol/QuoteBuilder.js.map +1 -0
  61. package/dist/types/eip712.d.ts +106 -0
  62. package/dist/types/eip712.d.ts.map +1 -0
  63. package/dist/types/eip712.js +84 -0
  64. package/dist/types/eip712.js.map +1 -0
  65. package/dist/types/escrow.d.ts +18 -0
  66. package/dist/types/escrow.d.ts.map +1 -0
  67. package/dist/types/escrow.js +3 -0
  68. package/dist/types/escrow.js.map +1 -0
  69. package/dist/types/index.d.ts +6 -0
  70. package/dist/types/index.d.ts.map +1 -0
  71. package/dist/types/index.js +22 -0
  72. package/dist/types/index.js.map +1 -0
  73. package/dist/types/message.d.ts +109 -0
  74. package/dist/types/message.d.ts.map +1 -0
  75. package/dist/types/message.js +3 -0
  76. package/dist/types/message.js.map +1 -0
  77. package/dist/types/state.d.ts +19 -0
  78. package/dist/types/state.d.ts.map +1 -0
  79. package/dist/types/state.js +49 -0
  80. package/dist/types/state.js.map +1 -0
  81. package/dist/types/transaction.d.ts +36 -0
  82. package/dist/types/transaction.d.ts.map +1 -0
  83. package/dist/types/transaction.js +3 -0
  84. package/dist/types/transaction.js.map +1 -0
  85. package/dist/utils/IPFSClient.d.ts +37 -0
  86. package/dist/utils/IPFSClient.d.ts.map +1 -0
  87. package/dist/utils/IPFSClient.js +128 -0
  88. package/dist/utils/IPFSClient.js.map +1 -0
  89. package/dist/utils/NonceManager.d.ts +34 -0
  90. package/dist/utils/NonceManager.d.ts.map +1 -0
  91. package/dist/utils/NonceManager.js +114 -0
  92. package/dist/utils/NonceManager.js.map +1 -0
  93. package/dist/utils/ReceivedNonceTracker.d.ts +35 -0
  94. package/dist/utils/ReceivedNonceTracker.d.ts.map +1 -0
  95. package/dist/utils/ReceivedNonceTracker.js +196 -0
  96. package/dist/utils/ReceivedNonceTracker.js.map +1 -0
  97. package/dist/utils/canonicalJson.d.ts +4 -0
  98. package/dist/utils/canonicalJson.d.ts.map +1 -0
  99. package/dist/utils/canonicalJson.js +21 -0
  100. package/dist/utils/canonicalJson.js.map +1 -0
  101. package/dist/utils/computeTypeHash.d.ts +3 -0
  102. package/dist/utils/computeTypeHash.d.ts.map +1 -0
  103. package/dist/utils/computeTypeHash.js +30 -0
  104. package/dist/utils/computeTypeHash.js.map +1 -0
  105. package/dist/utils/validation.d.ts +6 -0
  106. package/dist/utils/validation.d.ts.map +1 -0
  107. package/dist/utils/validation.js +46 -0
  108. package/dist/utils/validation.js.map +1 -0
  109. package/package.json +73 -0
  110. package/src/ACTPClient.ts +276 -0
  111. package/src/__tests__/ProofGenerator.test.ts +124 -0
  112. package/src/__tests__/QuoteBuilder.test.ts +516 -0
  113. package/src/__tests__/StateMachine.test.ts +82 -0
  114. package/src/__tests__/builders/DeliveryProofBuilder.test.ts +581 -0
  115. package/src/__tests__/integration/ACTPClient.test.ts +263 -0
  116. package/src/__tests__/integration.test.ts +289 -0
  117. package/src/__tests__/protocol/EASHelper.test.ts +472 -0
  118. package/src/__tests__/protocol/EventMonitor.test.ts +382 -0
  119. package/src/__tests__/security/ACTPKernel.security.test.ts +1167 -0
  120. package/src/__tests__/security/EscrowVault.security.test.ts +570 -0
  121. package/src/__tests__/security/MessageSigner.security.test.ts +286 -0
  122. package/src/__tests__/security/NonceReplay.security.test.ts +501 -0
  123. package/src/__tests__/security/validation.security.test.ts +376 -0
  124. package/src/__tests__/utils/IPFSClient.test.ts +262 -0
  125. package/src/__tests__/utils/NonceManager.test.ts +205 -0
  126. package/src/__tests__/utils/canonicalJson.test.ts +153 -0
  127. package/src/abi/ACTPKernel.json +1340 -0
  128. package/src/abi/ERC20.json +40 -0
  129. package/src/abi/EscrowVault.json +66 -0
  130. package/src/builders/DeliveryProofBuilder.ts +326 -0
  131. package/src/builders/QuoteBuilder.ts +483 -0
  132. package/src/builders/index.ts +17 -0
  133. package/src/config/networks.ts +165 -0
  134. package/src/errors/index.ts +130 -0
  135. package/src/index.ts +108 -0
  136. package/src/protocol/ACTPKernel.ts +625 -0
  137. package/src/protocol/EASHelper.ts +197 -0
  138. package/src/protocol/EscrowVault.ts +237 -0
  139. package/src/protocol/EventMonitor.ts +161 -0
  140. package/src/protocol/MessageSigner.ts +336 -0
  141. package/src/protocol/ProofGenerator.ts +119 -0
  142. package/src/protocol/QuoteBuilder.ts +15 -0
  143. package/src/types/eip712.ts +175 -0
  144. package/src/types/escrow.ts +26 -0
  145. package/src/types/index.ts +10 -0
  146. package/src/types/message.ts +145 -0
  147. package/src/types/state.ts +77 -0
  148. package/src/types/transaction.ts +54 -0
  149. package/src/utils/IPFSClient.ts +248 -0
  150. package/src/utils/NonceManager.ts +293 -0
  151. package/src/utils/ReceivedNonceTracker.ts +397 -0
  152. package/src/utils/canonicalJson.ts +38 -0
  153. package/src/utils/computeTypeHash.ts +50 -0
  154. package/src/utils/validation.ts +82 -0
@@ -0,0 +1,501 @@
1
+ /**
2
+ * Nonce Replay Attack Prevention Test Suite
3
+ *
4
+ * V4 Security Vulnerability: EIP-712 Replay Attack via Nonce Reuse
5
+ *
6
+ * Coverage Target: 100% (security-critical code)
7
+ *
8
+ * Test Categories:
9
+ * 1. ReceivedNonceTracker - Memory-Efficient Strategy (7 tests)
10
+ * 2. ReceivedNonceTracker - Set-Based Strategy (6 tests)
11
+ * 3. MessageSigner Integration - Replay Prevention (8 tests)
12
+ *
13
+ * Attack Vectors Tested:
14
+ * - Duplicate nonce replay (same message sent twice)
15
+ * - Old nonce replay (replay with lower nonce)
16
+ * - Out-of-order nonce handling
17
+ * - Cross-sender nonce isolation
18
+ * - Cross-message-type nonce isolation
19
+ *
20
+ * References:
21
+ * - ReceivedNonceTracker.ts implementation
22
+ * - MessageSigner.ts nonce validation
23
+ * - V4 vulnerability documentation
24
+ */
25
+
26
+ import { Wallet } from 'ethers';
27
+ import { MessageSigner } from '../../protocol/MessageSigner';
28
+ import {
29
+ InMemoryReceivedNonceTracker,
30
+ SetBasedReceivedNonceTracker,
31
+ createReceivedNonceTracker
32
+ } from '../../utils/ReceivedNonceTracker';
33
+ import { ACTPMessage } from '../../types';
34
+
35
+ // Test signers
36
+ const testPrivateKey1 = '0x' + '1'.repeat(64);
37
+ const testPrivateKey2 = '0x' + '2'.repeat(64);
38
+ const signer1 = new Wallet(testPrivateKey1);
39
+ const signer2 = new Wallet(testPrivateKey2);
40
+
41
+ const TEST_KERNEL_ADDRESS = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
42
+
43
+ describe('ReceivedNonceTracker - Memory-Efficient Strategy', () => {
44
+ let tracker: InMemoryReceivedNonceTracker;
45
+
46
+ beforeEach(() => {
47
+ tracker = new InMemoryReceivedNonceTracker();
48
+ });
49
+
50
+ it('should accept first nonce from sender', () => {
51
+ const sender = `did:ethr:${signer1.address}`;
52
+ const messageType = 'test.message';
53
+ const nonce = '0x' + '0'.repeat(63) + '1';
54
+
55
+ const result = tracker.validateAndRecord(sender, messageType, nonce);
56
+
57
+ expect(result.valid).toBe(true);
58
+ expect(tracker.getHighestNonce(sender, messageType)).toBe(nonce);
59
+ });
60
+
61
+ it('should accept monotonically increasing nonces', () => {
62
+ const sender = `did:ethr:${signer1.address}`;
63
+ const messageType = 'test.message';
64
+
65
+ // Nonce 1
66
+ const nonce1 = '0x' + '0'.repeat(63) + '1';
67
+ const result1 = tracker.validateAndRecord(sender, messageType, nonce1);
68
+ expect(result1.valid).toBe(true);
69
+
70
+ // Nonce 2
71
+ const nonce2 = '0x' + '0'.repeat(63) + '2';
72
+ const result2 = tracker.validateAndRecord(sender, messageType, nonce2);
73
+ expect(result2.valid).toBe(true);
74
+
75
+ // Nonce 10
76
+ const nonce10 = '0x' + '0'.repeat(63) + 'a'; // hex 'a' = 10
77
+ const result10 = tracker.validateAndRecord(sender, messageType, nonce10);
78
+ expect(result10.valid).toBe(true);
79
+ });
80
+
81
+ it('should reject duplicate nonce (exact replay)', () => {
82
+ const sender = `did:ethr:${signer1.address}`;
83
+ const messageType = 'test.message';
84
+ const nonce = '0x' + '0'.repeat(63) + '5';
85
+
86
+ // First use - accepted
87
+ const result1 = tracker.validateAndRecord(sender, messageType, nonce);
88
+ expect(result1.valid).toBe(true);
89
+
90
+ // Duplicate - rejected
91
+ const result2 = tracker.validateAndRecord(sender, messageType, nonce);
92
+ expect(result2.valid).toBe(false);
93
+ expect(result2.reason).toContain('Nonce replay detected');
94
+ expect(result2.receivedNonce).toBe(nonce);
95
+ });
96
+
97
+ it('should reject old nonce (lower than highest seen)', () => {
98
+ const sender = `did:ethr:${signer1.address}`;
99
+ const messageType = 'test.message';
100
+
101
+ // Nonce 10
102
+ const nonce10 = '0x' + '0'.repeat(63) + 'a';
103
+ const result10 = tracker.validateAndRecord(sender, messageType, nonce10);
104
+ expect(result10.valid).toBe(true);
105
+
106
+ // Nonce 5 (old replay attempt)
107
+ const nonce5 = '0x' + '0'.repeat(63) + '5';
108
+ const result5 = tracker.validateAndRecord(sender, messageType, nonce5);
109
+ expect(result5.valid).toBe(false);
110
+ expect(result5.reason).toContain('Nonce replay detected');
111
+ });
112
+
113
+ it('should isolate nonces by sender', () => {
114
+ const sender1 = `did:ethr:${signer1.address}`;
115
+ const sender2 = `did:ethr:${signer2.address}`;
116
+ const messageType = 'test.message';
117
+ const nonce = '0x' + '0'.repeat(63) + '1';
118
+
119
+ // Sender 1 uses nonce 1
120
+ const result1 = tracker.validateAndRecord(sender1, messageType, nonce);
121
+ expect(result1.valid).toBe(true);
122
+
123
+ // Sender 2 can also use nonce 1 (different sender)
124
+ const result2 = tracker.validateAndRecord(sender2, messageType, nonce);
125
+ expect(result2.valid).toBe(true);
126
+ });
127
+
128
+ it('should isolate nonces by message type', () => {
129
+ const sender = `did:ethr:${signer1.address}`;
130
+ const messageType1 = 'test.message.v1';
131
+ const messageType2 = 'test.message.v2';
132
+ const nonce = '0x' + '0'.repeat(63) + '1';
133
+
134
+ // Message type 1 uses nonce 1
135
+ const result1 = tracker.validateAndRecord(sender, messageType1, nonce);
136
+ expect(result1.valid).toBe(true);
137
+
138
+ // Message type 2 can also use nonce 1 (different type)
139
+ const result2 = tracker.validateAndRecord(sender, messageType2, nonce);
140
+ expect(result2.valid).toBe(true);
141
+ });
142
+
143
+ it('should reject invalid nonce format', () => {
144
+ const sender = `did:ethr:${signer1.address}`;
145
+ const messageType = 'test.message';
146
+
147
+ // Too short
148
+ const result1 = tracker.validateAndRecord(sender, messageType, '0x123');
149
+ expect(result1.valid).toBe(false);
150
+ expect(result1.reason).toContain('Invalid nonce format');
151
+
152
+ // Not hex
153
+ const result2 = tracker.validateAndRecord(sender, messageType, 'invalid');
154
+ expect(result2.valid).toBe(false);
155
+ expect(result2.reason).toContain('Invalid nonce format');
156
+ });
157
+ });
158
+
159
+ describe('ReceivedNonceTracker - Set-Based Strategy', () => {
160
+ let tracker: SetBasedReceivedNonceTracker;
161
+
162
+ beforeEach(() => {
163
+ tracker = new SetBasedReceivedNonceTracker();
164
+ });
165
+
166
+ it('should accept first nonce from sender', () => {
167
+ const sender = `did:ethr:${signer1.address}`;
168
+ const messageType = 'test.message';
169
+ const nonce = '0x' + '0'.repeat(63) + '1';
170
+
171
+ const result = tracker.validateAndRecord(sender, messageType, nonce);
172
+
173
+ expect(result.valid).toBe(true);
174
+ expect(tracker.hasBeenUsed(sender, messageType, nonce)).toBe(true);
175
+ });
176
+
177
+ it('should accept out-of-order nonces', () => {
178
+ const sender = `did:ethr:${signer1.address}`;
179
+ const messageType = 'test.message';
180
+
181
+ // Nonce 10 first
182
+ const nonce10 = '0x' + '0'.repeat(63) + 'a';
183
+ const result10 = tracker.validateAndRecord(sender, messageType, nonce10);
184
+ expect(result10.valid).toBe(true);
185
+
186
+ // Nonce 5 (lower, but still accepted in set-based strategy)
187
+ const nonce5 = '0x' + '0'.repeat(63) + '5';
188
+ const result5 = tracker.validateAndRecord(sender, messageType, nonce5);
189
+ expect(result5.valid).toBe(true); // Set-based allows out-of-order
190
+
191
+ // Nonce 15
192
+ const nonce15 = '0x' + '0'.repeat(63) + 'f';
193
+ const result15 = tracker.validateAndRecord(sender, messageType, nonce15);
194
+ expect(result15.valid).toBe(true);
195
+ });
196
+
197
+ it('should reject duplicate nonce (exact replay)', () => {
198
+ const sender = `did:ethr:${signer1.address}`;
199
+ const messageType = 'test.message';
200
+ const nonce = '0x' + '0'.repeat(63) + '5';
201
+
202
+ // First use - accepted
203
+ const result1 = tracker.validateAndRecord(sender, messageType, nonce);
204
+ expect(result1.valid).toBe(true);
205
+
206
+ // Duplicate - rejected
207
+ const result2 = tracker.validateAndRecord(sender, messageType, nonce);
208
+ expect(result2.valid).toBe(false);
209
+ expect(result2.reason).toContain('Nonce replay detected');
210
+ });
211
+
212
+ it('should track nonce count correctly', () => {
213
+ const sender = `did:ethr:${signer1.address}`;
214
+ const messageType = 'test.message';
215
+
216
+ expect(tracker.getNonceCount(sender, messageType)).toBe(0);
217
+
218
+ tracker.validateAndRecord(sender, messageType, '0x' + '0'.repeat(63) + '1');
219
+ expect(tracker.getNonceCount(sender, messageType)).toBe(1);
220
+
221
+ tracker.validateAndRecord(sender, messageType, '0x' + '0'.repeat(63) + '2');
222
+ expect(tracker.getNonceCount(sender, messageType)).toBe(2);
223
+
224
+ tracker.validateAndRecord(sender, messageType, '0x' + '0'.repeat(63) + '3');
225
+ expect(tracker.getNonceCount(sender, messageType)).toBe(3);
226
+ });
227
+
228
+ it('should cleanup old nonces', () => {
229
+ const sender = `did:ethr:${signer1.address}`;
230
+ const messageType = 'test.message';
231
+
232
+ // Add 10 nonces
233
+ for (let i = 1; i <= 10; i++) {
234
+ const nonce = '0x' + '0'.repeat(64 - i.toString(16).length) + i.toString(16);
235
+ tracker.validateAndRecord(sender, messageType, nonce);
236
+ }
237
+
238
+ expect(tracker.getNonceCount(sender, messageType)).toBe(10);
239
+
240
+ // Cleanup, keep only last 5
241
+ tracker.cleanup(sender, messageType, 5);
242
+
243
+ expect(tracker.getNonceCount(sender, messageType)).toBe(5);
244
+
245
+ // Oldest nonces should be removed
246
+ expect(tracker.hasBeenUsed(sender, messageType, '0x' + '0'.repeat(63) + '1')).toBe(false);
247
+ expect(tracker.hasBeenUsed(sender, messageType, '0x' + '0'.repeat(63) + '6')).toBe(true);
248
+ });
249
+
250
+ it('should compute highest nonce correctly', () => {
251
+ const sender = `did:ethr:${signer1.address}`;
252
+ const messageType = 'test.message';
253
+
254
+ // Add nonces out of order
255
+ tracker.validateAndRecord(sender, messageType, '0x' + '0'.repeat(63) + '5');
256
+ tracker.validateAndRecord(sender, messageType, '0x' + '0'.repeat(63) + '3');
257
+ tracker.validateAndRecord(sender, messageType, '0x' + '0'.repeat(63) + 'a'); // 10
258
+
259
+ const highestNonce = tracker.getHighestNonce(sender, messageType);
260
+ expect(highestNonce).toBe('0x' + '0'.repeat(63) + 'a'); // 10 is highest
261
+ });
262
+ });
263
+
264
+ describe('MessageSigner - Nonce Replay Attack Prevention', () => {
265
+ let messageSigner: MessageSigner;
266
+ let nonceTracker: InMemoryReceivedNonceTracker;
267
+
268
+ beforeEach(async () => {
269
+ nonceTracker = new InMemoryReceivedNonceTracker();
270
+ messageSigner = new MessageSigner(signer1, nonceTracker);
271
+ await messageSigner.initDomain(TEST_KERNEL_ADDRESS, 84532);
272
+ });
273
+
274
+ it('should accept message with valid nonce on first verification', async () => {
275
+ const message: ACTPMessage = {
276
+ type: 'test.message',
277
+ version: '1.0.0',
278
+ from: `did:ethr:${signer1.address}`,
279
+ to: `did:ethr:${signer2.address}`,
280
+ timestamp: Math.floor(Date.now() / 1000),
281
+ nonce: '0x' + '0'.repeat(63) + '1',
282
+ data: { test: 'value' }
283
+ };
284
+
285
+ const signature = await messageSigner.signMessage(message);
286
+ const isValid = await messageSigner.verifySignature(message, signature);
287
+
288
+ expect(isValid).toBe(true);
289
+ });
290
+
291
+ it('should reject replayed message (same nonce twice)', async () => {
292
+ const message: ACTPMessage = {
293
+ type: 'test.message',
294
+ version: '1.0.0',
295
+ from: `did:ethr:${signer1.address}`,
296
+ to: `did:ethr:${signer2.address}`,
297
+ timestamp: Math.floor(Date.now() / 1000),
298
+ nonce: '0x' + '0'.repeat(63) + '1',
299
+ data: { test: 'value' }
300
+ };
301
+
302
+ const signature = await messageSigner.signMessage(message);
303
+
304
+ // First verification - accepted
305
+ const isValid1 = await messageSigner.verifySignature(message, signature);
306
+ expect(isValid1).toBe(true);
307
+
308
+ // Second verification (replay) - rejected
309
+ const isValid2 = await messageSigner.verifySignature(message, signature);
310
+ expect(isValid2).toBe(false);
311
+ });
312
+
313
+ it('should reject old nonce replay attack', async () => {
314
+ const sender = `did:ethr:${signer1.address}`;
315
+
316
+ // Send message with nonce 10
317
+ const message10: ACTPMessage = {
318
+ type: 'test.message',
319
+ version: '1.0.0',
320
+ from: sender,
321
+ to: `did:ethr:${signer2.address}`,
322
+ timestamp: Math.floor(Date.now() / 1000),
323
+ nonce: '0x' + '0'.repeat(63) + 'a', // nonce 10
324
+ data: { test: 'value10' }
325
+ };
326
+
327
+ const signature10 = await messageSigner.signMessage(message10);
328
+ const isValid10 = await messageSigner.verifySignature(message10, signature10);
329
+ expect(isValid10).toBe(true);
330
+
331
+ // Attempt to send message with nonce 5 (old replay)
332
+ const message5: ACTPMessage = {
333
+ type: 'test.message',
334
+ version: '1.0.0',
335
+ from: sender,
336
+ to: `did:ethr:${signer2.address}`,
337
+ timestamp: Math.floor(Date.now() / 1000),
338
+ nonce: '0x' + '0'.repeat(63) + '5', // nonce 5 (old)
339
+ data: { test: 'value5' }
340
+ };
341
+
342
+ // Create fresh MessageSigner to sign with signer1 (to get valid signature)
343
+ const tempSigner = new MessageSigner(signer1);
344
+ await tempSigner.initDomain(TEST_KERNEL_ADDRESS, 84532);
345
+ const signature5 = await tempSigner.signMessage(message5);
346
+
347
+ // Verify with tracker-enabled MessageSigner - should reject
348
+ const isValid5 = await messageSigner.verifySignature(message5, signature5);
349
+ expect(isValid5).toBe(false);
350
+ });
351
+
352
+ it('should accept monotonically increasing nonces', async () => {
353
+ const sender = `did:ethr:${signer1.address}`;
354
+
355
+ for (let i = 1; i <= 5; i++) {
356
+ const message: ACTPMessage = {
357
+ type: 'test.message',
358
+ version: '1.0.0',
359
+ from: sender,
360
+ to: `did:ethr:${signer2.address}`,
361
+ timestamp: Math.floor(Date.now() / 1000),
362
+ nonce: '0x' + '0'.repeat(64 - i.toString(16).length) + i.toString(16),
363
+ data: { test: `value${i}` }
364
+ };
365
+
366
+ const signature = await messageSigner.signMessage(message);
367
+ const isValid = await messageSigner.verifySignature(message, signature);
368
+
369
+ expect(isValid).toBe(true);
370
+ }
371
+ });
372
+
373
+ it('should isolate nonces by sender (cross-sender replay protection)', async () => {
374
+ // Create tracker-enabled signers for both
375
+ const nonceTracker1 = new InMemoryReceivedNonceTracker();
376
+ const signer1MessageSigner = new MessageSigner(signer1, nonceTracker1);
377
+ await signer1MessageSigner.initDomain(TEST_KERNEL_ADDRESS, 84532);
378
+
379
+ const sender1 = `did:ethr:${signer1.address}`;
380
+ const sender2 = `did:ethr:${signer2.address}`;
381
+
382
+ // Sender 1 sends message with nonce 1
383
+ const message1: ACTPMessage = {
384
+ type: 'test.message',
385
+ version: '1.0.0',
386
+ from: sender1,
387
+ to: sender2,
388
+ timestamp: Math.floor(Date.now() / 1000),
389
+ nonce: '0x' + '0'.repeat(63) + '1',
390
+ data: { from: 'sender1' }
391
+ };
392
+
393
+ const signature1 = await signer1MessageSigner.signMessage(message1);
394
+ const isValid1 = await signer1MessageSigner.verifySignature(message1, signature1);
395
+ expect(isValid1).toBe(true);
396
+
397
+ // Sender 2 can send message with same nonce 1 (different sender)
398
+ const message2: ACTPMessage = {
399
+ type: 'test.message',
400
+ version: '1.0.0',
401
+ from: sender2,
402
+ to: sender1,
403
+ timestamp: Math.floor(Date.now() / 1000),
404
+ nonce: '0x' + '0'.repeat(63) + '1', // Same nonce, different sender
405
+ data: { from: 'sender2' }
406
+ };
407
+
408
+ const signer2MessageSigner = new MessageSigner(signer2, nonceTracker1);
409
+ await signer2MessageSigner.initDomain(TEST_KERNEL_ADDRESS, 84532);
410
+ const signature2 = await signer2MessageSigner.signMessage(message2);
411
+ const isValid2 = await signer1MessageSigner.verifySignature(message2, signature2);
412
+ expect(isValid2).toBe(true);
413
+ });
414
+
415
+ it('should throw descriptive error for replay attack (verifySignatureOrThrow)', async () => {
416
+ const message: ACTPMessage = {
417
+ type: 'test.message',
418
+ version: '1.0.0',
419
+ from: `did:ethr:${signer1.address}`,
420
+ to: `did:ethr:${signer2.address}`,
421
+ timestamp: Math.floor(Date.now() / 1000),
422
+ nonce: '0x' + '0'.repeat(63) + '1',
423
+ data: { test: 'value' }
424
+ };
425
+
426
+ const signature = await messageSigner.signMessage(message);
427
+
428
+ // First verification - succeeds
429
+ await expect(messageSigner.verifySignatureOrThrow(message, signature)).resolves.not.toThrow();
430
+
431
+ // Second verification (replay) - throws
432
+ await expect(messageSigner.verifySignatureOrThrow(message, signature))
433
+ .rejects
434
+ .toThrow(/Nonce replay attack detected/);
435
+ });
436
+
437
+ it('should work without nonce tracker (backward compatibility)', async () => {
438
+ // Create MessageSigner WITHOUT nonce tracker
439
+ const signerWithoutTracker = new MessageSigner(signer1);
440
+ await signerWithoutTracker.initDomain(TEST_KERNEL_ADDRESS, 84532);
441
+
442
+ const message: ACTPMessage = {
443
+ type: 'test.message',
444
+ version: '1.0.0',
445
+ from: `did:ethr:${signer1.address}`,
446
+ to: `did:ethr:${signer2.address}`,
447
+ timestamp: Math.floor(Date.now() / 1000),
448
+ nonce: '0x' + '0'.repeat(63) + '1',
449
+ data: { test: 'value' }
450
+ };
451
+
452
+ const signature = await signerWithoutTracker.signMessage(message);
453
+
454
+ // Without tracker, same message can be verified multiple times
455
+ const isValid1 = await signerWithoutTracker.verifySignature(message, signature);
456
+ expect(isValid1).toBe(true);
457
+
458
+ const isValid2 = await signerWithoutTracker.verifySignature(message, signature);
459
+ expect(isValid2).toBe(true); // Still valid (no replay protection)
460
+ });
461
+
462
+ it('should handle invalid signature with nonce tracker enabled', async () => {
463
+ const message: ACTPMessage = {
464
+ type: 'test.message',
465
+ version: '1.0.0',
466
+ from: `did:ethr:${signer1.address}`,
467
+ to: `did:ethr:${signer2.address}`,
468
+ timestamp: Math.floor(Date.now() / 1000),
469
+ nonce: '0x' + '0'.repeat(63) + '1',
470
+ data: { test: 'value' }
471
+ };
472
+
473
+ // Sign with signer1 but claim it's from signer2 (invalid signature)
474
+ const invalidMessage = { ...message, from: `did:ethr:${signer2.address}` };
475
+ const signature = await messageSigner.signMessage(message);
476
+
477
+ // Should reject due to invalid signature (before nonce check)
478
+ const isValid = await messageSigner.verifySignature(invalidMessage, signature);
479
+ expect(isValid).toBe(false);
480
+
481
+ // Nonce should NOT be recorded for invalid signatures
482
+ expect(nonceTracker.getHighestNonce(`did:ethr:${signer2.address}`, 'test.message')).toBeNull();
483
+ });
484
+ });
485
+
486
+ describe('ReceivedNonceTracker - Factory Function', () => {
487
+ it('should create memory-efficient tracker by default', () => {
488
+ const tracker = createReceivedNonceTracker();
489
+ expect(tracker).toBeInstanceOf(InMemoryReceivedNonceTracker);
490
+ });
491
+
492
+ it('should create memory-efficient tracker explicitly', () => {
493
+ const tracker = createReceivedNonceTracker('memory-efficient');
494
+ expect(tracker).toBeInstanceOf(InMemoryReceivedNonceTracker);
495
+ });
496
+
497
+ it('should create set-based tracker', () => {
498
+ const tracker = createReceivedNonceTracker('set-based');
499
+ expect(tracker).toBeInstanceOf(SetBasedReceivedNonceTracker);
500
+ });
501
+ });