diff --git a/packages/automated-dispute/src/exceptions/index.ts b/packages/automated-dispute/src/exceptions/index.ts index 7e0802a..5554c4c 100644 --- a/packages/automated-dispute/src/exceptions/index.ts +++ b/packages/automated-dispute/src/exceptions/index.ts @@ -12,6 +12,7 @@ export * from "./invalidActorState.exception.js"; export * from "./invalidBlockHash.exception.js"; export * from "./invalidBlockRangeError.exception.js"; export * from "./invalidDisputeStatus.exception.js"; +export * from "./prophetChecksumMismatch.exception.js"; export * from "./prophetDecodingError.exception.js"; export * from "./requestAlreadyHandled.exception.js"; export * from "./requestMismatch.exception.js"; diff --git a/packages/automated-dispute/src/exceptions/prophetChecksumMismatch.exception.ts b/packages/automated-dispute/src/exceptions/prophetChecksumMismatch.exception.ts new file mode 100644 index 0000000..5dc2ea3 --- /dev/null +++ b/packages/automated-dispute/src/exceptions/prophetChecksumMismatch.exception.ts @@ -0,0 +1,7 @@ +export class ProphetChecksumMismatch extends Error { + constructor() { + super(`Failed to validate checksum.`); + + this.name = "ProphetChecksumMismatch"; + } +} diff --git a/packages/automated-dispute/src/services/eboActor.ts b/packages/automated-dispute/src/services/eboActor.ts index 43e4f98..909016b 100644 --- a/packages/automated-dispute/src/services/eboActor.ts +++ b/packages/automated-dispute/src/services/eboActor.ts @@ -31,6 +31,7 @@ import { InvalidActorState, InvalidDisputeStatus, PastEventEnqueueError, + ProphetChecksumMismatch, ProphetDecodingError, RequestMismatch, ResponseAlreadyProposed, @@ -195,6 +196,10 @@ export class EboActor { { eventName: event.name }, ); + continue; + } else if (err instanceof ProphetChecksumMismatch) { + this.logger.warn("Could not validate entity ID.", { event }); + continue; } else { throw err; diff --git a/packages/automated-dispute/src/services/eboRegistry/commands/addDispute.ts b/packages/automated-dispute/src/services/eboRegistry/commands/addDispute.ts index 9d61e50..b408218 100644 --- a/packages/automated-dispute/src/services/eboRegistry/commands/addDispute.ts +++ b/packages/automated-dispute/src/services/eboRegistry/commands/addDispute.ts @@ -1,5 +1,10 @@ -import { CommandAlreadyRun, CommandNotRun } from "../../../exceptions/index.js"; +import { + CommandAlreadyRun, + CommandNotRun, + ProphetChecksumMismatch, +} from "../../../exceptions/index.js"; import { EboRegistry, EboRegistryCommand } from "../../../interfaces/index.js"; +import { ProphetCodec } from "../../../services/index.js"; import { Dispute, EboEvent } from "../../../types/index.js"; export class AddDispute implements EboRegistryCommand { @@ -14,6 +19,9 @@ export class AddDispute implements EboRegistryCommand { event: EboEvent<"ResponseDisputed" | "DisputeEscalated">, registry: EboRegistry, ): AddDispute { + if (!ProphetCodec.validateDispute(event.metadata.disputeId, event.metadata.dispute)) + throw new ProphetChecksumMismatch(); + const dispute: Dispute = { id: event.metadata.disputeId, createdAt: { diff --git a/packages/automated-dispute/src/services/eboRegistry/commands/addResponse.ts b/packages/automated-dispute/src/services/eboRegistry/commands/addResponse.ts index 7717fcd..f30bb69 100644 --- a/packages/automated-dispute/src/services/eboRegistry/commands/addResponse.ts +++ b/packages/automated-dispute/src/services/eboRegistry/commands/addResponse.ts @@ -1,4 +1,8 @@ -import { CommandAlreadyRun, CommandNotRun } from "../../../exceptions/index.js"; +import { + CommandAlreadyRun, + CommandNotRun, + ProphetChecksumMismatch, +} from "../../../exceptions/index.js"; import { EboRegistry, EboRegistryCommand } from "../../../interfaces/index.js"; import { EboEvent, Response, ResponseBody } from "../../../types/index.js"; import { ProphetCodec } from "../../prophetCodec.js"; @@ -12,6 +16,9 @@ export class AddResponse implements EboRegistryCommand { ) {} static buildFromEvent(event: EboEvent<"ResponseProposed">, registry: EboRegistry) { + if (!ProphetCodec.validateResponse(event.metadata.responseId, event.metadata.response)) + throw new ProphetChecksumMismatch(); + const encodedResponse = event.metadata.response.response; const responseBody: ResponseBody = ProphetCodec.decodeResponse(encodedResponse); diff --git a/packages/automated-dispute/src/services/prophetCodec.ts b/packages/automated-dispute/src/services/prophetCodec.ts index ebeda58..8e50599 100644 --- a/packages/automated-dispute/src/services/prophetCodec.ts +++ b/packages/automated-dispute/src/services/prophetCodec.ts @@ -1,16 +1,20 @@ import { Caip2ChainId } from "@ebo-agent/shared"; import { + AbiEventParametersToPrimitiveTypes, AbiParameter, Address, ByteArray, decodeAbiParameters, encodeAbiParameters, + getAbiItem, Hex, + keccak256, toHex, } from "viem"; -import type { BondEscalationStatus } from "../types/prophet.js"; +import type { BondEscalationStatus, Dispute } from "../types/prophet.js"; import { ProphetDecodingError } from "../exceptions/index.js"; +import { oracleAbi } from "../external.js"; import { DisputeStatus, Request, Response } from "../types/prophet.js"; const REQUEST_MODULE_DATA_REQUEST_ABI_FIELDS = [ @@ -278,4 +282,24 @@ export class ProphetCodec { throw new ProphetDecodingError("escalation.status", toHex(status.toString())); } } + + static validateResponse(id: Response["id"], response: Response["prophetData"]): boolean { + const proposeResponseAbi = getAbiItem({ abi: oracleAbi, name: "proposeResponse" }); + const responseAbi = proposeResponseAbi.inputs[1]; + + const encodedResponse = encodeAbiParameters([responseAbi], [response]); + const hashedResponse = keccak256(encodedResponse); + + return id.toLowerCase() === hashedResponse.toLowerCase(); + } + + static validateDispute(id: Dispute["id"], dispute: Dispute["prophetData"]): boolean { + const disputeResponseAbi = getAbiItem({ abi: oracleAbi, name: "disputeResponse" }); + const disputeAbi = disputeResponseAbi.inputs[2]; + + const encodedDispute = encodeAbiParameters([disputeAbi], [dispute]); + const hashedDispute = keccak256(encodedDispute); + + return id.toLowerCase() === hashedDispute.toLowerCase(); + } } diff --git a/packages/automated-dispute/tests/mocks/eboActor.mocks.ts b/packages/automated-dispute/tests/mocks/eboActor.mocks.ts index d9b6e55..7ab3933 100644 --- a/packages/automated-dispute/tests/mocks/eboActor.mocks.ts +++ b/packages/automated-dispute/tests/mocks/eboActor.mocks.ts @@ -1,7 +1,7 @@ import { BlockNumberService } from "@ebo-agent/blocknumber"; import { Caip2ChainId, ILogger, NotificationService, UnixTimestamp } from "@ebo-agent/shared"; import { Mutex } from "async-mutex"; -import { Block, pad } from "viem"; +import { Block, pad, padHex } from "viem"; import { vi } from "vitest"; import { ProtocolProvider } from "../../src/providers/index.js"; @@ -220,7 +220,7 @@ export function buildDispute( status: "Active", }, prophetData: { - disputer: "0x01", + disputer: padHex("0x1", { size: 20 }), proposer: response.prophetData.proposer, requestId: request.id, responseId: response.id, diff --git a/packages/automated-dispute/tests/services/eboActor.spec.ts b/packages/automated-dispute/tests/services/eboActor.spec.ts index 0f873fb..1d07d41 100644 --- a/packages/automated-dispute/tests/services/eboActor.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor.spec.ts @@ -8,6 +8,7 @@ import { PastEventEnqueueError, RequestMismatch, } from "../../src/exceptions/index.js"; +import { ProphetCodec } from "../../src/external.js"; import { EboEvent, Request, RequestId, ResponseId } from "../../src/types/index.js"; import mocks from "../mocks/index.js"; import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "./eboActor/fixtures.js"; @@ -186,6 +187,10 @@ describe("EboActor", () => { }); it("does not allow interleaved event processing", async () => { + const validateResponseMock = vi + .spyOn(ProphetCodec, "validateResponse") + .mockReturnValue(true); + /** * This case aims to cover the scenario in which the first call keeps awaiting to * resolve its internal promises while a second call to `processEvents` with @@ -280,6 +285,8 @@ describe("EboActor", () => { expect(callOrder).toEqual([1, 1, 2, 2]); expect(callOrder).not.toEqual([1, 2, 2, 1]); // Case with no mutexes + + validateResponseMock.mockRestore(); }); }); diff --git a/packages/automated-dispute/tests/services/eboActor/onDisputeEscalated.spec.ts b/packages/automated-dispute/tests/services/eboActor/onDisputeEscalated.spec.ts index c8de119..b9c757c 100644 --- a/packages/automated-dispute/tests/services/eboActor/onDisputeEscalated.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor/onDisputeEscalated.spec.ts @@ -1,7 +1,8 @@ import { ILogger, UnixTimestamp } from "@ebo-agent/shared"; import { Address } from "viem"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { ProphetCodec } from "../../../src/external.js"; import { EboEvent } from "../../../src/types/index.js"; import mocks from "../../mocks/index.js"; import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "./fixtures.js"; @@ -17,6 +18,14 @@ describe("onDisputeEscalated", () => { const actorRequest = DEFAULT_MOCKED_REQUEST_CREATED_DATA; const response = mocks.buildResponse(actorRequest); + beforeEach(() => { + vi.spyOn(ProphetCodec, "validateDispute").mockReturnValue(true); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + it("creates the dispute if it does not exist", async () => { const dispute = mocks.buildDispute(actorRequest, response, { decodedData: { status: "Escalated" }, diff --git a/packages/automated-dispute/tests/services/eboActor/onDisputeStatusUpdated.spec.ts b/packages/automated-dispute/tests/services/eboActor/onDisputeStatusUpdated.spec.ts index 0c18b66..9f52f98 100644 --- a/packages/automated-dispute/tests/services/eboActor/onDisputeStatusUpdated.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor/onDisputeStatusUpdated.spec.ts @@ -1,7 +1,7 @@ import { ILogger, UnixTimestamp } from "@ebo-agent/shared"; import { describe, expect, it, vi } from "vitest"; -import { ProphetCodec } from "../../../src/services/prophetCodec.js"; +import { ProphetCodec } from "../../../src/services/index.js"; import { DisputeId, EboEvent } from "../../../src/types/index.js"; import mocks from "../../mocks/index.js"; import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "./fixtures.js"; diff --git a/packages/automated-dispute/tests/services/eboActor/onRequestCreated.spec.ts b/packages/automated-dispute/tests/services/eboActor/onRequestCreated.spec.ts index 0232546..a875ca7 100644 --- a/packages/automated-dispute/tests/services/eboActor/onRequestCreated.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor/onRequestCreated.spec.ts @@ -3,7 +3,7 @@ import { Hex } from "viem"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ResponseAlreadyProposed } from "../../../src/exceptions/index.js"; -import { ProphetCodec } from "../../../src/services/prophetCodec.js"; +import { ProphetCodec } from "../../../src/services/index.js"; import { EboEvent, Epoch, @@ -51,6 +51,9 @@ describe("EboActor", () => { vi.spyOn(ProphetCodec, "decodeRequestResponseModuleData").mockReturnValue( request.decodedData.responseModuleData, ); + + vi.spyOn(ProphetCodec, "validateResponse").mockReturnValue(true); + vi.spyOn(ProphetCodec, "encodeResponse").mockReturnValue("0x" as Hex); }); afterEach(() => { diff --git a/packages/automated-dispute/tests/services/eboActor/onResponseDisputed.spec.ts b/packages/automated-dispute/tests/services/eboActor/onResponseDisputed.spec.ts index 038f8db..4b70bab 100644 --- a/packages/automated-dispute/tests/services/eboActor/onResponseDisputed.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor/onResponseDisputed.spec.ts @@ -1,6 +1,7 @@ import { ILogger, UnixTimestamp } from "@ebo-agent/shared"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { ProphetCodec } from "../../../src/services/index.js"; import { EboEvent } from "../../../src/types/events.js"; import { Dispute, Response } from "../../../src/types/prophet.js"; import mocks from "../../mocks/index.js"; @@ -26,6 +27,14 @@ describe("onResponseDisputed", () => { }, }; + beforeEach(() => { + vi.spyOn(ProphetCodec, "validateDispute").mockReturnValue(true); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + it("pledges for dispute and proposes a new response if proposal should be different", async () => { const { actor, registry, blockNumberService, protocolProvider } = mocks.buildEboActor( actorRequest, diff --git a/packages/automated-dispute/tests/services/eboActor/onResponseProposed.spec.ts b/packages/automated-dispute/tests/services/eboActor/onResponseProposed.spec.ts index 2208bd5..b7a9e47 100644 --- a/packages/automated-dispute/tests/services/eboActor/onResponseProposed.spec.ts +++ b/packages/automated-dispute/tests/services/eboActor/onResponseProposed.spec.ts @@ -1,6 +1,6 @@ import { ILogger, UnixTimestamp } from "@ebo-agent/shared"; import { ContractFunctionRevertedError } from "viem"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ErrorHandler } from "../../../src/exceptions/errorHandler.js"; import { ErrorFactory } from "../../../src/exceptions/index.js"; @@ -38,6 +38,14 @@ describe("EboActor", () => { }, }; + beforeEach(() => { + vi.spyOn(ProphetCodec, "validateResponse").mockReturnValue(true); + }); + + afterEach(() => { + vi.spyOn(ProphetCodec, "validateResponse").mockRestore(); + }); + it("handles error when disputing the response", async () => { expect.assertions(3); diff --git a/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/addDispute.spec.ts b/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/addDispute.spec.ts index 7c8b696..097dd32 100644 --- a/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/addDispute.spec.ts +++ b/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/addDispute.spec.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; import { CommandAlreadyRun, CommandNotRun } from "../../../../src/exceptions/index.js"; import { EboRegistry } from "../../../../src/interfaces/index.js"; -import { AddDispute } from "../../../../src/services/index.js"; +import { AddDispute, ProphetCodec } from "../../../../src/services/index.js"; import { EboEvent } from "../../../../src/types/index.js"; import mocks from "../../../mocks/index.js"; import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "../../../services/eboActor/fixtures.js"; @@ -32,6 +32,8 @@ describe("AddDispute", () => { addDispute: vi.fn(), removeDispute: vi.fn(), } as unknown as EboRegistry; + + vi.spyOn(ProphetCodec, "validateDispute").mockReturnValue(true); }); describe("run", () => { diff --git a/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/addResponse.spec.ts b/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/addResponse.spec.ts index 18d5f4b..fc76688 100644 --- a/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/addResponse.spec.ts +++ b/packages/automated-dispute/tests/services/eboMemoryRegistry/commands/addResponse.spec.ts @@ -1,14 +1,15 @@ import { fail } from "assert"; import { Hex } from "viem"; -import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, Mock, vi } from "vitest"; import { CommandAlreadyRun, CommandNotRun, + ProphetChecksumMismatch, ProphetDecodingError, } from "../../../../src/exceptions/index.js"; import { EboRegistry } from "../../../../src/interfaces/index.js"; -import { AddResponse } from "../../../../src/services/index.js"; +import { AddResponse, ProphetCodec } from "../../../../src/services/index.js"; import { EboEvent } from "../../../../src/types/index.js"; import mocks from "../../../mocks/index.js"; import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "../../../services/eboActor/fixtures.js"; @@ -36,6 +37,20 @@ describe("AddResponse", () => { addResponse: vi.fn(), removeResponse: vi.fn(), } as unknown as EboRegistry; + + vi.spyOn(ProphetCodec, "validateResponse").mockReturnValue(true); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + describe("buildFromEvent", () => { + it("throws when response has invalid ID", () => { + expect(() => { + AddResponse.buildFromEvent(event, registry); + }).toThrowError(ProphetChecksumMismatch); + }); }); describe("run", () => { diff --git a/packages/automated-dispute/tests/services/prophetCodec.spec.ts b/packages/automated-dispute/tests/services/prophetCodec.spec.ts index 388e38b..8e8b052 100644 --- a/packages/automated-dispute/tests/services/prophetCodec.spec.ts +++ b/packages/automated-dispute/tests/services/prophetCodec.spec.ts @@ -1,9 +1,10 @@ -import { isHex } from "viem"; +import { isHex, padHex } from "viem"; import { describe, expect, it } from "vitest"; import { ProphetDecodingError } from "../../src/exceptions"; import { ProphetCodec } from "../../src/services"; -import { DisputeStatus, Response } from "../../src/types"; +import { DisputeId, DisputeStatus, Response, ResponseId } from "../../src/types"; +import { buildDispute, buildRequest, buildResponse } from "../mocks/eboActor.mocks"; describe("ProphetCodec", () => { describe("encodeResponse", () => { @@ -105,4 +106,55 @@ describe("ProphetCodec", () => { describe.todo("encodeRequestResponseModuleData"); describe.todo("encodeRequestRequestModuleData"); describe.todo("encodeRequestDisputeModuleData"); + + describe("validateResponse", () => { + const request = buildRequest(); + const response = buildResponse(request, { + id: "0xa36520fabcf8d19d153972ff5c123e3e3597e587fb1a23dd7711b79117b9bab0" as ResponseId, + }); + + it("returns true if checksum is successful", () => { + const validateResponse = ProphetCodec.validateResponse( + response.id, + response["prophetData"], + ); + + expect(validateResponse).toBe(true); + }); + + it("returns false if checksum is not success", () => { + const validateResponse = ProphetCodec.validateResponse( + padHex("0x01") as ResponseId, + response["prophetData"], + ); + + expect(validateResponse).toBe(false); + }); + }); + + describe("validateDispute", () => { + const request = buildRequest(); + const response = buildResponse(request); + const dispute = buildDispute(request, response, { + id: "0x0e58386b5c7eaa97b4bd898e06b83c7a5f2094ef90994596f6e57d7b314a29c6", + }); + + it("returns true if checksum is successful", () => { + const validateDispute = ProphetCodec.validateDispute( + dispute.id, + dispute["prophetData"], + ); + + expect(validateDispute).toBe(true); + }); + + it("returns false if checksum is not success", () => { + const validateDispute = ProphetCodec.validateDispute( + padHex("0x01") as DisputeId, + dispute["prophetData"], + ); + + expect(validateDispute).toBe(false); + }); + }); });