diff --git a/packages/fast-usdc/src/exos/transaction-feed.js b/packages/fast-usdc/src/exos/transaction-feed.js index 3d150f5db42..231ef4f828d 100644 --- a/packages/fast-usdc/src/exos/transaction-feed.js +++ b/packages/fast-usdc/src/exos/transaction-feed.js @@ -1,6 +1,7 @@ import { makeTracer } from '@agoric/internal'; import { prepareDurablePublishKit } from '@agoric/notifier'; -import { M } from '@endo/patterns'; +import { keyEQ, M } from '@endo/patterns'; +import { Fail } from '@endo/errors'; import { CctpTxEvidenceShape } from '../type-guards.js'; import { defineInertInvitation } from '../utils/zoe.js'; import { prepareOperatorKit } from './operator-kit.js'; @@ -127,6 +128,7 @@ export const prepareTransactionFeedKit = (zone, zcf) => { const { operators, pending } = this.state; trace('submitEvidence', operatorId, evidence); + // TODO https://github.com/Agoric/agoric-sdk/pull/10720 // TODO validate that it's a valid for Fast USDC before accepting // E.g. that the `recipientAddress` is the FU settlement account and that // the EUD is a chain supported by FU. @@ -160,7 +162,26 @@ export const prepareTransactionFeedKit = (zone, zcf) => { return; } - // TODO verify that all found deep equal + let lastEvidence; + for (const store of found) { + const next = store.get(txHash); + if (lastEvidence) { + if (keyEQ(lastEvidence, next)) { + lastEvidence = next; + } else { + trace( + '🚨 conflicting evidence for', + txHash, + ':', + lastEvidence, + '!=', + next, + ); + Fail`conflicting evidence for ${txHash}`; + } + } + lastEvidence = next; + } // sufficient agreement, so remove from pending and publish for (const store of found) { diff --git a/packages/fast-usdc/test/exos/transaction-feed.test.ts b/packages/fast-usdc/test/exos/transaction-feed.test.ts index 5c5b74e094b..8f5084af0ea 100644 --- a/packages/fast-usdc/test/exos/transaction-feed.test.ts +++ b/packages/fast-usdc/test/exos/transaction-feed.test.ts @@ -47,6 +47,7 @@ test('happy aggregation', async t => { const evidenceSubscriber = feedKit.public.getEvidenceSubscriber(); const { op1, op2, op3 } = await makeOperators(feedKit); + const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); op1.operator.submitEvidence(e1); op2.operator.submitEvidence(e1); @@ -74,6 +75,48 @@ test('happy aggregation', async t => { }); }); +test('disagreement', async t => { + const feedKit = makeFeedKit(); + const { op1, op2 } = await makeOperators(feedKit); + const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); + const e1bad = { ...e1, tx: { ...e1.tx, amount: 999_999_999n } }; + assert(e1.txHash === e1bad.txHash); + op1.operator.submitEvidence(e1); + + t.throws(() => op2.operator.submitEvidence(e1bad), { + message: + 'conflicting evidence for "0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702"', + }); +}); + +test('disagreement after publishing', async t => { + const feedKit = makeFeedKit(); + const evidenceSubscriber = feedKit.public.getEvidenceSubscriber(); + const { op1, op2, op3 } = await makeOperators(feedKit); + const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); + const e1bad = { ...e1, tx: { ...e1.tx, amount: 999_999_999n } }; + assert(e1.txHash === e1bad.txHash); + op1.operator.submitEvidence(e1); + op2.operator.submitEvidence(e1); + + t.like(await evidenceSubscriber.getUpdateSince(0), { + updateCount: 1n, + }); + + // it's simply ignored + t.notThrows(() => op3.operator.submitEvidence(e1bad)); + t.like(await evidenceSubscriber.getUpdateSince(0), { + updateCount: 1n, + }); + + // now another op repeats the bad evidence, so it's published to the stream. + // It's the responsibility of the Advancer to fail because it has already processed that tx hash. + op1.operator.submitEvidence(e1bad); + t.like(await evidenceSubscriber.getUpdateSince(0), { + updateCount: 2n, + }); +}); + test('disabled operator', async t => { const feedKit = makeFeedKit(); const { op1 } = await makeOperators(feedKit);