@agoric/fast-usdc 0.1.1-other-dev-3eb1a1d.0 → 0.2.0-u18.0

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.
@@ -1,28 +1,39 @@
1
- import { M } from '@endo/patterns';
2
- import { makeError, q } from '@endo/errors';
3
-
1
+ import { makeTracer } from '@agoric/internal';
4
2
  import { appendToStoredArray } from '@agoric/store/src/stores/store-utils.js';
5
- import { CctpTxEvidenceShape, PendingTxShape } from '../type-guards.js';
6
- import { PendingTxStatus } from '../constants.js';
3
+ import { AmountKeywordRecordShape } from '@agoric/zoe/src/typeGuards.js';
4
+ import { Fail, makeError, q } from '@endo/errors';
5
+ import { E } from '@endo/eventual-send';
6
+ import { M } from '@endo/patterns';
7
+ import { PendingTxStatus, TerminalTxStatus, TxStatus } from '../constants.js';
8
+ import {
9
+ CctpTxEvidenceShape,
10
+ EvmHashShape,
11
+ PendingTxShape,
12
+ } from '../type-guards.js';
7
13
 
8
14
  /**
9
15
  * @import {MapStore, SetStore} from '@agoric/store';
10
16
  * @import {Zone} from '@agoric/zone';
11
- * @import {CctpTxEvidence, NobleAddress, SeenTxKey, PendingTxKey, PendingTx} from '../types.js';
17
+ * @import {CctpTxEvidence, NobleAddress, PendingTx, EvmHash, LogFn, TransactionRecord, EvidenceWithRisk, RiskAssessment} from '../types.js';
18
+ */
19
+
20
+ /**
21
+ * @typedef {`pendingTx:${bigint}:${NobleAddress}`} PendingTxKey
22
+ * The string template is for developer visibility but not meant to ever be parsed.
12
23
  */
13
24
 
14
25
  /**
15
26
  * Create the key for the pendingTxs MapStore.
16
27
  *
17
- * The key is a composite of `txHash` and `chainId` and not meant to be
18
- * parsable.
28
+ * The key is a composite but not meant to be parsable.
19
29
  *
20
- * @param {NobleAddress} addr
30
+ * @param {NobleAddress} nfa Noble Forwarding Account (implies EUD)
21
31
  * @param {bigint} amount
22
32
  * @returns {PendingTxKey}
23
33
  */
24
- const makePendingTxKey = (addr, amount) =>
25
- `pendingTx:${JSON.stringify([addr, String(amount)])}`;
34
+ const makePendingTxKey = (nfa, amount) =>
35
+ // amount can't contain colon
36
+ `pendingTx:${amount}:${nfa}`;
26
37
 
27
38
  /**
28
39
  * Get the key for the pendingTxs MapStore.
@@ -36,18 +47,11 @@ const pendingTxKeyOf = evidence => {
36
47
  };
37
48
 
38
49
  /**
39
- * Get the key for the seenTxs SetStore.
40
- *
41
- * The key is a composite of `NobleAddress` and transaction `amount` and not
42
- * meant to be parsable.
43
- *
44
- * @param {CctpTxEvidence} evidence
45
- * @returns {SeenTxKey}
50
+ * @typedef {{
51
+ * log?: LogFn;
52
+ * marshaller: ERef<Marshaller>;
53
+ * }} StatusManagerPowers
46
54
  */
47
- const seenTxKeyOf = evidence => {
48
- const { txHash, chainId } = evidence;
49
- return `seenTx:${JSON.stringify([txHash, chainId])}`;
50
- };
51
55
 
52
56
  /**
53
57
  * The `StatusManager` keeps track of Pending and Seen Transactions
@@ -57,19 +61,83 @@ const seenTxKeyOf = evidence => {
57
61
  * XXX consider separate facets for `Advancing` and `Settling` capabilities.
58
62
  *
59
63
  * @param {Zone} zone
64
+ * @param {ERef<StorageNode>} txnsNode
65
+ * @param {StatusManagerPowers} caps
60
66
  */
