@twin.org/synchronised-storage-service 0.0.1-next.3 → 0.0.1-next.5
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.
- package/dist/cjs/index.cjs +963 -276
- package/dist/esm/index.mjs +965 -279
- package/dist/types/entities/syncSnapshotEntry.d.ts +12 -5
- package/dist/types/helpers/blobStorageHelper.d.ts +39 -0
- package/dist/types/helpers/changeSetHelper.d.ts +26 -7
- package/dist/types/helpers/localSyncStateHelper.d.ts +11 -25
- package/dist/types/helpers/remoteSyncStateHelper.d.ts +17 -12
- package/dist/types/helpers/versions.d.ts +3 -0
- package/dist/types/index.d.ts +0 -2
- package/dist/types/models/ISyncPointerStore.d.ts +4 -0
- package/dist/types/models/ISyncSnapshot.d.ts +9 -1
- package/dist/types/models/ISyncState.d.ts +8 -0
- package/dist/types/models/ISynchronisedStorageServiceConfig.d.ts +17 -6
- package/dist/types/models/ISynchronisedStorageServiceConstructorOptions.d.ts +6 -2
- package/dist/types/synchronisedStorageRoutes.d.ts +9 -1
- package/dist/types/synchronisedStorageService.d.ts +13 -4
- package/docs/architecture.md +128 -0
- package/docs/changelog.md +29 -0
- package/docs/open-api/spec.json +246 -18
- package/docs/reference/classes/SyncSnapshotEntry.md +22 -6
- package/docs/reference/classes/SynchronisedStorageService.md +38 -5
- package/docs/reference/functions/synchronisedStorageGetDecryptionKeyRequest.md +31 -0
- package/docs/reference/index.md +1 -2
- package/docs/reference/interfaces/ISyncPointerStore.md +8 -0
- package/docs/reference/interfaces/ISyncSnapshot.md +18 -2
- package/docs/reference/interfaces/ISyncState.md +16 -0
- package/docs/reference/interfaces/ISynchronisedStorageServiceConfig.md +44 -10
- package/docs/reference/interfaces/ISynchronisedStorageServiceConstructorOptions.md +11 -3
- package/locales/en.json +60 -25
- package/package.json +3 -2
- package/dist/types/models/ISyncChange.d.ts +0 -18
- package/dist/types/models/ISyncChangeSet.d.ts +0 -36
- package/docs/reference/interfaces/ISyncChange.md +0 -33
- package/docs/reference/interfaces/ISyncChangeSet.md +0 -65
package/dist/esm/index.mjs
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { property, entity, EntitySchemaFactory, EntitySchemaHelper, ComparisonOperator } from '@twin.org/entity';
|
|
2
|
-
import { Guards, ComponentFactory, Is,
|
|
2
|
+
import { Guards, ComponentFactory, Is, Converter, Compression, CompressionType, ObjectHelper, BaseError, GeneralError, RandomHelper, NotFoundError, UnauthorizedError } from '@twin.org/core';
|
|
3
3
|
import { HttpStatusCode } from '@twin.org/web';
|
|
4
|
+
import { BlobStorageConnectorFactory } from '@twin.org/blob-storage-models';
|
|
4
5
|
import { EntityStorageConnectorFactory } from '@twin.org/entity-storage-models';
|
|
5
6
|
import { DocumentHelper, IdentityConnectorFactory } from '@twin.org/identity-models';
|
|
6
7
|
import { LoggingConnectorFactory } from '@twin.org/logging-models';
|
|
7
|
-
import { SyncChangeOperation, SynchronisedStorageTopics } from '@twin.org/synchronised-storage-models';
|
|
8
|
-
import { VerifiableStorageConnectorFactory } from '@twin.org/verifiable-storage-models';
|
|
9
|
-
import { BlobStorageCompressionType } from '@twin.org/blob-storage-models';
|
|
10
8
|
import { ProofTypes } from '@twin.org/standards-w3c-did';
|
|
9
|
+
import { SyncChangeOperation, SynchronisedStorageTopics, SyncNodeIdentityMode } from '@twin.org/synchronised-storage-models';
|
|
10
|
+
import { VaultEncryptionType, VaultConnectorFactory } from '@twin.org/vault-models';
|
|
11
|
+
import { VerifiableStorageConnectorFactory } from '@twin.org/verifiable-storage-models';
|
|
12
|
+
import { RSA } from '@twin.org/crypto';
|
|
11
13
|
|
|
12
14
|
// Copyright 2024 IOTA Stiftung.
|
|
13
15
|
// SPDX-License-Identifier: Apache-2.0.
|
|
@@ -19,6 +21,10 @@ let SyncSnapshotEntry = class SyncSnapshotEntry {
|
|
|
19
21
|
* The id for the snapshot.
|
|
20
22
|
*/
|
|
21
23
|
id;
|
|
24
|
+
/**
|
|
25
|
+
* The version for the snapshot.
|
|
26
|
+
*/
|
|
27
|
+
version;
|
|
22
28
|
/**
|
|
23
29
|
* The storage key for the snapshot i.e. which entity is being synchronized.
|
|
24
30
|
*/
|
|
@@ -32,9 +38,13 @@ let SyncSnapshotEntry = class SyncSnapshotEntry {
|
|
|
32
38
|
*/
|
|
33
39
|
dateModified;
|
|
34
40
|
/**
|
|
35
|
-
* The flag to determine if this is the
|
|
41
|
+
* The flag to determine if this is the snapshot is the local one containing changes for this node.
|
|
36
42
|
*/
|
|
37
|
-
|
|
43
|
+
isLocal;
|
|
44
|
+
/**
|
|
45
|
+
* The flag to determine if this is a consolidated snapshot.
|
|
46
|
+
*/
|
|
47
|
+
isConsolidated;
|
|
38
48
|
/**
|
|
39
49
|
* The ids of the storage for the change sets in the snapshot, if this is not a local snapshot.
|
|
40
50
|
*/
|
|
@@ -48,6 +58,10 @@ __decorate([
|
|
|
48
58
|
property({ type: "string", isPrimary: true }),
|
|
49
59
|
__metadata("design:type", String)
|
|
50
60
|
], SyncSnapshotEntry.prototype, "id", void 0);
|
|
61
|
+
__decorate([
|
|
62
|
+
property({ type: "string" }),
|
|
63
|
+
__metadata("design:type", String)
|
|
64
|
+
], SyncSnapshotEntry.prototype, "version", void 0);
|
|
51
65
|
__decorate([
|
|
52
66
|
property({ type: "string", isSecondary: true }),
|
|
53
67
|
__metadata("design:type", String)
|
|
@@ -57,13 +71,17 @@ __decorate([
|
|
|
57
71
|
__metadata("design:type", String)
|
|
58
72
|
], SyncSnapshotEntry.prototype, "dateCreated", void 0);
|
|
59
73
|
__decorate([
|
|
60
|
-
property({ type: "string"
|
|
74
|
+
property({ type: "string" }),
|
|
61
75
|
__metadata("design:type", String)
|
|
62
76
|
], SyncSnapshotEntry.prototype, "dateModified", void 0);
|
|
63
77
|
__decorate([
|
|
64
|
-
property({ type: "boolean"
|
|
78
|
+
property({ type: "boolean" }),
|
|
65
79
|
__metadata("design:type", Boolean)
|
|
66
|
-
], SyncSnapshotEntry.prototype, "
|
|
80
|
+
], SyncSnapshotEntry.prototype, "isLocal", void 0);
|
|
81
|
+
__decorate([
|
|
82
|
+
property({ type: "boolean" }),
|
|
83
|
+
__metadata("design:type", Boolean)
|
|
84
|
+
], SyncSnapshotEntry.prototype, "isConsolidated", void 0);
|
|
67
85
|
__decorate([
|
|
68
86
|
property({ type: "array", itemType: "string", optional: true }),
|
|
69
87
|
__metadata("design:type", Array)
|
|
@@ -100,8 +118,8 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
|
|
|
100
118
|
operationId: "synchronisedStorageSyncChangeSetRequest",
|
|
101
119
|
summary: "Request that the node perform a sync request for a changeset.",
|
|
102
120
|
tag: tagsSynchronisedStorage[0].name,
|
|
103
|
-
method: "
|
|
104
|
-
path: `${baseRouteName}
|
|
121
|
+
method: "POST",
|
|
122
|
+
path: `${baseRouteName}/sync-changeset`,
|
|
105
123
|
handler: async (httpRequestContext, request) => synchronisedStorageSyncChangeSetRequest(httpRequestContext, componentName, request),
|
|
106
124
|
requestType: {
|
|
107
125
|
type: "ISyncChangeSetRequest",
|
|
@@ -109,8 +127,30 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
|
|
|
109
127
|
{
|
|
110
128
|
id: "synchronisedStorageSyncChangeSetRequestExample",
|
|
111
129
|
request: {
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
body: {
|
|
131
|
+
id: "0909090909090909090909090909090909090909090909090909090909090909",
|
|
132
|
+
dateCreated: "2025-05-29T01:00:00.000Z",
|
|
133
|
+
dateModified: "2025-05-29T01:00:00.000Z",
|
|
134
|
+
nodeIdentity: "did:entity-storage:0xd2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
|
135
|
+
changes: [
|
|
136
|
+
{
|
|
137
|
+
entity: {
|
|
138
|
+
dateModified: "2025-01-01T00:00:00.000Z"
|
|
139
|
+
},
|
|
140
|
+
id: "test-id-1",
|
|
141
|
+
operation: "set"
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
proof: {
|
|
145
|
+
"@context": "https://www.w3.org/ns/credentials/v2",
|
|
146
|
+
created: "2025-05-29T01:00:00.000Z",
|
|
147
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
148
|
+
proofPurpose: "assertionMethod",
|
|
149
|
+
proofValue: "z5efBErQs3YBLZoH7jgKMQaRc9YjAxA5XSYKmW3FmTBDw9WionT2NS2x1SMvcRyBvw53cSSoaCT1xQH9tkWngGCX3",
|
|
150
|
+
type: "DataIntegrityProof",
|
|
151
|
+
verificationMethod: "did:entity-storage:0xd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0#synchronised-storage-assertion"
|
|
152
|
+
},
|
|
153
|
+
storageKey: "test-type"
|
|
114
154
|
}
|
|
115
155
|
}
|
|
116
156
|
}
|
|
@@ -120,9 +160,61 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
|
|
|
120
160
|
{
|
|
121
161
|
type: "INoContentResponse"
|
|
122
162
|
}
|
|
123
|
-
]
|
|
163
|
+
],
|
|
164
|
+
// Authentication is provided by the proof in the request body.
|
|
165
|
+
skipAuth: true
|
|
124
166
|
};
|
|
125
|
-
|
|
167
|
+
const getDecryptionKeyRoute = {
|
|
168
|
+
operationId: "synchronisedStorageGetDecryptionKeyRequest",
|
|
169
|
+
summary: "Request the decryption key.",
|
|
170
|
+
tag: tagsSynchronisedStorage[0].name,
|
|
171
|
+
method: "POST",
|
|
172
|
+
path: `${baseRouteName}/decryption-key`,
|
|
173
|
+
handler: async (httpRequestContext, request) => synchronisedStorageGetDecryptionKeyRequest(httpRequestContext, componentName, request),
|
|
174
|
+
requestType: {
|
|
175
|
+
type: "ISyncChangeSetRequest",
|
|
176
|
+
examples: [
|
|
177
|
+
{
|
|
178
|
+
id: "synchronisedStorageSyncGetDecryptionKeyRequestExample",
|
|
179
|
+
request: {
|
|
180
|
+
body: {
|
|
181
|
+
nodeIdentity: "did:entity-storage:0xd2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
|
182
|
+
proof: {
|
|
183
|
+
"@context": "https://www.w3.org/ns/credentials/v2",
|
|
184
|
+
created: "2025-05-29T01:00:00.000Z",
|
|
185
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
186
|
+
proofPurpose: "assertionMethod",
|
|
187
|
+
proofValue: "z5efBErQs3YBLZoH7jgKMQaRc9YjAxA5XSYKmW3FmTBDw9WionT2NS2x1SMvcRyBvw53cSSoaCT1xQH9tkWngGCX3",
|
|
188
|
+
type: "DataIntegrityProof",
|
|
189
|
+
verificationMethod: "did:entity-storage:0xd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0#synchronised-storage-assertion"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
responseType: [
|
|
197
|
+
{
|
|
198
|
+
type: "ISyncDecryptionKeyResponse",
|
|
199
|
+
examples: [
|
|
200
|
+
{
|
|
201
|
+
id: "synchronisedStorageSyncGetDecryptionKeyResponseExample",
|
|
202
|
+
response: {
|
|
203
|
+
body: {
|
|
204
|
+
decryptionKey: "z5efBErQs3YBLZoH7jgKMQaRc9YjAxA5XSYKmW3FmTBDw9WionT2NS2x1SMvcRyBvw53cSSoaCT1xQH9tkWngGCX3"
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
type: "IUnauthorizedResponse"
|
|
212
|
+
}
|
|
213
|
+
],
|
|
214
|
+
// Authentication is provided by the proof in the request body.
|
|
215
|
+
skipAuth: true
|
|
216
|
+
};
|
|
217
|
+
return [syncChangeSetRoute, getDecryptionKeyRoute];
|
|
126
218
|
}
|
|
127
219
|
/**
|
|
128
220
|
* Perform the sync change set operation.
|
|
@@ -133,13 +225,31 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
|
|
|
133
225
|
*/
|
|
134
226
|
async function synchronisedStorageSyncChangeSetRequest(httpRequestContext, componentName, request) {
|
|
135
227
|
Guards.object(ROUTES_SOURCE, "request", request);
|
|
136
|
-
Guards.object(ROUTES_SOURCE, "request.
|
|
228
|
+
Guards.object(ROUTES_SOURCE, "request.body", request.body);
|
|
137
229
|
const component = ComponentFactory.get(componentName);
|
|
138
|
-
await component.syncChangeSet(request.
|
|
230
|
+
await component.syncChangeSet(request.body);
|
|
139
231
|
return {
|
|
140
232
|
statusCode: HttpStatusCode.noContent
|
|
141
233
|
};
|
|
142
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Request the decryption key.
|
|
237
|
+
* @param httpRequestContext The request context for the API.
|
|
238
|
+
* @param componentName The name of the component to use in the routes.
|
|
239
|
+
* @param request The request.
|
|
240
|
+
* @returns The response object with additional http response properties.
|
|
241
|
+
*/
|
|
242
|
+
async function synchronisedStorageGetDecryptionKeyRequest(httpRequestContext, componentName, request) {
|
|
243
|
+
Guards.object(ROUTES_SOURCE, "request", request);
|
|
244
|
+
Guards.object(ROUTES_SOURCE, "request.body", request.body);
|
|
245
|
+
const component = ComponentFactory.get(componentName);
|
|
246
|
+
const key = await component.getDecryptionKey(request.body.nodeIdentity, request.body.proof);
|
|
247
|
+
return {
|
|
248
|
+
body: {
|
|
249
|
+
decryptionKey: key
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
143
253
|
|
|
144
254
|
const restEntryPoints = [
|
|
145
255
|
{
|
|
@@ -159,6 +269,208 @@ function initSchema() {
|
|
|
159
269
|
EntitySchemaFactory.register("SyncSnapshotEntry", () => EntitySchemaHelper.getSchema(SyncSnapshotEntry));
|
|
160
270
|
}
|
|
161
271
|
|
|
272
|
+
var mainnet = "";
|
|
273
|
+
var testnet = "";
|
|
274
|
+
var devnet = "";
|
|
275
|
+
var verifiableStorageKeys = {
|
|
276
|
+
mainnet: mainnet,
|
|
277
|
+
testnet: testnet,
|
|
278
|
+
devnet: devnet
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Class for performing blob storage operations.
|
|
283
|
+
*/
|
|
284
|
+
class BlobStorageHelper {
|
|
285
|
+
/**
|
|
286
|
+
* Runtime name for the class.
|
|
287
|
+
*/
|
|
288
|
+
CLASS_NAME = "BlobStorageHelper";
|
|
289
|
+
/**
|
|
290
|
+
* The logging connector to use for logging.
|
|
291
|
+
* @internal
|
|
292
|
+
*/
|
|
293
|
+
_logging;
|
|
294
|
+
/**
|
|
295
|
+
* The vault connector.
|
|
296
|
+
* @internal
|
|
297
|
+
*/
|
|
298
|
+
_vaultConnector;
|
|
299
|
+
/**
|
|
300
|
+
* The blob storage connector to use.
|
|
301
|
+
* @internal
|
|
302
|
+
*/
|
|
303
|
+
_blobStorageConnector;
|
|
304
|
+
/**
|
|
305
|
+
* The id of the vault key to use for encrypting/decrypting blobs.
|
|
306
|
+
* @internal
|
|
307
|
+
*/
|
|
308
|
+
_blobStorageEncryptionKeyId;
|
|
309
|
+
/**
|
|
310
|
+
* Is this a trusted node.
|
|
311
|
+
* @internal
|
|
312
|
+
*/
|
|
313
|
+
_isTrustedNode;
|
|
314
|
+
/**
|
|
315
|
+
* Create a new instance of BlobStorageHelper.
|
|
316
|
+
* @param logging The logging connector to use for logging.
|
|
317
|
+
* @param vaultConnector The vault connector to use for for the encryption key.
|
|
318
|
+
* @param blobStorageConnector The blob storage component to use.
|
|
319
|
+
* @param blobStorageEncryptionKeyId The id of the vault key to use for encrypting/decrypting blobs.
|
|
320
|
+
* @param isTrustedNode Is this a trusted node.
|
|
321
|
+
*/
|
|
322
|
+
constructor(logging, vaultConnector, blobStorageConnector, blobStorageEncryptionKeyId, isTrustedNode) {
|
|
323
|
+
this._logging = logging;
|
|
324
|
+
this._vaultConnector = vaultConnector;
|
|
325
|
+
this._blobStorageConnector = blobStorageConnector;
|
|
326
|
+
this._blobStorageEncryptionKeyId = blobStorageEncryptionKeyId;
|
|
327
|
+
this._isTrustedNode = isTrustedNode;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Load a blob from storage.
|
|
331
|
+
* @param blobId The id of the blob to apply.
|
|
332
|
+
* @returns The blob.
|
|
333
|
+
*/
|
|
334
|
+
async loadBlob(blobId) {
|
|
335
|
+
await this._logging?.log({
|
|
336
|
+
level: "info",
|
|
337
|
+
source: this.CLASS_NAME,
|
|
338
|
+
message: "loadBlob",
|
|
339
|
+
data: {
|
|
340
|
+
blobId
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
try {
|
|
344
|
+
const encryptedBlob = await this._blobStorageConnector.get(blobId);
|
|
345
|
+
if (Is.uint8Array(encryptedBlob)) {
|
|
346
|
+
let compressedBlob;
|
|
347
|
+
// If this is a trusted node, we can decrypt the blob using the vault
|
|
348
|
+
if (this._isTrustedNode) {
|
|
349
|
+
compressedBlob = await this._vaultConnector.decrypt(this._blobStorageEncryptionKeyId, VaultEncryptionType.Rsa2048, encryptedBlob);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
// Otherwise we need the public key stored as a secret in the vault
|
|
353
|
+
const key = await this._vaultConnector.getSecret(this._blobStorageEncryptionKeyId);
|
|
354
|
+
const rsa = new RSA(Converter.base64ToBytes(key));
|
|
355
|
+
compressedBlob = rsa.decrypt(encryptedBlob);
|
|
356
|
+
}
|
|
357
|
+
const decompressedBlob = await Compression.decompress(compressedBlob, CompressionType.Gzip);
|
|
358
|
+
await this._logging?.log({
|
|
359
|
+
level: "info",
|
|
360
|
+
source: this.CLASS_NAME,
|
|
361
|
+
message: "loadedBlob",
|
|
362
|
+
data: {
|
|
363
|
+
blobId
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
return ObjectHelper.fromBytes(decompressedBlob);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
await this._logging?.log({
|
|
371
|
+
level: "error",
|
|
372
|
+
source: this.CLASS_NAME,
|
|
373
|
+
message: "loadBlobFailed",
|
|
374
|
+
data: {
|
|
375
|
+
blobId
|
|
376
|
+
},
|
|
377
|
+
error: BaseError.fromError(error)
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
await this._logging?.log({
|
|
381
|
+
level: "info",
|
|
382
|
+
source: this.CLASS_NAME,
|
|
383
|
+
message: "loadBlobEmpty",
|
|
384
|
+
data: {
|
|
385
|
+
blobId
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Save a blob.
|
|
391
|
+
* @param blob The blob to save.
|
|
392
|
+
* @returns The id of the blob.
|
|
393
|
+
*/
|
|
394
|
+
async saveBlob(blob) {
|
|
395
|
+
await this._logging?.log({
|
|
396
|
+
level: "info",
|
|
397
|
+
source: this.CLASS_NAME,
|
|
398
|
+
message: "saveBlob"
|
|
399
|
+
});
|
|
400
|
+
if (!this._isTrustedNode) {
|
|
401
|
+
throw new GeneralError(this.CLASS_NAME, "notTrustedNode");
|
|
402
|
+
}
|
|
403
|
+
const compressedBlob = await Compression.compress(ObjectHelper.toBytes(blob), CompressionType.Gzip);
|
|
404
|
+
const encryptedBlob = await this._vaultConnector.encrypt(this._blobStorageEncryptionKeyId, VaultEncryptionType.Rsa2048, compressedBlob);
|
|
405
|
+
try {
|
|
406
|
+
const blobId = await this._blobStorageConnector.set(encryptedBlob);
|
|
407
|
+
await this._logging?.log({
|
|
408
|
+
level: "info",
|
|
409
|
+
source: this.CLASS_NAME,
|
|
410
|
+
message: "savedBlob",
|
|
411
|
+
data: {
|
|
412
|
+
blobId
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
return blobId;
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
await this._logging?.log({
|
|
419
|
+
level: "error",
|
|
420
|
+
source: this.CLASS_NAME,
|
|
421
|
+
message: "saveBlobFailed",
|
|
422
|
+
error: BaseError.fromError(error)
|
|
423
|
+
});
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Remove a blob from storage.
|
|
429
|
+
* @param blobId The id of the blob to remove.
|
|
430
|
+
* @returns Nothing.
|
|
431
|
+
*/
|
|
432
|
+
async removeBlob(blobId) {
|
|
433
|
+
await this._logging?.log({
|
|
434
|
+
level: "info",
|
|
435
|
+
source: this.CLASS_NAME,
|
|
436
|
+
message: "removeBlob",
|
|
437
|
+
data: {
|
|
438
|
+
blobId
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
try {
|
|
442
|
+
await this._blobStorageConnector.remove(blobId);
|
|
443
|
+
await this._logging?.log({
|
|
444
|
+
level: "info",
|
|
445
|
+
source: this.CLASS_NAME,
|
|
446
|
+
message: "removedBlob",
|
|
447
|
+
data: {
|
|
448
|
+
blobId
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
await this._logging?.log({
|
|
454
|
+
level: "error",
|
|
455
|
+
source: this.CLASS_NAME,
|
|
456
|
+
message: "removeBlobFailed",
|
|
457
|
+
data: {
|
|
458
|
+
blobId
|
|
459
|
+
},
|
|
460
|
+
error: BaseError.fromError(error)
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
await this._logging?.log({
|
|
464
|
+
level: "info",
|
|
465
|
+
source: this.CLASS_NAME,
|
|
466
|
+
message: "removeBlobEmpty",
|
|
467
|
+
data: {
|
|
468
|
+
blobId
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
162
474
|
// Copyright 2024 IOTA Stiftung.
|
|
163
475
|
// SPDX-License-Identifier: Apache-2.0.
|
|
164
476
|
/**
|
|
@@ -180,10 +492,10 @@ class ChangeSetHelper {
|
|
|
180
492
|
*/
|
|
181
493
|
_eventBusComponent;
|
|
182
494
|
/**
|
|
183
|
-
* The blob storage
|
|
495
|
+
* The blob storage helper to use for remote sync states.
|
|
184
496
|
* @internal
|
|
185
497
|
*/
|
|
186
|
-
|
|
498
|
+
_blobStorageHelper;
|
|
187
499
|
/**
|
|
188
500
|
* The identity connector to use for signing/verifying changesets.
|
|
189
501
|
* @internal
|
|
@@ -194,37 +506,73 @@ class ChangeSetHelper {
|
|
|
194
506
|
* @internal
|
|
195
507
|
*/
|
|
196
508
|
_decentralisedStorageMethodId;
|
|
509
|
+
/**
|
|
510
|
+
* The identity of the node that is performing the update.
|
|
511
|
+
* @internal
|
|
512
|
+
*/
|
|
513
|
+
_nodeIdentity;
|
|
197
514
|
/**
|
|
198
515
|
* Create a new instance of ChangeSetHelper.
|
|
199
516
|
* @param logging The logging connector to use for logging.
|
|
200
517
|
* @param eventBusComponent The event bus component to use for events.
|
|
201
|
-
* @param blobStorageComponent The blob storage component to use for remote sync states.
|
|
202
518
|
* @param identityConnector The identity connector to use for signing/verifying changesets.
|
|
519
|
+
* @param blobStorageHelper The blob storage component to use for remote sync states.
|
|
203
520
|
* @param decentralisedStorageMethodId The id of the identity method to use when signing/verifying changesets.
|
|
204
521
|
*/
|
|
205
|
-
constructor(logging, eventBusComponent,
|
|
522
|
+
constructor(logging, eventBusComponent, identityConnector, blobStorageHelper, decentralisedStorageMethodId) {
|
|
206
523
|
this._logging = logging;
|
|
207
524
|
this._eventBusComponent = eventBusComponent;
|
|
208
525
|
this._decentralisedStorageMethodId = decentralisedStorageMethodId;
|
|
209
|
-
this.
|
|
526
|
+
this._blobStorageHelper = blobStorageHelper;
|
|
210
527
|
this._identityConnector = identityConnector;
|
|
211
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* Set the node identity to use for signing changesets.
|
|
531
|
+
* @param nodeIdentity The identity of the node that is performing the update.
|
|
532
|
+
*/
|
|
533
|
+
setNodeIdentity(nodeIdentity) {
|
|
534
|
+
this._nodeIdentity = nodeIdentity;
|
|
535
|
+
}
|
|
212
536
|
/**
|
|
213
537
|
* Get and verify a changeset.
|
|
214
538
|
* @param changeSetStorageId The id of the sync changeset to apply.
|
|
215
539
|
* @returns The changeset if it was verified.
|
|
216
540
|
*/
|
|
217
541
|
async getAndVerifyChangeset(changeSetStorageId) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
542
|
+
await this._logging?.log({
|
|
543
|
+
level: "info",
|
|
544
|
+
source: this.CLASS_NAME,
|
|
545
|
+
message: "getChangeSet",
|
|
546
|
+
data: {
|
|
547
|
+
changeSetStorageId
|
|
548
|
+
}
|
|
222
549
|
});
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
550
|
+
try {
|
|
551
|
+
const syncChangeSet = await this._blobStorageHelper.loadBlob(changeSetStorageId);
|
|
552
|
+
if (Is.object(syncChangeSet)) {
|
|
553
|
+
const verified = await this.verifyChangesetProof(syncChangeSet);
|
|
554
|
+
return verified ? syncChangeSet : undefined;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
await this._logging?.log({
|
|
559
|
+
level: "warn",
|
|
560
|
+
source: this.CLASS_NAME,
|
|
561
|
+
message: "getChangeSetError",
|
|
562
|
+
data: {
|
|
563
|
+
changeSetStorageId
|
|
564
|
+
},
|
|
565
|
+
error: BaseError.fromError(error)
|
|
566
|
+
});
|
|
227
567
|
}
|
|
568
|
+
await this._logging?.log({
|
|
569
|
+
level: "info",
|
|
570
|
+
source: this.CLASS_NAME,
|
|
571
|
+
message: "getChangeSetEmpty",
|
|
572
|
+
data: {
|
|
573
|
+
changeSetStorageId
|
|
574
|
+
}
|
|
575
|
+
});
|
|
228
576
|
}
|
|
229
577
|
/**
|
|
230
578
|
* Apply a sync changeset.
|
|
@@ -233,7 +581,9 @@ class ChangeSetHelper {
|
|
|
233
581
|
*/
|
|
234
582
|
async getAndApplyChangeset(changeSetStorageId) {
|
|
235
583
|
const syncChangeset = await this.getAndVerifyChangeset(changeSetStorageId);
|
|
236
|
-
|
|
584
|
+
// Only apply changesets from other nodes, we don't want to overwrite
|
|
585
|
+
// any changes we have made to local entity storage
|
|
586
|
+
if (!Is.empty(syncChangeset) && syncChangeset.nodeIdentity !== this._nodeIdentity) {
|
|
237
587
|
await this.applyChangeset(syncChangeset);
|
|
238
588
|
}
|
|
239
589
|
return syncChangeset;
|
|
@@ -258,13 +608,18 @@ class ChangeSetHelper {
|
|
|
258
608
|
switch (change.operation) {
|
|
259
609
|
case SyncChangeOperation.Set:
|
|
260
610
|
if (!Is.empty(change.entity)) {
|
|
261
|
-
// The
|
|
611
|
+
// The id was stripped from the entity as it is part of the operation
|
|
612
|
+
// we make sure we reinstate it in the publish
|
|
613
|
+
// Also the node identity was stripped when stored in the changeset
|
|
262
614
|
// as the changeset is signed with the node identity.
|
|
263
615
|
// so we need to restore it here.
|
|
264
|
-
change.entity.nodeIdentity = syncChangeset.nodeIdentity;
|
|
265
616
|
await this._eventBusComponent.publish(SynchronisedStorageTopics.RemoteItemSet, {
|
|
266
617
|
storageKey: syncChangeset.storageKey,
|
|
267
|
-
entity:
|
|
618
|
+
entity: {
|
|
619
|
+
...change.entity,
|
|
620
|
+
id: change.id,
|
|
621
|
+
nodeIdentity: syncChangeset.nodeIdentity
|
|
622
|
+
}
|
|
268
623
|
});
|
|
269
624
|
}
|
|
270
625
|
break;
|
|
@@ -272,7 +627,8 @@ class ChangeSetHelper {
|
|
|
272
627
|
if (!Is.empty(change.id)) {
|
|
273
628
|
await this._eventBusComponent.publish(SynchronisedStorageTopics.RemoteItemRemove, {
|
|
274
629
|
storageKey: syncChangeset.storageKey,
|
|
275
|
-
id: change.id
|
|
630
|
+
id: change.id,
|
|
631
|
+
nodeIdentity: syncChangeset.nodeIdentity
|
|
276
632
|
});
|
|
277
633
|
}
|
|
278
634
|
break;
|
|
@@ -283,10 +639,9 @@ class ChangeSetHelper {
|
|
|
283
639
|
/**
|
|
284
640
|
* Store the changeset.
|
|
285
641
|
* @param syncChangeSet The sync change set to store.
|
|
286
|
-
* @param nodeIdentity The node identity to use for the changeset.
|
|
287
642
|
* @returns The id of the change set.
|
|
288
643
|
*/
|
|
289
|
-
async storeChangeSet(syncChangeSet
|
|
644
|
+
async storeChangeSet(syncChangeSet) {
|
|
290
645
|
await this._logging?.log({
|
|
291
646
|
level: "info",
|
|
292
647
|
source: this.CLASS_NAME,
|
|
@@ -295,12 +650,7 @@ class ChangeSetHelper {
|
|
|
295
650
|
id: syncChangeSet.id
|
|
296
651
|
}
|
|
297
652
|
});
|
|
298
|
-
|
|
299
|
-
// the blob storage also needs to be publicly accessible so that other nodes can retrieve it
|
|
300
|
-
return this._blobStorageComponent.create(Converter.bytesToBase64(ObjectHelper.toBytes(syncChangeSet)), undefined, undefined, undefined, {
|
|
301
|
-
disableEncryption: true,
|
|
302
|
-
compress: BlobStorageCompressionType.Gzip
|
|
303
|
-
}, undefined, nodeIdentity);
|
|
653
|
+
return this._blobStorageHelper.saveBlob(syncChangeSet);
|
|
304
654
|
}
|
|
305
655
|
/**
|
|
306
656
|
* Verify the proof of a sync changeset.
|
|
@@ -319,6 +669,32 @@ class ChangeSetHelper {
|
|
|
319
669
|
});
|
|
320
670
|
return false;
|
|
321
671
|
}
|
|
672
|
+
// If the proof or verification method is missing, the proof is invalid
|
|
673
|
+
const verificationMethod = syncChangeset.proof?.verificationMethod;
|
|
674
|
+
if (!Is.stringValue(verificationMethod)) {
|
|
675
|
+
await this._logging?.log({
|
|
676
|
+
level: "error",
|
|
677
|
+
source: this.CLASS_NAME,
|
|
678
|
+
message: "verifyChangeSetProofMissing",
|
|
679
|
+
data: {
|
|
680
|
+
id: syncChangeset.id
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
// Parse the verification method and extract the node identity
|
|
685
|
+
// this should match the node identity of the changeset
|
|
686
|
+
// otherwise you could sign a changeset for another node
|
|
687
|
+
const changeSetNodeIdentity = DocumentHelper.parseId(verificationMethod ?? "");
|
|
688
|
+
if (changeSetNodeIdentity.id !== syncChangeset.nodeIdentity) {
|
|
689
|
+
await this._logging?.log({
|
|
690
|
+
level: "error",
|
|
691
|
+
source: this.CLASS_NAME,
|
|
692
|
+
message: "verifyChangeSetProofNodeIdentityMismatch",
|
|
693
|
+
data: {
|
|
694
|
+
id: syncChangeset.id
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
322
698
|
const changeSetWithoutProof = ObjectHelper.clone(syncChangeset);
|
|
323
699
|
delete changeSetWithoutProof.proof;
|
|
324
700
|
const isValid = await this._identityConnector.verifyProof(changeSetWithoutProof, syncChangeset.proof);
|
|
@@ -350,9 +726,10 @@ class ChangeSetHelper {
|
|
|
350
726
|
* @returns The proof.
|
|
351
727
|
*/
|
|
352
728
|
async createChangeSetProof(syncChangeset) {
|
|
729
|
+
Guards.stringValue(this.CLASS_NAME, "nodeIdentity", this._nodeIdentity);
|
|
353
730
|
const changeSetWithoutProof = ObjectHelper.clone(syncChangeset);
|
|
354
731
|
delete changeSetWithoutProof.proof;
|
|
355
|
-
const proof = await this._identityConnector.createProof(
|
|
732
|
+
const proof = await this._identityConnector.createProof(this._nodeIdentity, DocumentHelper.joinId(this._nodeIdentity, this._decentralisedStorageMethodId), ProofTypes.DataIntegrityProof, changeSetWithoutProof);
|
|
356
733
|
await this._logging?.log({
|
|
357
734
|
level: "info",
|
|
358
735
|
source: this.CLASS_NAME,
|
|
@@ -364,8 +741,65 @@ class ChangeSetHelper {
|
|
|
364
741
|
});
|
|
365
742
|
return proof;
|
|
366
743
|
}
|
|
744
|
+
/**
|
|
745
|
+
* Copy a change set.
|
|
746
|
+
* @param syncChangeSet The sync changeset to copy.
|
|
747
|
+
* @returns The id of the updated change set.
|
|
748
|
+
*/
|
|
749
|
+
async copyChangeset(syncChangeSet) {
|
|
750
|
+
if (Is.stringValue(this._nodeIdentity)) {
|
|
751
|
+
const verified = await this.verifyChangesetProof(syncChangeSet);
|
|
752
|
+
if (verified) {
|
|
753
|
+
await this._logging?.log({
|
|
754
|
+
level: "info",
|
|
755
|
+
source: this.CLASS_NAME,
|
|
756
|
+
message: "copyChangeSet",
|
|
757
|
+
data: {
|
|
758
|
+
changeSetStorageId: syncChangeSet.id
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
// Allocate a new id to the changeset copy and re-create a proof using this nodes identity
|
|
762
|
+
const copy = ObjectHelper.clone(syncChangeSet);
|
|
763
|
+
copy.id = Converter.bytesToHex(RandomHelper.generate(32));
|
|
764
|
+
copy.proof = await this.createChangeSetProof(copy);
|
|
765
|
+
// Store the copy
|
|
766
|
+
return {
|
|
767
|
+
syncChangeSet: copy,
|
|
768
|
+
changeSetStorageId: await this.storeChangeSet(copy)
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Reset the storage for a given storage key.
|
|
775
|
+
* @param storageKey The key of the storage to reset.
|
|
776
|
+
* @param resetMode The reset mode, this will use the nodeIdentity in the entities to determine which are local/remote.
|
|
777
|
+
* @returns Nothing.
|
|
778
|
+
*/
|
|
779
|
+
async reset(storageKey, resetMode) {
|
|
780
|
+
// If we are applying a consolidation we need to reset the local db
|
|
781
|
+
// but keep any entries from the local node, as they might have been updated
|
|
782
|
+
await this._logging?.log({
|
|
783
|
+
level: "info",
|
|
784
|
+
source: this.CLASS_NAME,
|
|
785
|
+
message: "storageReset",
|
|
786
|
+
data: {
|
|
787
|
+
storageKey
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
await this._eventBusComponent.publish(SynchronisedStorageTopics.Reset, {
|
|
791
|
+
storageKey,
|
|
792
|
+
resetMode
|
|
793
|
+
});
|
|
794
|
+
}
|
|
367
795
|
}
|
|
368
796
|
|
|
797
|
+
// Copyright 2024 IOTA Stiftung.
|
|
798
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
799
|
+
const SYNC_STATE_VERSION = "1";
|
|
800
|
+
const SYNC_POINTER_STORE_VERSION = "1";
|
|
801
|
+
const SYNC_SNAPSHOT_VERSION = "1";
|
|
802
|
+
|
|
369
803
|
// Copyright 2024 IOTA Stiftung.
|
|
370
804
|
// SPDX-License-Identifier: Apache-2.0.
|
|
371
805
|
/**
|
|
@@ -385,7 +819,7 @@ class LocalSyncStateHelper {
|
|
|
385
819
|
* The storage connector for the sync snapshot entries.
|
|
386
820
|
* @internal
|
|
387
821
|
*/
|
|
388
|
-
|
|
822
|
+
_snapshotEntryEntityStorage;
|
|
389
823
|
/**
|
|
390
824
|
* The change set helper to use for applying changesets.
|
|
391
825
|
* @internal
|
|
@@ -394,12 +828,12 @@ class LocalSyncStateHelper {
|
|
|
394
828
|
/**
|
|
395
829
|
* Create a new instance of LocalSyncStateHelper.
|
|
396
830
|
* @param logging The logging connector to use for logging.
|
|
397
|
-
* @param
|
|
831
|
+
* @param snapshotEntryEntityStorage The storage connector for the sync snapshot entries.
|
|
398
832
|
* @param changeSetHelper The change set helper to use for applying changesets.
|
|
399
833
|
*/
|
|
400
|
-
constructor(logging,
|
|
834
|
+
constructor(logging, snapshotEntryEntityStorage, changeSetHelper) {
|
|
401
835
|
this._logging = logging;
|
|
402
|
-
this.
|
|
836
|
+
this._snapshotEntryEntityStorage = snapshotEntryEntityStorage;
|
|
403
837
|
this._changeSetHelper = changeSetHelper;
|
|
404
838
|
}
|
|
405
839
|
/**
|
|
@@ -420,40 +854,44 @@ class LocalSyncStateHelper {
|
|
|
420
854
|
id
|
|
421
855
|
}
|
|
422
856
|
});
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
localChangeSnapshot.changes.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
857
|
+
const localChangeSnapshots = await this.getSnapshots(storageKey, true);
|
|
858
|
+
if (localChangeSnapshots.length > 0) {
|
|
859
|
+
const localChangeSnapshot = localChangeSnapshots[0];
|
|
860
|
+
localChangeSnapshot.changes ??= [];
|
|
861
|
+
// If we already have a change for this id we are
|
|
862
|
+
// about to supersede it, we remove the previous change
|
|
863
|
+
// to avoid having multiple changes for the same id
|
|
864
|
+
const previousChangeIndex = localChangeSnapshot.changes.findIndex(change => change.id === id);
|
|
865
|
+
if (previousChangeIndex !== -1) {
|
|
866
|
+
localChangeSnapshot.changes.splice(previousChangeIndex, 1);
|
|
867
|
+
}
|
|
868
|
+
if (localChangeSnapshot.changes.length > 0) {
|
|
869
|
+
localChangeSnapshot.dateModified = new Date(Date.now()).toISOString();
|
|
870
|
+
}
|
|
871
|
+
localChangeSnapshot.changes.push({ operation, id });
|
|
872
|
+
await this.setLocalChangeSnapshot(localChangeSnapshot);
|
|
434
873
|
}
|
|
435
|
-
localChangeSnapshot.changes.push({ operation, id });
|
|
436
|
-
await this.setLocalChangeSnapshot(localChangeSnapshot);
|
|
437
874
|
}
|
|
438
875
|
/**
|
|
439
|
-
* Get the
|
|
876
|
+
* Get the snapshot which contains just the changes for this node.
|
|
440
877
|
* @param storageKey The storage key of the snapshot to get.
|
|
878
|
+
* @param isLocal Whether to get the local snapshot or not.
|
|
441
879
|
* @returns The local snapshot entry.
|
|
442
880
|
*/
|
|
443
|
-
async
|
|
881
|
+
async getSnapshots(storageKey, isLocal) {
|
|
444
882
|
await this._logging?.log({
|
|
445
883
|
level: "info",
|
|
446
884
|
source: this.CLASS_NAME,
|
|
447
|
-
message: "
|
|
885
|
+
message: "getSnapshots",
|
|
448
886
|
data: {
|
|
449
887
|
storageKey
|
|
450
888
|
}
|
|
451
889
|
});
|
|
452
|
-
const queryResult = await this.
|
|
890
|
+
const queryResult = await this._snapshotEntryEntityStorage.query({
|
|
453
891
|
conditions: [
|
|
454
892
|
{
|
|
455
|
-
property: "
|
|
456
|
-
value:
|
|
893
|
+
property: "isLocal",
|
|
894
|
+
value: isLocal,
|
|
457
895
|
comparison: ComparisonOperator.Equals
|
|
458
896
|
},
|
|
459
897
|
{
|
|
@@ -467,31 +905,37 @@ class LocalSyncStateHelper {
|
|
|
467
905
|
await this._logging?.log({
|
|
468
906
|
level: "info",
|
|
469
907
|
source: this.CLASS_NAME,
|
|
470
|
-
message: "
|
|
908
|
+
message: "getSnapshotsExists",
|
|
471
909
|
data: {
|
|
472
910
|
storageKey
|
|
473
911
|
}
|
|
474
912
|
});
|
|
475
|
-
return queryResult.entities
|
|
913
|
+
return queryResult.entities;
|
|
476
914
|
}
|
|
477
915
|
await this._logging?.log({
|
|
478
916
|
level: "info",
|
|
479
917
|
source: this.CLASS_NAME,
|
|
480
|
-
message: "
|
|
918
|
+
message: "getSnapshotsDoesNotExist",
|
|
481
919
|
data: {
|
|
482
920
|
storageKey
|
|
483
921
|
}
|
|
484
922
|
});
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
923
|
+
const now = new Date(Date.now()).toISOString();
|
|
924
|
+
return [
|
|
925
|
+
{
|
|
926
|
+
version: SYNC_SNAPSHOT_VERSION,
|
|
927
|
+
id: Converter.bytesToHex(RandomHelper.generate(32)),
|
|
928
|
+
storageKey,
|
|
929
|
+
dateCreated: now,
|
|
930
|
+
dateModified: now,
|
|
931
|
+
changeSetStorageIds: [],
|
|
932
|
+
isLocal,
|
|
933
|
+
isConsolidated: false
|
|
934
|
+
}
|
|
935
|
+
];
|
|
492
936
|
}
|
|
493
937
|
/**
|
|
494
|
-
* Set the current local snapshot.
|
|
938
|
+
* Set the current local snapshot with changes for this node.
|
|
495
939
|
* @param localChangeSnapshot The local change snapshot to set.
|
|
496
940
|
* @returns Nothing.
|
|
497
941
|
*/
|
|
@@ -504,10 +948,10 @@ class LocalSyncStateHelper {
|
|
|
504
948
|
storageKey: localChangeSnapshot.storageKey
|
|
505
949
|
}
|
|
506
950
|
});
|
|
507
|
-
await this.
|
|
951
|
+
await this._snapshotEntryEntityStorage.set(localChangeSnapshot);
|
|
508
952
|
}
|
|
509
953
|
/**
|
|
510
|
-
* Get the current local snapshot.
|
|
954
|
+
* Get the current local snapshot with the changes for this node.
|
|
511
955
|
* @param localChangeSnapshot The local change snapshot to remove.
|
|
512
956
|
* @returns Nothing.
|
|
513
957
|
*/
|
|
@@ -520,85 +964,155 @@ class LocalSyncStateHelper {
|
|
|
520
964
|
snapshotId: localChangeSnapshot.id
|
|
521
965
|
}
|
|
522
966
|
});
|
|
523
|
-
await this.
|
|
967
|
+
await this._snapshotEntryEntityStorage.remove(localChangeSnapshot.id);
|
|
524
968
|
}
|
|
525
969
|
/**
|
|
526
|
-
*
|
|
970
|
+
* Apply a sync state to the local node.
|
|
527
971
|
* @param storageKey The storage key of the snapshot to sync with.
|
|
528
|
-
* @param
|
|
972
|
+
* @param syncState The sync state to sync with.
|
|
529
973
|
* @returns Nothing.
|
|
530
974
|
*/
|
|
531
|
-
async
|
|
975
|
+
async applySyncState(storageKey, syncState) {
|
|
532
976
|
await this._logging?.log({
|
|
533
977
|
level: "info",
|
|
534
978
|
source: this.CLASS_NAME,
|
|
535
|
-
message: "
|
|
979
|
+
message: "applySyncState",
|
|
536
980
|
data: {
|
|
537
|
-
snapshotCount:
|
|
981
|
+
snapshotCount: syncState.snapshots.length
|
|
538
982
|
}
|
|
539
983
|
});
|
|
984
|
+
// Get all the existing snapshots that we have processed previously
|
|
985
|
+
const existingRemoteSnapshots = await this.getSnapshots(storageKey, false);
|
|
540
986
|
// Sort from newest to oldest
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
987
|
+
const sortedSnapshots = syncState.snapshots.sort((a, b) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime());
|
|
988
|
+
// If we have no existing snapshots we can't have yet synced
|
|
989
|
+
// in this case we need to find the most recent consolidation
|
|
990
|
+
// and use that to build a complete DB table
|
|
991
|
+
if (existingRemoteSnapshots.length === 0) {
|
|
545
992
|
await this._logging?.log({
|
|
546
993
|
level: "info",
|
|
547
994
|
source: this.CLASS_NAME,
|
|
548
|
-
message: "
|
|
995
|
+
message: "applySnapshotNoExisting",
|
|
549
996
|
data: {
|
|
550
|
-
|
|
551
|
-
dateCreated: new Date(remoteSnapshot.dateCreated).toISOString()
|
|
997
|
+
storageKey
|
|
552
998
|
}
|
|
553
999
|
});
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
modifiedSnapshots.push({
|
|
566
|
-
localSnapshot,
|
|
567
|
-
remoteSnapshot: remoteSnapshotWithContext
|
|
1000
|
+
const firstConsolidated = sortedSnapshots.find(snapshot => snapshot.isConsolidated);
|
|
1001
|
+
if (firstConsolidated) {
|
|
1002
|
+
// We found a consolidated snapshot, we can use it
|
|
1003
|
+
await this._logging?.log({
|
|
1004
|
+
level: "info",
|
|
1005
|
+
source: this.CLASS_NAME,
|
|
1006
|
+
message: "applySnapshotFoundConsolidated",
|
|
1007
|
+
data: {
|
|
1008
|
+
storageKey,
|
|
1009
|
+
snapshotId: firstConsolidated.id
|
|
1010
|
+
}
|
|
568
1011
|
});
|
|
1012
|
+
// We need to reset the entity storage and remove all the remote items
|
|
1013
|
+
// so that we use just the ones from the consolidation
|
|
1014
|
+
await this._changeSetHelper.reset(storageKey, SyncNodeIdentityMode.Remote);
|
|
1015
|
+
await this.processNewSnapshots([
|
|
1016
|
+
{
|
|
1017
|
+
...firstConsolidated,
|
|
1018
|
+
storageKey,
|
|
1019
|
+
isLocal: false
|
|
1020
|
+
}
|
|
1021
|
+
]);
|
|
569
1022
|
}
|
|
570
1023
|
else {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
1024
|
+
await this._logging?.log({
|
|
1025
|
+
level: "info",
|
|
1026
|
+
source: this.CLASS_NAME,
|
|
1027
|
+
message: "applySnapshotNoConsolidated",
|
|
1028
|
+
data: {
|
|
1029
|
+
storageKey
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
// Create a lookup map for the existing snapshots
|
|
1036
|
+
const existingSnapshots = {};
|
|
1037
|
+
for (const snapshot of existingRemoteSnapshots) {
|
|
1038
|
+
existingSnapshots[snapshot.id] = snapshot;
|
|
1039
|
+
}
|
|
1040
|
+
const newSnapshots = [];
|
|
1041
|
+
const modifiedSnapshots = [];
|
|
1042
|
+
const referencedExistingSnapshots = Object.keys(existingSnapshots);
|
|
1043
|
+
for (const snapshot of sortedSnapshots) {
|
|
1044
|
+
await this._logging?.log({
|
|
1045
|
+
level: "info",
|
|
1046
|
+
source: this.CLASS_NAME,
|
|
1047
|
+
message: "applySnapshot",
|
|
1048
|
+
data: {
|
|
1049
|
+
snapshotId: snapshot.id,
|
|
1050
|
+
dateCreated: new Date(snapshot.dateCreated).toISOString()
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
// See if we have the local snapshot
|
|
1054
|
+
const currentSnapshot = existingSnapshots[snapshot.id];
|
|
1055
|
+
// As we are referencing an existing snapshot, we need to remove it from the list
|
|
1056
|
+
// to allow us to cleanup any unreferenced snapshots later
|
|
1057
|
+
const idx = referencedExistingSnapshots.indexOf(snapshot.id);
|
|
1058
|
+
if (idx !== -1) {
|
|
1059
|
+
referencedExistingSnapshots.splice(idx, 1);
|
|
1060
|
+
}
|
|
1061
|
+
const updatedSnapshot = {
|
|
1062
|
+
...snapshot,
|
|
1063
|
+
storageKey,
|
|
1064
|
+
isLocal: false
|
|
1065
|
+
};
|
|
1066
|
+
if (Is.empty(currentSnapshot)) {
|
|
1067
|
+
// We don't have the snapshot locally, so we need to process it
|
|
1068
|
+
newSnapshots.push(updatedSnapshot);
|
|
1069
|
+
}
|
|
1070
|
+
else if (currentSnapshot.dateModified !== snapshot.dateModified) {
|
|
1071
|
+
// If the local snapshot has a different dateModified, we need to update it
|
|
1072
|
+
modifiedSnapshots.push({
|
|
1073
|
+
currentSnapshot,
|
|
1074
|
+
updatedSnapshot
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
// we sorted the snapshots from newest to oldest, so if we found a local snapshot
|
|
1079
|
+
// with the same dateModified as the remote snapshot, we can stop processing further
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
// We reverse the order of the snapshots to process them from oldest to newest
|
|
1084
|
+
// because we want to apply the changes in the order they were created
|
|
1085
|
+
await this.processModifiedSnapshots(modifiedSnapshots.reverse());
|
|
1086
|
+
await this.processNewSnapshots(newSnapshots.reverse());
|
|
1087
|
+
// Any ids remaining in this list are no longer referenced in the global state
|
|
1088
|
+
// so we should remove them from the local storage as they will never be updated again
|
|
1089
|
+
for (const referencedSnapshotId of referencedExistingSnapshots) {
|
|
1090
|
+
await this._snapshotEntryEntityStorage.remove(referencedSnapshotId);
|
|
574
1091
|
}
|
|
575
1092
|
}
|
|
576
|
-
// We reverse the order of the snapshots to process them from oldest to newest
|
|
577
|
-
// because we want to apply the changes in the order they were created
|
|
578
|
-
await this.processModifiedSnapshots(modifiedSnapshots.reverse());
|
|
579
|
-
await this.processNewSnapshots(newSnapshots.reverse());
|
|
580
1093
|
}
|
|
581
1094
|
/**
|
|
582
1095
|
* Process the modified snapshots and store them in the local storage.
|
|
583
1096
|
* @param modifiedSnapshots The modified snapshots to process.
|
|
584
1097
|
* @returns Nothing.
|
|
1098
|
+
* @internal
|
|
585
1099
|
*/
|
|
586
1100
|
async processModifiedSnapshots(modifiedSnapshots) {
|
|
587
1101
|
for (const modifiedSnapshot of modifiedSnapshots) {
|
|
588
1102
|
await this._logging?.log({
|
|
589
1103
|
level: "info",
|
|
590
1104
|
source: this.CLASS_NAME,
|
|
591
|
-
message: "
|
|
1105
|
+
message: "processModifiedSnapshot",
|
|
592
1106
|
data: {
|
|
593
|
-
snapshotId: modifiedSnapshot.
|
|
594
|
-
localModified: new Date(modifiedSnapshot.
|
|
595
|
-
modifiedSnapshot.
|
|
596
|
-
remoteModified: new Date(modifiedSnapshot.
|
|
597
|
-
modifiedSnapshot.
|
|
1107
|
+
snapshotId: modifiedSnapshot.updatedSnapshot.id,
|
|
1108
|
+
localModified: new Date(modifiedSnapshot.currentSnapshot.dateModified ??
|
|
1109
|
+
modifiedSnapshot.currentSnapshot.dateCreated).toISOString(),
|
|
1110
|
+
remoteModified: new Date(modifiedSnapshot.updatedSnapshot.dateModified ??
|
|
1111
|
+
modifiedSnapshot.updatedSnapshot.dateCreated).toISOString()
|
|
598
1112
|
}
|
|
599
1113
|
});
|
|
600
|
-
const remoteChangeSetStorageIds = modifiedSnapshot.
|
|
601
|
-
const localChangeSetStorageIds = modifiedSnapshot.
|
|
1114
|
+
const remoteChangeSetStorageIds = modifiedSnapshot.updatedSnapshot.changeSetStorageIds;
|
|
1115
|
+
const localChangeSetStorageIds = modifiedSnapshot.currentSnapshot.changeSetStorageIds ?? [];
|
|
602
1116
|
if (Is.arrayValue(remoteChangeSetStorageIds)) {
|
|
603
1117
|
for (const storageId of remoteChangeSetStorageIds) {
|
|
604
1118
|
// Check if the local snapshot does not have the storageId
|
|
@@ -607,23 +1121,24 @@ class LocalSyncStateHelper {
|
|
|
607
1121
|
}
|
|
608
1122
|
}
|
|
609
1123
|
}
|
|
610
|
-
await this.
|
|
1124
|
+
await this._snapshotEntryEntityStorage.set(modifiedSnapshot.updatedSnapshot);
|
|
611
1125
|
}
|
|
612
1126
|
}
|
|
613
1127
|
/**
|
|
614
1128
|
* Process the new snapshots and store them in the local storage.
|
|
615
1129
|
* @param newSnapshots The new snapshots to process.
|
|
616
1130
|
* @returns Nothing.
|
|
1131
|
+
* @internal
|
|
617
1132
|
*/
|
|
618
1133
|
async processNewSnapshots(newSnapshots) {
|
|
619
1134
|
for (const newSnapshot of newSnapshots) {
|
|
620
1135
|
await this._logging?.log({
|
|
621
1136
|
level: "info",
|
|
622
1137
|
source: this.CLASS_NAME,
|
|
623
|
-
message: "
|
|
1138
|
+
message: "processNewSnapshot",
|
|
624
1139
|
data: {
|
|
625
1140
|
snapshotId: newSnapshot.id,
|
|
626
|
-
|
|
1141
|
+
dateCreated: newSnapshot.dateCreated
|
|
627
1142
|
}
|
|
628
1143
|
});
|
|
629
1144
|
const newSnapshotChangeSetStorageIds = newSnapshot.changeSetStorageIds ?? [];
|
|
@@ -632,7 +1147,7 @@ class LocalSyncStateHelper {
|
|
|
632
1147
|
await this._changeSetHelper.getAndApplyChangeset(storageId);
|
|
633
1148
|
}
|
|
634
1149
|
}
|
|
635
|
-
await this.
|
|
1150
|
+
await this._snapshotEntryEntityStorage.set(newSnapshot);
|
|
636
1151
|
}
|
|
637
1152
|
}
|
|
638
1153
|
}
|
|
@@ -658,10 +1173,10 @@ class RemoteSyncStateHelper {
|
|
|
658
1173
|
*/
|
|
659
1174
|
_eventBusComponent;
|
|
660
1175
|
/**
|
|
661
|
-
* The blob storage
|
|
1176
|
+
* The blob storage helper.
|
|
662
1177
|
* @internal
|
|
663
1178
|
*/
|
|
664
|
-
|
|
1179
|
+
_blobStorageHelper;
|
|
665
1180
|
/**
|
|
666
1181
|
* The verifiable storage connector to use for storing sync pointers.
|
|
667
1182
|
* @internal
|
|
@@ -692,22 +1207,34 @@ class RemoteSyncStateHelper {
|
|
|
692
1207
|
* @internal
|
|
693
1208
|
*/
|
|
694
1209
|
_nodeIdentity;
|
|
1210
|
+
/**
|
|
1211
|
+
* Whether the node is trusted or not.
|
|
1212
|
+
* @internal
|
|
1213
|
+
*/
|
|
1214
|
+
_isTrustedNode;
|
|
1215
|
+
/**
|
|
1216
|
+
* Maximum number of consolidations to keep in storage.
|
|
1217
|
+
* @internal
|
|
1218
|
+
*/
|
|
1219
|
+
_maxConsolidations;
|
|
695
1220
|
/**
|
|
696
1221
|
* Create a new instance of DecentralisedEntityStorageConnector.
|
|
697
1222
|
* @param logging The logging connector to use for logging.
|
|
698
1223
|
* @param eventBusComponent The event bus component to use for events.
|
|
699
|
-
* @param blobStorageComponent The blob storage component to use for remote sync states.
|
|
700
1224
|
* @param verifiableSyncPointerStorageConnector The verifiable storage connector to use for storing sync pointers.
|
|
1225
|
+
* @param blobStorageHelper The blob storage helper to use for remote sync states.
|
|
701
1226
|
* @param changeSetHelper The change set helper to use for managing changesets.
|
|
702
|
-
* @param
|
|
1227
|
+
* @param isTrustedNode Whether the node is trusted or not.
|
|
1228
|
+
* @param maxConsolidations The maximum number of consolidations to keep in storage.
|
|
703
1229
|
*/
|
|
704
|
-
constructor(logging, eventBusComponent,
|
|
1230
|
+
constructor(logging, eventBusComponent, verifiableSyncPointerStorageConnector, blobStorageHelper, changeSetHelper, isTrustedNode, maxConsolidations) {
|
|
705
1231
|
this._logging = logging;
|
|
706
1232
|
this._eventBusComponent = eventBusComponent;
|
|
707
|
-
this._blobStorageComponent = blobStorageComponent;
|
|
708
1233
|
this._verifiableSyncPointerStorageConnector = verifiableSyncPointerStorageConnector;
|
|
709
1234
|
this._changeSetHelper = changeSetHelper;
|
|
710
|
-
this.
|
|
1235
|
+
this._blobStorageHelper = blobStorageHelper;
|
|
1236
|
+
this._isTrustedNode = isTrustedNode;
|
|
1237
|
+
this._maxConsolidations = maxConsolidations;
|
|
711
1238
|
this._batchResponseStorageIds = {};
|
|
712
1239
|
this._populateFullChanges = {};
|
|
713
1240
|
this._eventBusComponent.subscribe(SynchronisedStorageTopics.BatchResponse, async (response) => {
|
|
@@ -725,17 +1252,24 @@ class RemoteSyncStateHelper {
|
|
|
725
1252
|
this._nodeIdentity = nodeIdentity;
|
|
726
1253
|
}
|
|
727
1254
|
/**
|
|
728
|
-
*
|
|
1255
|
+
* Set the synchronised storage key.
|
|
1256
|
+
* @param synchronisedStorageKey The synchronised storage key to use.
|
|
1257
|
+
*/
|
|
1258
|
+
setSynchronisedStorageKey(synchronisedStorageKey) {
|
|
1259
|
+
this._synchronisedStorageKey = synchronisedStorageKey;
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Build a changeset.
|
|
729
1263
|
* @param storageKey The storage key of the change set.
|
|
730
1264
|
* @param changes The changes to apply.
|
|
731
1265
|
* @param completeCallback The callback to call when the changeset is created and stored.
|
|
732
1266
|
* @returns The storage id of the change set if created.
|
|
733
1267
|
*/
|
|
734
|
-
async
|
|
1268
|
+
async buildChangeSet(storageKey, changes, completeCallback) {
|
|
735
1269
|
await this._logging?.log({
|
|
736
1270
|
level: "info",
|
|
737
1271
|
source: this.CLASS_NAME,
|
|
738
|
-
message: "
|
|
1272
|
+
message: "buildingChangeSet",
|
|
739
1273
|
data: {
|
|
740
1274
|
storageKey,
|
|
741
1275
|
changeCount: changes.length
|
|
@@ -794,15 +1328,19 @@ class RemoteSyncStateHelper {
|
|
|
794
1328
|
for (const change of changes) {
|
|
795
1329
|
change.entity = this._populateFullChanges[storageKey].entities[change.id] ?? change.entity;
|
|
796
1330
|
if (change.operation === SyncChangeOperation.Set && Is.objectValue(change.entity)) {
|
|
1331
|
+
// Remove the id from the entity as this is stored in the operation
|
|
1332
|
+
// and will be reinstated when the changeset is reconstituted
|
|
1333
|
+
ObjectHelper.propertyDelete(change.entity, "id");
|
|
797
1334
|
// Remove the node identity as the changeset has this stored at the top level
|
|
798
1335
|
// and we do not want to store it in the change itself to reduce redundancy
|
|
799
1336
|
ObjectHelper.propertyDelete(change.entity, "nodeIdentity");
|
|
800
1337
|
}
|
|
801
1338
|
}
|
|
802
|
-
|
|
1339
|
+
const now = new Date(Date.now()).toISOString();
|
|
803
1340
|
const syncChangeSet = {
|
|
804
1341
|
id: Converter.bytesToHex(RandomHelper.generate(32)),
|
|
805
|
-
dateCreated:
|
|
1342
|
+
dateCreated: now,
|
|
1343
|
+
dateModified: now,
|
|
806
1344
|
storageKey,
|
|
807
1345
|
changes,
|
|
808
1346
|
nodeIdentity: this._nodeIdentity
|
|
@@ -810,9 +1348,12 @@ class RemoteSyncStateHelper {
|
|
|
810
1348
|
try {
|
|
811
1349
|
// And sign it with the node identity
|
|
812
1350
|
syncChangeSet.proof = await this._changeSetHelper.createChangeSetProof(syncChangeSet);
|
|
813
|
-
//
|
|
814
|
-
|
|
815
|
-
|
|
1351
|
+
// If this is a trusted node, we also store the changeset
|
|
1352
|
+
let changeSetStorageId;
|
|
1353
|
+
if (this._isTrustedNode) {
|
|
1354
|
+
changeSetStorageId = await this._changeSetHelper.storeChangeSet(syncChangeSet);
|
|
1355
|
+
}
|
|
1356
|
+
await completeCallback(syncChangeSet, changeSetStorageId);
|
|
816
1357
|
}
|
|
817
1358
|
catch (err) {
|
|
818
1359
|
await this._logging?.log({
|
|
@@ -851,27 +1392,33 @@ class RemoteSyncStateHelper {
|
|
|
851
1392
|
const syncPointerStore = await this.getVerifiableSyncPointerStore();
|
|
852
1393
|
let syncState;
|
|
853
1394
|
if (!Is.empty(syncPointerStore.syncPointers[storageKey])) {
|
|
854
|
-
syncState = await this.
|
|
1395
|
+
syncState = await this.getSyncState(syncPointerStore.syncPointers[storageKey]);
|
|
855
1396
|
}
|
|
856
1397
|
// No current sync state, so we create a new one
|
|
857
1398
|
if (Is.empty(syncState)) {
|
|
858
|
-
syncState = { snapshots: [] };
|
|
1399
|
+
syncState = { version: SYNC_STATE_VERSION, storageKey, snapshots: [] };
|
|
859
1400
|
}
|
|
860
1401
|
// Sort the snapshots so the newest snapshot is last in the array
|
|
861
1402
|
const sortedSnapshots = syncState.snapshots.sort((a, b) => a.dateCreated.localeCompare(b.dateCreated));
|
|
862
1403
|
// Get the current snapshot, if it does not exist we create a new one
|
|
863
1404
|
let currentSnapshot = sortedSnapshots[sortedSnapshots.length - 1];
|
|
864
|
-
|
|
1405
|
+
const now = new Date(Date.now()).toISOString();
|
|
1406
|
+
// If there is no snapshot or the current one is a consolidation
|
|
1407
|
+
// we start a new snapshot
|
|
1408
|
+
if (Is.empty(currentSnapshot) || currentSnapshot.isConsolidated) {
|
|
865
1409
|
currentSnapshot = {
|
|
1410
|
+
version: SYNC_SNAPSHOT_VERSION,
|
|
866
1411
|
id: Converter.bytesToHex(RandomHelper.generate(32)),
|
|
867
|
-
dateCreated:
|
|
1412
|
+
dateCreated: now,
|
|
1413
|
+
dateModified: now,
|
|
1414
|
+
isConsolidated: false,
|
|
868
1415
|
changeSetStorageIds: []
|
|
869
1416
|
};
|
|
870
1417
|
syncState.snapshots.push(currentSnapshot);
|
|
871
1418
|
}
|
|
872
1419
|
else {
|
|
873
1420
|
// Snapshot exists, we update the dateModified
|
|
874
|
-
currentSnapshot.dateModified =
|
|
1421
|
+
currentSnapshot.dateModified = now;
|
|
875
1422
|
}
|
|
876
1423
|
// Add the changeset storage id to the current snapshot
|
|
877
1424
|
currentSnapshot.changeSetStorageIds.push(changeSetStorageId);
|
|
@@ -886,57 +1433,61 @@ class RemoteSyncStateHelper {
|
|
|
886
1433
|
* @param batchSize The batch size to use for consolidation.
|
|
887
1434
|
* @returns Nothing.
|
|
888
1435
|
*/
|
|
889
|
-
async
|
|
1436
|
+
async consolidationStart(storageKey, batchSize) {
|
|
890
1437
|
await this._logging?.log({
|
|
891
1438
|
level: "info",
|
|
892
1439
|
source: this.CLASS_NAME,
|
|
893
1440
|
message: "consolidationStarting"
|
|
894
1441
|
});
|
|
895
|
-
|
|
1442
|
+
// Perform a batch request to start the consolidation
|
|
1443
|
+
await this._eventBusComponent.publish(SynchronisedStorageTopics.BatchRequest, { storageKey, batchSize, requestMode: SyncNodeIdentityMode.All });
|
|
896
1444
|
}
|
|
897
1445
|
/**
|
|
898
1446
|
* Get the sync pointer store.
|
|
899
1447
|
* @returns The sync pointer store.
|
|
900
1448
|
*/
|
|
901
1449
|
async getVerifiableSyncPointerStore() {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
level: "info",
|
|
905
|
-
source: this.CLASS_NAME,
|
|
906
|
-
message: "verifiableSyncPointerStoreRetrieving",
|
|
907
|
-
data: {
|
|
908
|
-
key: this._synchronisedStorageKey
|
|
909
|
-
}
|
|
910
|
-
});
|
|
911
|
-
const syncPointerStore = await this._verifiableSyncPointerStorageConnector.get(this._synchronisedStorageKey, { includeData: true });
|
|
912
|
-
if (Is.uint8Array(syncPointerStore.data)) {
|
|
913
|
-
const syncPointer = ObjectHelper.fromBytes(syncPointerStore.data);
|
|
1450
|
+
if (Is.stringValue(this._synchronisedStorageKey)) {
|
|
1451
|
+
try {
|
|
914
1452
|
await this._logging?.log({
|
|
915
1453
|
level: "info",
|
|
916
1454
|
source: this.CLASS_NAME,
|
|
917
|
-
message: "
|
|
1455
|
+
message: "verifiableSyncPointerStoreRetrieving",
|
|
918
1456
|
data: {
|
|
919
1457
|
key: this._synchronisedStorageKey
|
|
920
1458
|
}
|
|
921
1459
|
});
|
|
922
|
-
|
|
1460
|
+
const syncPointerStore = await this._verifiableSyncPointerStorageConnector.get(this._synchronisedStorageKey, { includeData: true });
|
|
1461
|
+
if (Is.uint8Array(syncPointerStore.data)) {
|
|
1462
|
+
const syncPointer = ObjectHelper.fromBytes(syncPointerStore.data);
|
|
1463
|
+
await this._logging?.log({
|
|
1464
|
+
level: "info",
|
|
1465
|
+
source: this.CLASS_NAME,
|
|
1466
|
+
message: "verifiableSyncPointerStoreRetrieved",
|
|
1467
|
+
data: {
|
|
1468
|
+
key: this._synchronisedStorageKey
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
return syncPointer;
|
|
1472
|
+
}
|
|
923
1473
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1474
|
+
catch (err) {
|
|
1475
|
+
if (!BaseError.someErrorName(err, NotFoundError.CLASS_NAME)) {
|
|
1476
|
+
throw err;
|
|
1477
|
+
}
|
|
928
1478
|
}
|
|
1479
|
+
await this._logging?.log({
|
|
1480
|
+
level: "info",
|
|
1481
|
+
source: this.CLASS_NAME,
|
|
1482
|
+
message: "verifiableSyncPointerStoreNotFound",
|
|
1483
|
+
data: {
|
|
1484
|
+
key: this._synchronisedStorageKey
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
929
1487
|
}
|
|
930
|
-
await this._logging?.log({
|
|
931
|
-
level: "info",
|
|
932
|
-
source: this.CLASS_NAME,
|
|
933
|
-
message: "verifiableSyncPointerStoreNotFound",
|
|
934
|
-
data: {
|
|
935
|
-
key: this._synchronisedStorageKey
|
|
936
|
-
}
|
|
937
|
-
});
|
|
938
1488
|
// If no sync pointer store exists, we return an empty one
|
|
939
1489
|
return {
|
|
1490
|
+
version: SYNC_POINTER_STORE_VERSION,
|
|
940
1491
|
syncPointers: {}
|
|
941
1492
|
};
|
|
942
1493
|
}
|
|
@@ -946,7 +1497,7 @@ class RemoteSyncStateHelper {
|
|
|
946
1497
|
* @returns Nothing.
|
|
947
1498
|
*/
|
|
948
1499
|
async storeVerifiableSyncPointerStore(syncPointerStore) {
|
|
949
|
-
if (this._nodeIdentity) {
|
|
1500
|
+
if (Is.stringValue(this._nodeIdentity) && Is.stringValue(this._synchronisedStorageKey)) {
|
|
950
1501
|
await this._logging?.log({
|
|
951
1502
|
level: "info",
|
|
952
1503
|
source: this.CLASS_NAME,
|
|
@@ -968,39 +1519,55 @@ class RemoteSyncStateHelper {
|
|
|
968
1519
|
await this._logging?.log({
|
|
969
1520
|
level: "info",
|
|
970
1521
|
source: this.CLASS_NAME,
|
|
971
|
-
message: "
|
|
1522
|
+
message: "syncStateStoring",
|
|
972
1523
|
data: {
|
|
973
1524
|
snapshotCount: syncState.snapshots.length
|
|
974
1525
|
}
|
|
975
1526
|
});
|
|
976
|
-
//
|
|
977
|
-
//
|
|
978
|
-
|
|
1527
|
+
// Limits the number of consolidations in the list so that we can shrink decentralised
|
|
1528
|
+
// storage requirements, sort from newest to oldest so that we can easily find the
|
|
1529
|
+
// oldest snapshots to remove.
|
|
1530
|
+
const snapshots = syncState.snapshots.sort((a, b) => new Date(a.dateCreated).getTime() - new Date(b.dateCreated).getTime());
|
|
1531
|
+
// Find all the consolidation indexes
|
|
1532
|
+
const consolidationIndexes = [];
|
|
1533
|
+
for (let i = 0; i < snapshots.length; i++) {
|
|
1534
|
+
const snapshot = snapshots[i];
|
|
1535
|
+
if (snapshot.isConsolidated) {
|
|
1536
|
+
consolidationIndexes.push(i);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
if (consolidationIndexes.length > this._maxConsolidations) {
|
|
1540
|
+
// Once we have reached the max for consolidations we need to remove
|
|
1541
|
+
// all the snapshots, including non consolidated ones, beyond this point
|
|
1542
|
+
const toRemove = snapshots.slice(consolidationIndexes[this._maxConsolidations - 1] + 1);
|
|
1543
|
+
syncState.snapshots = snapshots.slice(0, consolidationIndexes[this._maxConsolidations - 1] + 1);
|
|
1544
|
+
for (const snapshot of toRemove) {
|
|
1545
|
+
await this._blobStorageHelper.removeBlob(snapshot.id);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
return this._blobStorageHelper.saveBlob(syncState);
|
|
979
1549
|
}
|
|
980
1550
|
/**
|
|
981
1551
|
* Get the remote sync state.
|
|
982
1552
|
* @param syncPointerId The id of the sync pointer to retrieve the state for.
|
|
983
1553
|
* @returns The remote sync state.
|
|
984
1554
|
*/
|
|
985
|
-
async
|
|
1555
|
+
async getSyncState(syncPointerId) {
|
|
986
1556
|
try {
|
|
987
1557
|
await this._logging?.log({
|
|
988
1558
|
level: "info",
|
|
989
1559
|
source: this.CLASS_NAME,
|
|
990
|
-
message: "
|
|
1560
|
+
message: "syncStateRetrieving",
|
|
991
1561
|
data: {
|
|
992
1562
|
syncPointerId
|
|
993
1563
|
}
|
|
994
1564
|
});
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
}, undefined, this._nodeIdentity);
|
|
998
|
-
if (Is.stringBase64(blobEntry.blob)) {
|
|
999
|
-
const syncState = ObjectHelper.fromBytes(Converter.base64ToBytes(blobEntry.blob));
|
|
1565
|
+
const syncState = await this._blobStorageHelper.loadBlob(syncPointerId);
|
|
1566
|
+
if (Is.object(syncState)) {
|
|
1000
1567
|
await this._logging?.log({
|
|
1001
1568
|
level: "info",
|
|
1002
1569
|
source: this.CLASS_NAME,
|
|
1003
|
-
message: "
|
|
1570
|
+
message: "syncStateRetrieved",
|
|
1004
1571
|
data: {
|
|
1005
1572
|
syncPointerId,
|
|
1006
1573
|
snapshotCount: syncState.snapshots.length
|
|
@@ -1009,33 +1576,41 @@ class RemoteSyncStateHelper {
|
|
|
1009
1576
|
return syncState;
|
|
1010
1577
|
}
|
|
1011
1578
|
}
|
|
1012
|
-
catch (
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1579
|
+
catch (error) {
|
|
1580
|
+
await this._logging?.log({
|
|
1581
|
+
level: "warn",
|
|
1582
|
+
source: this.CLASS_NAME,
|
|
1583
|
+
message: "getSyncStateError",
|
|
1584
|
+
data: {
|
|
1585
|
+
syncPointerId
|
|
1586
|
+
},
|
|
1587
|
+
error: BaseError.fromError(error)
|
|
1588
|
+
});
|
|
1016
1589
|
}
|
|
1017
1590
|
await this._logging?.log({
|
|
1018
1591
|
level: "info",
|
|
1019
1592
|
source: this.CLASS_NAME,
|
|
1020
|
-
message: "
|
|
1593
|
+
message: "syncStateNotFound",
|
|
1021
1594
|
data: {
|
|
1022
1595
|
syncPointerId
|
|
1023
1596
|
}
|
|
1024
1597
|
});
|
|
1025
1598
|
}
|
|
1026
1599
|
/**
|
|
1027
|
-
* Handle the batch response.
|
|
1600
|
+
* Handle the batch response which is triggered from a consolidation request.
|
|
1028
1601
|
* @param response The batch response to handle.
|
|
1029
1602
|
*/
|
|
1030
1603
|
async handleBatchResponse(response) {
|
|
1031
1604
|
if (Is.stringValue(this._nodeIdentity)) {
|
|
1605
|
+
const now = new Date(Date.now()).toISOString();
|
|
1032
1606
|
// Create a new snapshot entry for the current batch
|
|
1033
1607
|
const syncChangeSet = {
|
|
1034
1608
|
id: Converter.bytesToHex(RandomHelper.generate(32)),
|
|
1035
|
-
dateCreated:
|
|
1609
|
+
dateCreated: now,
|
|
1610
|
+
dateModified: now,
|
|
1036
1611
|
changes: response.entities.map(change => ({
|
|
1037
1612
|
operation: SyncChangeOperation.Set,
|
|
1038
|
-
id: change
|
|
1613
|
+
id: change.id
|
|
1039
1614
|
})),
|
|
1040
1615
|
storageKey: response.storageKey,
|
|
1041
1616
|
nodeIdentity: this._nodeIdentity
|
|
@@ -1043,25 +1618,42 @@ class RemoteSyncStateHelper {
|
|
|
1043
1618
|
// And sign it with the node identity
|
|
1044
1619
|
syncChangeSet.proof = await this._changeSetHelper.createChangeSetProof(syncChangeSet);
|
|
1045
1620
|
// Store the changeset in the blob storage
|
|
1046
|
-
const changeSetStorageId = await this._changeSetHelper.storeChangeSet(syncChangeSet
|
|
1621
|
+
const changeSetStorageId = await this._changeSetHelper.storeChangeSet(syncChangeSet);
|
|
1047
1622
|
// Add the changeset storage id to the snapshot ids
|
|
1048
1623
|
this._batchResponseStorageIds[response.storageKey] ??= [];
|
|
1049
1624
|
this._batchResponseStorageIds[response.storageKey].push(changeSetStorageId);
|
|
1625
|
+
// If this is the last entry in the batch response, we can create the consolidated snapshot
|
|
1050
1626
|
if (response.lastEntry) {
|
|
1051
|
-
|
|
1627
|
+
// Get the current sync pointer store
|
|
1628
|
+
const syncPointerStore = await this.getVerifiableSyncPointerStore();
|
|
1629
|
+
let syncState;
|
|
1630
|
+
if (Is.stringValue(syncPointerStore.syncPointers[response.storageKey])) {
|
|
1631
|
+
// If the sync pointer exists, we load the current sync state
|
|
1632
|
+
syncState = await this.getSyncState(syncPointerStore.syncPointers[response.storageKey]);
|
|
1633
|
+
}
|
|
1634
|
+
// If the sync state does not exist, we create a new one
|
|
1635
|
+
syncState ??= {
|
|
1636
|
+
version: SYNC_STATE_VERSION,
|
|
1637
|
+
storageKey: response.storageKey,
|
|
1638
|
+
snapshots: []
|
|
1639
|
+
};
|
|
1052
1640
|
const batchSnapshot = {
|
|
1641
|
+
version: SYNC_SNAPSHOT_VERSION,
|
|
1053
1642
|
id: Converter.bytesToHex(RandomHelper.generate(32)),
|
|
1054
|
-
dateCreated:
|
|
1643
|
+
dateCreated: now,
|
|
1644
|
+
dateModified: now,
|
|
1645
|
+
isConsolidated: true,
|
|
1055
1646
|
changeSetStorageIds: this._batchResponseStorageIds[response.storageKey]
|
|
1056
1647
|
};
|
|
1057
1648
|
syncState.snapshots.push(batchSnapshot);
|
|
1058
|
-
// Store the sync state
|
|
1649
|
+
// Store the updated sync state
|
|
1059
1650
|
const syncStateId = await this.storeRemoteSyncState(syncState);
|
|
1060
|
-
// Get the current sync pointer store
|
|
1061
|
-
const syncPointerStore = await this.getVerifiableSyncPointerStore();
|
|
1062
1651
|
syncPointerStore.syncPointers[response.storageKey] = syncStateId;
|
|
1063
1652
|
// Store the verifiable sync pointer in the verifiable storage
|
|
1064
1653
|
await this.storeVerifiableSyncPointerStore(syncPointerStore);
|
|
1654
|
+
// Remove the batch response storage ids for the storage key
|
|
1655
|
+
// as we have consolidated the changes
|
|
1656
|
+
delete this._batchResponseStorageIds[response.storageKey];
|
|
1065
1657
|
await this._logging?.log({
|
|
1066
1658
|
level: "info",
|
|
1067
1659
|
source: this.CLASS_NAME,
|
|
@@ -1084,11 +1676,14 @@ class RemoteSyncStateHelper {
|
|
|
1084
1676
|
id: response.id
|
|
1085
1677
|
}
|
|
1086
1678
|
});
|
|
1679
|
+
// We have received a response to an item request, find the right storage
|
|
1680
|
+
// for the request id
|
|
1087
1681
|
if (!Is.empty(this._populateFullChanges[response.storageKey])) {
|
|
1088
1682
|
const idx = this._populateFullChanges[response.storageKey].requestIds.indexOf(response.id);
|
|
1089
1683
|
if (idx !== -1) {
|
|
1090
1684
|
this._populateFullChanges[response.storageKey].requestIds.splice(idx, 1);
|
|
1091
1685
|
this._populateFullChanges[response.storageKey].entities[response.id] = response.entity;
|
|
1686
|
+
// If there are no request ids remaining we can complete the population
|
|
1092
1687
|
if (this._populateFullChanges[response.storageKey].requestIds.length === 0) {
|
|
1093
1688
|
await this._populateFullChanges[response.storageKey].completeCallback();
|
|
1094
1689
|
}
|
|
@@ -1116,6 +1711,11 @@ class SynchronisedStorageService {
|
|
|
1116
1711
|
* @internal
|
|
1117
1712
|
*/
|
|
1118
1713
|
static _DEFAULT_CONSOLIDATION_BATCH_SIZE = 100;
|
|
1714
|
+
/**
|
|
1715
|
+
* The default max number of consolidations to keep in storage.
|
|
1716
|
+
* @internal
|
|
1717
|
+
*/
|
|
1718
|
+
static _DEFAULT_MAX_CONSOLIDATIONS = 5;
|
|
1119
1719
|
/**
|
|
1120
1720
|
* Runtime name for the class.
|
|
1121
1721
|
*/
|
|
@@ -1130,16 +1730,21 @@ class SynchronisedStorageService {
|
|
|
1130
1730
|
* @internal
|
|
1131
1731
|
*/
|
|
1132
1732
|
_eventBusComponent;
|
|
1733
|
+
/**
|
|
1734
|
+
* The vault connector.
|
|
1735
|
+
* @internal
|
|
1736
|
+
*/
|
|
1737
|
+
_vaultConnector;
|
|
1133
1738
|
/**
|
|
1134
1739
|
* The storage connector for the sync snapshot entries.
|
|
1135
1740
|
* @internal
|
|
1136
1741
|
*/
|
|
1137
1742
|
_localSyncSnapshotEntryEntityStorage;
|
|
1138
1743
|
/**
|
|
1139
|
-
* The blob storage
|
|
1744
|
+
* The blob storage connector to use for remote sync states.
|
|
1140
1745
|
* @internal
|
|
1141
1746
|
*/
|
|
1142
|
-
|
|
1747
|
+
_blobStorageConnector;
|
|
1143
1748
|
/**
|
|
1144
1749
|
* The verifiable storage connector to use for storing sync pointers.
|
|
1145
1750
|
* @internal
|
|
@@ -1160,6 +1765,11 @@ class SynchronisedStorageService {
|
|
|
1160
1765
|
* @internal
|
|
1161
1766
|
*/
|
|
1162
1767
|
_trustedSynchronisedStorageComponent;
|
|
1768
|
+
/**
|
|
1769
|
+
* The blob storage helper.
|
|
1770
|
+
* @internal
|
|
1771
|
+
*/
|
|
1772
|
+
_blobStorageHelper;
|
|
1163
1773
|
/**
|
|
1164
1774
|
* The change set helper.
|
|
1165
1775
|
* @internal
|
|
@@ -1180,6 +1790,11 @@ class SynchronisedStorageService {
|
|
|
1180
1790
|
* @internal
|
|
1181
1791
|
*/
|
|
1182
1792
|
_config;
|
|
1793
|
+
/**
|
|
1794
|
+
* The synchronised storage key to use for the remote synchronised storage.
|
|
1795
|
+
* @internal
|
|
1796
|
+
*/
|
|
1797
|
+
_synchronisedStorageKey;
|
|
1183
1798
|
/**
|
|
1184
1799
|
* The flag to determine if the service has been started.
|
|
1185
1800
|
* @internal
|
|
@@ -1204,13 +1819,13 @@ class SynchronisedStorageService {
|
|
|
1204
1819
|
Guards.object(this.CLASS_NAME, "options.config", options.config);
|
|
1205
1820
|
this._eventBusComponent = ComponentFactory.get(options.eventBusComponentType ?? "event-bus");
|
|
1206
1821
|
this._logging = LoggingConnectorFactory.getIfExists(options.loggingConnectorType ?? "logging");
|
|
1822
|
+
this._vaultConnector = VaultConnectorFactory.get(options.vaultConnectorType ?? "vault");
|
|
1207
1823
|
this._localSyncSnapshotEntryEntityStorage = EntityStorageConnectorFactory.get(options.syncSnapshotStorageConnectorType ?? "sync-snapshot-entry");
|
|
1208
1824
|
this._verifiableSyncPointerStorageConnector = VerifiableStorageConnectorFactory.get(options.verifiableStorageConnectorType ?? "verifiable-storage");
|
|
1209
|
-
this.
|
|
1825
|
+
this._blobStorageConnector = BlobStorageConnectorFactory.get(options.blobStorageConnectorType ?? "blob-storage");
|
|
1210
1826
|
this._identityConnector = IdentityConnectorFactory.get(options.identityConnectorType ?? "identity");
|
|
1211
1827
|
this._taskSchedulerComponent = ComponentFactory.get(options.taskSchedulerComponentType ?? "task-scheduler");
|
|
1212
1828
|
this._config = {
|
|
1213
|
-
synchronisedStorageKey: options.config.synchronisedStorageKey,
|
|
1214
1829
|
synchronisedStorageMethodId: options.config.synchronisedStorageMethodId ?? "synchronised-storage-assertion",
|
|
1215
1830
|
entityUpdateIntervalMinutes: options.config.entityUpdateIntervalMinutes ??
|
|
1216
1831
|
SynchronisedStorageService._DEFAULT_ENTITY_UPDATE_INTERVAL_MINUTES,
|
|
@@ -1218,8 +1833,13 @@ class SynchronisedStorageService {
|
|
|
1218
1833
|
consolidationIntervalMinutes: options.config.consolidationIntervalMinutes ??
|
|
1219
1834
|
SynchronisedStorageService._DEFAULT_CONSOLIDATION_INTERVAL_MINUTES,
|
|
1220
1835
|
consolidationBatchSize: options.config.consolidationBatchSize ??
|
|
1221
|
-
SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE
|
|
1836
|
+
SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE,
|
|
1837
|
+
maxConsolidations: options.config.maxConsolidations ?? SynchronisedStorageService._DEFAULT_MAX_CONSOLIDATIONS,
|
|
1838
|
+
blobStorageEncryptionKeyId: options.config.blobStorageEncryptionKeyId ?? "synchronised-storage-blob-encryption-key",
|
|
1839
|
+
verifiableStorageKeyId: options.config.verifiableStorageKeyId
|
|
1222
1840
|
};
|
|
1841
|
+
this._synchronisedStorageKey =
|
|
1842
|
+
verifiableStorageKeys[options.config.verifiableStorageKeyId] ?? options.config.verifiableStorageKeyId;
|
|
1223
1843
|
// If this is not a trusted node, we need to use a synchronised storage service
|
|
1224
1844
|
// to synchronise with a trusted node.
|
|
1225
1845
|
if (!this._config.isTrustedNode) {
|
|
@@ -1227,13 +1847,19 @@ class SynchronisedStorageService {
|
|
|
1227
1847
|
this._trustedSynchronisedStorageComponent =
|
|
1228
1848
|
ComponentFactory.get(options.trustedSynchronisedStorageComponentType);
|
|
1229
1849
|
}
|
|
1230
|
-
this.
|
|
1850
|
+
this._blobStorageHelper = new BlobStorageHelper(this._logging, this._vaultConnector, this._blobStorageConnector, this._config.blobStorageEncryptionKeyId, this._config.isTrustedNode);
|
|
1851
|
+
this._changeSetHelper = new ChangeSetHelper(this._logging, this._eventBusComponent, this._identityConnector, this._blobStorageHelper, this._config.synchronisedStorageMethodId);
|
|
1231
1852
|
this._localSyncStateHelper = new LocalSyncStateHelper(this._logging, this._localSyncSnapshotEntryEntityStorage, this._changeSetHelper);
|
|
1232
|
-
this._remoteSyncStateHelper = new RemoteSyncStateHelper(this._logging, this._eventBusComponent, this.
|
|
1853
|
+
this._remoteSyncStateHelper = new RemoteSyncStateHelper(this._logging, this._eventBusComponent, this._verifiableSyncPointerStorageConnector, this._blobStorageHelper, this._changeSetHelper, this._config.isTrustedNode, this._config.maxConsolidations);
|
|
1233
1854
|
this._serviceStarted = false;
|
|
1234
1855
|
this._activeStorageKeys = {};
|
|
1235
|
-
this._eventBusComponent.subscribe(SynchronisedStorageTopics.RegisterStorageKey, async (event) => this.
|
|
1236
|
-
this._eventBusComponent.subscribe(SynchronisedStorageTopics.LocalItemChange, async (event) =>
|
|
1856
|
+
this._eventBusComponent.subscribe(SynchronisedStorageTopics.RegisterStorageKey, async (event) => this.registerStorageKey(event.data));
|
|
1857
|
+
this._eventBusComponent.subscribe(SynchronisedStorageTopics.LocalItemChange, async (event) => {
|
|
1858
|
+
// Make sure the change event is from this node
|
|
1859
|
+
if (Is.stringValue(this._nodeIdentity) && this._nodeIdentity === event.data.nodeIdentity) {
|
|
1860
|
+
await this._localSyncStateHelper.addLocalChange(event.data.storageKey, event.data.operation, event.data.id);
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1237
1863
|
}
|
|
1238
1864
|
/**
|
|
1239
1865
|
* The component needs to be started when the node is initialized.
|
|
@@ -1245,7 +1871,16 @@ class SynchronisedStorageService {
|
|
|
1245
1871
|
async start(nodeIdentity, nodeLoggingConnectorType, componentState) {
|
|
1246
1872
|
this._nodeIdentity = nodeIdentity;
|
|
1247
1873
|
this._remoteSyncStateHelper.setNodeIdentity(nodeIdentity);
|
|
1874
|
+
this._changeSetHelper.setNodeIdentity(nodeIdentity);
|
|
1875
|
+
this._remoteSyncStateHelper.setSynchronisedStorageKey(this._synchronisedStorageKey);
|
|
1248
1876
|
this._serviceStarted = true;
|
|
1877
|
+
// If this is not a trusted node we need to request the decryption key from a trusted node
|
|
1878
|
+
if (!this._config.isTrustedNode && !Is.empty(this._trustedSynchronisedStorageComponent)) {
|
|
1879
|
+
const proof = await this._identityConnector.createProof(this._nodeIdentity, DocumentHelper.joinId(this._nodeIdentity, this._config.synchronisedStorageMethodId), ProofTypes.DataIntegrityProof, { nodeIdentity });
|
|
1880
|
+
const decryptionKey = await this._trustedSynchronisedStorageComponent.getDecryptionKey(this._nodeIdentity, proof);
|
|
1881
|
+
// We don't have the private key so instead we store the key as a secret in the vault
|
|
1882
|
+
await this._vaultConnector.setSecret(this._config.blobStorageEncryptionKeyId, decryptionKey);
|
|
1883
|
+
}
|
|
1249
1884
|
// If there are already storage keys registered, we need to activate them
|
|
1250
1885
|
for (const storageKey in this._activeStorageKeys) {
|
|
1251
1886
|
await this.activateStorageKey(storageKey);
|
|
@@ -1266,24 +1901,59 @@ class SynchronisedStorageService {
|
|
|
1266
1901
|
}
|
|
1267
1902
|
}
|
|
1268
1903
|
/**
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1904
|
+
* Get the decryption key for the synchronised storage.
|
|
1905
|
+
* This is used to decrypt the data stored in the synchronised storage.
|
|
1906
|
+
* @param nodeIdentity The identity of the node requesting the decryption key.
|
|
1907
|
+
* @param proof The proof of the request so we know the request is from the specified node.
|
|
1908
|
+
* @returns The decryption key.
|
|
1909
|
+
*/
|
|
1910
|
+
async getDecryptionKey(nodeIdentity, proof) {
|
|
1911
|
+
if (!this._config.isTrustedNode) {
|
|
1912
|
+
throw new GeneralError(this.CLASS_NAME, "notTrustedNode");
|
|
1913
|
+
}
|
|
1914
|
+
Guards.stringValue(this.CLASS_NAME, "nodeIdentity", nodeIdentity);
|
|
1915
|
+
Guards.object(this.CLASS_NAME, "proof", proof);
|
|
1916
|
+
const isValid = await this._identityConnector.verifyProof({ nodeIdentity }, proof);
|
|
1917
|
+
if (!isValid) {
|
|
1918
|
+
throw new UnauthorizedError(this.CLASS_NAME, "invalidProof");
|
|
1919
|
+
}
|
|
1920
|
+
// TODO: We need to check if the node has permissions to access the decryption key
|
|
1921
|
+
// using rights-management
|
|
1922
|
+
const key = await this._vaultConnector.getKey(this._config.blobStorageEncryptionKeyId);
|
|
1923
|
+
if (Is.undefined(key.publicKey)) {
|
|
1924
|
+
throw new UnauthorizedError(this.CLASS_NAME, "decryptionKeyNotFound");
|
|
1925
|
+
}
|
|
1926
|
+
return Converter.bytesToBase64(key.publicKey);
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Synchronise a set of changes from an untrusted node, assumes this is a trusted node.
|
|
1930
|
+
* @param syncChangeSet The change set to synchronise.
|
|
1271
1931
|
* @returns Nothing.
|
|
1272
1932
|
*/
|
|
1273
|
-
async syncChangeSet(
|
|
1933
|
+
async syncChangeSet(syncChangeSet) {
|
|
1274
1934
|
if (!this._config.isTrustedNode) {
|
|
1275
1935
|
throw new GeneralError(this.CLASS_NAME, "notTrustedNode");
|
|
1276
1936
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1937
|
+
Guards.object(this.CLASS_NAME, "syncChangeSet", syncChangeSet);
|
|
1938
|
+
await this._logging?.log({
|
|
1939
|
+
level: "info",
|
|
1940
|
+
source: this.CLASS_NAME,
|
|
1941
|
+
message: "syncChangeSetForRemoteNode",
|
|
1942
|
+
data: {
|
|
1943
|
+
changeSetStorageId: syncChangeSet.id
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1279
1946
|
// TODO: The change set has a proof signed by the originating node identity
|
|
1280
1947
|
// The proof is verified that the change set is valid and has not been tampered with.
|
|
1281
1948
|
// but we also need to check that the originating node has permissions
|
|
1282
1949
|
// to store the change set in the synchronised storage.
|
|
1283
1950
|
// This will be performed using rights-management
|
|
1284
|
-
const
|
|
1285
|
-
if (!Is.empty(
|
|
1286
|
-
|
|
1951
|
+
const copy = await this._changeSetHelper.copyChangeset(syncChangeSet);
|
|
1952
|
+
if (!Is.empty(copy)) {
|
|
1953
|
+
// Apply the changes to this node
|
|
1954
|
+
await this._changeSetHelper.applyChangeset(copy.syncChangeSet);
|
|
1955
|
+
// And update the sync state with the latest changes
|
|
1956
|
+
await this._remoteSyncStateHelper.addChangeSetToSyncState(copy.syncChangeSet.storageKey, copy.changeSetStorageId);
|
|
1287
1957
|
}
|
|
1288
1958
|
}
|
|
1289
1959
|
/**
|
|
@@ -1336,10 +2006,10 @@ class SynchronisedStorageService {
|
|
|
1336
2006
|
if (!Is.empty(verifiableSyncPointerStore.syncPointers[storageKey])) {
|
|
1337
2007
|
// Load the sync state from the remote blob storage using the sync pointer
|
|
1338
2008
|
// to load the sync state
|
|
1339
|
-
const remoteSyncState = await this._remoteSyncStateHelper.
|
|
2009
|
+
const remoteSyncState = await this._remoteSyncStateHelper.getSyncState(verifiableSyncPointerStore.syncPointers[storageKey]);
|
|
1340
2010
|
// If we got the sync state we can try and sync from it
|
|
1341
2011
|
if (!Is.undefined(remoteSyncState)) {
|
|
1342
|
-
await this._localSyncStateHelper.
|
|
2012
|
+
await this._localSyncStateHelper.applySyncState(storageKey, remoteSyncState);
|
|
1343
2013
|
}
|
|
1344
2014
|
}
|
|
1345
2015
|
}
|
|
@@ -1357,51 +2027,67 @@ class SynchronisedStorageService {
|
|
|
1357
2027
|
storageKey
|
|
1358
2028
|
}
|
|
1359
2029
|
});
|
|
1360
|
-
const
|
|
1361
|
-
if (
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
2030
|
+
const localChangeSnapshots = await this._localSyncStateHelper.getSnapshots(storageKey, true);
|
|
2031
|
+
if (localChangeSnapshots.length > 0) {
|
|
2032
|
+
const localChangeSnapshot = localChangeSnapshots[0];
|
|
2033
|
+
if (Is.arrayValue(localChangeSnapshot.changes)) {
|
|
2034
|
+
await this._remoteSyncStateHelper.buildChangeSet(storageKey, localChangeSnapshot.changes, async (syncChangeSet, changeSetStorageId) => {
|
|
2035
|
+
if (Is.empty(syncChangeSet) && Is.empty(changeSetStorageId)) {
|
|
2036
|
+
await this._logging?.log({
|
|
2037
|
+
level: "info",
|
|
2038
|
+
source: this.CLASS_NAME,
|
|
2039
|
+
message: "builtStorageChangeSetNone",
|
|
2040
|
+
data: {
|
|
2041
|
+
storageKey
|
|
2042
|
+
}
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
else {
|
|
2046
|
+
await this._logging?.log({
|
|
2047
|
+
level: "info",
|
|
2048
|
+
source: this.CLASS_NAME,
|
|
2049
|
+
message: "builtStorageChangeSet",
|
|
2050
|
+
data: {
|
|
2051
|
+
storageKey,
|
|
2052
|
+
changeSetStorageId
|
|
2053
|
+
}
|
|
2054
|
+
});
|
|
2055
|
+
// Send the local changes to the remote storage if we are a trusted node
|
|
2056
|
+
if (this._config.isTrustedNode && Is.stringValue(changeSetStorageId)) {
|
|
2057
|
+
// If we are a trusted node, we can add the change set to the sync state
|
|
2058
|
+
// and remove the local change snapshot
|
|
2059
|
+
await this._remoteSyncStateHelper.addChangeSetToSyncState(storageKey, changeSetStorageId);
|
|
2060
|
+
await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
|
|
2061
|
+
}
|
|
2062
|
+
else if (!Is.empty(this._trustedSynchronisedStorageComponent) &&
|
|
2063
|
+
Is.object(syncChangeSet)) {
|
|
2064
|
+
// If we are not a trusted node, we need to send the changes to the trusted node
|
|
2065
|
+
// and then remove the local change snapshot
|
|
2066
|
+
await this._logging?.log({
|
|
2067
|
+
level: "info",
|
|
2068
|
+
source: this.CLASS_NAME,
|
|
2069
|
+
message: "sendingChangeSetToTrustedNode",
|
|
2070
|
+
data: {
|
|
2071
|
+
storageKey,
|
|
2072
|
+
changeSetStorageId
|
|
2073
|
+
}
|
|
2074
|
+
});
|
|
2075
|
+
await this._trustedSynchronisedStorageComponent.syncChangeSet(syncChangeSet);
|
|
2076
|
+
await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
|
|
1371
2077
|
}
|
|
1372
|
-
});
|
|
1373
|
-
// Send the local changes to the remote storage if we are a trusted node
|
|
1374
|
-
if (this._config.isTrustedNode) {
|
|
1375
|
-
await this._remoteSyncStateHelper.addChangeSetToSyncState(storageKey, changeSetStorageId);
|
|
1376
|
-
await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
|
|
1377
2078
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
else {
|
|
2082
|
+
await this._logging?.log({
|
|
2083
|
+
level: "info",
|
|
2084
|
+
source: this.CLASS_NAME,
|
|
2085
|
+
message: "updateFromLocalSyncStateNoChanges",
|
|
2086
|
+
data: {
|
|
2087
|
+
storageKey
|
|
1382
2088
|
}
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
await this._logging?.log({
|
|
1386
|
-
level: "info",
|
|
1387
|
-
source: this.CLASS_NAME,
|
|
1388
|
-
message: "createdStorageChangeSetNone",
|
|
1389
|
-
data: {
|
|
1390
|
-
storageKey
|
|
1391
|
-
}
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
1394
|
-
});
|
|
1395
|
-
}
|
|
1396
|
-
else {
|
|
1397
|
-
await this._logging?.log({
|
|
1398
|
-
level: "info",
|
|
1399
|
-
source: this.CLASS_NAME,
|
|
1400
|
-
message: "updateFromLocalSyncStateNoChanges",
|
|
1401
|
-
data: {
|
|
1402
|
-
storageKey
|
|
1403
|
-
}
|
|
1404
|
-
});
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
1405
2091
|
}
|
|
1406
2092
|
}
|
|
1407
2093
|
/**
|
|
@@ -1413,17 +2099,17 @@ class SynchronisedStorageService {
|
|
|
1413
2099
|
async startConsolidationSync(storageKey) {
|
|
1414
2100
|
let localChangeSnapshot;
|
|
1415
2101
|
try {
|
|
1416
|
-
// If we are performing a consolidation, we can remove the local
|
|
1417
|
-
|
|
2102
|
+
// If we are performing a consolidation, we can remove the local change snapshot
|
|
2103
|
+
// as we are going to create a complete changeset from the DB
|
|
2104
|
+
const localChangeSnapshots = await this._localSyncStateHelper.getSnapshots(storageKey, true);
|
|
2105
|
+
localChangeSnapshot = localChangeSnapshots[0];
|
|
1418
2106
|
if (!Is.empty(localChangeSnapshot)) {
|
|
1419
2107
|
await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
|
|
1420
2108
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
localChangeSnapshot = undefined;
|
|
1426
|
-
}
|
|
2109
|
+
await this._remoteSyncStateHelper.consolidationStart(storageKey, this._config.consolidationBatchSize ??
|
|
2110
|
+
SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE);
|
|
2111
|
+
// The consolidation was successful, so we can remove the local change snapshot permanently
|
|
2112
|
+
localChangeSnapshot = undefined;
|
|
1427
2113
|
}
|
|
1428
2114
|
catch (error) {
|
|
1429
2115
|
if (localChangeSnapshot) {
|
|
@@ -1440,22 +2126,22 @@ class SynchronisedStorageService {
|
|
|
1440
2126
|
}
|
|
1441
2127
|
/**
|
|
1442
2128
|
* Register a new sync type.
|
|
1443
|
-
* @param
|
|
2129
|
+
* @param syncRegisterStorageKey The sync register type to register.
|
|
1444
2130
|
* @internal
|
|
1445
2131
|
*/
|
|
1446
|
-
async
|
|
2132
|
+
async registerStorageKey(syncRegisterStorageKey) {
|
|
1447
2133
|
await this._logging?.log({
|
|
1448
2134
|
level: "info",
|
|
1449
2135
|
source: this.CLASS_NAME,
|
|
1450
|
-
message: "
|
|
2136
|
+
message: "registerStorageKey",
|
|
1451
2137
|
data: {
|
|
1452
|
-
storageKey:
|
|
2138
|
+
storageKey: syncRegisterStorageKey.storageKey
|
|
1453
2139
|
}
|
|
1454
2140
|
});
|
|
1455
|
-
if (Is.empty(this._activeStorageKeys[
|
|
1456
|
-
this._activeStorageKeys[
|
|
2141
|
+
if (Is.empty(this._activeStorageKeys[syncRegisterStorageKey.storageKey])) {
|
|
2142
|
+
this._activeStorageKeys[syncRegisterStorageKey.storageKey] = false;
|
|
1457
2143
|
if (this._serviceStarted) {
|
|
1458
|
-
await this.activateStorageKey(
|
|
2144
|
+
await this.activateStorageKey(syncRegisterStorageKey.storageKey);
|
|
1459
2145
|
}
|
|
1460
2146
|
}
|
|
1461
2147
|
}
|
|
@@ -1469,7 +2155,7 @@ class SynchronisedStorageService {
|
|
|
1469
2155
|
await this._logging?.log({
|
|
1470
2156
|
level: "info",
|
|
1471
2157
|
source: this.CLASS_NAME,
|
|
1472
|
-
message: "
|
|
2158
|
+
message: "activateStorageKey",
|
|
1473
2159
|
data: {
|
|
1474
2160
|
storageKey
|
|
1475
2161
|
}
|
|
@@ -1495,4 +2181,4 @@ class SynchronisedStorageService {
|
|
|
1495
2181
|
}
|
|
1496
2182
|
}
|
|
1497
2183
|
|
|
1498
|
-
export { SyncSnapshotEntry, SynchronisedStorageService, generateRestRoutesSynchronisedStorage, initSchema, restEntryPoints, synchronisedStorageSyncChangeSetRequest, tagsSynchronisedStorage };
|
|
2184
|
+
export { SyncSnapshotEntry, SynchronisedStorageService, generateRestRoutesSynchronisedStorage, initSchema, restEntryPoints, synchronisedStorageGetDecryptionKeyRequest, synchronisedStorageSyncChangeSetRequest, tagsSynchronisedStorage };
|