cojson 0.8.12 → 0.8.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +89 -83
  2. package/dist/native/PeerKnownStates.js +1 -1
  3. package/dist/native/PeerKnownStates.js.map +1 -1
  4. package/dist/native/PeerState.js +1 -1
  5. package/dist/native/PeerState.js.map +1 -1
  6. package/dist/native/PriorityBasedMessageQueue.js +1 -10
  7. package/dist/native/PriorityBasedMessageQueue.js.map +1 -1
  8. package/dist/native/base64url.js.map +1 -1
  9. package/dist/native/base64url.test.js +1 -1
  10. package/dist/native/base64url.test.js.map +1 -1
  11. package/dist/native/coValue.js.map +1 -1
  12. package/dist/native/coValueCore.js +141 -149
  13. package/dist/native/coValueCore.js.map +1 -1
  14. package/dist/native/coValueState.js.map +1 -1
  15. package/dist/native/coValues/account.js +6 -6
  16. package/dist/native/coValues/account.js.map +1 -1
  17. package/dist/native/coValues/coList.js +2 -3
  18. package/dist/native/coValues/coList.js.map +1 -1
  19. package/dist/native/coValues/coMap.js +1 -1
  20. package/dist/native/coValues/coMap.js.map +1 -1
  21. package/dist/native/coValues/coStream.js +3 -5
  22. package/dist/native/coValues/coStream.js.map +1 -1
  23. package/dist/native/coValues/group.js +11 -11
  24. package/dist/native/coValues/group.js.map +1 -1
  25. package/dist/native/coreToCoValue.js +2 -2
  26. package/dist/native/coreToCoValue.js.map +1 -1
  27. package/dist/native/crypto/PureJSCrypto.js +4 -4
  28. package/dist/native/crypto/PureJSCrypto.js.map +1 -1
  29. package/dist/native/crypto/crypto.js.map +1 -1
  30. package/dist/native/exports.js +12 -12
  31. package/dist/native/exports.js.map +1 -1
  32. package/dist/native/ids.js.map +1 -1
  33. package/dist/native/jsonStringify.js.map +1 -1
  34. package/dist/native/localNode.js +5 -7
  35. package/dist/native/localNode.js.map +1 -1
  36. package/dist/native/permissions.js +4 -7
  37. package/dist/native/permissions.js.map +1 -1
  38. package/dist/native/priority.js.map +1 -1
  39. package/dist/native/storage/FileSystem.js.map +1 -1
  40. package/dist/native/storage/chunksAndKnownStates.js +2 -4
  41. package/dist/native/storage/chunksAndKnownStates.js.map +1 -1
  42. package/dist/native/storage/index.js +6 -15
  43. package/dist/native/storage/index.js.map +1 -1
  44. package/dist/native/streamUtils.js.map +1 -1
  45. package/dist/native/sync.js +2 -4
  46. package/dist/native/sync.js.map +1 -1
  47. package/dist/native/typeUtils/accountOrAgentIDfromSessionID.js.map +1 -1
  48. package/dist/native/typeUtils/expectGroup.js.map +1 -1
  49. package/dist/native/typeUtils/isAccountID.js.map +1 -1
  50. package/dist/native/typeUtils/isCoValue.js +1 -1
  51. package/dist/native/typeUtils/isCoValue.js.map +1 -1
  52. package/dist/web/PeerKnownStates.js +1 -1
  53. package/dist/web/PeerKnownStates.js.map +1 -1
  54. package/dist/web/PeerState.js +1 -1
  55. package/dist/web/PeerState.js.map +1 -1
  56. package/dist/web/PriorityBasedMessageQueue.js +1 -10
  57. package/dist/web/PriorityBasedMessageQueue.js.map +1 -1
  58. package/dist/web/base64url.js.map +1 -1
  59. package/dist/web/base64url.test.js +1 -1
  60. package/dist/web/base64url.test.js.map +1 -1
  61. package/dist/web/coValue.js.map +1 -1
  62. package/dist/web/coValueCore.js +141 -149
  63. package/dist/web/coValueCore.js.map +1 -1
  64. package/dist/web/coValueState.js.map +1 -1
  65. package/dist/web/coValues/account.js +6 -6
  66. package/dist/web/coValues/account.js.map +1 -1
  67. package/dist/web/coValues/coList.js +2 -3
  68. package/dist/web/coValues/coList.js.map +1 -1
  69. package/dist/web/coValues/coMap.js +1 -1
  70. package/dist/web/coValues/coMap.js.map +1 -1
  71. package/dist/web/coValues/coStream.js +3 -5
  72. package/dist/web/coValues/coStream.js.map +1 -1
  73. package/dist/web/coValues/group.js +11 -11
  74. package/dist/web/coValues/group.js.map +1 -1
  75. package/dist/web/coreToCoValue.js +2 -2
  76. package/dist/web/coreToCoValue.js.map +1 -1
  77. package/dist/web/crypto/PureJSCrypto.js +4 -4
  78. package/dist/web/crypto/PureJSCrypto.js.map +1 -1
  79. package/dist/web/crypto/WasmCrypto.js +5 -5
  80. package/dist/web/crypto/WasmCrypto.js.map +1 -1
  81. package/dist/web/crypto/crypto.js.map +1 -1
  82. package/dist/web/exports.js +12 -12
  83. package/dist/web/exports.js.map +1 -1
  84. package/dist/web/ids.js.map +1 -1
  85. package/dist/web/jsonStringify.js.map +1 -1
  86. package/dist/web/localNode.js +5 -7
  87. package/dist/web/localNode.js.map +1 -1
  88. package/dist/web/permissions.js +4 -7
  89. package/dist/web/permissions.js.map +1 -1
  90. package/dist/web/priority.js.map +1 -1
  91. package/dist/web/storage/FileSystem.js.map +1 -1
  92. package/dist/web/storage/chunksAndKnownStates.js +2 -4
  93. package/dist/web/storage/chunksAndKnownStates.js.map +1 -1
  94. package/dist/web/storage/index.js +6 -15
  95. package/dist/web/storage/index.js.map +1 -1
  96. package/dist/web/streamUtils.js.map +1 -1
  97. package/dist/web/sync.js +2 -4
  98. package/dist/web/sync.js.map +1 -1
  99. package/dist/web/typeUtils/accountOrAgentIDfromSessionID.js.map +1 -1
  100. package/dist/web/typeUtils/expectGroup.js.map +1 -1
  101. package/dist/web/typeUtils/isAccountID.js.map +1 -1
  102. package/dist/web/typeUtils/isCoValue.js +1 -1
  103. package/dist/web/typeUtils/isCoValue.js.map +1 -1
  104. package/package.json +4 -14
  105. package/src/PeerKnownStates.ts +91 -89
  106. package/src/PeerState.ts +72 -73
  107. package/src/PriorityBasedMessageQueue.ts +42 -49
  108. package/src/base64url.test.ts +24 -24
  109. package/src/base64url.ts +44 -45
  110. package/src/coValue.ts +45 -45
  111. package/src/coValueCore.ts +746 -785
  112. package/src/coValueState.ts +82 -72
  113. package/src/coValues/account.ts +143 -150
  114. package/src/coValues/coList.ts +520 -522
  115. package/src/coValues/coMap.ts +283 -285
  116. package/src/coValues/coStream.ts +320 -324
  117. package/src/coValues/group.ts +306 -305
  118. package/src/coreToCoValue.ts +28 -31
  119. package/src/crypto/PureJSCrypto.ts +188 -194
  120. package/src/crypto/WasmCrypto.ts +236 -254
  121. package/src/crypto/crypto.ts +302 -309
  122. package/src/exports.ts +116 -116
  123. package/src/ids.ts +9 -9
  124. package/src/jsonStringify.ts +46 -46
  125. package/src/jsonValue.ts +24 -10
  126. package/src/localNode.ts +635 -660
  127. package/src/media.ts +3 -3
  128. package/src/permissions.ts +272 -278
  129. package/src/priority.ts +21 -19
  130. package/src/storage/FileSystem.ts +91 -99
  131. package/src/storage/chunksAndKnownStates.ts +110 -115
  132. package/src/storage/index.ts +466 -497
  133. package/src/streamUtils.ts +60 -60
  134. package/src/sync.ts +593 -615
  135. package/src/tests/PeerKnownStates.test.ts +38 -34
  136. package/src/tests/PeerState.test.ts +101 -64
  137. package/src/tests/PriorityBasedMessageQueue.test.ts +91 -91
  138. package/src/tests/account.test.ts +59 -59
  139. package/src/tests/coList.test.ts +65 -65
  140. package/src/tests/coMap.test.ts +137 -137
  141. package/src/tests/coStream.test.ts +254 -257
  142. package/src/tests/coValueCore.test.ts +153 -156
  143. package/src/tests/crypto.test.ts +136 -144
  144. package/src/tests/cryptoImpl.test.ts +205 -197
  145. package/src/tests/group.test.ts +24 -24
  146. package/src/tests/permissions.test.ts +1306 -1371
  147. package/src/tests/priority.test.ts +65 -82
  148. package/src/tests/sync.test.ts +1300 -1291
  149. package/src/tests/testUtils.ts +52 -53
  150. package/src/typeUtils/accountOrAgentIDfromSessionID.ts +4 -4
  151. package/src/typeUtils/expectGroup.ts +9 -9
  152. package/src/typeUtils/isAccountID.ts +1 -1
  153. package/src/typeUtils/isCoValue.ts +9 -9
  154. package/tsconfig.json +4 -6
  155. package/tsconfig.native.json +9 -11
  156. package/tsconfig.web.json +4 -10
  157. package/.eslintrc.cjs +0 -25
  158. package/.prettierrc.js +0 -9
