diff --git a/package.json b/package.json index 05fa98f..b5773e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ucla-irl/ndnts-aux", - "version": "1.0.8", + "version": "1.0.9.rc-1", "description": "NDNts Auxiliary Package for Web and Deno", "scripts": { "test": "deno test --no-check", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91964cf..f941cd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,15 +100,15 @@ packages: /@types/imap@0.8.40: resolution: {integrity: sha512-kWFwOc88CGwWZlHqCnZiceS6EralsAHdjpQyk1+fIA875NQdIHvLpdD5NU3Pi1yZ8FKFdOF81UDNAo8/XS6HiQ==} dependencies: - '@types/node': 20.11.6 + '@types/node': 20.11.7 dev: true /@types/minimalistic-assert@1.0.3: resolution: {integrity: sha512-Ku87cam4YxiXcEpeUemo+vO8QWGQ7U2CwEEcLcYFhxG8b4CK8gWxSX/oWjePWKwqPaWWxxVqXAdAjGdlJtVzDA==} dev: true - /@types/node@20.11.6: - resolution: {integrity: sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==} + /@types/node@20.11.7: + resolution: {integrity: sha512-GPmeN1C3XAyV5uybAf4cMLWT9fDWcmQhZVtMFu7OR32WjrqGG+Wnk2V1d0bmtUyE/Zy1QJ9BxyiTih9z8Oks8A==} dependencies: undici-types: 5.26.5 dev: true @@ -116,7 +116,7 @@ packages: /@types/nodemailer@6.4.14: resolution: {integrity: sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==} dependencies: - '@types/node': 20.11.6 + '@types/node': 20.11.7 dev: true /@types/retry@0.12.5: @@ -134,7 +134,7 @@ packages: /@types/ws@8.5.10: resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} dependencies: - '@types/node': 20.11.6 + '@types/node': 20.11.7 dev: true /@yoursunny/asn1@0.0.20200718: diff --git a/src/sync-agent/deliveries.ts b/src/sync-agent/deliveries.ts index 95a1afd..7f70974 100644 --- a/src/sync-agent/deliveries.ts +++ b/src/sync-agent/deliveries.ts @@ -68,7 +68,8 @@ export abstract class SyncDelivery implements AsyncDisposable { SvSync.create({ endpoint: endpoint, syncPrefix: syncPrefix, - signer: digestSigning, + signer: signer, + verifier: verifier, initialStateVector: new SvStateVector(state), initialize: async (svSync) => { this._syncInst = svSync; diff --git a/src/sync-agent/delivery-alo.test.ts b/src/sync-agent/delivery-alo.test.ts index 3814de1..e9a0414 100644 --- a/src/sync-agent/delivery-alo.test.ts +++ b/src/sync-agent/delivery-alo.test.ts @@ -1,6 +1,6 @@ import { Endpoint } from '@ndn/endpoint'; import { Forwarder } from '@ndn/fw'; -import { Data, digestSigning, Name } from '@ndn/packet'; +import { Data, digestSigning, Name, type Signer, type Verifier } from '@ndn/packet'; import { GenericNumber } from '@ndn/naming-convention2'; import { Encoder } from '@ndn/tlv'; import { assert, hex } from '../dep.ts'; @@ -41,14 +41,14 @@ class DeliveryTester implements AsyncDisposable { }); } - async start(timeoutMs: number) { + async start(timeoutMs: number, signer: Signer = digestSigning, verifier: Verifier = digestSigning) { for (let i = 0; this.svsCount > i; i++) { const alo = await AtLeastOnceDelivery.create( name`/test/32=node/${i}`, this.endpoint, this.syncPrefix, - digestSigning, - digestSigning, + signer, + verifier, this.stores[i], Promise.resolve(this.onUpdate.bind(this)), ); @@ -112,7 +112,7 @@ const compareEvent = (a: SyncUpdateEvent, b: SyncUpdateEvent) => { } }; -Deno.test('basic test', async () => { +Deno.test('Alo.1 Basic test', async () => { let eventSet; { const { promise: stopSignal, resolve: stop } = Promise.withResolvers(); @@ -147,7 +147,7 @@ Deno.test('basic test', async () => { }); }); -Deno.test('no missing due to parallel', async () => { +Deno.test('Alo.2 No missing due to out-of-order', async () => { let eventSet; { const { promise: stopSignal1, resolve: stop1 } = Promise.withResolvers(); @@ -213,7 +213,127 @@ Deno.test('no missing due to parallel', async () => { }); }); -// Test recover after shutdown and replay -// Test crash during onUpdate -// Test unverified state interest -// Test unverified data +Deno.test('Alo.3 Recover after shutdown', async () => { + let eventSet; + { + const { promise: stopSignal0, resolve: stop0 } = Promise.withResolvers(); + const { promise: stopSignal1, resolve: stop1 } = Promise.withResolvers(); + const { promise: stopSignal2, resolve: stop2 } = Promise.withResolvers(); + await using tester = new DeliveryTester(2, () => { + if (tester.events.length === 1) { + stop0(); + } else if (tester.events.length === 2) { + stop1(); + } else if (tester.events.length === 4) { + stop2(); + } + return Promise.resolve(); + }); + await tester.start(2000); + + // Provide A and C. (no B) + await tester.alos[1].produce(new TextEncoder().encode('A')); + await stopSignal0; // Wait the database to be set + tester.alos[1].syncNode!.seqNum = 2; + await tester.alos[1].produce(new TextEncoder().encode('C')); + await stopSignal1; + + // Stop alo 0 + await tester.alos[0].destroy(); + + // Assert we have 'A' and 'C' + eventSet = tester.events.toSorted(compareEvent); + assert.assertEquals(eventSet.length, 2); + assert.assertEquals(eventSet[0], { + content: new TextEncoder().encode('A'), + origin: 1, + receiver: 0, + }); + assert.assertEquals(eventSet[1], { + content: new TextEncoder().encode('C'), + origin: 1, + receiver: 0, + }); + + // Provide 'B' + await tester.dispositData(1, 2, new TextEncoder().encode('B')); + + // Restart alo 0. It is supposed to deliver 'C' again. + tester.alos[0] = await AtLeastOnceDelivery.create( + name`/test/32=node/${0}`, + tester.endpoint, + tester.syncPrefix, + digestSigning, + digestSigning, + tester.stores[0], + Promise.resolve(tester.onUpdate.bind(tester)), + ); + tester.alos[0].start(); + + // Wait for 'B' and 'C' again + await stopSignal2; + + // Manually destroy alo0 since it is created outside + tester.alos[0].destroy(); + eventSet = tester.events; + } + + // Since it is unordered, we have to sort + eventSet.sort(compareEvent); + assert.assertEquals(eventSet.length, 4); + assert.assertEquals(eventSet[0], { + content: new TextEncoder().encode('A'), + origin: 1, + receiver: 0, + }); + assert.assertEquals(eventSet[1], { + content: new TextEncoder().encode('B'), + origin: 1, + receiver: 0, + }); + assert.assertEquals(eventSet[2], { + content: new TextEncoder().encode('C'), + origin: 1, + receiver: 0, + }); + assert.assertEquals(eventSet[3], { + content: new TextEncoder().encode('C'), + origin: 1, + receiver: 0, + }); +}); + +// Alo.4 Crash during onUpdate +// Unable to test for now + +// Alo.5 Data unverified +// Unable to test for now +// This kind of error may only come from two sources: +// 1. authenticated users(producers)'s doing this on purpose, +// 2. cache polution. +// Case 1 is not very bad, since today's cloud application may still suffer from it. +// If the user is allowed to modify data, it is kind of feature. +// Case 2 needs network operator's help to eliminate. + +Deno.test('Alo.Ex Unverified SVS Sync interest', async () => { + // Note: this is handled by NDNts, not this library. + + let eventSet; + { + await using tester = new DeliveryTester(2, () => { + throw new Error('Not suppsoed to handle any data'); + }); + await tester.start(2000, digestSigning, { + verify: () => Promise.reject('Always fail verifier'), + }); + + await tester.alos[1].produce(new TextEncoder().encode('0-Hello')); + await tester.alos[1].produce(new TextEncoder().encode('1-World')); + await new Promise((resolve) => setTimeout(resolve, 500)); + + eventSet = tester.events; + } + + // Should receive no data + assert.assertEquals(eventSet.length, 0); +});