Skip to content

Commit f7fea55

Browse files
committed
add previousEventHash to the transaction and verify it in applyEvent
1 parent 1fbec34 commit f7fea55

File tree

12 files changed

+146
-53
lines changed

12 files changed

+146
-53
lines changed

packages/graph-framework-space-events/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
},
2727
"dependencies": {
2828
"@noble/curves": "^1.6.0",
29+
"@noble/hashes": "^1.5.0",
2930
"graph-framework-utils": "workspace:*",
3031
"uuid": "^11.0.2"
3132
}

packages/graph-framework-space-events/src/apply-event.test.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { Cause, Effect, Exit } from 'effect';
33
import { canonicalize, stringToUint8Array } from 'graph-framework-utils';
44
import { expect, it } from 'vitest';
55
import { applyEvent } from './apply-event.js';
6+
import { createInvitation } from './create-invitation.js';
67
import { createSpace } from './create-space.js';
7-
import { VerifySignatureError } from './types.js';
8+
import { InvalidEventError, VerifySignatureError } from './types.js';
89

910
it('should fail in case of an invalid signature', async () => {
1011
const author = {
@@ -34,3 +35,58 @@ it('should fail in case of an invalid signature', async () => {
3435
}
3536
}
3637
});
38+
39+
it('should fail in case state is not provided for an event other thant createSpace', async () => {
40+
const author = {
41+
signaturePublicKey: '03594161eed61407084114a142d1ce05ef4c5a5279479fdd73a2b16944fbff003b',
42+
signaturePrivateKey: '76b78f644c19d6133018a97a3bc2d5038be0af5a2858b9e640ff3e2f2db63a0b',
43+
encryptionPublicKey: 'encryption',
44+
};
45+
46+
const result = await Effect.runPromiseExit(
47+
Effect.gen(function* () {
48+
const spaceEvent = yield* createSpace({ author });
49+
const state = yield* applyEvent({ event: spaceEvent });
50+
51+
const spaceEvent2 = yield* createInvitation({ author, id: state.id, previousEventHash: state.lastEventHash });
52+
return yield* applyEvent({ event: spaceEvent2 });
53+
}),
54+
);
55+
56+
expect(Exit.isFailure(result)).toBe(true);
57+
if (Exit.isFailure(result)) {
58+
const cause = result.cause;
59+
if (Cause.isFailType(cause)) {
60+
expect(cause.error).toBeInstanceOf(InvalidEventError);
61+
}
62+
}
63+
});
64+
65+
it('should fail in case of an event is applied that is not based on the previous event', async () => {
66+
const author = {
67+
signaturePublicKey: '03594161eed61407084114a142d1ce05ef4c5a5279479fdd73a2b16944fbff003b',
68+
signaturePrivateKey: '76b78f644c19d6133018a97a3bc2d5038be0af5a2858b9e640ff3e2f2db63a0b',
69+
encryptionPublicKey: 'encryption',
70+
};
71+
72+
const result = await Effect.runPromiseExit(
73+
Effect.gen(function* () {
74+
const spaceEvent = yield* createSpace({ author });
75+
const state = yield* applyEvent({ event: spaceEvent });
76+
77+
const spaceEvent2 = yield* createSpace({ author });
78+
const state2 = yield* applyEvent({ state, event: spaceEvent2 });
79+
80+
const spaceEvent3 = yield* createInvitation({ author, id: state.id, previousEventHash: state.lastEventHash });
81+
return yield* applyEvent({ state: state2, event: spaceEvent3 });
82+
}),
83+
);
84+
85+
expect(Exit.isFailure(result)).toBe(true);
86+
if (Exit.isFailure(result)) {
87+
const cause = result.cause;
88+
if (Cause.isFailType(cause)) {
89+
expect(cause.error).toBeInstanceOf(InvalidEventError);
90+
}
91+
}
92+
});

packages/graph-framework-space-events/src/apply-event.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,15 @@ import { secp256k1 } from '@noble/curves/secp256k1';
22
import { Effect, Schema } from 'effect';
33
import type { ParseError } from 'effect/ParseResult';
44
import { canonicalize, stringToUint8Array } from 'graph-framework-utils';
5-
import type { SpaceInvitation, SpaceMember, SpaceState } from './types.js';
6-
import { SpaceEvent, VerifySignatureError } from './types.js';
5+
import { hashEvent } from './hashEvent.js';
6+
import {
7+
InvalidEventError,
8+
SpaceEvent,
9+
type SpaceInvitation,
10+
type SpaceMember,
11+
type SpaceState,
12+
VerifySignatureError,
13+
} from './types.js';
714

815
type Params = {
916
state?: SpaceState;
@@ -15,13 +22,22 @@ const decodeSpaceEvent = Schema.decodeUnknownEither(SpaceEvent);
1522
export const applyEvent = ({
1623
state,
1724
event: rawEvent,
18-
}: Params): Effect.Effect<SpaceState, ParseError | VerifySignatureError> => {
25+
}: Params): Effect.Effect<SpaceState, ParseError | VerifySignatureError | InvalidEventError> => {
1926
const decodedEvent = decodeSpaceEvent(rawEvent);
2027
if (decodedEvent._tag === 'Left') {
2128
return decodedEvent.left;
2229
}
2330
const event = decodedEvent.right;
2431

32+
if (event.transaction.type !== 'create-space') {
33+
if (state === undefined) {
34+
return Effect.fail(new InvalidEventError());
35+
}
36+
if (event.transaction.previousEventHash !== state.lastEventHash) {
37+
return Effect.fail(new InvalidEventError());
38+
}
39+
}
40+
2541
const encodedTransaction = stringToUint8Array(canonicalize(event.transaction));
2642
const isValidSignature = secp256k1.verify(event.author.signature, encodedTransaction, event.author.publicKey, {
2743
prehash: true,
@@ -68,6 +84,6 @@ export const applyEvent = ({
6884
members,
6985
removedMembers,
7086
invitations,
71-
transactionHash: '', // TODO
87+
lastEventHash: hashEvent(event),
7288
});
7389
};

packages/graph-framework-space-events/src/create-invitation.test.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,29 @@ it('should create an invitation', async () => {
1616
Effect.gen(function* () {
1717
const spaceEvent = yield* createSpace({ author });
1818
const state = yield* applyEvent({ event: spaceEvent });
19-
const spaceEvent2 = yield* createInvitation({ author, id: state.id });
19+
const spaceEvent2 = yield* createInvitation({ author, id: state.id, previousEventHash: state.lastEventHash });
2020
const state2 = yield* applyEvent({ state, event: spaceEvent2 });
2121
return {
2222
state2,
2323
spaceEvent2,
2424
};
2525
}),
2626
);
27-
expect(state2).toEqual({
28-
id: state2.id,
29-
members: {
30-
[author.signaturePublicKey]: {
31-
signaturePublicKey: author.signaturePublicKey,
32-
encryptionPublicKey: author.encryptionPublicKey,
33-
role: 'admin',
34-
},
27+
28+
expect(state2.id).toBeTypeOf('string');
29+
expect(state2.invitations).toEqual({
30+
[spaceEvent2.transaction.id]: {
31+
signaturePublicKey: '',
32+
encryptionPublicKey: '',
3533
},
36-
removedMembers: {},
37-
invitations: {
38-
[spaceEvent2.transaction.id]: {
39-
signaturePublicKey: '',
40-
encryptionPublicKey: '',
41-
},
34+
});
35+
expect(state2.members).toEqual({
36+
[author.signaturePublicKey]: {
37+
signaturePublicKey: author.signaturePublicKey,
38+
encryptionPublicKey: author.encryptionPublicKey,
39+
role: 'admin',
4240
},
43-
transactionHash: '',
4441
});
42+
expect(state2.removedMembers).toEqual({});
43+
expect(state2.lastEventHash).toBeTypeOf('string');
4544
});

packages/graph-framework-space-events/src/create-invitation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@ import type { Author, SpaceEvent } from './types.js';
66
type Params = {
77
author: Author;
88
id: string;
9+
previousEventHash: string;
910
};
1011

11-
export const createInvitation = ({ author, id }: Params): Effect.Effect<SpaceEvent, undefined> => {
12+
export const createInvitation = ({ author, id, previousEventHash }: Params): Effect.Effect<SpaceEvent, undefined> => {
1213
const transaction = {
1314
type: 'create-invitation' as const,
1415
id,
1516
ciphertext: '',
1617
nonce: '',
1718
signaturePublicKey: '',
1819
encryptionPublicKey: '',
20+
previousEventHash,
1921
};
2022
const encodedTransaction = stringToUint8Array(canonicalize(transaction));
2123
const signature = secp256k1.sign(encodedTransaction, author.signaturePrivateKey, { prehash: true }).toCompactHex();

packages/graph-framework-space-events/src/create-space.test.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,15 @@ it('should create a space state', async () => {
1717
}),
1818
);
1919

20-
expect(state).toEqual({
21-
id: state.id,
22-
invitations: {},
23-
members: {
24-
[author.signaturePublicKey]: {
25-
signaturePublicKey: author.signaturePublicKey,
26-
encryptionPublicKey: author.encryptionPublicKey,
27-
role: 'admin',
28-
},
20+
expect(state.id).toBeTypeOf('string');
21+
expect(state.invitations).toEqual({});
22+
expect(state.members).toEqual({
23+
[author.signaturePublicKey]: {
24+
signaturePublicKey: author.signaturePublicKey,
25+
encryptionPublicKey: author.encryptionPublicKey,
26+
role: 'admin',
2927
},
30-
removedMembers: {},
31-
transactionHash: '',
3228
});
29+
expect(state.removedMembers).toEqual({});
30+
expect(state.lastEventHash).toBeTypeOf('string');
3331
});

packages/graph-framework-space-events/src/create-space.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { secp256k1 } from '@noble/curves/secp256k1';
22
import { Effect } from 'effect';
33
import { canonicalize, generateId, stringToUint8Array } from 'graph-framework-utils';
4-
import type { Author, SpaceEvent } from './types.js';
4+
import type { Author, CreateSpaceEvent, SpaceEvent } from './types.js';
55

66
type Params = {
77
author: Author;
@@ -17,11 +17,13 @@ export const createSpace = ({ author }: Params): Effect.Effect<SpaceEvent, undef
1717
const encodedTransaction = stringToUint8Array(canonicalize(transaction));
1818
const signature = secp256k1.sign(encodedTransaction, author.signaturePrivateKey, { prehash: true }).toCompactHex();
1919

20-
return Effect.succeed({
20+
const event: CreateSpaceEvent = {
2121
transaction,
2222
author: {
2323
publicKey: author.signaturePublicKey,
2424
signature,
2525
},
26-
});
26+
};
27+
28+
return Effect.succeed(event);
2729
};

packages/graph-framework-space-events/src/delete-space.test.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,20 @@ it('should delete a space', async () => {
1515
Effect.gen(function* () {
1616
const spaceEvent = yield* createSpace({ author });
1717
const state = yield* applyEvent({ event: spaceEvent });
18-
const spaceEvent2 = yield* deleteSpace({ author, id: state.id });
18+
const spaceEvent2 = yield* deleteSpace({ author, id: state.id, previousEventHash: state.lastEventHash });
1919
return yield* applyEvent({ state, event: spaceEvent2 });
2020
}),
2121
);
2222

23-
expect(state).toEqual({
24-
id: state.id,
25-
members: {},
26-
invitations: {},
27-
removedMembers: {
28-
[author.signaturePublicKey]: {
29-
signaturePublicKey: author.signaturePublicKey,
30-
encryptionPublicKey: author.encryptionPublicKey,
31-
role: 'admin',
32-
},
23+
expect(state.id).toBeTypeOf('string');
24+
expect(state.invitations).toEqual({});
25+
expect(state.members).toEqual({});
26+
expect(state.removedMembers).toEqual({
27+
[author.signaturePublicKey]: {
28+
signaturePublicKey: author.signaturePublicKey,
29+
encryptionPublicKey: author.encryptionPublicKey,
30+
role: 'admin',
3331
},
34-
transactionHash: '',
3532
});
33+
expect(state.lastEventHash).toBeTypeOf('string');
3634
});
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
11
import { secp256k1 } from '@noble/curves/secp256k1';
22
import { Effect } from 'effect';
33
import { canonicalize, stringToUint8Array } from 'graph-framework-utils';
4-
import type { Author, SpaceEvent } from './types.js';
4+
import type { Author, DeleteSpaceEvent, SpaceEvent } from './types.js';
55

66
type Params = {
77
author: Author;
88
id: string;
9+
previousEventHash: string;
910
};
1011

11-
export const deleteSpace = ({ author, id }: Params): Effect.Effect<SpaceEvent, undefined> => {
12+
export const deleteSpace = ({ author, id, previousEventHash }: Params): Effect.Effect<SpaceEvent, undefined> => {
1213
const transaction = {
1314
type: 'delete-space' as const,
1415
id,
16+
previousEventHash,
1517
};
1618
const encodedTransaction = stringToUint8Array(canonicalize(transaction));
1719
const signature = secp256k1.sign(encodedTransaction, author.signaturePrivateKey, { prehash: true }).toCompactHex();
1820

19-
return Effect.succeed({
21+
const event: DeleteSpaceEvent = {
2022
transaction,
2123
author: {
2224
publicKey: author.signaturePublicKey,
2325
signature,
2426
},
25-
});
27+
};
28+
return Effect.succeed(event);
2629
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { blake3 } from '@noble/hashes/blake3';
2+
import { bytesToHex } from '@noble/hashes/utils';
3+
import { canonicalize } from 'graph-framework-utils';
4+
import type { SpaceEvent } from './types.js';
5+
6+
export const hashEvent = (event: SpaceEvent): string => {
7+
const hash = blake3(canonicalize(event));
8+
return bytesToHex(hash);
9+
};

0 commit comments

Comments
 (0)