@@ -1,32 +1,32 @@
1
+ import { Result, err, ok } from "neverthrow";
1
2
  import { AnyRawCoValue, RawCoValue } from "./coValue.js";
3
+ import { ControlledAccountOrAgent, RawAccountID } from "./coValues/account.js";
4
+ import { RawGroup } from "./coValues/group.js";
5
+ import { coreToCoValue } from "./coreToCoValue.js";
2
6
  import {
3
- Encrypted,
4
- Hash,
5
- KeySecret,
6
- Signature,
7
- StreamingHash,
8
- KeyID,
9
- CryptoProvider,
10
- SignerID,
7
+ CryptoProvider,
8
+ Encrypted,
9
+ Hash,
10
+ KeyID,
11
+ KeySecret,
12
+ Signature,
13
+ SignerID,
14
+ StreamingHash,
11
15
  } from "./crypto/crypto.js";
16
+ import { RawCoID, SessionID, TransactionID } from "./ids.js";
17
+ import { Stringified, parseJSON, stableStringify } from "./jsonStringify.js";
12
18
  import { JsonObject, JsonValue } from "./jsonValue.js";
19
+ import { LocalNode, ResolveAccountAgentError } from "./localNode.js";
13
20
  import {
14
- PermissionsDef as RulesetDef,
15
- determineValidTransactions,
16
- isKeyForKeyField,
21
+ PermissionsDef as RulesetDef,
22
+ determineValidTransactions,
23
+ isKeyForKeyField,
17
24
  } from "./permissions.js";
18
- import { RawGroup } from "./coValues/group.js";
19
- import { LocalNode, ResolveAccountAgentError } from "./localNode.js";
25
+ import { getPriorityFromHeader } from "./priority.js";
20
26
  import { CoValueKnownState, NewContentMessage } from "./sync.js";
21
- import { RawCoID, SessionID, TransactionID } from "./ids.js";
22
- import { RawAccountID, ControlledAccountOrAgent } from "./coValues/account.js";
23
- import { Stringified, parseJSON, stableStringify } from "./jsonStringify.js";
24
- import { coreToCoValue } from "./coreToCoValue.js";
27
+ import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
25
28
  import { expectGroup } from "./typeUtils/expectGroup.js";
26
29
  import { isAccountID } from "./typeUtils/isAccountID.js";
27
- import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
28
- import { err, ok, Result } from "neverthrow";
29
- import { getPriorityFromHeader } from "./priority.js";
30
30
 
