@claw-network/node 0.2.0

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 (190) hide show
  1. package/README.md +49 -0
  2. package/dist/api/api-key-store.d.ts +74 -0
  3. package/dist/api/api-key-store.d.ts.map +1 -0
  4. package/dist/api/api-key-store.js +170 -0
  5. package/dist/api/api-key-store.js.map +1 -0
  6. package/dist/api/auth.d.ts +30 -0
  7. package/dist/api/auth.d.ts.map +1 -0
  8. package/dist/api/auth.js +115 -0
  9. package/dist/api/auth.js.map +1 -0
  10. package/dist/api/legacy.d.ts +26 -0
  11. package/dist/api/legacy.d.ts.map +1 -0
  12. package/dist/api/legacy.js +281 -0
  13. package/dist/api/legacy.js.map +1 -0
  14. package/dist/api/middleware.d.ts +35 -0
  15. package/dist/api/middleware.d.ts.map +1 -0
  16. package/dist/api/middleware.js +75 -0
  17. package/dist/api/middleware.js.map +1 -0
  18. package/dist/api/response.d.ts +85 -0
  19. package/dist/api/response.d.ts.map +1 -0
  20. package/dist/api/response.js +185 -0
  21. package/dist/api/response.js.map +1 -0
  22. package/dist/api/router.d.ts +45 -0
  23. package/dist/api/router.d.ts.map +1 -0
  24. package/dist/api/router.js +183 -0
  25. package/dist/api/router.js.map +1 -0
  26. package/dist/api/routes/admin.d.ts +11 -0
  27. package/dist/api/routes/admin.d.ts.map +1 -0
  28. package/dist/api/routes/admin.js +124 -0
  29. package/dist/api/routes/admin.js.map +1 -0
  30. package/dist/api/routes/contracts.d.ts +7 -0
  31. package/dist/api/routes/contracts.d.ts.map +1 -0
  32. package/dist/api/routes/contracts.js +665 -0
  33. package/dist/api/routes/contracts.js.map +1 -0
  34. package/dist/api/routes/dao.d.ts +7 -0
  35. package/dist/api/routes/dao.d.ts.map +1 -0
  36. package/dist/api/routes/dao.js +549 -0
  37. package/dist/api/routes/dao.js.map +1 -0
  38. package/dist/api/routes/dev.d.ts +9 -0
  39. package/dist/api/routes/dev.d.ts.map +1 -0
  40. package/dist/api/routes/dev.js +273 -0
  41. package/dist/api/routes/dev.js.map +1 -0
  42. package/dist/api/routes/escrows.d.ts +7 -0
  43. package/dist/api/routes/escrows.d.ts.map +1 -0
  44. package/dist/api/routes/escrows.js +454 -0
  45. package/dist/api/routes/escrows.js.map +1 -0
  46. package/dist/api/routes/identities.d.ts +7 -0
  47. package/dist/api/routes/identities.d.ts.map +1 -0
  48. package/dist/api/routes/identities.js +245 -0
  49. package/dist/api/routes/identities.js.map +1 -0
  50. package/dist/api/routes/markets-capabilities.d.ts +7 -0
  51. package/dist/api/routes/markets-capabilities.d.ts.map +1 -0
  52. package/dist/api/routes/markets-capabilities.js +477 -0
  53. package/dist/api/routes/markets-capabilities.js.map +1 -0
  54. package/dist/api/routes/markets-disputes.d.ts +7 -0
  55. package/dist/api/routes/markets-disputes.d.ts.map +1 -0
  56. package/dist/api/routes/markets-disputes.js +102 -0
  57. package/dist/api/routes/markets-disputes.js.map +1 -0
  58. package/dist/api/routes/markets-info.d.ts +7 -0
  59. package/dist/api/routes/markets-info.d.ts.map +1 -0
  60. package/dist/api/routes/markets-info.js +523 -0
  61. package/dist/api/routes/markets-info.js.map +1 -0
  62. package/dist/api/routes/markets-search.d.ts +7 -0
  63. package/dist/api/routes/markets-search.d.ts.map +1 -0
  64. package/dist/api/routes/markets-search.js +38 -0
  65. package/dist/api/routes/markets-search.js.map +1 -0
  66. package/dist/api/routes/markets-tasks.d.ts +7 -0
  67. package/dist/api/routes/markets-tasks.d.ts.map +1 -0
  68. package/dist/api/routes/markets-tasks.js +539 -0
  69. package/dist/api/routes/markets-tasks.js.map +1 -0
  70. package/dist/api/routes/node.d.ts +7 -0
  71. package/dist/api/routes/node.d.ts.map +1 -0
  72. package/dist/api/routes/node.js +53 -0
  73. package/dist/api/routes/node.js.map +1 -0
  74. package/dist/api/routes/nonce.d.ts +10 -0
  75. package/dist/api/routes/nonce.d.ts.map +1 -0
  76. package/dist/api/routes/nonce.js +65 -0
  77. package/dist/api/routes/nonce.js.map +1 -0
  78. package/dist/api/routes/reputations.d.ts +7 -0
  79. package/dist/api/routes/reputations.d.ts.map +1 -0
  80. package/dist/api/routes/reputations.js +243 -0
  81. package/dist/api/routes/reputations.js.map +1 -0
  82. package/dist/api/routes/transfers.d.ts +7 -0
  83. package/dist/api/routes/transfers.d.ts.map +1 -0
  84. package/dist/api/routes/transfers.js +88 -0
  85. package/dist/api/routes/transfers.js.map +1 -0
  86. package/dist/api/routes/wallets.d.ts +7 -0
  87. package/dist/api/routes/wallets.d.ts.map +1 -0
  88. package/dist/api/routes/wallets.js +132 -0
  89. package/dist/api/routes/wallets.js.map +1 -0
  90. package/dist/api/schemas/common.d.ts +45 -0
  91. package/dist/api/schemas/common.d.ts.map +1 -0
  92. package/dist/api/schemas/common.js +30 -0
  93. package/dist/api/schemas/common.js.map +1 -0
  94. package/dist/api/schemas/contracts.d.ts +284 -0
  95. package/dist/api/schemas/contracts.d.ts.map +1 -0
  96. package/dist/api/schemas/contracts.js +79 -0
  97. package/dist/api/schemas/contracts.js.map +1 -0
  98. package/dist/api/schemas/dao.d.ts +271 -0
  99. package/dist/api/schemas/dao.d.ts.map +1 -0
  100. package/dist/api/schemas/dao.js +78 -0
  101. package/dist/api/schemas/dao.js.map +1 -0
  102. package/dist/api/schemas/identity.d.ts +75 -0
  103. package/dist/api/schemas/identity.d.ts.map +1 -0
  104. package/dist/api/schemas/identity.js +32 -0
  105. package/dist/api/schemas/identity.js.map +1 -0
  106. package/dist/api/schemas/markets.d.ts +822 -0
  107. package/dist/api/schemas/markets.d.ts.map +1 -0
  108. package/dist/api/schemas/markets.js +246 -0
  109. package/dist/api/schemas/markets.js.map +1 -0
  110. package/dist/api/schemas/wallet.d.ts +163 -0
  111. package/dist/api/schemas/wallet.d.ts.map +1 -0
  112. package/dist/api/schemas/wallet.js +54 -0
  113. package/dist/api/schemas/wallet.js.map +1 -0
  114. package/dist/api/server.d.ts +45 -0
  115. package/dist/api/server.d.ts.map +1 -0
  116. package/dist/api/server.js +131 -0
  117. package/dist/api/server.js.map +1 -0
  118. package/dist/api/types.d.ts +69 -0
  119. package/dist/api/types.d.ts.map +1 -0
  120. package/dist/api/types.js +196 -0
  121. package/dist/api/types.js.map +1 -0
  122. package/dist/daemon.d.ts +11 -0
  123. package/dist/daemon.d.ts.map +1 -0
  124. package/dist/daemon.js +248 -0
  125. package/dist/daemon.js.map +1 -0
  126. package/dist/index.d.ts +137 -0
  127. package/dist/index.d.ts.map +1 -0
  128. package/dist/index.js +795 -0
  129. package/dist/index.js.map +1 -0
  130. package/dist/indexer/index.d.ts +10 -0
  131. package/dist/indexer/index.d.ts.map +1 -0
  132. package/dist/indexer/index.js +7 -0
  133. package/dist/indexer/index.js.map +1 -0
  134. package/dist/indexer/indexer.d.ts +60 -0
  135. package/dist/indexer/indexer.d.ts.map +1 -0
  136. package/dist/indexer/indexer.js +408 -0
  137. package/dist/indexer/indexer.js.map +1 -0
  138. package/dist/indexer/query.d.ts +141 -0
  139. package/dist/indexer/query.d.ts.map +1 -0
  140. package/dist/indexer/query.js +244 -0
  141. package/dist/indexer/query.js.map +1 -0
  142. package/dist/indexer/store.d.ts +95 -0
  143. package/dist/indexer/store.d.ts.map +1 -0
  144. package/dist/indexer/store.js +250 -0
  145. package/dist/indexer/store.js.map +1 -0
  146. package/dist/logger.d.ts +13 -0
  147. package/dist/logger.d.ts.map +1 -0
  148. package/dist/logger.js +37 -0
  149. package/dist/logger.js.map +1 -0
  150. package/dist/p2p/sync.d.ts +105 -0
  151. package/dist/p2p/sync.d.ts.map +1 -0
  152. package/dist/p2p/sync.js +875 -0
  153. package/dist/p2p/sync.js.map +1 -0
  154. package/dist/policy/liquidity-policy.d.ts +17 -0
  155. package/dist/policy/liquidity-policy.d.ts.map +1 -0
  156. package/dist/policy/liquidity-policy.js +112 -0
  157. package/dist/policy/liquidity-policy.js.map +1 -0
  158. package/dist/services/chain-config.d.ts +226 -0
  159. package/dist/services/chain-config.d.ts.map +1 -0
  160. package/dist/services/chain-config.js +105 -0
  161. package/dist/services/chain-config.js.map +1 -0
  162. package/dist/services/contract-provider.d.ts +44 -0
  163. package/dist/services/contract-provider.d.ts.map +1 -0
  164. package/dist/services/contract-provider.js +167 -0
  165. package/dist/services/contract-provider.js.map +1 -0
  166. package/dist/services/contracts-service.d.ts +192 -0
  167. package/dist/services/contracts-service.d.ts.map +1 -0
  168. package/dist/services/contracts-service.js +336 -0
  169. package/dist/services/contracts-service.js.map +1 -0
  170. package/dist/services/dao-service.d.ts +245 -0
  171. package/dist/services/dao-service.d.ts.map +1 -0
  172. package/dist/services/dao-service.js +389 -0
  173. package/dist/services/dao-service.js.map +1 -0
  174. package/dist/services/identity-service.d.ts +150 -0
  175. package/dist/services/identity-service.d.ts.map +1 -0
  176. package/dist/services/identity-service.js +286 -0
  177. package/dist/services/identity-service.js.map +1 -0
  178. package/dist/services/index.d.ts +20 -0
  179. package/dist/services/index.d.ts.map +1 -0
  180. package/dist/services/index.js +15 -0
  181. package/dist/services/index.js.map +1 -0
  182. package/dist/services/reputation-service.d.ts +128 -0
  183. package/dist/services/reputation-service.d.ts.map +1 -0
  184. package/dist/services/reputation-service.js +204 -0
  185. package/dist/services/reputation-service.js.map +1 -0
  186. package/dist/services/wallet-service.d.ts +201 -0
  187. package/dist/services/wallet-service.d.ts.map +1 -0
  188. package/dist/services/wallet-service.js +402 -0
  189. package/dist/services/wallet-service.js.map +1 -0
  190. package/package.json +66 -0
