From e77f76480acc791787d1d3b7baaf1aae0cc3cc8d Mon Sep 17 00:00:00 2001 From: Yaco 0x Date: Mon, 16 Sep 2024 10:55:26 -0300 Subject: [PATCH] fix: improve actor termination checks --- .../src/services/eboActor.ts | 9 ++- .../src/services/eboProcessor.ts | 12 ++- .../tests/services/eboActor.spec.ts | 73 ++++++++++++++++++- 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/packages/automated-dispute/src/services/eboActor.ts b/packages/automated-dispute/src/services/eboActor.ts index cdd5667..948ae39 100644 --- a/packages/automated-dispute/src/services/eboActor.ts +++ b/packages/automated-dispute/src/services/eboActor.ts @@ -10,6 +10,7 @@ import type { DisputeStatus, EboEvent, EboEventName, + Epoch, Request, RequestId, Response, @@ -431,15 +432,19 @@ export class EboActor { * * Be aware that a request can be finalized but some of its disputes can still be pending resolution. * + * At last, actors must be kept alive until their epoch concludes, to ensure no actor/request duplication. + * + * @param currentEpoch the epoch to check against actor termination * @param blockNumber block number to check entities at * @returns `true` if all entities are settled, otherwise `false` */ - public canBeTerminated(blockNumber: bigint): boolean { + public canBeTerminated(currentEpoch: Epoch["epoch"], blockNumber: bigint): boolean { const request = this.getActorRequest(); + const isPastEpoch = currentEpoch > request.epoch; const isRequestFinalized = request.status === "Finalized"; const nonSettledProposals = this.activeProposals(blockNumber); - return isRequestFinalized && nonSettledProposals.length === 0; + return isPastEpoch && isRequestFinalized && nonSettledProposals.length === 0; } /** diff --git a/packages/automated-dispute/src/services/eboProcessor.ts b/packages/automated-dispute/src/services/eboProcessor.ts index 4c7bdc9..0ad22a1 100644 --- a/packages/automated-dispute/src/services/eboProcessor.ts +++ b/packages/automated-dispute/src/services/eboProcessor.ts @@ -77,7 +77,7 @@ export class EboProcessor { try { const events = eventsByRequestId.get(requestId) ?? []; - await this.syncRequest(requestId, events, lastBlock); + await this.syncRequest(requestId, events, currentEpoch.epoch, lastBlock); } catch (err) { this.onActorError(requestId, err as Error); } @@ -187,9 +187,15 @@ export class EboProcessor { * * @param requestId the ID of the `Request` * @param events a stream of consumed events + * @param currentEpoch the current epoch based on the last block * @param lastBlock the last block checked */ - private async syncRequest(requestId: RequestId, events: EboEventStream, lastBlock: bigint) { + private async syncRequest( + requestId: RequestId, + events: EboEventStream, + currentEpoch: Epoch["epoch"], + lastBlock: bigint, + ) { const firstEvent = events[0]; const actor = this.getOrCreateActor(requestId, firstEvent); @@ -204,7 +210,7 @@ export class EboProcessor { await actor.processEvents(); await actor.onLastBlockUpdated(lastBlock); - if (actor.canBeTerminated(lastBlock)) { + if (actor.canBeTerminated(currentEpoch, lastBlock)) { this.terminateActor(requestId); } } diff --git a/packages/automated-dispute/tests/services/eboActor.spec.ts b/packages/automated-dispute/tests/services/eboActor.spec.ts index ec4af4e..6329837 100644 --- a/packages/automated-dispute/tests/services/eboActor.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor.spec.ts @@ -253,7 +253,9 @@ describe("EboActor", () => { vi.spyOn(registry, "getRequest").mockReturnValue(request); vi.spyOn(registry, "getResponses").mockReturnValue([]); - expect(actor.canBeTerminated(currentBlockNumber)).toBe(false); + expect(actor.canBeTerminated(actor.actorRequest.epoch + 1n, currentBlockNumber)).toBe( + false, + ); }); it("returns false if there's one disputable response", () => { @@ -266,7 +268,9 @@ describe("EboActor", () => { vi.spyOn(registry, "getResponses").mockReturnValue([response]); vi.spyOn(registry, "getResponseDispute").mockReturnValue(undefined); - expect(actor.canBeTerminated(currentBlockNumber)).toBe(false); + expect(actor.canBeTerminated(actor.actorRequest.epoch + 1n, currentBlockNumber)).toBe( + false, + ); }); it("returns false if the request is finalized but there's one active dispute", () => { @@ -286,11 +290,69 @@ describe("EboActor", () => { vi.spyOn(registry, "getResponses").mockReturnValue([response]); vi.spyOn(registry, "getResponseDispute").mockReturnValue(dispute); - const canBeTerminated = actor.canBeTerminated(currentBlockNumber); + const canBeTerminated = actor.canBeTerminated( + actor.actorRequest.epoch + 1n, + currentBlockNumber, + ); expect(canBeTerminated).toBe(false); }); + it("returns false if we are still in the same epoch", () => { + const request: Request = { + ...DEFAULT_MOCKED_REQUEST_CREATED_DATA, + status: "Finalized", + }; + + const disputedResponse = mocks.buildResponse(request, { id: "0x01" }); + const undisputedResponse = mocks.buildResponse(request, { + id: "0x02", + createdAt: request.prophetData.responseModuleData.deadline - 1n, + }); + + const escalatedDispute = mocks.buildDispute(request, disputedResponse, { + status: "Escalated", + }); + + const { actor, registry } = mocks.buildEboActor(request, logger); + const currentBlockNumber = + undisputedResponse.createdAt + + request.prophetData.disputeModuleData.disputeWindow + + 1n; + + vi.spyOn(registry, "getRequest").mockReturnValue(request); + + vi.spyOn(registry, "getResponses").mockReturnValue([ + disputedResponse, + undisputedResponse, + ]); + + vi.spyOn(registry, "getResponseDispute").mockImplementation((response) => { + switch (response.id) { + case disputedResponse.id: + return escalatedDispute; + + case undisputedResponse.id: + return undefined; + } + }); + + const canBeTerminatedDuringCurrentEpoch = actor.canBeTerminated( + actor.actorRequest.epoch, + currentBlockNumber, + ); + + const canBeTerminatedDuringNextEpoch = actor.canBeTerminated( + actor.actorRequest.epoch + 1n, + currentBlockNumber, + ); + + expect(canBeTerminatedDuringCurrentEpoch).toBe(false); + // This is to validate that the change in the current epoch is the one that + // changes the output + expect(canBeTerminatedDuringNextEpoch).toBe(true); + }); + it("returns true once everything is settled", () => { const request: Request = { ...DEFAULT_MOCKED_REQUEST_CREATED_DATA, @@ -330,7 +392,10 @@ describe("EboActor", () => { } }); - const canBeTerminated = actor.canBeTerminated(currentBlockNumber); + const canBeTerminated = actor.canBeTerminated( + actor.actorRequest.epoch + 1n, + currentBlockNumber, + ); expect(canBeTerminated).toBe(true); });