31
31
  /**
32
32
  In order to not block other concurrently syncing CoValues we introduce a maximum size of transactions,
@@ -38,227 +38,221 @@ import { getPriorityFromHeader } from "./priority.js";
38
38
  export const MAX_RECOMMENDED_TX_SIZE = 100 * 1024;
39
39
 
40
40
  export type CoValueHeader = {
41
- type: AnyRawCoValue["type"];
42
- ruleset: RulesetDef;
43
- meta: JsonObject | null;
41
+ type: AnyRawCoValue["type"];
42
+ ruleset: RulesetDef;
43
+ meta: JsonObject | null;
44
44
  } & CoValueUniqueness;
45
45
 
46
46
  export type CoValueUniqueness = {
47
- uniqueness: JsonValue;
48
- createdAt?: `2${string}` | null;
47
+ uniqueness: JsonValue;
48
+ createdAt?: `2${string}` | null;
49
49
  };
50
50
 
51
51
  export function idforHeader(
52
- header: CoValueHeader,
53
- crypto: CryptoProvider,
52
+ header: CoValueHeader,
53
+ crypto: CryptoProvider,
54
54
  ): RawCoID {
55
- const hash = crypto.shortHash(header);
56
- return `co_z${hash.slice("shortHash_z".length)}`;
55
+ const hash = crypto.shortHash(header);
56
+ return `co_z${hash.slice("shortHash_z".length)}`;
57
57
  }
58
58
 
59
59
  type SessionLog = {
60
- transactions: Transaction[];
61
- lastHash?: Hash;
62
- streamingHash: StreamingHash;
63
- signatureAfter: { [txIdx: number]: Signature | undefined };
64
- lastSignature: Signature;
60
+ transactions: Transaction[];
61
+ lastHash?: Hash;
62
+ streamingHash: StreamingHash;
63
+ signatureAfter: { [txIdx: number]: Signature | undefined };
64
+ lastSignature: Signature;
65
65
  };
66
66
 
67
67
  export type PrivateTransaction = {
68
- privacy: "private";
69
- madeAt: number;
70
- keyUsed: KeyID;
71
- encryptedChanges: Encrypted<
72
- JsonValue[],
73
- { in: RawCoID; tx: TransactionID }
74
- >;
68
+ privacy: "private";
69
+ madeAt: number;
70
+ keyUsed: KeyID;
71
+ encryptedChanges: Encrypted<JsonValue[], { in: RawCoID; tx: TransactionID }>;
75
72
  };
76
73
 
77
74
  export type TrustingTransaction = {
78
- privacy: "trusting";
79
- madeAt: number;
80
- changes: Stringified<JsonValue[]>;
75
+ privacy: "trusting";
76
+ madeAt: number;
77
+ changes: Stringified<JsonValue[]>;
81
78
  };
82
79
 
83
80
  export type Transaction = PrivateTransaction | TrustingTransaction;
84
81
 
85
82
  export type DecryptedTransaction = {
86
- txID: TransactionID;
87
- changes: JsonValue[];
88
- madeAt: number;
83
+ txID: TransactionID;
84
+ changes: JsonValue[];
85
+ madeAt: number;
89
86
  };
90
87
 
91
88
  const readKeyCache = new WeakMap<CoValueCore, { [id: KeyID]: KeySecret }>();
92
89
 
93
90
  export class CoValueCore {
94
- id: RawCoID;
95
- node: LocalNode;
96
- crypto: CryptoProvider;
97
- header: CoValueHeader;
98
- _sessionLogs: Map<SessionID, SessionLog>;
99
- _cachedContent?: RawCoValue;
100
- listeners: Set<(content?: RawCoValue) => void> = new Set();
101
- _decryptionCache: {
102
- [key: Encrypted<JsonValue[], JsonValue>]: JsonValue[] | undefined;
103
- } = {};
104
- _cachedKnownState?: CoValueKnownState;
105
- _cachedDependentOn?: RawCoID[];
106
- _cachedNewContentSinceEmpty?: NewContentMessage[] | undefined;
107
- _currentAsyncAddTransaction?: Promise<void>;
108
-
109
- constructor(
110
- header: CoValueHeader,
111
- node: LocalNode,
112
- internalInitSessions: Map<SessionID, SessionLog> = new Map(),
113
- ) {
114
- this.crypto = node.crypto;
115
- this.id = idforHeader(header, node.crypto);
116
- this.header = header;
117
- this._sessionLogs = internalInitSessions;
118
- this.node = node;
119
-
120
- if (header.ruleset.type == "ownedByGroup") {
121
- this.node
122
- .expectCoValueLoaded(header.ruleset.group)
123
- .subscribe((_groupUpdate) => {
124
- this._cachedContent = undefined;
125
- const newContent = this.getCurrentContent();
126
- for (const listener of this.listeners) {
127
- listener(newContent);
128
- }
129
- });
130
- }
91
+ id: RawCoID;
92
+ node: LocalNode;
93
+ crypto: CryptoProvider;
94
+ header: CoValueHeader;
95
+ _sessionLogs: Map<SessionID, SessionLog>;
96
+ _cachedContent?: RawCoValue;
97
+ listeners: Set<(content?: RawCoValue) => void> = new Set();
98
+ _decryptionCache: {
99
+ [key: Encrypted<JsonValue[], JsonValue>]: JsonValue[] | undefined;
100
+ } = {};
101
+ _cachedKnownState?: CoValueKnownState;
102
+ _cachedDependentOn?: RawCoID[];
103
+ _cachedNewContentSinceEmpty?: NewContentMessage[] | undefined;
104
+ _currentAsyncAddTransaction?: Promise<void>;
105
+
106
+ constructor(
107
+ header: CoValueHeader,
108
+ node: LocalNode,
109
+ internalInitSessions: Map<SessionID, SessionLog> = new Map(),
110
+ ) {
111
+ this.crypto = node.crypto;
112
+ this.id = idforHeader(header, node.crypto);
113
+ this.header = header;
114
+ this._sessionLogs = internalInitSessions;
115
+ this.node = node;
116
+
117
+ if (header.ruleset.type == "ownedByGroup") {
118
+ this.node
119
+ .expectCoValueLoaded(header.ruleset.group)
120
+ .subscribe((_groupUpdate) => {
121
+ this._cachedContent = undefined;
122
+ const newContent = this.getCurrentContent();
123
+ for (const listener of this.listeners) {
124
+ listener(newContent);
125
+ }
126
+ });
131
127
  }
132
-
133
- get sessionLogs(): Map<SessionID, SessionLog> {
134
- return this._sessionLogs;
128
+ }
129
+
130
+ get sessionLogs(): Map<SessionID, SessionLog> {
131
+ return this._sessionLogs;
132
+ }
133
+
134
+ testWithDifferentAccount(
135
+ account: ControlledAccountOrAgent,
136
+ currentSessionID: SessionID,
137
+ ): CoValueCore {
138
+ const newNode = this.node.testWithDifferentAccount(
139
+ account,
140
+ currentSessionID,
141
+ );
142
+
143
+ return newNode.expectCoValueLoaded(this.id);
144
+ }
145
+
146
+ knownState(): CoValueKnownState {
147
+ if (this._cachedKnownState) {
148
+ return this._cachedKnownState;
149
+ } else {
150
+ const knownState = this.knownStateUncached();
151
+ this._cachedKnownState = knownState;
152
+ return knownState;
135
153
  }
136
-
137
- testWithDifferentAccount(
138
- account: ControlledAccountOrAgent,
139
- currentSessionID: SessionID,
140
- ): CoValueCore {
141
- const newNode = this.node.testWithDifferentAccount(
142
- account,
143
- currentSessionID,
154
+ }
155
+
156
+ /** @internal */
157
+ knownStateUncached(): CoValueKnownState {
158
+ return {
159
+ id: this.id,
160
+ header: true,
161
+ sessions: Object.fromEntries(
162
+ [...this.sessionLogs.entries()].map(([k, v]) => [
163
+ k,
164
+ v.transactions.length,
165
+ ]),
166
+ ),
167
+ };
168
+ }
169
+
170
+ get meta(): JsonValue {
171
+ return this.header?.meta ?? null;
172
+ }
173
+
174
+ nextTransactionID(): TransactionID {
175
+ // This is an ugly hack to get a unique but stable session ID for editing the current account
176
+ const sessionID =
177
+ this.header.meta?.type === "account"
178
+ ? (this.node.currentSessionID.replace(
179
+ this.node.account.id,
180
+ this.node.account
181
+ .currentAgentID()
182
+ ._unsafeUnwrap({ withStackTrace: true }),
183
+ ) as SessionID)
184
+ : this.node.currentSessionID;
185
+
186
+ return {
187
+ sessionID,
188
+ txIndex: this.sessionLogs.get(sessionID)?.transactions.length || 0,
189
+ };
190
+ }
191
+
192
+ tryAddTransactions(
193
+ sessionID: SessionID,
194
+ newTransactions: Transaction[],
195
+ givenExpectedNewHash: Hash | undefined,
196
+ newSignature: Signature,
197
+ ): Result<true, TryAddTransactionsError> {
198
+ return this.node
199
+ .resolveAccountAgent(
200
+ accountOrAgentIDfromSessionID(sessionID),
201
+ "Expected to know signer of transaction",
202
+ )
203
+ .andThen((agent) => {
204
+ const signerID = this.crypto.getAgentSignerID(agent);
205
+
206
+ // const beforeHash = performance.now();
207
+ const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
208
+ sessionID,
209
+ newTransactions,
144
210
  );
145
-
146
- return newNode.expectCoValueLoaded(this.id);
147
- }
148
-
149
- knownState(): CoValueKnownState {
150
- if (this._cachedKnownState) {
151
- return this._cachedKnownState;
152
- } else {
153
- const knownState = this.knownStateUncached();
154
- this._cachedKnownState = knownState;
155
- return knownState;
211
+ // const afterHash = performance.now();
212
+ // console.log(
213
+ // "Hashing took",
214
+ // afterHash - beforeHash
215
+ // );
216
+
217
+ if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
218
+ return err({
219
+ type: "InvalidHash",
220
+ id: this.id,
221
+ expectedNewHash,
222
+ givenExpectedNewHash,
223
+ } satisfies InvalidHashError);
156
224
  }
157
- }
158
225
 
159
- /** @internal */
160
- knownStateUncached(): CoValueKnownState {
161
- return {
226
+ // const beforeVerify = performance.now();
227
+ if (!this.crypto.verify(newSignature, expectedNewHash, signerID)) {
228
+ return err({
229
+ type: "InvalidSignature",
162
230
  id: this.id,
163
- header: true,
164
- sessions: Object.fromEntries(
165
- [...this.sessionLogs.entries()].map(([k, v]) => [
166
- k,
167
- v.transactions.length,
168
- ]),
169
- ),
170
- };
171
- }
172
-
173
- get meta(): JsonValue {
174
- return this.header?.meta ?? null;
175
- }
176
-
177
- nextTransactionID(): TransactionID {
178
- // This is an ugly hack to get a unique but stable session ID for editing the current account
179
- const sessionID =
180
- this.header.meta?.type === "account"
181
- ? (this.node.currentSessionID.replace(
182
- this.node.account.id,
183
- this.node.account
184
- .currentAgentID()
185
- ._unsafeUnwrap({ withStackTrace: true }),
186
- ) as SessionID)
187
- : this.node.currentSessionID;
188
-
189
- return {
231
+ newSignature,
190
232
  sessionID,
191
- txIndex: this.sessionLogs.get(sessionID)?.transactions.length || 0,
192
- };
193
- }
233
+ signerID,
234
+ } satisfies InvalidSignatureError);
235
+ }
236
+ // const afterVerify = performance.now();
237
+ // console.log(
238
+ // "Verify took",
239
+ // afterVerify - beforeVerify
240
+ // );
241
+
242
+ this.doAddTransactions(
243
+ sessionID,
244
+ newTransactions,
245
+ newSignature,
246
+ expectedNewHash,
247
+ newStreamingHash,
248
+ "immediate",
249
+ );
194
250
 
195
- tryAddTransactions(
196
- sessionID: SessionID,
197
- newTransactions: Transaction[],
198
- givenExpectedNewHash: Hash | undefined,
199
- newSignature: Signature,
200
- ): Result<true, TryAddTransactionsError> {
201
- return this.node
202
- .resolveAccountAgent(
203
- accountOrAgentIDfromSessionID(sessionID),
204
- "Expected to know signer of transaction",
205
- )
206
- .andThen((agent) => {
207
- const signerID = this.crypto.getAgentSignerID(agent);
208
-
209
- // const beforeHash = performance.now();
210
- const { expectedNewHash, newStreamingHash } =
211
- this.expectedNewHashAfter(sessionID, newTransactions);
212
- // const afterHash = performance.now();
213
- // console.log(
214
- // "Hashing took",
215
- // afterHash - beforeHash
216
- // );
217
-
218
- if (
219
- givenExpectedNewHash &&
220
- givenExpectedNewHash !== expectedNewHash
221
- ) {
222
- return err({
223
- type: "InvalidHash",
224
- id: this.id,
225
- expectedNewHash,
226
- givenExpectedNewHash,
227
- } satisfies InvalidHashError);
228
- }
229
-
230
- // const beforeVerify = performance.now();
231
- if (
232
- !this.crypto.verify(newSignature, expectedNewHash, signerID)
233
- ) {
234
- return err({
235
- type: "InvalidSignature",
236
- id: this.id,
237
- newSignature,
238
- sessionID,
239
- signerID,
240
- } satisfies InvalidSignatureError);
241
- }
242
- // const afterVerify = performance.now();
243
- // console.log(
244
- // "Verify took",
245
- // afterVerify - beforeVerify
246
- // );
247
-
248
- this.doAddTransactions(
249
- sessionID,
250
- newTransactions,
251
- newSignature,
252
- expectedNewHash,
253
- newStreamingHash,
254
- "immediate",
255
- );
256
-
257
- return ok(true as const);
258
- });
259
- }
251
+ return ok(true as const);
252
+ });
253
+ }
260
254
 