@@ -0,0 +1,875 @@
1
+ import { bytesToUtf8, canonicalizeBytes, concatBytes, eventHashHex, hexToBytes, MAX_CLOCK_SKEW_MS, TOPIC_EVENTS, TOPIC_MARKETS, TOPIC_REQUESTS, TOPIC_RESPONSES, verifySnapshotHash, verifySnapshotSignatures, } from '@claw-network/core';
2
+ import { CONTENT_TYPE, RequestType, ResponseType, decodeP2PEnvelopeBytes, decodeRequestMessageBytes, decodeResponseMessageBytes, encodeP2PEnvelopeBytes, encodeRequestMessageBytes, encodeResponseMessageBytes, powTicketHashHex, signP2PEnvelope, verifyPeerRotateNewSignature, verifyPeerRotateOldSignature, verifyPowTicketSignature, verifyP2PEnvelopeSignature, verifyStakeProofControllerSignature, verifyStakeProofPeerSignature, } from '@claw-network/protocol';
3
+ export const DEFAULT_P2P_SYNC_CONFIG = {
4
+ maxEnvelopeBytes: 1_000_000,
5
+ maxRangeLimit: 256,
6
+ maxRangeBytes: 900_000,
7
+ maxSnapshotBytes: 900_000,
8
+ maxSnapshotTotalBytes: 8_000_000,
9
+ minSnapshotSignatures: 2,
10
+ rateLimitWindowMs: 60_000,
11
+ maxMessagesPerWindow: 200,
12
+ maxBytesPerWindow: 2_000_000,
13
+ minPeerScore: -10,
14
+ scoreIncrease: 1,
15
+ scoreDecrease: 1,
16
+ scoreDecayMs: 10 * 60 * 1000,
17
+ sybilPolicy: 'none',
18
+ allowlist: [],
19
+ powTicketTtlMs: 10 * 60 * 1000,
20
+ stakeProofTtlMs: 60 * 60 * 1000,
21
+ minPowDifficulty: 0,
22
+ verifySignatures: true,
23
+ verifyEventHash: true,
24
+ verifySnapshotHash: true,
25
+ verifySnapshotSignatures: true,
26
+ verifySnapshotState: true,
27
+ verifyPeerId: true,
28
+ subscribeEvents: true,
29
+ };
30
+ export class P2PSync {
31
+ node;
32
+ eventStore;
33
+ snapshotStore;
34
+ options;
35
+ config;
36
+ resolvePeerPublicKey;
37
+ resolveControllerPublicKey;
38
+ onEventApplied;
39
+ validateSnapshotState;
40
+ unsubscribeRequests;
41
+ unsubscribeResponses;
42
+ unsubscribeEvents;
43
+ unsubscribeMarketEvents;
44
+ snapshotChunks = new Map();
45
+ snapshotChunkTtlMs = 5 * 60 * 1000;
46
+ allowlist = new Set();
47
+ powTickets = new Map();
48
+ stakeProofs = new Map();
49
+ peerStats = new Map();
50
+ peerScores = new Map();
51
+ peerRotations = new Map();
52
+ constructor(node, eventStore, snapshotStore, options) {
53
+ this.node = node;
54
+ this.eventStore = eventStore;
55
+ this.snapshotStore = snapshotStore;
56
+ this.options = options;
57
+ this.config = { ...DEFAULT_P2P_SYNC_CONFIG, ...options };
58
+ this.resolvePeerPublicKey = options.resolvePeerPublicKey;
59
+ this.resolveControllerPublicKey = options.resolveControllerPublicKey;
60
+ this.onEventApplied = options.onEventApplied;
61
+ this.validateSnapshotState = options.validateSnapshotState;
62
+ for (const peer of this.config.allowlist ?? []) {
63
+ if (peer) {
64
+ this.allowlist.add(peer);
65
+ }
66
+ }
67
+ }
68
+ async start() {
69
+ this.unsubscribeRequests = await this.node.subscribe(TOPIC_REQUESTS, (message) => this.handleRequest(message));
70
+ this.unsubscribeResponses = await this.node.subscribe(TOPIC_RESPONSES, (message) => this.handleResponse(message));
71
+ if (this.config.subscribeEvents) {
72
+ this.unsubscribeEvents = await this.node.subscribe(TOPIC_EVENTS, (message) => this.handleEventEnvelope(message));
73
+ this.unsubscribeMarketEvents = await this.node.subscribe(TOPIC_MARKETS, (message) => this.handleEventEnvelope(message));
74
+ }
75
+ }
76
+ async stop() {
77
+ this.unsubscribeRequests?.();
78
+ this.unsubscribeResponses?.();
79
+ this.unsubscribeEvents?.();
80
+ this.unsubscribeMarketEvents?.();
81
+ this.unsubscribeRequests = undefined;
82
+ this.unsubscribeResponses = undefined;
83
+ this.unsubscribeEvents = undefined;
84
+ this.unsubscribeMarketEvents = undefined;
85
+ }
86
+ async requestRange(from, limit) {
87
+ const request = {
88
+ type: RequestType.RangeRequest,
89
+ rangeRequest: {
90
+ from,
91
+ limit: limit ?? this.config.maxRangeLimit,
92
+ },
93
+ };
94
+ await this.publishRequest(request);
95
+ }
96
+ async requestSnapshot(from = '') {
97
+ const request = {
98
+ type: RequestType.SnapshotRequest,
99
+ snapshotRequest: {
100
+ from,
101
+ },
102
+ };
103
+ await this.publishRequest(request);
104
+ }
105
+ async publishRequest(request) {
106
+ const payload = encodeRequestMessageBytes(request);
107
+ const envelope = await this.signEnvelope({
108
+ v: 1,
109
+ topic: TOPIC_REQUESTS,
110
+ sender: this.options.peerId,
111
+ ts: BigInt(Date.now()),
112
+ contentType: CONTENT_TYPE,
113
+ payload,
114
+ });
115
+ const bytes = encodeP2PEnvelopeBytes(envelope);
116
+ await this.node.publish(TOPIC_REQUESTS, bytes);
117
+ }
118
+ async publishResponse(response) {
119
+ const payload = encodeResponseMessageBytes(response);
120
+ const envelope = await this.signEnvelope({
121
+ v: 1,
122
+ topic: TOPIC_RESPONSES,
123
+ sender: this.options.peerId,
124
+ ts: BigInt(Date.now()),
125
+ contentType: CONTENT_TYPE,
126
+ payload,
127
+ });
128
+ const bytes = encodeP2PEnvelopeBytes(envelope);
129
+ await this.node.publish(TOPIC_RESPONSES, bytes);
130
+ }
131
+ async signEnvelope(envelope) {
132
+ return signP2PEnvelope(envelope, this.options.peerPrivateKey);
133
+ }
134
+ async handleRequest(message) {
135
+ const envelope = await this.decodeEnvelope(message);
136
+ if (!envelope) {
137
+ return;
138
+ }
139
+ const request = this.decodeRequest(envelope);
140
+ if (!request) {
141
+ return;
142
+ }
143
+ switch (request.type) {
144
+ case RequestType.RangeRequest:
145
+ await this.handleRangeRequest(request);
146
+ break;
147
+ case RequestType.SnapshotRequest:
148
+ await this.handleSnapshotRequest(request);
149
+ break;
150
+ case RequestType.PeerRotate:
151
+ await this.handlePeerRotate(request, envelope.sender);
152
+ break;
153
+ case RequestType.PowTicket:
154
+ await this.handlePowTicket(request, envelope.sender);
155
+ break;
156
+ case RequestType.StakeProof:
157
+ await this.handleStakeProof(request, envelope.sender);
158
+ break;
159
+ default:
160
+ break;
161
+ }
162
+ }
163
+ async handleResponse(message) {
164
+ const envelope = await this.decodeEnvelope(message);
165
+ if (!envelope) {
166
+ return;
167
+ }
168
+ const response = this.decodeResponse(envelope);
169
+ if (!response) {
170
+ return;
171
+ }
172
+ switch (response.type) {
173
+ case ResponseType.RangeResponse:
174
+ await this.applyRangeResponse(response.rangeResponse ?? null);
175
+ break;
176
+ case ResponseType.SnapshotResponse:
177
+ await this.applySnapshotResponse(response.snapshotResponse ?? null);
178
+ break;
179
+ default:
180
+ break;
181
+ }
182
+ }
183
+ async handleEventEnvelope(message) {
184
+ const envelope = await this.decodeEnvelope(message);
185
+ if (!envelope) {
186
+ return;
187
+ }
188
+ if (envelope.topic !== TOPIC_EVENTS && envelope.topic !== TOPIC_MARKETS) {
189
+ return;
190
+ }
191
+ await this.applyEventBytes(envelope.payload);
192
+ }
193
+ async handleRangeRequest(request) {
194
+ if (!request.rangeRequest) {
195
+ return;
196
+ }
197
+ const limit = Math.min(Math.max(request.rangeRequest.limit, 0), this.config.maxRangeLimit);
198
+ if (limit <= 0) {
199
+ return;
200
+ }
201
+ const from = request.rangeRequest.from || '';
202
+ const range = await this.eventStore.getEventLogRange(from, limit, this.config.maxRangeBytes);
203
+ const response = {
204
+ type: ResponseType.RangeResponse,
205
+ rangeResponse: range,
206
+ };
207
+ await this.publishResponse(response);
208
+ }
209
+ async handleSnapshotRequest(request) {
210
+ if (!request.snapshotRequest || !this.snapshotStore) {
211
+ return;
212
+ }
213
+ const latest = await this.snapshotStore.loadLatestSnapshotBytes();
214
+ if (!latest) {
215
+ return;
216
+ }
217
+ if (request.snapshotRequest.from && request.snapshotRequest.from === latest.hash) {
218
+ return;
219
+ }
220
+ if (latest.bytes.length > this.config.maxSnapshotTotalBytes) {
221
+ return;
222
+ }
223
+ if (latest.bytes.length <= this.config.maxSnapshotBytes) {
224
+ const response = {
225
+ type: ResponseType.SnapshotResponse,
226
+ snapshotResponse: {
227
+ hash: latest.hash,
228
+ snapshot: latest.bytes,
229
+ totalBytes: latest.bytes.length,
230
+ chunkIndex: 0,
231
+ chunkCount: 1,
232
+ },
233
+ };
234
+ await this.publishResponse(response);
235
+ return;
236
+ }
237
+ const chunkCount = Math.ceil(latest.bytes.length / this.config.maxSnapshotBytes);
238
+ for (let i = 0; i < chunkCount; i++) {
239
+ const start = i * this.config.maxSnapshotBytes;
240
+ const end = Math.min(start + this.config.maxSnapshotBytes, latest.bytes.length);
241
+ const chunk = latest.bytes.subarray(start, end);
242
+ const response = {
243
+ type: ResponseType.SnapshotResponse,
244
+ snapshotResponse: {
245
+ hash: latest.hash,
246
+ snapshot: chunk,
247
+ totalBytes: latest.bytes.length,
248
+ chunkIndex: i,
249
+ chunkCount,
250
+ },
251
+ };
252
+ await this.publishResponse(response);
253
+ }
254
+ }
255
+ async applyRangeResponse(response) {
256
+ if (!response) {
257
+ return;
258
+ }
259
+ for (const eventBytes of response.events) {
260
+ await this.applyEventBytes(eventBytes);
261
+ }
262
+ }
263
+ async applySnapshotResponse(response) {
264
+ if (!response || !this.snapshotStore) {
265
+ return;
266
+ }
267
+ if (!response.hash || !response.snapshot.length) {
268
+ return;
269
+ }
270
+ const snapshotBytes = this.collectSnapshotBytes(response);
271
+ if (!snapshotBytes) {
272
+ return;
273
+ }
274
+ let snapshot;
275
+ try {
276
+ snapshot = JSON.parse(bytesToUtf8(snapshotBytes));
277
+ }
278
+ catch {
279
+ return;
280
+ }
281
+ if (!snapshot?.hash || snapshot.hash !== response.hash) {
282
+ return;
283
+ }
284
+ if (this.config.verifySnapshotHash && !verifySnapshotHash(snapshot)) {
285
+ return;
286
+ }
287
+ if (this.config.verifySnapshotSignatures) {
288
+ const { validPeers } = await verifySnapshotSignatures(snapshot, (peerId) => this.resolvePublicKey(peerId), { minSignatures: 1 });
289
+ const eligiblePeers = validPeers.filter((peer) => this.isPeerEligible(peer));
290
+ if (eligiblePeers.length < this.config.minSnapshotSignatures) {
291
+ return;
292
+ }
293
+ }
294
+ const latest = await this.snapshotStore.loadLatestSnapshot();
295
+ if (snapshot.prev) {
296
+ if (!latest || latest.hash !== snapshot.prev) {
297
+ return;
298
+ }
299
+ }
300
+ else if (latest) {
301
+ return;
302
+ }
303
+ if (this.config.verifySnapshotState) {
304
+ if (!this.validateSnapshotState) {
305
+ return;
306
+ }
307
+ // collectEventsForSnapshot uses event hashes as cursors (matching
308
+ // getEventLogRange semantics). `snapshot.at` is the hash of the
309
+ // last event the snapshot covers; `latest?.at` is the same for the
310
+ // previous snapshot (i.e. the point from which new events start).
311
+ // When there is no previous snapshot we pass null to start from the
312
+ // beginning of the event log.
313
+ const fromHash = latest?.at ?? null;
314
+ const toHash = snapshot.at;
315
+ const events = await this.collectEventsForSnapshot(fromHash, toHash);
316
+ if (!events) {
317
+ return;
318
+ }
319
+ const ok = await this.validateSnapshotState(snapshot, events);
320
+ if (!ok) {
321
+ return;
322
+ }
323
+ }
324
+ await this.snapshotStore.saveSnapshot(snapshot);
325
+ }
326
+ async applyEventBytes(eventBytes) {
327
+ const envelope = this.parseEventEnvelope(eventBytes);
328
+ if (!envelope) {
329
+ return;
330
+ }
331
+ let canonical;
332
+ try {
333
+ canonical = canonicalizeBytes(envelope);
334
+ }
335
+ catch {
336
+ return;
337
+ }
338
+ if (!bytesEqual(canonical, eventBytes)) {
339
+ return;
340
+ }
341
+ const hash = envelope.hash;
342
+ if (typeof hash !== 'string' || !hash.length) {
343
+ return;
344
+ }
345
+ if (this.config.verifyEventHash) {
346
+ const computed = eventHashHex(envelope);
347
+ if (computed !== hash) {
348
+ return;
349
+ }
350
+ }
351
+ let appended = false;
352
+ try {
353
+ appended = await this.eventStore.appendEvent(hash, eventBytes);
354
+ }
355
+ catch {
356
+ return;
357
+ }
358
+ if (!appended) {
359
+ return;
360
+ }
361
+ if (this.onEventApplied) {
362
+ try {
363
+ await this.onEventApplied(envelope, eventBytes);
364
+ }
365
+ catch {
366
+ return;
367
+ }
368
+ }
369
+ }
370
+ parseEventEnvelope(eventBytes) {
371
+ try {
372
+ return JSON.parse(bytesToUtf8(eventBytes));
373
+ }
374
+ catch {
375
+ return null;
376
+ }
377
+ }
378
+ decodeRequest(envelope) {
379
+ if (envelope.topic !== TOPIC_REQUESTS) {
380
+ return null;
381
+ }
382
+ try {
383
+ return decodeRequestMessageBytes(envelope.payload);
384
+ }
385
+ catch {
386
+ return null;
387
+ }
388
+ }
389
+ decodeResponse(envelope) {
390
+ if (envelope.topic !== TOPIC_RESPONSES) {
391
+ return null;
392
+ }
393
+ try {
394
+ return decodeResponseMessageBytes(envelope.payload);
395
+ }
396
+ catch {
397
+ return null;
398
+ }
399
+ }
400
+ async decodeEnvelope(message) {
401
+ if (message.data.length > this.config.maxEnvelopeBytes) {
402
+ if (message.from) {
403
+ this.updatePeerScore(message.from, -this.config.scoreDecrease);
404
+ }
405
+ return null;
406
+ }
407
+ let envelope;
408
+ try {
409
+ envelope = decodeP2PEnvelopeBytes(message.data);
410
+ }
411
+ catch {
412
+ if (message.from) {
413
+ this.updatePeerScore(message.from, -this.config.scoreDecrease);
414
+ }
415
+ return null;
416
+ }
417
+ const sender = envelope.sender || message.from || '';
418
+ if (!sender) {
419
+ return null;
420
+ }
421
+ if (!envelope.sender && sender) {
422
+ envelope.sender = sender;
423
+ }
424
+ if (message.from && envelope.sender && message.from !== envelope.sender) {
425
+ this.updatePeerScore(sender, -this.config.scoreDecrease);
426
+ return null;
427
+ }
428
+ if (this.isRateLimited(sender, message.data.length)) {
429
+ this.updatePeerScore(sender, -this.config.scoreDecrease);
430
+ return null;
431
+ }
432
+ if (!this.isPeerScoreEligible(sender)) {
433
+ return null;
434
+ }
435
+ if (envelope.contentType !== CONTENT_TYPE) {
436
+ this.updatePeerScore(sender, -this.config.scoreDecrease);
437
+ return null;
438
+ }
439
+ if (sender === this.options.peerId) {
440
+ return null;
441
+ }
442
+ let publicKey = null;
443
+ if (this.config.verifyPeerId || this.config.verifySignatures) {
444
+ publicKey = await this.resolvePublicKey(sender);
445
+ if (!publicKey) {
446
+ this.updatePeerScore(sender, -this.config.scoreDecrease);
447
+ return null;
448
+ }
449
+ }
450
+ if (this.config.verifyPeerId) {
451
+ if (!publicKey) {
452
+ return null;
453
+ }
454
+ const okPeerId = await this.verifyPeerId(sender, publicKey);
455
+ if (!okPeerId) {
456
+ this.updatePeerScore(sender, -this.config.scoreDecrease);
457
+ return null;
458
+ }
459
+ }
460
+ if (this.config.verifySignatures) {
461
+ if (!publicKey) {
462
+ return null;
463
+ }
464
+ const ok = await verifyP2PEnvelopeSignature(envelope, publicKey);
465
+ if (!ok) {
466
+ this.updatePeerScore(sender, -this.config.scoreDecrease);
467
+ return null;
468
+ }
469
+ }
470
+ this.updatePeerScore(sender, this.config.scoreIncrease);
471
+ return envelope;
472
+ }
473
+ async resolvePublicKey(peerId) {
474
+ if (this.resolvePeerPublicKey) {
475
+ return this.resolvePeerPublicKey(peerId);
476
+ }
477
+ return this.node.getPeerPublicKey(peerId);
478
+ }
479
+ async handlePowTicket(request, sender) {
480
+ const ticket = request.powTicket;
481
+ if (!ticket || ticket.peer !== sender) {
482
+ return;
483
+ }
484
+ const now = Date.now();
485
+ const ts = Number(ticket.ts ?? 0n);
486
+ if (Number.isNaN(ts) || Math.abs(now - ts) > MAX_CLOCK_SKEW_MS) {
487
+ return;
488
+ }
489
+ const expected = powTicketHashHex(ticket);
490
+ if (ticket.hash !== ticket.hash.toLowerCase()) {
491
+ return;
492
+ }
493
+ if (!/^[0-9a-f]{64}$/.test(ticket.hash)) {
494
+ return;
495
+ }
496
+ if (expected !== ticket.hash) {
497
+ return;
498
+ }
499
+ if (!hasLeadingZeroBits(expected, ticket.difficulty)) {
500
+ return;
501
+ }
502
+ if (ticket.difficulty < this.config.minPowDifficulty) {
503
+ return;
504
+ }
505
+ const publicKey = await this.resolvePublicKey(ticket.peer);
506
+ if (!publicKey) {
507
+ return;
508
+ }
509
+ const ok = await verifyPowTicketSignature(ticket, publicKey);
510
+ if (!ok) {
511
+ return;
512
+ }
513
+ this.powTickets.set(ticket.peer, { receivedAt: now });
514
+ }
515
+ async handleStakeProof(request, sender) {
516
+ const proof = request.stakeProof;
517
+ if (!proof || proof.peer !== sender) {
518
+ return;
519
+ }
520
+ const stakeBytes = await this.eventStore.getEvent(proof.stakeEvent);
521
+ if (!stakeBytes) {
522
+ return;
523
+ }
524
+ const stakeEnvelope = this.parseEventEnvelope(stakeBytes);
525
+ if (!stakeEnvelope) {
526
+ return;
527
+ }
528
+ if (stakeEnvelope.hash !== proof.stakeEvent) {
529
+ return;
530
+ }
531
+ if (stakeEnvelope.type !== 'wallet.stake') {
532
+ return;
533
+ }
534
+ if (stakeEnvelope.issuer !== proof.controller) {
535
+ return;
536
+ }
537
+ const payload = stakeEnvelope.payload;
538
+ const amount = typeof payload?.amount === 'string' ? payload.amount : null;
539
+ if (!amount) {
540
+ return;
541
+ }
542
+ if (compareTokenAmounts(amount, proof.minStake) < 0) {
543
+ return;
544
+ }
545
+ const peerPublicKey = await this.resolvePublicKey(proof.peer);
546
+ if (!peerPublicKey) {
547
+ return;
548
+ }
549
+ const okPeer = await verifyStakeProofPeerSignature(proof, peerPublicKey);
550
+ if (!okPeer) {
551
+ return;
552
+ }
553
+ if (!this.resolveControllerPublicKey) {
554
+ return;
555
+ }
556
+ const controllerKey = await this.resolveControllerPublicKey(proof.controller);
557
+ if (!controllerKey) {
558
+ return;
559
+ }
560
+ const okController = await verifyStakeProofControllerSignature(proof, controllerKey);
561
+ if (!okController) {
562
+ return;
563
+ }
564
+ this.stakeProofs.set(proof.peer, { receivedAt: Date.now() });
565
+ }
566
+ async handlePeerRotate(request, sender) {
567
+ const rotate = request.peerRotate;
568
+ if (!rotate) {
569
+ return;
570
+ }
571
+ if (sender !== rotate.old && sender !== rotate['new']) {
572
+ return;
573
+ }
574
+ const ts = Number(rotate.ts ?? 0n);
575
+ if (Number.isNaN(ts) || Math.abs(Date.now() - ts) > MAX_CLOCK_SKEW_MS) {
576
+ return;
577
+ }
578
+ const oldKey = await this.resolvePublicKey(rotate.old);
579
+ const newKey = await this.resolvePublicKey(rotate['new']);
580
+ if (!oldKey || !newKey) {
581
+ return;
582
+ }
583
+ const okOld = await verifyPeerRotateOldSignature(rotate, oldKey);
584
+ if (!okOld) {
585
+ return;
586
+ }
587
+ const okNew = await verifyPeerRotateNewSignature(rotate, newKey);
588
+ if (!okNew) {
589
+ return;
590
+ }
591
+ this.peerRotations.set(rotate.old, { newPeer: rotate['new'], ts });
592
+ if (this.allowlist.has(rotate.old)) {
593
+ this.allowlist.add(rotate['new']);
594
+ }
595
+ }
596
+ isPeerEligible(peerId) {
597
+ if (this.allowlist.has(peerId)) {
598
+ return true;
599
+ }
600
+ switch (this.config.sybilPolicy) {
601
+ case 'none':
602
+ return true;
603
+ case 'allowlist':
604
+ return this.allowlist.has(peerId);
605
+ case 'pow':
606
+ return this.isPowEligible(peerId);
607
+ case 'stake':
608
+ return this.isStakeEligible(peerId);
609
+ default:
610
+ return false;
611
+ }
612
+ }
613
+ isPowEligible(peerId) {
614
+ this.cleanupSybilCaches();
615
+ return this.powTickets.has(peerId);
616
+ }
617
+ isStakeEligible(peerId) {
618
+ this.cleanupSybilCaches();
619
+ return this.stakeProofs.has(peerId);
620
+ }
621
+ cleanupSybilCaches() {
622
+ const now = Date.now();
623
+ for (const [peer, entry] of this.powTickets.entries()) {
624
+ if (now - entry.receivedAt > this.config.powTicketTtlMs) {
625
+ this.powTickets.delete(peer);
626
+ }
627
+ }
628
+ for (const [peer, entry] of this.stakeProofs.entries()) {
629
+ if (now - entry.receivedAt > this.config.stakeProofTtlMs) {
630
+ this.stakeProofs.delete(peer);
631
+ }
632
+ }
633
+ }
634
+ collectSnapshotBytes(response) {
635
+ const chunkCount = response.chunkCount ?? 0;
636
+ const chunkIndex = response.chunkIndex ?? 0;
637
+ const totalBytes = response.totalBytes && response.totalBytes > 0
638
+ ? response.totalBytes
639
+ : response.snapshot.length;
640
+ if (totalBytes <= 0 || totalBytes > this.config.maxSnapshotTotalBytes) {
641
+ return null;
642
+ }
643
+ if (chunkCount <= 1) {
644
+ if (response.snapshot.length > this.config.maxSnapshotBytes) {
645
+ return null;
646
+ }
647
+ if (response.snapshot.length !== totalBytes) {
648
+ return null;
649
+ }
650
+ return response.snapshot;
651
+ }
652
+ if (chunkIndex < 0 || chunkIndex >= chunkCount) {
653
+ return null;
654
+ }
655
+ if (response.snapshot.length > this.config.maxSnapshotBytes) {
656
+ return null;
657
+ }
658
+ this.cleanupSnapshotChunks();
659
+ const key = response.hash;
660
+ let state = this.snapshotChunks.get(key);
661
+ if (!state || state.totalBytes !== totalBytes || state.chunkCount !== chunkCount) {
662
+ state = {
663
+ totalBytes,
664
+ chunkCount,
665
+ received: new Map(),
666
+ receivedBytes: 0,
667
+ updatedAt: Date.now(),
668
+ };
669
+ this.snapshotChunks.set(key, state);
670
+ }
671
+ if (!state.received.has(chunkIndex)) {
672
+ state.received.set(chunkIndex, response.snapshot);
673
+ state.receivedBytes += response.snapshot.length;
674
+ state.updatedAt = Date.now();
675
+ }
676
+ if (state.received.size !== chunkCount) {
677
+ return null;
678
+ }
679
+ const parts = [];
680
+ let assembledBytes = 0;
681
+ for (let i = 0; i < chunkCount; i++) {
682
+ const part = state.received.get(i);
683
+ if (!part) {
684
+ return null;
685
+ }
686
+ parts.push(part);
687
+ assembledBytes += part.length;
688
+ }
689
+ this.snapshotChunks.delete(key);
690
+ if (assembledBytes !== totalBytes) {
691
+ return null;
692
+ }
693
+ return concatBytes(...parts);
694
+ }
695
+ /**
696
+ * Collect all events between two event-hash cursors.
697
+ *
698
+ * @param fromHash Event hash to start after (exclusive). null = start of log.
699
+ * @param toHash Event hash to stop at (inclusive). Must be an event hash
700
+ * present in the event store so the cursor comparison works.
701
+ * @returns Array of raw event bytes, or null if collection failed.
702
+ */
703
+ async collectEventsForSnapshot(fromHash, toHash) {
704
+ if (!toHash) {
705
+ return null;
706
+ }
707
+ const events = [];
708
+ let cursor = fromHash ?? '';
709
+ let totalBytes = 0;
710
+ const maxTotalBytes = this.config.maxSnapshotTotalBytes;
711
+ let guard = 0;
712
+ while (guard < 10_000) {
713
+ guard += 1;
714
+ const range = await this.eventStore.getEventLogRange(cursor, this.config.maxRangeLimit, this.config.maxRangeBytes);
715
+ if (!range.events.length) {
716
+ return null;
717
+ }
718
+ for (const eventBytes of range.events) {
719
+ events.push(eventBytes);
720
+ totalBytes += eventBytes.length;
721
+ if (totalBytes > maxTotalBytes) {
722
+ return null;
723
+ }
724
+ }
725
+ if (!range.cursor) {
726
+ return null;
727
+ }
728
+ cursor = range.cursor;
729
+ if (cursor === toHash) {
730
+ return events;
731
+ }
732
+ }
733
+ return null;
734
+ }
735
+ cleanupSnapshotChunks() {
736
+ const now = Date.now();
737
+ for (const [hash, state] of this.snapshotChunks.entries()) {
738
+ if (now - state.updatedAt > this.snapshotChunkTtlMs) {
739
+ this.snapshotChunks.delete(hash);
740
+ }
741
+ }
742
+ }
743
+ isRateLimited(peerId, bytes) {
744
+ const now = Date.now();
745
+ const windowMs = this.config.rateLimitWindowMs;
746
+ const current = this.peerStats.get(peerId);
747
+ if (!current || now - current.windowStart >= windowMs) {
748
+ this.peerStats.set(peerId, { windowStart: now, count: 1, bytes });
749
+ return false;
750
+ }
751
+ current.count += 1;
752
+ current.bytes += bytes;
753
+ this.peerStats.set(peerId, current);
754
+ return (current.count > this.config.maxMessagesPerWindow ||
755
+ current.bytes > this.config.maxBytesPerWindow);
756
+ }
757
+ updatePeerScore(peerId, delta) {
758
+ const now = Date.now();
759
+ const current = this.peerScores.get(peerId);
760
+ let score = current?.score ?? 0;
761
+ const updatedAt = current?.updatedAt ?? now;
762
+ if (now - updatedAt > this.config.scoreDecayMs) {
763
+ score = 0;
764
+ }
765
+ score += delta;
766
+ this.peerScores.set(peerId, { score, updatedAt: now });
767
+ }
768
+ isPeerScoreEligible(peerId) {
769
+ const score = this.peerScores.get(peerId)?.score ?? 0;
770
+ return score >= this.config.minPeerScore;
771
+ }
772
+ async verifyPeerId(peerId, publicKey) {
773
+ try {
774
+ const factory = (await import('@libp2p/peer-id-factory'));
775
+ const createFromPubKey = factory.createFromPubKey ?? factory.createFromPublicKey ?? factory.createFromPubKeyBytes;
776
+ if (typeof createFromPubKey !== 'function') {
777
+ return false;
778
+ }
779
+ try {
780
+ const derived = await createFromPubKey(publicKey);
781
+ if (derived?.toString?.() === peerId) {
782
+ return true;
783
+ }
784
+ }
785
+ catch {
786
+ // try raw key conversion below
787
+ }
788
+ if (publicKey.length === 32) {
789
+ try {
790
+ const keys = (await import('@libp2p/crypto/keys'));
791
+ if (typeof keys.publicKeyFromRaw === 'function') {
792
+ const keyObj = keys.publicKeyFromRaw(publicKey);
793
+ const keyBytes = keyObj?.bytes ??
794
+ (typeof keyObj?.marshal === 'function' ? keyObj.marshal() : null) ??
795
+ (typeof keyObj?.toBytes === 'function' ? keyObj.toBytes() : null);
796
+ if (keyBytes) {
797
+ const derived = await createFromPubKey(keyBytes);
798
+ return derived?.toString?.() === peerId;
799
+ }
800
+ }
801
+ }
802
+ catch {
803
+ return false;
804
+ }
805
+ }
806
+ }
807
+ catch {
808
+ return false;
809
+ }
810
+ return false;
811
+ }
812
+ }
813
+ function hasLeadingZeroBits(hashHex, difficulty) {
814
+ if (difficulty <= 0) {
815
+ return true;
816
+ }
817
+ const bytes = hexToBytes(hashHex);
818
+ let remaining = difficulty;
819
+ for (const byte of bytes) {
820
+ if (remaining <= 0) {
821
+ return true;
822
+ }
823
+ if (byte === 0) {
824
+ remaining -= 8;
825
+ continue;
826
+ }
827
+ let mask = 0x80;
828
+ while (mask > 0) {
829
+ if ((byte & mask) !== 0) {
830
+ return remaining <= 0;
831
+ }
832
+ remaining -= 1;
833
+ if (remaining <= 0) {
834
+ return true;
835
+ }
836
+ mask >>= 1;
837
+ }
838
+ return remaining <= 0;
839
+ }
840
+ return remaining <= 0;
841
+ }
842
+ function bytesEqual(a, b) {
843
+ if (a.length !== b.length) {
844
+ return false;
845
+ }
846
+ for (let i = 0; i < a.length; i++) {
847
+ if (a[i] !== b[i]) {
848
+ return false;
849
+ }
850
+ }
851
+ return true;
852
+ }
853
+ function parseTokenAmount(amount) {
854
+ if (!/^[0-9]+(\\.[0-9]+)?$/.test(amount)) {
855
+ return null;
856
+ }
857
+ const [whole, fraction = ''] = amount.split('.');
858
+ const scale = fraction.length;
859
+ const value = BigInt(`${whole}${fraction}`);
860
+ return { value, scale };
861
+ }
862
+ function compareTokenAmounts(a, b) {
863
+ const left = parseTokenAmount(a);
864
+ const right = parseTokenAmount(b);
865
+ if (!left || !right) {
866
+ return -1;
867
+ }
868
+ const scale = Math.max(left.scale, right.scale);
869
+ const leftValue = left.value * 10n ** BigInt(scale - left.scale);
870
+ const rightValue = right.value * 10n ** BigInt(scale - right.scale);
871
+ if (leftValue === rightValue)
872
+ return 0;
873
+ return leftValue > rightValue ? 1 : -1;
874
+ }
875
+ //# sourceMappingURL=sync.js.map