Skip to content

Commit

Permalink
Fix bugs and add tests for NtSchema
Browse files Browse the repository at this point in the history
  • Loading branch information
zjkmxy committed Feb 11, 2024
1 parent 3499c52 commit 9b976d8
Show file tree
Hide file tree
Showing 5 changed files with 336 additions and 13 deletions.
13 changes: 10 additions & 3 deletions src/namespace/expressing-point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface ExpressingPointEvents extends BaseNodeEvents {
target: schemaTree.StrictMatch<ExpressingPoint>;
deadline: number;
pkt: Verifier.Verifiable;
prevResult: VerifyResult;
},
): Promise<VerifyResult>;

Expand All @@ -41,7 +42,7 @@ export type ExpressingPointOpts = {
};

export class ExpressingPoint extends BaseNode {
/** Called when Interest received */
/** Called when Interest received. Note: calling `need` on this node will not trigger this callback. */
public readonly onInterest = new EventChain<ExpressingPointEvents['interest']>();

/** Verify Interest event. Also verifies Data if this is a LeafNode */
Expand Down Expand Up @@ -71,8 +72,14 @@ export class ExpressingPoint extends BaseNode {
) {
const verifyResult = await this.onVerify.chain(
VerifyResult.Unknown,
(ret, args) => Promise.resolve((ret < VerifyResult.Unknown || ret >= VerifyResult.Bypass) ? Stop : [args]),
{ target: matched, pkt, deadline },
(ret, args) =>
Promise.resolve(
(ret < VerifyResult.Unknown || ret >= VerifyResult.Bypass) ? Stop : [{
...args,
prevResult: ret,
}],
),
{ target: matched, pkt, deadline, prevResult: VerifyResult.Unknown },
);
return verifyResult >= VerifyResult.Pass;
}
Expand Down
2 changes: 1 addition & 1 deletion src/namespace/leaf-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface LeafNodeEvents extends ExpressingPointEvents {
wire: Uint8Array;
validUntil: number;
},
): Promise<Data | undefined>;
): Promise<void>;
}

