From caf6d7ac9334672ec9d9d5246bd231ee2a900e0f Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 17 Dec 2024 10:59:30 -0800 Subject: [PATCH 1/7] chore: constrain fast-usdc config for Mainnet --- packages/fast-usdc/src/utils/deploy-config.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/fast-usdc/src/utils/deploy-config.js b/packages/fast-usdc/src/utils/deploy-config.js index 7b160a7996c..22b2cd5a593 100644 --- a/packages/fast-usdc/src/utils/deploy-config.js +++ b/packages/fast-usdc/src/utils/deploy-config.js @@ -156,3 +156,11 @@ export const configurations = { }, }; harden(configurations); + +// Constraints on the configurations +const MAINNET_EXPECTED_ORACLES = 3; +assert( + new Set(Object.values(configurations.MAINNET.oracles)).size === + MAINNET_EXPECTED_ORACLES, + `Mainnet must have exactly ${MAINNET_EXPECTED_ORACLES} oracles`, +); From 621e22fdfe01706e22a3ebb81164b372ef9c80e0 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 17 Dec 2024 11:10:42 -0800 Subject: [PATCH 2/7] test: disabled operators --- .../fast-usdc/test/exos/transaction-feed.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/fast-usdc/test/exos/transaction-feed.test.ts b/packages/fast-usdc/test/exos/transaction-feed.test.ts index 5f11c504dc2..ff3f2705aa6 100644 --- a/packages/fast-usdc/test/exos/transaction-feed.test.ts +++ b/packages/fast-usdc/test/exos/transaction-feed.test.ts @@ -79,6 +79,21 @@ test('happy aggregation', async t => { }); }); +test('disabled operator', async t => { + const feedKit = makeFeedKit(); + const { op1 } = await makeOperators(feedKit); + const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); + + // works before disabling + await op1.operator.submitEvidence(evidence); + + op1.admin.disable(); + + await t.throwsAsync(() => op1.operator.submitEvidence(evidence), { + message: 'submitEvidence for disabled operator', + }); +}); + // TODO: find a way to get this working test.skip('forged source', async t => { const feedKit = makeFeedKit(); From 0b57a6559fe627c70767b0a6d4e687cf9f1deaca Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 17 Dec 2024 11:18:27 -0800 Subject: [PATCH 3/7] refactor: TransactionFeed attest --- packages/fast-usdc/src/exos/operator-kit.js | 4 ++-- packages/fast-usdc/src/exos/transaction-feed.js | 6 ++++-- packages/fast-usdc/test/exos/transaction-feed.test.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/fast-usdc/src/exos/operator-kit.js b/packages/fast-usdc/src/exos/operator-kit.js index 08b3ac9c368..b5e847f30d5 100644 --- a/packages/fast-usdc/src/exos/operator-kit.js +++ b/packages/fast-usdc/src/exos/operator-kit.js @@ -12,7 +12,7 @@ const trace = makeTracer('TxOperator'); /** * @typedef {object} OperatorPowers - * @property {(evidence: CctpTxEvidence, operatorKit: OperatorKit) => void} submitEvidence + * @property {(evidence: CctpTxEvidence, operatorKit: OperatorKit) => void} attest */ /** @@ -102,7 +102,7 @@ export const prepareOperatorKit = (zone, staticPowers) => async submitEvidence(evidence) { const { state } = this; !state.disabled || Fail`submitEvidence for disabled operator`; - const result = state.powers.submitEvidence(evidence, this.facets); + const result = state.powers.attest(evidence, this.facets); return result; }, /** @returns {OperatorStatus} */ diff --git a/packages/fast-usdc/src/exos/transaction-feed.js b/packages/fast-usdc/src/exos/transaction-feed.js index 782236d615f..40d456ef9ea 100644 --- a/packages/fast-usdc/src/exos/transaction-feed.js +++ b/packages/fast-usdc/src/exos/transaction-feed.js @@ -18,7 +18,7 @@ export const INVITATION_MAKERS_DESC = 'oracle operator invitation'; const TransactionFeedKitI = harden({ operatorPowers: M.interface('Transaction Feed Admin', { - submitEvidence: M.call(CctpTxEvidenceShape, M.any()).returns(), + attest: M.call(CctpTxEvidenceShape, M.any()).returns(), }), creator: M.interface('Transaction Feed Creator', { // TODO narrow the return shape to OperatorKit @@ -118,10 +118,12 @@ export const prepareTransactionFeedKit = (zone, zcf) => { /** * Add evidence from an operator. * + * NB: the operatorKit is responsible for + * * @param {CctpTxEvidence} evidence * @param {OperatorKit} operatorKit */ - submitEvidence(evidence, operatorKit) { + attest(evidence, operatorKit) { const { pending } = this.state; trace( 'submitEvidence', diff --git a/packages/fast-usdc/test/exos/transaction-feed.test.ts b/packages/fast-usdc/test/exos/transaction-feed.test.ts index ff3f2705aa6..64c6f8c34a1 100644 --- a/packages/fast-usdc/test/exos/transaction-feed.test.ts +++ b/packages/fast-usdc/test/exos/transaction-feed.test.ts @@ -102,7 +102,7 @@ test.skip('forged source', async t => { // op1 is different than the facets object the evidence must come from t.throws(() => - feedKit.operatorPowers.submitEvidence( + feedKit.operatorPowers.attest( evidence, // @ts-expect-error XXX Types of property '[GET_INTERFACE_GUARD]' are incompatible. op1, From 1e52a415b7d9fc6a446263b4a8c22d5eac1fd28e Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 17 Dec 2024 11:41:05 -0800 Subject: [PATCH 4/7] refactor: sync submitEvidence --- packages/fast-usdc/src/exos/operator-kit.js | 10 +++---- .../test/exos/transaction-feed.test.ts | 28 ++++++++----------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/fast-usdc/src/exos/operator-kit.js b/packages/fast-usdc/src/exos/operator-kit.js index b5e847f30d5..310b10ad0a9 100644 --- a/packages/fast-usdc/src/exos/operator-kit.js +++ b/packages/fast-usdc/src/exos/operator-kit.js @@ -35,7 +35,7 @@ const OperatorKitI = { }), operator: M.interface('Operator', { - submitEvidence: M.call(CctpTxEvidenceShape).returns(M.promise()), + submitEvidence: M.call(CctpTxEvidenceShape).returns(), getStatus: M.call().returns(M.record()), }), }; @@ -87,7 +87,7 @@ export const prepareOperatorKit = (zone, staticPowers) => const { operator } = this.facets; // TODO(bootstrap integration): cause this call to throw and confirm that it // shows up in the the smart-wallet UpdateRecord `error` property - await operator.submitEvidence(evidence); + operator.submitEvidence(evidence); return staticPowers.makeInertInvitation( 'evidence was pushed in the invitation maker call', ); @@ -98,12 +98,12 @@ export const prepareOperatorKit = (zone, staticPowers) => * submit evidence from this operator * * @param {CctpTxEvidence} evidence + * @returns {void} */ - async submitEvidence(evidence) { + submitEvidence(evidence) { const { state } = this; !state.disabled || Fail`submitEvidence for disabled operator`; - const result = state.powers.attest(evidence, this.facets); - return result; + state.powers.attest(evidence, this.facets); }, /** @returns {OperatorStatus} */ getStatus() { diff --git a/packages/fast-usdc/test/exos/transaction-feed.test.ts b/packages/fast-usdc/test/exos/transaction-feed.test.ts index 64c6f8c34a1..9f1c902aaae 100644 --- a/packages/fast-usdc/test/exos/transaction-feed.test.ts +++ b/packages/fast-usdc/test/exos/transaction-feed.test.ts @@ -48,12 +48,9 @@ test('happy aggregation', async t => { const { op1, op2, op3 } = await makeOperators(feedKit); const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); - const results = await Promise.all([ - op1.operator.submitEvidence(evidence), - op2.operator.submitEvidence(evidence), - op3.operator.submitEvidence(evidence), - ]); - t.deepEqual(results, [undefined, undefined, undefined]); + op1.operator.submitEvidence(evidence); + op2.operator.submitEvidence(evidence); + op3.operator.submitEvidence(evidence); const accepted = await evidenceSubscriber.getUpdateSince(0); t.deepEqual(accepted, { @@ -62,18 +59,17 @@ test('happy aggregation', async t => { }); // verify that it doesn't publish until three match - await Promise.all([ - // once it publishes, it doesn't remember that it already saw these - op1.operator.submitEvidence(evidence), - op2.operator.submitEvidence(evidence), - // but this time the third is different - op3.operator.submitEvidence(MockCctpTxEvidences.AGORIC_PLUS_DYDX()), - ]); + // once it publishes, it doesn't remember that it already saw these + op1.operator.submitEvidence(evidence); + op2.operator.submitEvidence(evidence); + // but this time the third is different + op3.operator.submitEvidence(MockCctpTxEvidences.AGORIC_PLUS_DYDX()); + t.like(await evidenceSubscriber.getUpdateSince(0), { // Update count is still 1 updateCount: 1n, }); - await op3.operator.submitEvidence(evidence); + op3.operator.submitEvidence(evidence); t.like(await evidenceSubscriber.getUpdateSince(0), { updateCount: 2n, }); @@ -85,11 +81,11 @@ test('disabled operator', async t => { const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); // works before disabling - await op1.operator.submitEvidence(evidence); + op1.operator.submitEvidence(evidence); op1.admin.disable(); - await t.throwsAsync(() => op1.operator.submitEvidence(evidence), { + t.throws(() => op1.operator.submitEvidence(evidence), { message: 'submitEvidence for disabled operator', }); }); From fb4ff309bdc2ebb15a1c6632db231d7f378f11f6 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 17 Dec 2024 11:51:16 -0800 Subject: [PATCH 5/7] refactor: attest by operatorId --- packages/fast-usdc/src/exos/operator-kit.js | 4 ++-- .../fast-usdc/src/exos/transaction-feed.js | 18 ++++-------------- .../test/exos/transaction-feed.test.ts | 16 ---------------- 3 files changed, 6 insertions(+), 32 deletions(-) diff --git a/packages/fast-usdc/src/exos/operator-kit.js b/packages/fast-usdc/src/exos/operator-kit.js index 310b10ad0a9..7a98af3da5a 100644 --- a/packages/fast-usdc/src/exos/operator-kit.js +++ b/packages/fast-usdc/src/exos/operator-kit.js @@ -12,7 +12,7 @@ const trace = makeTracer('TxOperator'); /** * @typedef {object} OperatorPowers - * @property {(evidence: CctpTxEvidence, operatorKit: OperatorKit) => void} attest + * @property {(evidence: CctpTxEvidence, operatorId: string) => void} attest */ /** @@ -103,7 +103,7 @@ export const prepareOperatorKit = (zone, staticPowers) => submitEvidence(evidence) { const { state } = this; !state.disabled || Fail`submitEvidence for disabled operator`; - state.powers.attest(evidence, this.facets); + state.powers.attest(evidence, state.operatorId); }, /** @returns {OperatorStatus} */ getStatus() { diff --git a/packages/fast-usdc/src/exos/transaction-feed.js b/packages/fast-usdc/src/exos/transaction-feed.js index 40d456ef9ea..b991c570022 100644 --- a/packages/fast-usdc/src/exos/transaction-feed.js +++ b/packages/fast-usdc/src/exos/transaction-feed.js @@ -18,7 +18,7 @@ export const INVITATION_MAKERS_DESC = 'oracle operator invitation'; const TransactionFeedKitI = harden({ operatorPowers: M.interface('Transaction Feed Admin', { - attest: M.call(CctpTxEvidenceShape, M.any()).returns(), + attest: M.call(CctpTxEvidenceShape, M.string()).returns(), }), creator: M.interface('Transaction Feed Creator', { // TODO narrow the return shape to OperatorKit @@ -121,21 +121,11 @@ export const prepareTransactionFeedKit = (zone, zcf) => { * NB: the operatorKit is responsible for * * @param {CctpTxEvidence} evidence - * @param {OperatorKit} operatorKit + * @param {string} operatorId */ - attest(evidence, operatorKit) { + attest(evidence, operatorId) { const { pending } = this.state; - trace( - 'submitEvidence', - operatorKit.operator.getStatus().operatorId, - evidence, - ); - const { operatorId } = operatorKit.operator.getStatus(); - - // TODO should this verify that the operator is one made by this exo? - // This doesn't work... - // operatorKit === operators.get(operatorId) || - // Fail`operatorKit mismatch`; + trace('submitEvidence', operatorId, evidence); // TODO validate that it's a valid for Fast USDC before accepting // E.g. that the `recipientAddress` is the FU settlement account and that diff --git a/packages/fast-usdc/test/exos/transaction-feed.test.ts b/packages/fast-usdc/test/exos/transaction-feed.test.ts index 9f1c902aaae..8e3b04f9271 100644 --- a/packages/fast-usdc/test/exos/transaction-feed.test.ts +++ b/packages/fast-usdc/test/exos/transaction-feed.test.ts @@ -89,19 +89,3 @@ test('disabled operator', async t => { message: 'submitEvidence for disabled operator', }); }); - -// TODO: find a way to get this working -test.skip('forged source', async t => { - const feedKit = makeFeedKit(); - const { op1 } = await makeOperators(feedKit); - const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); - - // op1 is different than the facets object the evidence must come from - t.throws(() => - feedKit.operatorPowers.attest( - evidence, - // @ts-expect-error XXX Types of property '[GET_INTERFACE_GUARD]' are incompatible. - op1, - ), - ); -}); From bc28201f60978263d4c88375130da15128f8fd5c Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 17 Dec 2024 12:46:18 -0800 Subject: [PATCH 6/7] feat: operator majority logic --- .../fast-usdc/src/exos/transaction-feed.js | 23 +++++++---- .../test/exos/transaction-feed.test.ts | 29 +++++++------ .../fast-usdc/test/fast-usdc.contract.test.ts | 41 +++++++++++-------- 3 files changed, 55 insertions(+), 38 deletions(-) diff --git a/packages/fast-usdc/src/exos/transaction-feed.js b/packages/fast-usdc/src/exos/transaction-feed.js index b991c570022..3d150f5db42 100644 --- a/packages/fast-usdc/src/exos/transaction-feed.js +++ b/packages/fast-usdc/src/exos/transaction-feed.js @@ -124,7 +124,7 @@ export const prepareTransactionFeedKit = (zone, zcf) => { * @param {string} operatorId */ attest(evidence, operatorId) { - const { pending } = this.state; + const { operators, pending } = this.state; trace('submitEvidence', operatorId, evidence); // TODO validate that it's a valid for Fast USDC before accepting @@ -146,18 +146,27 @@ export const prepareTransactionFeedKit = (zone, zcf) => { const found = [...pending.values()].filter(store => store.has(txHash), ); - // TODO determine the real policy for checking agreement - if (found.length < pending.getSize()) { - // not all have seen it + const minAttestations = Math.ceil(operators.getSize() / 2); + trace( + 'transaction', + txHash, + 'has', + found.length, + 'of', + minAttestations, + 'necessary attestations', + ); + if (found.length < minAttestations) { return; } // TODO verify that all found deep equal - // all agree, so remove from pending and publish - for (const pendingStore of pending.values()) { - pendingStore.delete(txHash); + // sufficient agreement, so remove from pending and publish + for (const store of found) { + store.delete(txHash); } + trace('publishing evidence', evidence); publisher.publish(evidence); }, }, diff --git a/packages/fast-usdc/test/exos/transaction-feed.test.ts b/packages/fast-usdc/test/exos/transaction-feed.test.ts index 8e3b04f9271..5c5b74e094b 100644 --- a/packages/fast-usdc/test/exos/transaction-feed.test.ts +++ b/packages/fast-usdc/test/exos/transaction-feed.test.ts @@ -47,31 +47,30 @@ test('happy aggregation', async t => { const evidenceSubscriber = feedKit.public.getEvidenceSubscriber(); const { op1, op2, op3 } = await makeOperators(feedKit); - const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); - op1.operator.submitEvidence(evidence); - op2.operator.submitEvidence(evidence); - op3.operator.submitEvidence(evidence); + const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); + op1.operator.submitEvidence(e1); + op2.operator.submitEvidence(e1); + // Publishes with 2 of 3 const accepted = await evidenceSubscriber.getUpdateSince(0); t.deepEqual(accepted, { - value: evidence, + value: e1, updateCount: 1n, }); - // verify that it doesn't publish until three match - // once it publishes, it doesn't remember that it already saw these - op1.operator.submitEvidence(evidence); - op2.operator.submitEvidence(evidence); - // but this time the third is different - op3.operator.submitEvidence(MockCctpTxEvidences.AGORIC_PLUS_DYDX()); - + // Now third operator catches up with same evidence already published + op3.operator.submitEvidence(e1); t.like(await evidenceSubscriber.getUpdateSince(0), { - // Update count is still 1 + // The confirming evidence doesn't change anything updateCount: 1n, }); - op3.operator.submitEvidence(evidence); + + const e2 = MockCctpTxEvidences.AGORIC_PLUS_DYDX(); + assert(e1.txHash !== e2.txHash); + op1.operator.submitEvidence(e2); t.like(await evidenceSubscriber.getUpdateSince(0), { - updateCount: 2n, + // op1 attestation insufficient + updateCount: 1n, }); }); diff --git a/packages/fast-usdc/test/fast-usdc.contract.test.ts b/packages/fast-usdc/test/fast-usdc.contract.test.ts index f7869f3def1..bc73389a5b9 100644 --- a/packages/fast-usdc/test/fast-usdc.contract.test.ts +++ b/packages/fast-usdc/test/fast-usdc.contract.test.ts @@ -1,8 +1,11 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import type { ExecutionContext, TestFn } from 'ava'; +import { + decodeAddressHook, + encodeAddressHook, +} from '@agoric/cosmic-proto/address-hooks.js'; import { AmountMath } from '@agoric/ertp/src/amountMath.js'; -import { deeplyFulfilledObject } from '@agoric/internal'; import { eventLoopIteration, inspectMapStore, @@ -24,13 +27,9 @@ import { import type { Instance } from '@agoric/zoe/src/zoeService/utils.js'; import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; import { E } from '@endo/far'; -import { matches, objectMap } from '@endo/patterns'; +import { matches } from '@endo/patterns'; import { makePromiseKit } from '@endo/promise-kit'; import path from 'path'; -import { - decodeAddressHook, - encodeAddressHook, -} from '@agoric/cosmic-proto/address-hooks.js'; import type { OperatorKit } from '../src/exos/operator-kit.js'; import type { FastUsdcSF } from '../src/fast-usdc.contract.js'; import { PoolMetricsShape } from '../src/type-guards.js'; @@ -56,10 +55,12 @@ const getInvitationProperties = async ( return amount.value[0]; }; +// Spec for Mainnet. Other values are covered in unit tests of TransactionFeed. +const operatorQty = 3; + type CommonSetup = Awaited>; const startContract = async ( common: Pick, - operatorQty = 1, ) => { const { brands: { usdc }, @@ -104,7 +105,7 @@ const makeTestContext = async (t: ExecutionContext) => { const common = await commonSetup(t); await E(common.mocks.ibcBridge).setAddressPrefix('noble'); - const startKit = await startContract(common, 2); + const startKit = await startContract(common); const { transferBridge } = common.mocks; const evm = makeEVM(); @@ -227,12 +228,17 @@ const makeOracleOperator = async ( ]); const { invitationMakers } = operatorKit; + let active = true; + return harden({ watch: () => { void observeIteration(subscribeEach(txSubscriber), { - updateState: tx => + updateState: tx => { + if (!active) { + return; + } // KLUDGE: tx wouldn't include aux. OCW looks it up - E.when( + return E.when( E(invitationMakers).SubmitEvidence(tx), inv => E.when(E(E(zoe).offer(inv)).getOfferResult(), res => { @@ -242,13 +248,17 @@ const makeOracleOperator = async ( reason => { failures.push(reason.message); }, - ), + ); + }, }); }, getDone: () => done, getFailures: () => harden([...failures]), // operator only gets .invitationMakers getKit: () => operatorKit, + setActive: flag => { + active = flag; + }, }); }; @@ -760,10 +770,12 @@ test.serial('Settlement for unknown transaction (operator down)', async t => { } = t.context; const operators = await sync.ocw.promise; + // Simulate 2 of 3 operators being unavailable + operators[0].setActive(false); + operators[1].setActive(false); + const opDown = makeCustomer('Otto', cctp, txPub.publisher, feeConfig); - // what removeOperator will do - await E(E.get(E(operators[1]).getKit()).admin).disable(); const bridgePos = snapshot(); const sent = await opDown.sendFast(t, 20_000_000n, 'osmo12345'); await mint(sent); @@ -788,9 +800,6 @@ test.serial('Settlement for unknown transaction (operator down)', async t => { t.deepEqual(bridgeTraffic.local, [], 'no IBC transfers'); await transmitTransferAck(); - t.deepEqual(await E(operators[1]).getFailures(), [ - 'submitEvidence for disabled operator', - ]); }); test.todo( From cd2a40c0e4e5923510e7c77edc710b6c7ba8bc8c Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 17 Dec 2024 13:07:19 -0800 Subject: [PATCH 7/7] feat: error on conflicting evidence --- .../fast-usdc/src/exos/transaction-feed.js | 25 ++++++++++- .../test/exos/transaction-feed.test.ts | 43 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) 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);