261
- /*tryAddTransactionsAsync(
255
+ /*tryAddTransactionsAsync(
262
256
  sessionID: SessionID,
263
257
  newTransactions: Transaction[],
264
258
  givenExpectedNewHash: Hash | undefined,
@@ -390,655 +384,622 @@ export class CoValueCore {
390
384
  });
391
385
  }*/
392
386
 
393
- private doAddTransactions(
394
- sessionID: SessionID,
395
- newTransactions: Transaction[],
396
- newSignature: Signature,
397
- expectedNewHash: Hash,
398
- newStreamingHash: StreamingHash,
399
- notifyMode: "immediate" | "deferred",
400
- ) {
401
- if (this.node.crashed) {
402
- throw new Error("Trying to add transactions after node is crashed");
403
- }
404
- const transactions =
405
- this.sessionLogs.get(sessionID)?.transactions ?? [];
406
- transactions.push(...newTransactions);
407
-
408
- const signatureAfter =
409
- this.sessionLogs.get(sessionID)?.signatureAfter ?? {};
410
-
411
- const lastInbetweenSignatureIdx = Object.keys(signatureAfter).reduce(
412
- (max, idx) => (parseInt(idx) > max ? parseInt(idx) : max),
413
- -1,
414
- );
415
-
416
- const sizeOfTxsSinceLastInbetweenSignature = transactions
417
- .slice(lastInbetweenSignatureIdx + 1)
418
- .reduce(
419
- (sum, tx) =>
420
- sum +
421
- (tx.privacy === "private"
422
- ? tx.encryptedChanges.length
423
- : tx.changes.length),
424
- 0,
425
- );
387
+ private doAddTransactions(
388
+ sessionID: SessionID,
389
+ newTransactions: Transaction[],
390
+ newSignature: Signature,
391
+ expectedNewHash: Hash,
392
+ newStreamingHash: StreamingHash,
393
+ notifyMode: "immediate" | "deferred",
394
+ ) {
395
+ if (this.node.crashed) {
396
+ throw new Error("Trying to add transactions after node is crashed");
397
+ }
398
+ const transactions = this.sessionLogs.get(sessionID)?.transactions ?? [];
399
+ transactions.push(...newTransactions);
400
+
401
+ const signatureAfter =
402
+ this.sessionLogs.get(sessionID)?.signatureAfter ?? {};
403
+
404
+ const lastInbetweenSignatureIdx = Object.keys(signatureAfter).reduce(
405
+ (max, idx) => (parseInt(idx) > max ? parseInt(idx) : max),
406
+ -1,
407
+ );
408
+
409
+ const sizeOfTxsSinceLastInbetweenSignature = transactions
410
+ .slice(lastInbetweenSignatureIdx + 1)
411
+ .reduce(
412
+ (sum, tx) =>
413
+ sum +
414
+ (tx.privacy === "private"
415
+ ? tx.encryptedChanges.length
416
+ : tx.changes.length),
417
+ 0,
418
+ );
419
+
420
+ if (sizeOfTxsSinceLastInbetweenSignature > MAX_RECOMMENDED_TX_SIZE) {
421
+ // console.log(
422
+ // "Saving inbetween signature for tx ",
423
+ // sessionID,
424
+ // transactions.length - 1,
425
+ // sizeOfTxsSinceLastInbetweenSignature
426
+ // );
427
+ signatureAfter[transactions.length - 1] = newSignature;
428
+ }
426
429
 
427
- if (sizeOfTxsSinceLastInbetweenSignature > MAX_RECOMMENDED_TX_SIZE) {
428
- // console.log(
429
- // "Saving inbetween signature for tx ",
430
- // sessionID,
431
- // transactions.length - 1,
432
- // sizeOfTxsSinceLastInbetweenSignature
433
- // );
434
- signatureAfter[transactions.length - 1] = newSignature;
430
+ this._sessionLogs.set(sessionID, {
431
+ transactions,
432
+ lastHash: expectedNewHash,
433
+ streamingHash: newStreamingHash,
434
+ lastSignature: newSignature,
435
+ signatureAfter: signatureAfter,
436
+ });
437
+
438
+ this._cachedContent = undefined;
439
+ this._cachedKnownState = undefined;
440
+ this._cachedDependentOn = undefined;
441
+ this._cachedNewContentSinceEmpty = undefined;
442
+
443
+ if (this.listeners.size > 0) {
444
+ if (notifyMode === "immediate") {
445
+ const content = this.getCurrentContent();
446
+ for (const listener of this.listeners) {
447
+ listener(content);
435
448
  }
436
-
437
- this._sessionLogs.set(sessionID, {
438
- transactions,
439
- lastHash: expectedNewHash,
440
- streamingHash: newStreamingHash,
441
- lastSignature: newSignature,
442
- signatureAfter: signatureAfter,
443
- });
444
-
445
- this._cachedContent = undefined;
446
- this._cachedKnownState = undefined;
447
- this._cachedDependentOn = undefined;
448
- this._cachedNewContentSinceEmpty = undefined;
449
-
450
- if (this.listeners.size > 0) {
451
- if (notifyMode === "immediate") {
452
- const content = this.getCurrentContent();
453
- for (const listener of this.listeners) {
454
- listener(content);
455
- }
456
- } else {
457
- if (!this.nextDeferredNotify) {
458
- this.nextDeferredNotify = new Promise((resolve) => {
459
- setTimeout(() => {
460
- this.nextDeferredNotify = undefined;
461
- this.deferredUpdates = 0;
462
- const content = this.getCurrentContent();
463
- for (const listener of this.listeners) {
464
- listener(content);
465
- }
466
- resolve();
467
- }, 0);
468
- });
469
- }
470
- this.deferredUpdates++;
471
- }
449
+ } else {
450
+ if (!this.nextDeferredNotify) {
451
+ this.nextDeferredNotify = new Promise((resolve) => {
452
+ setTimeout(() => {
453
+ this.nextDeferredNotify = undefined;
454
+ this.deferredUpdates = 0;
455
+ const content = this.getCurrentContent();
456
+ for (const listener of this.listeners) {
457
+ listener(content);
458
+ }
459
+ resolve();
460
+ }, 0);
461
+ });
472
462
  }
463
+ this.deferredUpdates++;
464
+ }
473
465
  }
474
-
475
- deferredUpdates = 0;
476
- nextDeferredNotify: Promise<void> | undefined;
477
-
478
- subscribe(listener: (content?: RawCoValue) => void): () => void {
479
- this.listeners.add(listener);
480
- listener(this.getCurrentContent());
481
-
482
- return () => {
483
- this.listeners.delete(listener);
484
- };
466
+ }
467
+
468
+ deferredUpdates = 0;
469
+ nextDeferredNotify: Promise<void> | undefined;
470
+
471
+ subscribe(listener: (content?: RawCoValue) => void): () => void {
472
+ this.listeners.add(listener);
473
+ listener(this.getCurrentContent());
474
+
475
+ return () => {
476
+ this.listeners.delete(listener);
477
+ };
478
+ }
479
+
480
+ expectedNewHashAfter(
481
+ sessionID: SessionID,
482
+ newTransactions: Transaction[],
483
+ ): { expectedNewHash: Hash; newStreamingHash: StreamingHash } {
484
+ const streamingHash =
485
+ this.sessionLogs.get(sessionID)?.streamingHash.clone() ??
486
+ new StreamingHash(this.crypto);
487
+ for (const transaction of newTransactions) {
488
+ streamingHash.update(transaction);
485
489
  }
486
490
 
487
- expectedNewHashAfter(
488
- sessionID: SessionID,
489
- newTransactions: Transaction[],
490
- ): { expectedNewHash: Hash; newStreamingHash: StreamingHash } {
491
- const streamingHash =
492
- this.sessionLogs.get(sessionID)?.streamingHash.clone() ??
493
- new StreamingHash(this.crypto);
494
- for (const transaction of newTransactions) {
495
- streamingHash.update(transaction);
496
- }
497
-
498
- const newStreamingHash = streamingHash.clone();
491
+ const newStreamingHash = streamingHash.clone();
492
+
493
+ return {
494
+ expectedNewHash: streamingHash.digest(),
495
+ newStreamingHash,
496
+ };
497
+ }
498
+
499
+ async expectedNewHashAfterAsync(
500
+ sessionID: SessionID,
501
+ newTransactions: Transaction[],
502
+ ): Promise<{ expectedNewHash: Hash; newStreamingHash: StreamingHash }> {
503
+ const streamingHash =
504
+ this.sessionLogs.get(sessionID)?.streamingHash.clone() ??
505
+ new StreamingHash(this.crypto);
506
+ let before = performance.now();
507
+ for (const transaction of newTransactions) {
508
+ streamingHash.update(transaction);
509
+ const after = performance.now();
510
+ if (after - before > 1) {
511
+ // console.log("Hashing blocked for", after - before);
512
+ await new Promise((resolve) => setTimeout(resolve, 0));
513
+ before = performance.now();
514
+ }
515
+ }
499
516
 
500
- return {
501
- expectedNewHash: streamingHash.digest(),
502
- newStreamingHash,
503
- };
517
+ const newStreamingHash = streamingHash.clone();
518
+
519
+ return {
520
+ expectedNewHash: streamingHash.digest(),
521
+ newStreamingHash,
522
+ };
523
+ }
524
+
525
+ makeTransaction(
526
+ changes: JsonValue[],
527
+ privacy: "private" | "trusting",
528
+ ): boolean {
529
+ const madeAt = Date.now();
530
+
531
+ let transaction: Transaction;
532
+
533
+ if (privacy === "private") {
534
+ const { secret: keySecret, id: keyID } = this.getCurrentReadKey();
535
+
536
+ if (!keySecret) {
537
+ throw new Error("Can't make transaction without read key secret");
538
+ }
539
+
540
+ const encrypted = this.crypto.encryptForTransaction(changes, keySecret, {
541
+ in: this.id,
542
+ tx: this.nextTransactionID(),
543
+ });
544
+
545
+ this._decryptionCache[encrypted] = changes;
546
+
547
+ transaction = {
548
+ privacy: "private",
549
+ madeAt,
550
+ keyUsed: keyID,
551
+ encryptedChanges: encrypted,
552
+ };
553
+ } else {
554
+ transaction = {
555
+ privacy: "trusting",
556
+ madeAt,
557
+ changes: stableStringify(changes),
558
+ };
504
559
  }
505
560
 
506
- async expectedNewHashAfterAsync(
507
- sessionID: SessionID,
508
- newTransactions: Transaction[],
509
- ): Promise<{ expectedNewHash: Hash; newStreamingHash: StreamingHash }> {
510
- const streamingHash =
511
- this.sessionLogs.get(sessionID)?.streamingHash.clone() ??
512
- new StreamingHash(this.crypto);
513
- let before = performance.now();
514
- for (const transaction of newTransactions) {
515
- streamingHash.update(transaction);
516
- const after = performance.now();
517
- if (after - before > 1) {
518
- // console.log("Hashing blocked for", after - before);
519
- await new Promise((resolve) => setTimeout(resolve, 0));
520
- before = performance.now();
521
- }
522
- }
561
+ // This is an ugly hack to get a unique but stable session ID for editing the current account
562
+ const sessionID =
563
+ this.header.meta?.type === "account"
564
+ ? (this.node.currentSessionID.replace(
565
+ this.node.account.id,
566
+ this.node.account
567
+ .currentAgentID()
568
+ ._unsafeUnwrap({ withStackTrace: true }),
569
+ ) as SessionID)
570
+ : this.node.currentSessionID;
571
+
572
+ const { expectedNewHash } = this.expectedNewHashAfter(sessionID, [
573
+ transaction,
574
+ ]);
575
+
576
+ const signature = this.crypto.sign(
577
+ this.node.account.currentSignerSecret(),
578
+ expectedNewHash,
579
+ );
580
+
581
+ const success = this.tryAddTransactions(
582
+ sessionID,
583
+ [transaction],
584
+ expectedNewHash,
585
+ signature,
586
+ )._unsafeUnwrap({ withStackTrace: true });
587
+
588
+ if (success) {
589
+ void this.node.syncManager.syncCoValue(this);
590
+ }
523
591
 
524
- const newStreamingHash = streamingHash.clone();
592
+ return success;
593
+ }
525
594
 
526
- return {
527
- expectedNewHash: streamingHash.digest(),
528
- newStreamingHash,
529
- };
595
+ getCurrentContent(options?: {
596
+ ignorePrivateTransactions: true;
597
+ }): RawCoValue {
598
+ if (!options?.ignorePrivateTransactions && this._cachedContent) {
599
+ return this._cachedContent;
530
600
  }
531
601
 
532
- makeTransaction(
533
- changes: JsonValue[],
534
- privacy: "private" | "trusting",
535
- ): boolean {
536
- const madeAt = Date.now();
602
+ const newContent = coreToCoValue(this, options);
537
603
 
538
- let transaction: Transaction;
604
+ if (!options?.ignorePrivateTransactions) {
605
+ this._cachedContent = newContent;
606
+ }
539
607
 
540
- if (privacy === "private") {
541
- const { secret: keySecret, id: keyID } = this.getCurrentReadKey();
608
+ return newContent;
609
+ }
610
+
611
+ getValidSortedTransactions(options?: {
612
+ ignorePrivateTransactions: true;
613
+ }): DecryptedTransaction[] {
614
+ const validTransactions = determineValidTransactions(this);
615
+
616
+ const allTransactions: DecryptedTransaction[] = validTransactions
617
+ .flatMap(({ txID, tx }) => {
618
+ if (tx.privacy === "trusting") {
619
+ return {
620
+ txID,
621
+ madeAt: tx.madeAt,
622
+ changes: parseJSON(tx.changes),
623
+ };
624
+ } else {
625
+ if (options?.ignorePrivateTransactions) {
626
+ return undefined;
627
+ }
628
+ const readKey = this.getReadKey(tx.keyUsed);
542
629
 
543
- if (!keySecret) {
544
- throw new Error(
545
- "Can't make transaction without read key secret",
546
- );
547
- }
630
+ if (!readKey) {
631
+ return undefined;
632
+ } else {
633
+ let decrytedChanges = this._decryptionCache[tx.encryptedChanges];
548
634
 
549
- const encrypted = this.crypto.encryptForTransaction(
550
- changes,
551
- keySecret,
635
+ if (!decrytedChanges) {
636
+ const decryptedString = this.crypto.decryptRawForTransaction(
637
+ tx.encryptedChanges,
638
+ readKey,
552
639
  {
553
- in: this.id,
554
- tx: this.nextTransactionID(),
640
+ in: this.id,
641
+ tx: txID,
555
642
  },
556
- );
557
-
558
- this._decryptionCache[encrypted] = changes;
643
+ );
644
+ decrytedChanges = decryptedString && parseJSON(decryptedString);
645
+ this._decryptionCache[tx.encryptedChanges] = decrytedChanges;
646
+ }
559
647
 
560
- transaction = {
561
- privacy: "private",
562
- madeAt,
563
- keyUsed: keyID,
564
- encryptedChanges: encrypted,
565
- };
566
- } else {
567
- transaction = {
568
- privacy: "trusting",
569
- madeAt,
570
- changes: stableStringify(changes),
648
+ if (!decrytedChanges) {
649
+ console.error("Failed to decrypt transaction despite having key");
650
+ return undefined;
651
+ }
652
+ return {
653
+ txID,
654
+ madeAt: tx.madeAt,
655
+ changes: decrytedChanges,
571
656
  };
657
+ }
572
658
  }
573
-
574
- // This is an ugly hack to get a unique but stable session ID for editing the current account
575
- const sessionID =
576
- this.header.meta?.type === "account"
577
- ? (this.node.currentSessionID.replace(
578
- this.node.account.id,
579
- this.node.account
580
- .currentAgentID()
581
- ._unsafeUnwrap({ withStackTrace: true }),
582
- ) as SessionID)
583
- : this.node.currentSessionID;
584
-
585
- const { expectedNewHash } = this.expectedNewHashAfter(sessionID, [
586
- transaction,
587
- ]);
588
-
589
- const signature = this.crypto.sign(
590
- this.node.account.currentSignerSecret(),
591
- expectedNewHash,
592
- );
593
-
594
- const success = this.tryAddTransactions(
595
- sessionID,
596
- [transaction],
597
- expectedNewHash,
598
- signature,
599
- )._unsafeUnwrap({ withStackTrace: true });
600
-
601
- if (success) {
602
- void this.node.syncManager.syncCoValue(this);
603
- }
604
-
605
- return success;
659
+ })
660
+ .filter((x): x is Exclude<typeof x, undefined> => !!x);
661
+ allTransactions.sort(
662
+ (a, b) =>
663
+ a.madeAt - b.madeAt ||
664
+ (a.txID.sessionID < b.txID.sessionID ? -1 : 1) ||
665
+ a.txID.txIndex - b.txID.txIndex,
666
+ );
667
+
668
+ return allTransactions;
669
+ }
670
+
671
+ getCurrentReadKey(): { secret: KeySecret | undefined; id: KeyID } {
672
+ if (this.header.ruleset.type === "group") {
673
+ const content = expectGroup(this.getCurrentContent());
674
+
675
+ const currentKeyId = content.get("readKey");
676
+
677
+ if (!currentKeyId) {
678
+ throw new Error("No readKey set");
679
+ }
680
+
681
+ const secret = this.getReadKey(currentKeyId);
682
+
683
+ return {
684
+ secret: secret,
685
+ id: currentKeyId,
686
+ };
687
+ } else if (this.header.ruleset.type === "ownedByGroup") {
688
+ return this.node
689
+ .expectCoValueLoaded(this.header.ruleset.group)
690
+ .getCurrentReadKey();
691
+ } else {
692
+ throw new Error(
693
+ "Only groups or values owned by groups have read secrets",
694
+ );
606
695
  }
607
-
608
- getCurrentContent(options?: {
609
- ignorePrivateTransactions: true;
610
- }): RawCoValue {
611
- if (!options?.ignorePrivateTransactions && this._cachedContent) {
612
- return this._cachedContent;
613
- }
614
-
615
- const newContent = coreToCoValue(this, options);
616
-
617
- if (!options?.ignorePrivateTransactions) {
618
- this._cachedContent = newContent;
696
+ }
697
+
698
+ getReadKey(keyID: KeyID): KeySecret | undefined {
699
+ let key = readKeyCache.get(this)?.[keyID];
700
+ if (!key) {
701
+ key = this.getUncachedReadKey(keyID);
702
+ if (key) {
703
+ let cache = readKeyCache.get(this);
704
+ if (!cache) {
705
+ cache = {};
706
+ readKeyCache.set(this, cache);
619
707
  }
620
-
621
- return newContent;
708
+ cache[keyID] = key;
709
+ }
622
710
  }
623
-
624
- getValidSortedTransactions(options?: {
625
- ignorePrivateTransactions: true;
626
- }): DecryptedTransaction[] {
627
- const validTransactions = determineValidTransactions(this);
628
-
629
- const allTransactions: DecryptedTransaction[] = validTransactions
630
- .flatMap(({ txID, tx }) => {
631
- if (tx.privacy === "trusting") {
632
- return {
633
- txID,
634
- madeAt: tx.madeAt,
635
- changes: parseJSON(tx.changes),
636
- };
637
- } else {
638
- if (options?.ignorePrivateTransactions) {
639
- return undefined;
640
- }
641
- const readKey = this.getReadKey(tx.keyUsed);
642
-
643
- if (!readKey) {
644
- return undefined;
645
- } else {
646
- let decrytedChanges =
647
- this._decryptionCache[tx.encryptedChanges];
648
-
649
- if (!decrytedChanges) {
650
- const decryptedString =
651
- this.crypto.decryptRawForTransaction(
652
- tx.encryptedChanges,
653
- readKey,
654
- {
655
- in: this.id,
656
- tx: txID,
657
- },
658
- );
659
- decrytedChanges =
660
- decryptedString && parseJSON(decryptedString);
661
- this._decryptionCache[tx.encryptedChanges] =
662
- decrytedChanges;
663
- }
664
-
665
- if (!decrytedChanges) {
666
- console.error(
667
- "Failed to decrypt transaction despite having key",
668
- );
669
- return undefined;
670
- }
671
- return {
672
- txID,
673
- madeAt: tx.madeAt,
674
- changes: decrytedChanges,
675
- };
676
- }
677
- }
678
- })
679
- .filter((x): x is Exclude<typeof x, undefined> => !!x);
680
- allTransactions.sort(
681
- (a, b) =>
682
- a.madeAt - b.madeAt ||
683
- (a.txID.sessionID < b.txID.sessionID ? -1 : 1) ||
684
- a.txID.txIndex - b.txID.txIndex,
711
+ return key;
712
+ }
713
+
714
+ getUncachedReadKey(keyID: KeyID): KeySecret | undefined {
715
+ if (this.header.ruleset.type === "group") {
716
+ const content = expectGroup(
717
+ this.getCurrentContent({ ignorePrivateTransactions: true }),
718
+ );
719
+
720
+ const keyForEveryone = content.get(`${keyID}_for_everyone`);
721
+ if (keyForEveryone) return keyForEveryone;
722
+
723
+ // Try to find key revelation for us
724
+ const lookupAccountOrAgentID =
725
+ this.header.meta?.type === "account"
726
+ ? this.node.account
727
+ .currentAgentID()
728
+ ._unsafeUnwrap({ withStackTrace: true })
729
+ : this.node.account.id;
730
+
731
+ const lastReadyKeyEdit = content.lastEditAt(
732
+ `${keyID}_for_${lookupAccountOrAgentID}`,
733
+ );
734
+
735
+ if (lastReadyKeyEdit?.value) {
736
+ const revealer = lastReadyKeyEdit.by;
737
+ const revealerAgent = this.node
738
+ .resolveAccountAgent(revealer, "Expected to know revealer")
739
+ ._unsafeUnwrap({ withStackTrace: true });
740
+
741
+ const secret = this.crypto.unseal(
742
+ lastReadyKeyEdit.value,
743
+ this.node.account.currentSealerSecret(),
744
+ this.crypto.getAgentSealerID(revealerAgent),
745
+ {
746
+ in: this.id,
747
+ tx: lastReadyKeyEdit.tx,
748
+ },
685
749
  );
686
750
 
687
- return allTransactions;
688
- }
689
-
690
- getCurrentReadKey(): { secret: KeySecret | undefined; id: KeyID } {
691
- if (this.header.ruleset.type === "group") {
692
- const content = expectGroup(this.getCurrentContent());
693
-
694
- const currentKeyId = content.get("readKey");
695
-
696
- if (!currentKeyId) {
697
- throw new Error("No readKey set");
698
- }
699
-
700
- const secret = this.getReadKey(currentKeyId);
701
-
702
- return {
703
- secret: secret,
704
- id: currentKeyId,
705
- };
706
- } else if (this.header.ruleset.type === "ownedByGroup") {
707
- return this.node
708
- .expectCoValueLoaded(this.header.ruleset.group)
709
- .getCurrentReadKey();
710
- } else {
711
- throw new Error(
712
- "Only groups or values owned by groups have read secrets",
751
+ if (secret) {
752
+ return secret as KeySecret;
753
+ }
754
+ }
755
+
756
+ // Try to find indirect revelation through previousKeys
757
+
758
+ for (const co of content.keys()) {
759
+ if (isKeyForKeyField(co) && co.startsWith(keyID)) {
760
+ const encryptingKeyID = co.split("_for_")[1] as KeyID;
761
+ const encryptingKeySecret = this.getReadKey(encryptingKeyID);
762
+
763
+ if (!encryptingKeySecret) {
764
+ continue;
765
+ }
766
+
767
+ const encryptedPreviousKey = content.get(co)!;
768
+
769
+ const secret = this.crypto.decryptKeySecret(
770
+ {
771
+ encryptedID: keyID,
772
+ encryptingID: encryptingKeyID,
773
+ encrypted: encryptedPreviousKey,
774
+ },
775
+ encryptingKeySecret,
776
+ );
777
+
778
+ if (secret) {
779
+ return secret as KeySecret;
780
+ } else {
781
+ console.error(
782
+ `Encrypting ${encryptingKeyID} key didn't decrypt ${keyID}`,
713
783
  );
784
+ }
714
785
  }
786
+ }
787
+
788
+ return undefined;
789
+ } else if (this.header.ruleset.type === "ownedByGroup") {
790
+ return this.node
791
+ .expectCoValueLoaded(this.header.ruleset.group)
792
+ .getReadKey(keyID);
793
+ } else {
794
+ throw new Error(
795
+ "Only groups or values owned by groups have read secrets",
796
+ );
715
797
  }
798
+ }
716
799
 
717
- getReadKey(keyID: KeyID): KeySecret | undefined {
718
- let key = readKeyCache.get(this)?.[keyID];
719
- if (!key) {
720
- key = this.getUncachedReadKey(keyID);
721
- if (key) {
722
- let cache = readKeyCache.get(this);
723
- if (!cache) {
724
- cache = {};
725
- readKeyCache.set(this, cache);
726
- }
727
- cache[keyID] = key;
728
- }
729
- }
730
- return key;
800
+ getGroup(): RawGroup {
801
+ if (this.header.ruleset.type !== "ownedByGroup") {
802
+ throw new Error("Only values owned by groups have groups");
731
803
  }
732
804
 
733
- getUncachedReadKey(keyID: KeyID): KeySecret | undefined {
734
- if (this.header.ruleset.type === "group") {
735
- const content = expectGroup(
736
- this.getCurrentContent({ ignorePrivateTransactions: true }),
737
- );
805
+ return expectGroup(
806
+ this.node
807
+ .expectCoValueLoaded(this.header.ruleset.group)
808
+ .getCurrentContent(),
809
+ );
810
+ }
738
811
 
739
- const keyForEveryone = content.get(`${keyID}_for_everyone`);
740
- if (keyForEveryone) return keyForEveryone;
812
+ getTx(txID: TransactionID): Transaction | undefined {
813
+ return this.sessionLogs.get(txID.sessionID)?.transactions[txID.txIndex];
814
+ }
741
815
 
742
- // Try to find key revelation for us
743
- const lookupAccountOrAgentID =
744
- this.header.meta?.type === "account"
745
- ? this.node.account
746
- .currentAgentID()
747
- ._unsafeUnwrap({ withStackTrace: true })
748
- : this.node.account.id;
816
+ newContentSince(
817
+ knownState: CoValueKnownState | undefined,
818
+ ): NewContentMessage[] | undefined {
819
+ const isKnownStateEmpty = !knownState?.header && !knownState?.sessions;
749
820
 
750
- const lastReadyKeyEdit = content.lastEditAt(
751
- `${keyID}_for_${lookupAccountOrAgentID}`,
752
- );
821
+ if (isKnownStateEmpty && this._cachedNewContentSinceEmpty) {
822
+ return this._cachedNewContentSinceEmpty;
823
+ }
753
824
 
754
- if (lastReadyKeyEdit?.value) {
755
- const revealer = lastReadyKeyEdit.by;
756
- const revealerAgent = this.node
757
- .resolveAccountAgent(revealer, "Expected to know revealer")
758
- ._unsafeUnwrap({ withStackTrace: true });
759
-
760
- const secret = this.crypto.unseal(
761
- lastReadyKeyEdit.value,
762
- this.node.account.currentSealerSecret(),
763
- this.crypto.getAgentSealerID(revealerAgent),
764
- {
765
- in: this.id,
766
- tx: lastReadyKeyEdit.tx,
767
- },
768
- );
769
-
770
- if (secret) {
771
- return secret as KeySecret;
772
- }
773
- }
825
+ let currentPiece: NewContentMessage = {
826
+ action: "content",
827
+ id: this.id,
828
+ header: knownState?.header ? undefined : this.header,
829
+ priority: getPriorityFromHeader(this.header),
830
+ new: {},
831
+ };
832
+
833
+ const pieces = [currentPiece];
834
+
835
+ const sentState: CoValueKnownState["sessions"] = {};
836
+
837
+ let pieceSize = 0;
838
+
839
+ let sessionsTodoAgain: Set<SessionID> | undefined | "first" = "first";
840
+
841
+ while (sessionsTodoAgain === "first" || sessionsTodoAgain?.size || 0 > 0) {
842
+ if (sessionsTodoAgain === "first") {
843
+ sessionsTodoAgain = undefined;
844
+ }
845
+ const sessionsTodo = sessionsTodoAgain ?? this.sessionLogs.keys();
846
+
847
+ for (const sessionIDKey of sessionsTodo) {
848
+ const sessionID = sessionIDKey as SessionID;
849
+ const log = this.sessionLogs.get(sessionID)!;
850
+ const knownStateForSessionID = knownState?.sessions[sessionID];
851
+ const sentStateForSessionID = sentState[sessionID];
852
+ const nextKnownSignatureIdx = getNextKnownSignatureIdx(
853
+ log,
854
+ knownStateForSessionID,
855
+ sentStateForSessionID,
856
+ );
774
857
 
775
- // Try to find indirect revelation through previousKeys
776
-
777
- for (const co of content.keys()) {
778
- if (isKeyForKeyField(co) && co.startsWith(keyID)) {
779
- const encryptingKeyID = co.split("_for_")[1] as KeyID;
780
- const encryptingKeySecret =
781
- this.getReadKey(encryptingKeyID);
782
-
783
- if (!encryptingKeySecret) {
784
- continue;
785
- }
786
-
787
- const encryptedPreviousKey = content.get(co)!;
788
-
789
- const secret = this.crypto.decryptKeySecret(
790
- {
791
- encryptedID: keyID,
792
- encryptingID: encryptingKeyID,
793
- encrypted: encryptedPreviousKey,
794
- },
795
- encryptingKeySecret,
796
- );
797
-
798
- if (secret) {
799
- return secret as KeySecret;
800
- } else {
801
- console.error(
802
- `Encrypting ${encryptingKeyID} key didn't decrypt ${keyID}`,
803
- );
804
- }
805
- }
806
- }
858
+ const firstNewTxIdx =
859
+ sentStateForSessionID ?? knownStateForSessionID ?? 0;
860
+ const afterLastNewTxIdx =
861
+ nextKnownSignatureIdx === undefined
862
+ ? log.transactions.length
863
+ : nextKnownSignatureIdx + 1;
807
864
 
808
- return undefined;
809
- } else if (this.header.ruleset.type === "ownedByGroup") {
810
- return this.node
811
- .expectCoValueLoaded(this.header.ruleset.group)
812
- .getReadKey(keyID);
813
- } else {
814
- throw new Error(
815
- "Only groups or values owned by groups have read secrets",
816
- );
817
- }
818
- }
865
+ const nNewTx = Math.max(0, afterLastNewTxIdx - firstNewTxIdx);
819
866
 
820
- getGroup(): RawGroup {
821
- if (this.header.ruleset.type !== "ownedByGroup") {
822
- throw new Error("Only values owned by groups have groups");
867
+ if (nNewTx === 0) {
868
+ sessionsTodoAgain?.delete(sessionID);
869
+ continue;
823
870
  }
824
871
 
825
- return expectGroup(
826
- this.node
827
- .expectCoValueLoaded(this.header.ruleset.group)
828
- .getCurrentContent(),
829
- );
830
- }
831
-
832
- getTx(txID: TransactionID): Transaction | undefined {
833
- return this.sessionLogs.get(txID.sessionID)?.transactions[txID.txIndex];
834
- }
835
-
836
- newContentSince(
837
- knownState: CoValueKnownState | undefined,
838
- ): NewContentMessage[] | undefined {
839
- const isKnownStateEmpty = !knownState?.header && !knownState?.sessions;
872
+ if (afterLastNewTxIdx < log.transactions.length) {
873
+ if (!sessionsTodoAgain) {
874
+ sessionsTodoAgain = new Set();
875
+ }
876
+ sessionsTodoAgain.add(sessionID);
877
+ }
840
878
 
841
- if (isKnownStateEmpty && this._cachedNewContentSinceEmpty) {
842
- return this._cachedNewContentSinceEmpty;
879
+ const oldPieceSize = pieceSize;
880
+ for (let txIdx = firstNewTxIdx; txIdx < afterLastNewTxIdx; txIdx++) {
881
+ const tx = log.transactions[txIdx]!;
882
+ pieceSize +=
883
+ tx.privacy === "private"
884
+ ? tx.encryptedChanges.length
885
+ : tx.changes.length;
843
886
  }
844
887
 
845
- let currentPiece: NewContentMessage = {
888
+ if (pieceSize >= MAX_RECOMMENDED_TX_SIZE) {
889
+ currentPiece = {
846
890
  action: "content",
847
891
  id: this.id,
848
- header: knownState?.header ? undefined : this.header,
849
- priority: getPriorityFromHeader(this.header),
892
+ header: undefined,
850
893
  new: {},
851
- };
852
-
853
- const pieces = [currentPiece];
854
-
855
- const sentState: CoValueKnownState["sessions"] = {};
856
-
857
- let pieceSize = 0;
894
+ priority: getPriorityFromHeader(this.header),
895
+ };
896
+ pieces.push(currentPiece);
897
+ pieceSize = pieceSize - oldPieceSize;
898
+ }
858
899
 
859
- let sessionsTodoAgain: Set<SessionID> | undefined | "first" = "first";
900
+ let sessionEntry = currentPiece.new[sessionID];
901
+ if (!sessionEntry) {
902
+ sessionEntry = {
903
+ after: sentStateForSessionID ?? knownStateForSessionID ?? 0,
904
+ newTransactions: [],
905
+ lastSignature: "WILL_BE_REPLACED" as Signature,
906
+ };
907
+ currentPiece.new[sessionID] = sessionEntry;
908
+ }
860
909
 
861
- while (
862
- sessionsTodoAgain === "first" ||
863
- sessionsTodoAgain?.size ||
864
- 0 > 0
865
- ) {
866
- if (sessionsTodoAgain === "first") {
867
- sessionsTodoAgain = undefined;
868
- }
869
- const sessionsTodo = sessionsTodoAgain ?? this.sessionLogs.keys();
870
-
871
- for (const sessionIDKey of sessionsTodo) {
872
- const sessionID = sessionIDKey as SessionID;
873
- const log = this.sessionLogs.get(sessionID)!;
874
- const knownStateForSessionID = knownState?.sessions[sessionID];
875
- const sentStateForSessionID = sentState[sessionID];
876
- const nextKnownSignatureIdx = getNextKnownSignatureIdx(
877
- log,
878
- knownStateForSessionID,
879
- sentStateForSessionID,
880
- );
881
-
882
- const firstNewTxIdx =
883
- sentStateForSessionID ?? knownStateForSessionID ?? 0;
884
- const afterLastNewTxIdx =
885
- nextKnownSignatureIdx === undefined
886
- ? log.transactions.length
887
- : nextKnownSignatureIdx + 1;
888
-
889
- const nNewTx = Math.max(0, afterLastNewTxIdx - firstNewTxIdx);
890
-
891
- if (nNewTx === 0) {
892
- sessionsTodoAgain?.delete(sessionID);
893
- continue;
894
- }
895
-
896
- if (afterLastNewTxIdx < log.transactions.length) {
897
- if (!sessionsTodoAgain) {
898
- sessionsTodoAgain = new Set();
899
- }
900
- sessionsTodoAgain.add(sessionID);
901
- }
902
-
903
- const oldPieceSize = pieceSize;
904
- for (
905
- let txIdx = firstNewTxIdx;
906
- txIdx < afterLastNewTxIdx;
907
- txIdx++
908
- ) {
909
- const tx = log.transactions[txIdx]!;
910
- pieceSize +=
911
- tx.privacy === "private"
912
- ? tx.encryptedChanges.length
913
- : tx.changes.length;
914
- }
915
-
916
- if (pieceSize >= MAX_RECOMMENDED_TX_SIZE) {
917
- currentPiece = {
918
- action: "content",
919
- id: this.id,
920
- header: undefined,
921
- new: {},
922
- priority: getPriorityFromHeader(this.header),
923
- };
924
- pieces.push(currentPiece);
925
- pieceSize = pieceSize - oldPieceSize;
926
- }
927
-
928
- let sessionEntry = currentPiece.new[sessionID];
929
- if (!sessionEntry) {
930
- sessionEntry = {
931
- after:
932
- sentStateForSessionID ??
933
- knownStateForSessionID ??
934
- 0,
935
- newTransactions: [],
936
- lastSignature: "WILL_BE_REPLACED" as Signature,
937
- };
938
- currentPiece.new[sessionID] = sessionEntry;
939
- }
940
-
941
- for (
942
- let txIdx = firstNewTxIdx;
943
- txIdx < afterLastNewTxIdx;
944
- txIdx++
945
- ) {
946
- const tx = log.transactions[txIdx]!;
947
- sessionEntry.newTransactions.push(tx);
948
- }
949
-
950
- sessionEntry.lastSignature =
951
- nextKnownSignatureIdx === undefined
952
- ? log.lastSignature!
953
- : log.signatureAfter[nextKnownSignatureIdx]!;
954
-
955
- sentState[sessionID] =
956
- (sentStateForSessionID ?? knownStateForSessionID ?? 0) +
957
- nNewTx;
958
- }
910
+ for (let txIdx = firstNewTxIdx; txIdx < afterLastNewTxIdx; txIdx++) {
911
+ const tx = log.transactions[txIdx]!;
912
+ sessionEntry.newTransactions.push(tx);
959
913
  }
960
914
 
961
- const piecesWithContent = pieces.filter(
962
- (piece) => Object.keys(piece.new).length > 0 || piece.header,
963
- );
915
+ sessionEntry.lastSignature =
916
+ nextKnownSignatureIdx === undefined
917
+ ? log.lastSignature!
918
+ : log.signatureAfter[nextKnownSignatureIdx]!;
964
919
 
965
- if (piecesWithContent.length === 0) {
966
- return undefined;
967
- }
920
+ sentState[sessionID] =
921
+ (sentStateForSessionID ?? knownStateForSessionID ?? 0) + nNewTx;
922
+ }
923
+ }
968
924
 
969
- if (isKnownStateEmpty) {
970
- this._cachedNewContentSinceEmpty = piecesWithContent;
971
- }
925
+ const piecesWithContent = pieces.filter(
926
+ (piece) => Object.keys(piece.new).length > 0 || piece.header,
927
+ );
972
928
 
973
- return piecesWithContent;
929
+ if (piecesWithContent.length === 0) {
930
+ return undefined;
974
931
  }
975
932
 
976
- getDependedOnCoValues(): RawCoID[] {
977
- if (this._cachedDependentOn) {
978
- return this._cachedDependentOn;
979
- } else {
980
- const dependentOn = this.getDependedOnCoValuesUncached();
981
- this._cachedDependentOn = dependentOn;
982
- return dependentOn;
983
- }
933
+ if (isKnownStateEmpty) {
934
+ this._cachedNewContentSinceEmpty = piecesWithContent;
984
935
  }
985
936
 
986
- /** @internal */
987
- getDependedOnCoValuesUncached(): RawCoID[] {
988
- return this.header.ruleset.type === "group"
989
- ? expectGroup(this.getCurrentContent())
990
- .keys()
991
- .filter((k): k is RawAccountID => k.startsWith("co_"))
992
- : this.header.ruleset.type === "ownedByGroup"
993
- ? [
994
- this.header.ruleset.group,
995
- ...new Set(
996
- [...this.sessionLogs.keys()]
997
- .map((sessionID) =>
998
- accountOrAgentIDfromSessionID(
999
- sessionID as SessionID,
1000
- ),
1001
- )
1002
- .filter(
1003
- (session): session is RawAccountID =>
1004
- isAccountID(session) && session !== this.id,
1005
- ),
1006
- ),
1007
- ]
1008
- : [];
937
+ return piecesWithContent;
938
+ }
939
+
940
+ getDependedOnCoValues(): RawCoID[] {
941
+ if (this._cachedDependentOn) {
942
+ return this._cachedDependentOn;
943
+ } else {
944
+ const dependentOn = this.getDependedOnCoValuesUncached();
945
+ this._cachedDependentOn = dependentOn;
946
+ return dependentOn;
1009
947
  }
948
+ }
949
+
950
+ /** @internal */
951
+ getDependedOnCoValuesUncached(): RawCoID[] {
952
+ return this.header.ruleset.type === "group"
953
+ ? expectGroup(this.getCurrentContent())
954
+ .keys()
955
+ .filter((k): k is RawAccountID => k.startsWith("co_"))
956
+ : this.header.ruleset.type === "ownedByGroup"
957
+ ? [
958
+ this.header.ruleset.group,
959
+ ...new Set(
960
+ [...this.sessionLogs.keys()]
961
+ .map((sessionID) =>
962
+ accountOrAgentIDfromSessionID(sessionID as SessionID),
963
+ )
964
+ .filter(
965
+ (session): session is RawAccountID =>
966
+ isAccountID(session) && session !== this.id,
967
+ ),
968
+ ),
969
+ ]
970
+ : [];
971
+ }
1010
972
  }
1011
973
 
1012
974
  function getNextKnownSignatureIdx(
1013
- log: SessionLog,
1014
- knownStateForSessionID?: number,
1015
- sentStateForSessionID?: number,
975
+ log: SessionLog,
976
+ knownStateForSessionID?: number,
977
+ sentStateForSessionID?: number,
1016
978
  ) {
1017
- return Object.keys(log.signatureAfter)
1018
- .map(Number)
1019
- .sort((a, b) => a - b)
1020
- .find(
1021
- (idx) =>
1022
- idx >= (sentStateForSessionID ?? knownStateForSessionID ?? -1),
1023
- );
979
+ return Object.keys(log.signatureAfter)
980
+ .map(Number)
981
+ .sort((a, b) => a - b)
982
+ .find(
983
+ (idx) => idx >= (sentStateForSessionID ?? knownStateForSessionID ?? -1),
984
+ );
1024
985
  }
1025
986
 
1026
987
  export type InvalidHashError = {
1027
- type: "InvalidHash";
1028
- id: RawCoID;
1029
- expectedNewHash: Hash;
1030
- givenExpectedNewHash: Hash;
988
+ type: "InvalidHash";
989
+ id: RawCoID;
990
+ expectedNewHash: Hash;
991
+ givenExpectedNewHash: Hash;
1031
992
  };
1032
993
 
1033
994
  export type InvalidSignatureError = {
1034
- type: "InvalidSignature";
1035
- id: RawCoID;
1036
- newSignature: Signature;
1037
- sessionID: SessionID;
1038
- signerID: SignerID;
995
+ type: "InvalidSignature";
996
+ id: RawCoID;
997
+ newSignature: Signature;
998
+ sessionID: SessionID;
999
+ signerID: SignerID;
1039
1000
  };
1040
1001
 
1041
1002
  export type TryAddTransactionsError =
1042
- | ResolveAccountAgentError
1043
- | InvalidHashError
1044
- | InvalidSignatureError;
1003
+ | ResolveAccountAgentError
1004
+ | InvalidHashError
1005
+ | InvalidSignatureError;