61
- export const prepareStatusManager = zone => {
62
- /** @type {MapStore<PendingTxKey, PendingTx[]>} */
67
+ export const prepareStatusManager = (
68
+ zone,
69
+ txnsNode,
70
+ {
71
+ marshaller,
72
+ log = makeTracer('Advancer', true),
73
+ } = /** @type {StatusManagerPowers} */ ({}),
74
+ ) => {
75
+ /**
76
+ * Keyed by a tuple of the Noble Forwarding Account and amount.
77
+ * @type {MapStore<PendingTxKey, PendingTx[]>}
78
+ */
63
79
  const pendingTxs = zone.mapStore('PendingTxs', {
64
80
  keyShape: M.string(),
65
81
  valueShape: M.arrayOf(PendingTxShape),
66
82
  });
67
83
 
68
- /** @type {SetStore<SeenTxKey>} */
84
+ /**
85
+ * Transactions seen *ever* by the contract.
86
+ *
87
+ * Note that like all durable stores, this SetStore is stored in IAVL. It
88
+ * grows without bound (though the amount of growth per incoming message to
89
+ * the contract is bounded). At some point in the future we may want to prune.
90
+ * @type {SetStore<EvmHash>}
91
+ */
69
92
  const seenTxs = zone.setStore('SeenTxs', {
70
93
  keyShape: M.string(),
71
94
  });
72
95
 
96
+ /**
97
+ * Transactions that have completed, but are still in vstorage.
98
+ *
99
+ * @type {SetStore<EvmHash>}
100
+ */
101
+ const storedCompletedTxs = zone.setStore('StoredCompletedTxs', {
102
+ keyShape: M.string(),
103
+ });
104
+
105
+ /**
106
+ * @param {EvmHash} txId
107
+ * @param {TransactionRecord} record
108
+ * @returns {Promise<void>}
109
+ */
110
+ const publishTxnRecord = async (txId, record) => {
111
+ const txNode = E(txnsNode).makeChildNode(txId, {
112
+ sequence: true, // avoid overwriting other output in the block
113
+ });
114
+
115
+ // XXX awkward for publish* to update a store, but it's temporary
116
+ if (record.status && TerminalTxStatus[record.status]) {
117
+ // UNTIL https://github.com/Agoric/agoric-sdk/issues/7405
118
+ // Queue it for deletion later because if we deleted it now the earlier
119
+ // writes in this block would be wiped. For now we keep track of what to
120
+ // delete when we know it'll be another block.
121
+ storedCompletedTxs.add(txId);
122
+ }
123
+
124
+ const capData = await E(marshaller).toCapData(record);
125
+
126
+ await E(txNode).setValue(JSON.stringify(capData));
127
+ };
128
+
129
+ /**
130
+ * @param {CctpTxEvidence['txHash']} hash
131
+ * @param {CctpTxEvidence} evidence
132
+ */
133
+ const publishEvidence = (hash, evidence) => {
134
+ // Don't await, just writing to vstorage.
135
+ void publishTxnRecord(
136
+ hash,
137
+ harden({ evidence, status: TxStatus.Observed }),
138
+ );
139
+ };
140
+
73
141
  /**
74
142
  * Ensures that `txHash+chainId` has not been processed
75
143
  * and adds entry to `seenTxs` set.
@@ -78,39 +146,130 @@ export const prepareStatusManager = zone => {
78
146
  *
79
147
  * @param {CctpTxEvidence} evidence
80
148
  * @param {PendingTxStatus} status
149
+ * @param {string[]} [risksIdentified]
81
150
  */
82
- const recordPendingTx = (evidence, status) => {
83
- const seenKey = seenTxKeyOf(evidence);
84
- if (seenTxs.has(seenKey)) {
85
- throw makeError(`Transaction already seen: ${q(seenKey)}`);
151
+ const initPendingTx = (evidence, status, risksIdentified) => {
152
+ const { txHash } = evidence;
153
+ if (seenTxs.has(txHash)) {
154
+ throw makeError(`Transaction already seen: ${q(txHash)}`);
86
155
  }
87
- seenTxs.add(seenKey);
156
+ seenTxs.add(txHash);
88
157
 
89
158
  appendToStoredArray(
90
159
  pendingTxs,
91
160
  pendingTxKeyOf(evidence),
92
161
  harden({ ...evidence, status }),
93
162
  );
163
+ publishEvidence(txHash, evidence);
164
+ if (status === PendingTxStatus.AdvanceSkipped) {
165
+ void publishTxnRecord(txHash, harden({ status, risksIdentified }));
166
+ } else if (status !== PendingTxStatus.Observed) {
167
+ // publishEvidence publishes Observed
168
+ void publishTxnRecord(txHash, harden({ status }));
169
+ }
94
170
  };
95
171
 
172
+ /**
173
+ * Update the pending transaction status.
174
+ *
175
+ * @param {{nfa: NobleAddress, amount: bigint}} keyParts
176
+ * @param {PendingTxStatus} status
177
+ */
178
+ function setPendingTxStatus({ nfa, amount }, status) {
179
+ const key = makePendingTxKey(nfa, amount);
180
+ pendingTxs.has(key) || Fail`no advancing tx with ${{ nfa, amount }}`;
181
+ const pending = pendingTxs.get(key);
182
+ const ix = pending.findIndex(tx => tx.status === PendingTxStatus.Advancing);
183
+ ix >= 0 || Fail`no advancing tx with ${{ nfa, amount }}`;
184
+ const [prefix, tx, suffix] = [
185
+ pending.slice(0, ix),
186
+ pending[ix],
187
+ pending.slice(ix + 1),
188
+ ];
189
+ const txpost = { ...tx, status };
190
+ pendingTxs.set(key, harden([...prefix, txpost, ...suffix]));
191
+ void publishTxnRecord(tx.txHash, harden({ status }));
192
+ }
193
+
96
194
  return zone.exo(
97
195
  'Fast USDC Status Manager',
98
196
  M.interface('StatusManagerI', {
197
+ // TODO: naming scheme for transition events
99
198
  advance: M.call(CctpTxEvidenceShape).returns(M.undefined()),
199
+ advanceOutcome: M.call(M.string(), M.nat(), M.boolean()).returns(),
200
+ skipAdvance: M.call(CctpTxEvidenceShape, M.arrayOf(M.string())).returns(
201
+ M.undefined(),
202
+ ),
100
203
  observe: M.call(CctpTxEvidenceShape).returns(M.undefined()),
101
- hasPendingSettlement: M.call(M.string(), M.bigint()).returns(M.boolean()),
102
- settle: M.call(M.string(), M.bigint()).returns(M.undefined()),
204
+ hasBeenObserved: M.call(CctpTxEvidenceShape).returns(M.boolean()),
205
+ deleteCompletedTxs: M.call().returns(M.undefined()),
206
+ dequeueStatus: M.call(M.string(), M.bigint()).returns(
207
+ M.or(
208
+ {
209
+ txHash: EvmHashShape,
210
+ status: M.or(
211
+ PendingTxStatus.Advanced,
212
+ PendingTxStatus.AdvanceSkipped,
213
+ PendingTxStatus.AdvanceFailed,
214
+ PendingTxStatus.Observed,
215
+ ),
216
+ },
217
+ M.undefined(),
218
+ ),
219
+ ),
220
+ disbursed: M.call(EvmHashShape, AmountKeywordRecordShape).returns(
221
+ M.undefined(),
222
+ ),
223
+ forwarded: M.call(M.opt(EvmHashShape), M.string(), M.nat()).returns(
224
+ M.undefined(),
225
+ ),
103
226
  lookupPending: M.call(M.string(), M.bigint()).returns(
104
227
  M.arrayOf(PendingTxShape),
105
228
  ),
106
229
  }),
107
230
  {
108
231
  /**
109
- * Add a new transaction with ADVANCED status
232
+ * Add a new transaction with ADVANCING status
233
+ *
234
+ * NB: this acts like observe() but subsequently records an ADVANCING
235
+ * state
236
+ *
110
237
  * @param {CctpTxEvidence} evidence
111
238
  */
112
239
  advance(evidence) {
113
- recordPendingTx(evidence, PendingTxStatus.Advanced);
240
+ initPendingTx(evidence, PendingTxStatus.Advancing);
241
+ },
242
+
243
+ /**
244
+ * Add a new transaction with ADVANCE_SKIPPED status
245
+ *
246
+ * NB: this acts like observe() but subsequently records an
247
+ * ADVANCE_SKIPPED state along with risks identified
248
+ *
249
+ * @param {CctpTxEvidence} evidence
250
+ * @param {string[]} risksIdentified
251
+ */
252
+ skipAdvance(evidence, risksIdentified) {
253
+ initPendingTx(
254
+ evidence,
255
+ PendingTxStatus.AdvanceSkipped,
256
+ risksIdentified,
257
+ );
258
+ },
259
+
260
+ /**
261
+ * Record result of ADVANCING
262
+ *
263
+ * @param {NobleAddress} nfa Noble Forwarding Account
264
+ * @param {import('@agoric/ertp').NatValue} amount
265
+ * @param {boolean} success - Advanced vs. AdvanceFailed
266
+ * @throws {Error} if nothing to advance
267
+ */
268
+ advanceOutcome(nfa, amount, success) {
269
+ setPendingTxStatus(
270
+ { nfa, amount },
271
+ success ? PendingTxStatus.Advanced : PendingTxStatus.AdvanceFailed,
272
+ );
114
273
  },
115
274
 
116
275
  /**
@@ -118,53 +277,107 @@ export const prepareStatusManager = zone => {
118
277
  * @param {CctpTxEvidence} evidence
119
278
  */
120
279
  observe(evidence) {
121
- recordPendingTx(evidence, PendingTxStatus.Observed);
280
+ initPendingTx(evidence, PendingTxStatus.Observed);
122
281
  },
123
282
 
124
283
  /**
125
- * Find an `ADVANCED` or `OBSERVED` tx waiting to be `SETTLED`
284
+ * Note: ADVANCING state implies tx has been OBSERVED
126
285
  *
127
- * @param {NobleAddress} address
128
- * @param {bigint} amount
129
- * @returns {boolean}
286
+ * @param {CctpTxEvidence} evidence
130
287
  */
131
- hasPendingSettlement(address, amount) {
132
- const key = makePendingTxKey(address, amount);
133
- const pending = pendingTxs.get(key);
134
- return !!pending.length;
288
+ hasBeenObserved(evidence) {
289
+ return seenTxs.has(evidence.txHash);
290
+ },
291
+
292
+ // UNTIL https://github.com/Agoric/agoric-sdk/issues/7405
293
+ deleteCompletedTxs() {
294
+ for (const txHash of storedCompletedTxs.values()) {
295
+ // As of now, setValue('') on a non-sequence node will delete it
296
+ const txNode = E(txnsNode).makeChildNode(txHash, {
297
+ sequence: false,
298
+ });
299
+ void E(txNode)
300
+ .setValue('')
301
+ .then(() => storedCompletedTxs.delete(txHash));
302
+ }
135
303
  },
136
304
 
137
305
  /**
138
- * Mark an `ADVANCED` or `OBSERVED` transaction as `SETTLED` and remove it
306
+ * Remove and return an `ADVANCED` or `OBSERVED` tx waiting to be `SETTLED`.
139
307
  *
140
- * @param {NobleAddress} address
308
+ * @param {NobleAddress} nfa
141
309
  * @param {bigint} amount
310
+ * @returns {Pick<PendingTx, 'status' | 'txHash'> | undefined} undefined if nothing
311
+ * with this address and amount has been marked pending.
142
312
  */
143
- settle(address, amount) {
144
- const key = makePendingTxKey(address, amount);
313
+ dequeueStatus(nfa, amount) {
314
+ const key = makePendingTxKey(nfa, amount);
315
+ if (!pendingTxs.has(key)) return undefined;
145
316
  const pending = pendingTxs.get(key);
146
317
 
147
- if (!pending.length) {
148
- throw makeError(`No unsettled entry for ${q(key)}`);
318
+ const dequeueIdx = pending.findIndex(
319
+ x => x.status !== PendingTxStatus.Advancing,
320
+ );
321
+ if (dequeueIdx < 0) return undefined;
322
+
323
+ if (pending.length > 1) {
324
+ const pendingCopy = [...pending];
325
+ pendingCopy.splice(dequeueIdx, 1);
326
+ pendingTxs.set(key, harden(pendingCopy));
327
+ } else {
328
+ pendingTxs.delete(key);
149
329
  }
150
330
 
151
- const pendingCopy = [...pending];
152
- pendingCopy.shift();
153
- // TODO, vstorage update for `TxStatus.Settled`
154
- pendingTxs.set(key, harden(pendingCopy));
331
+ const { status, txHash } = pending[dequeueIdx];
332
+ // TODO: store txHash -> evidence for txs pending settlement?
333
+ // If necessary for vstorage writes in `forwarded` and `settled`
334
+ return harden({ status, txHash });
335
+ },
336
+
337
+ /**
338
+ * Mark a transaction as `DISBURSED`
339
+ *
340
+ * @param {EvmHash} txHash
341
+ * @param {import('./liquidity-pool.js').RepayAmountKWR} split
342
+ */
343
+ disbursed(txHash, split) {
344
+ void publishTxnRecord(
345
+ txHash,
346
+ harden({ split, status: TxStatus.Disbursed }),
347
+ );
348
+ },
349
+
350
+ /**
351
+ * Mark a transaction as `FORWARDED`
352
+ *
353
+ * @param {EvmHash | undefined} txHash - undefined in case mint before observed
354
+ * @param {NobleAddress} nfa
355
+ * @param {bigint} amount
356
+ */
357
+ forwarded(txHash, nfa, amount) {
358
+ if (txHash) {
359
+ void publishTxnRecord(txHash, harden({ status: TxStatus.Forwarded }));
360
+ } else {
361
+ // TODO store (early) `Minted` transactions to check against incoming evidence
362
+ log(
363
+ `⚠️ Forwarded minted amount ${amount} from account ${nfa} before it was observed.`,
364
+ );
365
+ }
155
366
  },
156
367
 
157
368
  /**
158
369
  * Lookup all pending entries for a given address and amount
159
370
  *
160
- * @param {NobleAddress} address
371
+ * XXX only used in tests. should we remove?
372
+ *
373
+ * @param {NobleAddress} nfa
161
374
  * @param {bigint} amount
162
375
  * @returns {PendingTx[]}
163
376
  */
164
- lookupPending(address, amount) {
165
- const key = makePendingTxKey(address, amount);
377
+ lookupPending(nfa, amount) {
378
+ const key = makePendingTxKey(nfa, amount);
166
379
  if (!pendingTxs.has(key)) {
167
- throw makeError(`Key ${q(key)} not yet observed`);
380
+ return harden([]);
168
381
  }
169
382
  return pendingTxs.get(key);
170
383
  },
@@ -1,14 +1,16 @@
1
1
  import { makeTracer } from '@agoric/internal';
2
2
  import { prepareDurablePublishKit } from '@agoric/notifier';
3
- import { M } from '@endo/patterns';
4
- import { CctpTxEvidenceShape } from '../type-guards.js';
3
+ import { keyEQ, M } from '@endo/patterns';
4
+ import { Fail } from '@endo/errors';
5
+ import { CctpTxEvidenceShape, RiskAssessmentShape } from '../type-guards.js';
5
6
  import { defineInertInvitation } from '../utils/zoe.js';
6
7
  import { prepareOperatorKit } from './operator-kit.js';
7
8
 
8
9
  /**
9
10
  * @import {Zone} from '@agoric/zone';
11
+ * @import {MapStore} from '@agoric/store';
10
12
  * @import {OperatorKit} from './operator-kit.js';
11
- * @import {CctpTxEvidence} from '../types.js';
13
+ * @import {CctpTxEvidence, EvidenceWithRisk, RiskAssessment} from '../types.js';
12
14
  */
13
15
 
14
16
  const trace = makeTracer('TxFeed', true);
@@ -18,7 +20,11 @@ export const INVITATION_MAKERS_DESC = 'oracle operator invitation';
18
20
 
19
21
  const TransactionFeedKitI = harden({
20
22
  operatorPowers: M.interface('Transaction Feed Admin', {
21
- submitEvidence: M.call(CctpTxEvidenceShape, M.any()).returns(),
23
+ attest: M.call(
24
+ CctpTxEvidenceShape,
25
+ RiskAssessmentShape,
26
+ M.string(),
27
+ ).returns(),
22
28
  }),
23
29
  creator: M.interface('Transaction Feed Creator', {
24
30
  // TODO narrow the return shape to OperatorKit
@@ -31,6 +37,22 @@ const TransactionFeedKitI = harden({
31
37
  }),
32
38
  });
33
39
 
40
+ /**
41
+ * @param {MapStore<string, RiskAssessment>[]} riskStores
42
+ * @param {string} txHash
43
+ */
44
+ const allRisksIdentified = (riskStores, txHash) => {
45
+ /** @type {Set<string>} */
46
+ const setOfRisks = new Set();
47
+ for (const store of riskStores) {
48
+ const next = store.get(txHash);
49
+ for (const risk of next.risksIdentified ?? []) {
50
+ setOfRisks.add(risk);
51
+ }
52
+ }
53
+ return [...setOfRisks.values()].sort();
54
+ };
55
+
34
56
  /**
35
57
  * @param {Zone} zone
36
58
  * @param {ZCF} zcf
@@ -41,7 +63,7 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
41
63
  kinds,
42
64
  'Transaction Feed',
43
65
  );
44
- /** @type {PublishKit<CctpTxEvidence>} */
66
+ /** @type {PublishKit<EvidenceWithRisk>} */
45
67
  const { publisher, subscriber } = makeDurablePublishKit();
46
68
 
47
69
  const makeInertInvitation = defineInertInvitation(zcf, 'submitting evidence');
@@ -55,14 +77,12 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
55
77
  TransactionFeedKitI,
56
78
  () => {
57
79
  /** @type {MapStore<string, OperatorKit>} */
58
- const operators = zone.mapStore('operators', {
59
- durable: true,
60
- });
80
+ const operators = zone.mapStore('operators');
61
81
  /** @type {MapStore<string, MapStore<string, CctpTxEvidence>>} */
62
- const pending = zone.mapStore('pending', {
63
- durable: true,
64
- });
65
- return { operators, pending };
82
+ const pending = zone.mapStore('pending');
83
+ /** @type {MapStore<string, MapStore<string, RiskAssessment>>} */
84
+ const risks = zone.mapStore('risks');
85
+ return { operators, pending, risks };
66
86
  },
67
87
  {
68
88
  creator: {
@@ -89,7 +109,7 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
89
109
  },
90
110
  /** @param {string} operatorId */
91
111
  initOperator(operatorId) {
92
- const { operators, pending } = this.state;
112
+ const { operators, pending, risks } = this.state;
93
113
  trace('initOperator', operatorId);
94
114
 
95
115
  const operatorKit = makeOperatorKit(
@@ -101,6 +121,7 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
101
121
  operatorId,
102
122
  zone.detached().mapStore('pending evidence'),
103
123
  );
124
+ risks.init(operatorId, zone.detached().mapStore('risk assessments'));
104
125
 
105
126
  return operatorKit;
106
127
  },
@@ -118,23 +139,17 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
118
139
  /**
119
140
  * Add evidence from an operator.
120
141
  *
142
+ * NB: the operatorKit is responsible for
143
+ *
121
144
  * @param {CctpTxEvidence} evidence
122
- * @param {OperatorKit} operatorKit
145
+ * @param {RiskAssessment} riskAssessment
146
+ * @param {string} operatorId
123
147
  */
124
- submitEvidence(evidence, operatorKit) {
125
- const { pending } = this.state;
126
- trace(
127
- 'submitEvidence',
128
- operatorKit.operator.getStatus().operatorId,
129
- evidence,
130
- );
131
- const { operatorId } = operatorKit.operator.getStatus();
132
-
133
- // TODO should this verify that the operator is one made by this exo?
134
- // This doesn't work...
135
- // operatorKit === operators.get(operatorId) ||
136
- // Fail`operatorKit mismatch`;
148
+ attest(evidence, riskAssessment, operatorId) {
149
+ const { operators, pending, risks } = this.state;
150
+ trace('attest', operatorId, evidence);
137
151
 
152
+ // TODO https://github.com/Agoric/agoric-sdk/pull/10720
138
153
  // TODO validate that it's a valid for Fast USDC before accepting
139
154
  // E.g. that the `recipientAddress` is the FU settlement account and that
140
155
  // the EUD is a chain supported by FU.
@@ -147,6 +162,9 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
147
162
  trace(`operator ${operatorId} already reported ${txHash}`);
148
163
  } else {
149
164
  pendingStore.init(txHash, evidence);
165
+ // accept the risk assessment as well
166
+ const riskStore = risks.get(operatorId);
167
+ riskStore.init(txHash, riskAssessment);
150
168
  }
151
169
  }
152
170
 
@@ -154,19 +172,59 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
154
172
  const found = [...pending.values()].filter(store =>
155
173
  store.has(txHash),
156
174
  );
157
- // TODO determine the real policy for checking agreement
158
- if (found.length < pending.getSize()) {
159
- // not all have seen it
175
+ const minAttestations = Math.ceil(operators.getSize() / 2);
176
+ trace(
177
+ 'transaction',
178
+ txHash,
179
+ 'has',
180
+ found.length,
181
+ 'of',
182
+ minAttestations,
183
+ 'necessary attestations',
184
+ );
185
+ if (found.length < minAttestations) {
160
186
  return;
161
187
  }
162
188
 
163
- // TODO verify that all found deep equal
189
+ let lastEvidence;
190
+ for (const store of found) {
191
+ const next = store.get(txHash);
192
+ if (lastEvidence) {
193
+ if (keyEQ(lastEvidence, next)) {
194
+ lastEvidence = next;
195
+ } else {
196
+ trace(
197
+ '🚨 conflicting evidence for',
198
+ txHash,
199
+ ':',
200
+ lastEvidence,
201
+ '!=',
202
+ next,
203
+ );
204
+ Fail`conflicting evidence for ${txHash}`;
205
+ }
206
+ }
207
+ lastEvidence = next;
208
+ }
164
209
 
165
- // all agree, so remove from pending and publish
166
- for (const pendingStore of pending.values()) {
167
- pendingStore.delete(txHash);
210
+ const riskStores = [...risks.values()].filter(store =>
211
+ store.has(txHash),
212
+ );
213
+ // take the union of risks identified from all operators
214
+ const risksIdentified = allRisksIdentified(riskStores, txHash);
215
+
216
+ // sufficient agreement, so remove from pending risks, then publish
217
+ for (const store of found) {
218
+ store.delete(txHash);
168
219
  }
169
- publisher.publish(evidence);
220
+ for (const store of riskStores) {
221
+ store.delete(txHash);
222
+ }
223
+ trace('publishing evidence', evidence, risksIdentified);
224
+ publisher.publish({
225
+ evidence,
226
+ risk: { risksIdentified },
227
+ });
170
228
  },
171
229
  },
172
230
  public: {