export type LeafNodeOpts = ExpressingPointOpts & {
Expand Down
302 changes: 301 additions & 1 deletion src/namespace/nt-schema.test.ts
Original file line number Diff line number Diff line change
@@ -1 +1,301 @@
// TODO: To be added
import { assert } from '../dep.ts';
import { AsyncDisposableStack, name, Responder } from '../utils/mod.ts';
import { Endpoint } from '@ndn/endpoint';
import { Data, digestSigning, SigType } from '@ndn/packet';
import { Decoder, Encoder } from '@ndn/tlv';
import { Bridge } from '@ndn/l3face';
import { Ed25519, generateSigningKey } from '@ndn/keychain';
import { NtSchema, VerifyResult } from './nt-schema.ts';
import { InMemoryStorage } from '../storage/mod.ts';
import { LeafNode } from './leaf-node.ts';
import * as namePattern from './name-pattern.ts';
import * as Tree from './schema-tree.ts';
import { BaseNode } from './base-node.ts';

const { pattern } = namePattern;
export const b = ([value]: TemplateStringsArray) => new TextEncoder().encode(value);

Deno.test('NtSchema.1 Basic Interest and Data', async () => {
using bridge = Bridge.create({});
const { fwA, fwB } = bridge;
const epA = new Endpoint({ fw: fwA });
const epB = new Endpoint({ fw: fwB });
await using closers = new AsyncDisposableStack();
const appPrefix = name`/prefix`;

// NTSchema side
const schema = new NtSchema();
const leaf = new LeafNode({
lifetimeMs: 100,
freshnessMs: 60000,
signer: digestSigning,
});
const leafNode = Tree.touch<BaseNode, LeafNode>(
schema.tree,
pattern`/records/<8=recordId:string>`,
leaf,
);
leaf.onVerify.addListener(async ({ pkt }) => {
try {
await digestSigning.verify(pkt);
return VerifyResult.Pass;
} catch {
return VerifyResult.Fail;
}
});
leaf.onInterest.addListener(async () => {
const data3 = new Data(
name`${appPrefix}/records/8=rec3`,
Data.FreshnessPeriod(60000),
b`Hello, World.`,
);
await digestSigning.sign(data3);
return data3;
});
await schema.attach(appPrefix, epA);
closers.defer(async () => await schema.detach());

// Responder side
const storage = new InMemoryStorage();
closers.use(storage);
const responder = new Responder(appPrefix, epB, storage);
closers.use(responder);
const data1 = new Data(
name`${appPrefix}/records/8=rec1`,
Data.FreshnessPeriod(1000),
b`Hello,`,
);
await digestSigning.sign(data1);
await storage.set(data1.name.toString(), Encoder.encode(data1));
const data2 = new Data(
name`${appPrefix}/records/8=rec2`,
Data.FreshnessPeriod(1000),
b`World.`,
);
await digestSigning.sign(data2);
await storage.set(data2.name.toString(), Encoder.encode(data2));

// Express Interest
const recved1 = await Tree.call(
Tree.apply(leafNode, { 'recordId': 'rec1' }),
'need',
);
assert.assertExists(recved1);
assert.assert(recved1.name.equals(name`${appPrefix}/records/8=rec1`));
assert.assertEquals(recved1.content, b`Hello,`);

const recved2 = await Tree.call(
Tree.cast(schema.match(name`${appPrefix}/records/8=rec2`), LeafNode)!,
'need',
);
assert.assertExists(recved2);
assert.assert(recved2.name.equals(name`${appPrefix}/records/8=rec2`));
assert.assertEquals(recved2.content, b`World.`);

// Test NTSchema's producing data (on request, without storage)
const recved3 = await epB.consume(name`${appPrefix}/records/8=rec3`, {
verifier: digestSigning,
});
assert.assertExists(recved3);
assert.assert(recved3.name.equals(name`${appPrefix}/records/8=rec3`));
assert.assertEquals(recved3.freshnessPeriod, 60000);
assert.assertEquals(recved3.content, b`Hello, World.`);
});

Deno.test('NtSchema.2 Data Storage', async () => {
using bridge = Bridge.create({});
const { fwA, fwB } = bridge;
const epA = new Endpoint({ fw: fwA });
const epB = new Endpoint({ fw: fwB });
await using closers = new AsyncDisposableStack();
const appPrefix = name`/prefix`;

// NTSchema side
const schema = new NtSchema();
const storageA = new InMemoryStorage();
const leaf = new LeafNode({
lifetimeMs: 100,
freshnessMs: 60000,
signer: digestSigning,
});
const leafNode = Tree.touch<BaseNode, LeafNode>(
schema.tree,
pattern`/records/<8=recordId:string>`,
leaf,
);
leaf.onVerify.addListener(async ({ pkt }) => {
try {
await digestSigning.verify(pkt);
return VerifyResult.Pass;
} catch {
return VerifyResult.Fail;
}
});
leaf.onSaveStorage.addListener(({ data, wire }) => storageA.set(data.name.toString(), wire));
leaf.onSearchStorage.addListener(async ({ interest }) => {
const wire = await storageA.get(interest.name.toString());
if (wire) {
return Decoder.decode(wire, Data);
} else {
return undefined;
}
});
await schema.attach(appPrefix, epA);
closers.defer(async () => await schema.detach());

// Responder side
const storageB = new InMemoryStorage();
closers.use(storageB);
const responder = new Responder(appPrefix, epB, storageB);
closers.use(responder);
const data1 = new Data(
name`${appPrefix}/records/8=rec1`,
Data.FreshnessPeriod(1000),
b`Hello,`,
);
await digestSigning.sign(data1);
await storageB.set(data1.name.toString(), Encoder.encode(data1));

// Test NTSchema's producing data (with storage)
await Tree.call(
Tree.cast(schema.match(name`${appPrefix}/records/8=rec2`), LeafNode)!,
'provide',
b`World.`,
);
const received = await epB.consume(name`${appPrefix}/records/8=rec2`, {
verifier: digestSigning,
});
assert.assertExists(received);
assert.assert(received.name.equals(name`${appPrefix}/records/8=rec2`));
assert.assertEquals(received.freshnessPeriod, 60000);
assert.assertEquals(received.contentType, 0);
assert.assertEquals(received.content, b`World.`);

// Test NTSchema can cache received Data
const recved1 = await Tree.call(
Tree.apply(leafNode, { 'recordId': 'rec1' }),
'need',
);
assert.assertExists(recved1);
// Remove the responder and test again
await storageB.delete(data1.name.toString());
const recved2 = await Tree.call(
Tree.apply(leafNode, { 'recordId': 'rec1' }),
'need',
);
assert.assertExists(recved2);
assert.assert(recved2.name.equals(name`${appPrefix}/records/8=rec1`));
assert.assertEquals(recved2.content, b`Hello,`);
});

Deno.test('NtSchema.3 Verification', async () => {
using bridge = Bridge.create({});
const { fwA, fwB } = bridge;
const epA = new Endpoint({ fw: fwA });
const epB = new Endpoint({ fw: fwB });
await using closers = new AsyncDisposableStack();
const appPrefix = name`/prefix`;
const [prvKey, pubKey] = await generateSigningKey(/*identity*/ appPrefix, Ed25519);
const [wrongKey, _wrongPubKey] = await generateSigningKey(/*identity*/ appPrefix, Ed25519);

// NTSchema side
const schema = new NtSchema();
const leaf = new LeafNode({
lifetimeMs: 100,
freshnessMs: 60000,
signer: digestSigning,
});
const leafNode = Tree.touch<BaseNode, LeafNode>(
schema.tree,
pattern`/records/<8=recordId:string>`,
leaf,
);
leaf.onVerify.addListener(async ({ pkt, prevResult }) => {
if (pkt.sigInfo?.type === SigType.Sha256) {
try {
await digestSigning.verify(pkt);
return VerifyResult.Pass;
} catch {
return VerifyResult.Fail;
}
} else {
return prevResult;
}
});
leaf.onVerify.addListener(async ({ pkt, prevResult }) => {
if (pkt.sigInfo?.type === SigType.Ed25519) {
try {
await pubKey.verify(pkt);
return VerifyResult.Pass;
} catch {
return VerifyResult.Fail;
}
} else {
return prevResult;
}
});
await schema.attach(appPrefix, epA);
closers.defer(async () => await schema.detach());

// Responder side
const storage = new InMemoryStorage();
closers.use(storage);
const responder = new Responder(appPrefix, epB, storage);
closers.use(responder);
const data1 = new Data(
name`${appPrefix}/records/8=rec1`,
Data.FreshnessPeriod(1000),
b`Hello,`,
);
await digestSigning.sign(data1);
await storage.set(data1.name.toString(), Encoder.encode(data1));
const data2 = new Data(
name`${appPrefix}/records/8=rec2`,
Data.FreshnessPeriod(1000),
b`World.`,
);
await prvKey.sign(data2);
await storage.set(data2.name.toString(), Encoder.encode(data2));
const data3 = new Data(
name`${appPrefix}/records/8=rec3`,
Data.FreshnessPeriod(1000),
b`World.`,
);
// data3 is unsigned
await storage.set(data3.name.toString(), Encoder.encode(data3));
const data4 = new Data(
name`${appPrefix}/records/8=rec4`,
Data.FreshnessPeriod(1000),
b`World.`,
);
await wrongKey.sign(data4);
await storage.set(data4.name.toString(), Encoder.encode(data4));

// Express Interest
const recved1 = await Tree.call(
Tree.apply(leafNode, { 'recordId': 'rec1' }),
'need',
);
assert.assertExists(recved1);
const recved2 = await Tree.call(
Tree.apply(leafNode, { 'recordId': 'rec2' }),
'need',
);
assert.assertExists(recved2);
assert.assertRejects(() =>
Tree.call(
Tree.apply(leafNode, { 'recordId': 'rec3' }),
'need',
)
);
assert.assertRejects(() =>
Tree.call(
Tree.apply(leafNode, { 'recordId': 'rec4' }),
'need',
)
);
});

Deno.test('NtSchema.4 Signed Interest', async () => {
// TODO:
});
10 changes: 7 additions & 3 deletions src/namespace/nt-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,20 @@ export class NtSchema implements NamespaceHandler, AsyncDisposable {
this._attachedPrefix = prefix;
this._endpoint = endpoint;
await schemaTree.traverse(this.tree, {
post: async (node, path) => await node.resource?.onAttach?.emit(path, endpoint),
post: async (node, path) => await node.resource?.processAttach(path, this),
});

this._producer = endpoint.produce(prefix, this.onInterest.bind(this));
this._producer = endpoint.produce(prefix, this.onInterest.bind(this), {
describe: `NtSchema[${prefix.toString()}]`,
routeCapture: false,
announcement: prefix,
});
}

public async detach() {
this._producer!.close();
await schemaTree.traverse(this.tree, {
pre: async (node) => await node.resource?.onDetach?.emit(this.endpoint!),
pre: async (node) => await node.resource?.processDetach(),
});
this._endpoint = undefined;
this._attachedPrefix = undefined;
Expand Down
Loading

0 comments on commit 9b976d8

Please sign in to comment.