diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 1c12e8837..c859e067b 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -52,6 +52,7 @@ type QuorumEvent = Log & { providers: string[] }; export class EventManager { public readonly chain: string; public readonly events: { [blockNumber: number]: QuorumEvent[] } = {}; + public readonly processedEvents: Set = new Set(); private blockNumber: number; @@ -82,6 +83,28 @@ export class EventManager { ); } + /** + * For a given Log, verify whether it has already been processed. + * @param event An Log instance to check. + * @returns True if the event has been processed, else false. + */ + protected isEventProcessed(event: Log): boolean { + // Protect against re-sending this event if it later arrives from another provider. + const eventKey = this.hashEvent(event); + return this.processedEvents.has(eventKey); + } + + /** + * For a given Log, mark it has having been been processed. + * @param event A Log instance to mark processed. + * @returns void + */ + protected markEventProcessed(event: Log): void { + // Protect against re-sending this event if it later arrives from another provider. + const eventKey = this.hashEvent(event); + this.processedEvents.add(eventKey); + } + /** * For a given Log, identify its quorum based on the number of unique providers that have supplied it. * @param event A Log instance with appended provider information. @@ -104,6 +127,10 @@ export class EventManager { add(event: Log, provider: string): void { assert(!event.removed); + if (this.isEventProcessed(event)) { + return; + } + // If `eventHash` is not recorded in `eventHashes` then it's presumed to be a new event. If it is // already found in the `eventHashes` array, then at least one provider has already supplied it. const events = (this.events[event.blockNumber] ??= []); @@ -172,6 +199,7 @@ export class EventManager { return true; // No quorum; retain for next time. } + this.markEventProcessed(event); quorumEvents.push(event); return false; }); diff --git a/test/EventManager.ts b/test/EventManager.ts index 8144d14c1..4b2ae67e3 100644 --- a/test/EventManager.ts +++ b/test/EventManager.ts @@ -7,7 +7,7 @@ import { createSpyLogger, expect, randomAddress } from "./utils"; describe("EventManager: Event Handling ", async function () { const chainId = CHAIN_IDs.MAINNET; - const providers = ["infura", "alchemy", "llamanodes"]; + const providers = ["infura", "alchemy", "llamanodes", "quicknode"]; const randomNumber = (ceil = 1_000_000) => Math.floor(Math.random() * ceil); const makeHash = () => ethersUtils.id(randomNumber().toString()); @@ -117,8 +117,41 @@ describe("EventManager: Event Handling ", async function () { }); it("Hashes events correctly", async function () { - const log = eventTemplate; - const hash = eventMgr.hashEvent(log); - expect(hash).to.exist; + const log1 = eventTemplate; + const hash1 = eventMgr.hashEvent(log1); + expect(hash1).to.exist; + + const log2 = { ...log1, logIndex: log1.logIndex + 1 }; + const hash2 = eventMgr.hashEvent(log2); + expect(hash2).to.not.equal(hash1); + + const log3 = { ...log2, logIndex: log2.logIndex - 1 }; + const hash3 = eventMgr.hashEvent(log3); + expect(hash3).to.equal(hash1); + }); + + it("Does not submit duplicate events", async function () { + expect(quorum).to.equal(2); + + const [provider1, provider2, provider3, provider4] = providers; + let { blockNumber } = eventTemplate; + + // Add the event once (not finalised). + eventMgr.add(eventTemplate, provider1); + let events = eventMgr.tick(++blockNumber); + expect(events.length).to.equal(0); + + // Add the same event from a different provider. + eventMgr.add(eventTemplate, provider2); + events = eventMgr.tick(++blockNumber); + expect(events.length).to.equal(1); + + // Re-add the same event again, from two new providers. + eventMgr.add(eventTemplate, provider3); + eventMgr.add(eventTemplate, provider4); + + // Verify that the same event was not replayed. + events = eventMgr.tick(++blockNumber); + expect(events.length).to.equal(0); }); });