Skip to content

Commit

Permalink
test: e2e dispute (#72)
Browse files Browse the repository at this point in the history
# 🤖 Linear

Closes GRT-192, GRT-216

## Description
* Test agent reaction to wrong response

Miscelanous fixes:
* Improve `waitForEvent` E2E testing util to return the found event
* Move dispute status mapping into `ProphetCodec` class
* Normalize all IDs read from events as soon as they are fetched
* Fix dispute settling errors not being handled correctly (thus not
escalating if needed)
* Fix proposing a new response when the active response has been
disputed
  • Loading branch information
0xyaco authored Oct 25, 2024
1 parent 892e22e commit f6379bf
Show file tree
Hide file tree
Showing 19 changed files with 529 additions and 206 deletions.
269 changes: 264 additions & 5 deletions apps/agent/test/e2e/scenarios/01_happy_path/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ProphetCodec,
ProtocolProvider,
} from "@ebo-agent/automated-dispute";
import { RequestId } from "@ebo-agent/automated-dispute/dist/types/prophet.js";
import { BlockNumberService } from "@ebo-agent/blocknumber";
import { Caip2ChainId, Logger } from "@ebo-agent/shared";
import { CreateServerReturnType } from "prool";
Expand Down Expand Up @@ -78,6 +79,10 @@ const FORK_L2_URL = "https://arbitrum-sepolia.gateway.tenderly.co";

const PROTOCOL_L2_LOCAL_URL = `http://${PROTOCOL_L2_LOCAL_RPC_HOST}:${PROTOCOL_L2_LOCAL_RPC_PORT}/1`;

const newEventAbi = parseAbiItem(
"event NewEpoch(uint256 indexed _epoch, string indexed _chainId, uint256 _blockNumber)",
);

describe.sequential("single agent", () => {
let l2ProtocolAnvil: CreateServerReturnType;

Expand Down Expand Up @@ -258,7 +263,7 @@ describe.sequential("single agent", () => {
blockTimeout: initBlock + 1000n,
});

expect(requestCreatedEvent).toBe(true);
expect(requestCreatedEvent).toBeDefined();

const responseProposedAbi = getAbiItem({ abi: oracleAbi, name: "ResponseProposed" });

Expand All @@ -277,7 +282,7 @@ describe.sequential("single agent", () => {
blockTimeout: initBlock + 1000n,
});

expect(responseProposedEvent).toBe(true);
expect(responseProposedEvent).toBeDefined();

await anvilClient.increaseTime({ seconds: 60 * 60 * 24 * 7 * 4 });

Expand Down Expand Up @@ -306,9 +311,7 @@ describe.sequential("single agent", () => {
filter: {
address: protocolContracts["EBOFinalityModule"],
fromBlock: initBlock,
event: parseAbiItem(
"event NewEpoch(uint256 indexed _epoch, string indexed _chainId, uint256 _blockNumber)",
),
event: newEventAbi,
strict: true,
},
matcher: (log) => {
Expand All @@ -325,4 +328,260 @@ describe.sequential("single agent", () => {
expect(oracleRequestFinalizedEvent).toBeDefined();
expect(newEpochEvent).toBeDefined();
});

/**
* Given:
* - A single agent A1 operating for chain CHAIN1
* - A request REQ1 for E1, a wrong response RESP1(REQ1)
* - Within the RESP1 dispute window
*
* When:
* - A1 detects RESP1 is wrong
*
* Then:
* - A1 disputes RESP1 with DISP1
* - `ResponseDisputed(RESP1.id, DISP1.id, DISP1)`
* - A1 proposes RESP2
* - `ResponseProposed(REQ1.id, RESP2.id, RESP2)`
* - A1 settles DISP1 and it ends with status `Won`
* - `DisputeStatusUpdated(DISP1.id, DISP1, "Won")`
* - A1 finalizes REQ1 with RESP2
* - `EBOFinalityModule.newEpoch(E1, CHAIN1, RESP2.response)`
* - `OracleRequestFinalized(REQ1.id, RESP2.id, A1.address)`
*/
test.skip("dispute response and propose a new one", { timeout: E2E_TEST_TIMEOUT }, async () => {
const logger = Logger.getInstance();

const protocolProvider = new ProtocolProvider(
{
l1: {
chainId: PROTOCOL_L2_CHAIN_ID,
// Using the same RPC due to Anvil's arbitrum block number bug
urls: [PROTOCOL_L2_LOCAL_URL],
transactionReceiptConfirmations: 1,
timeout: 1_000,
retryInterval: 500,
},
l2: {
chainId: PROTOCOL_L2_CHAIN_ID,
urls: [PROTOCOL_L2_LOCAL_URL],
transactionReceiptConfirmations: 1,
timeout: 1_000,
retryInterval: 500,
},
},
{
bondEscalationModule: protocolContracts["BondEscalationModule"],
eboRequestCreator: protocolContracts["EBORequestCreator"],
epochManager: EPOCH_MANAGER_ADDRESS,
oracle: protocolContracts["Oracle"],
horizonAccountingExtension: protocolContracts["HorizonAccountingExtension"],
},
accounts[0].privateKey,
);

vi.spyOn(protocolProvider, "getAccountingApprovedModules").mockResolvedValue([
protocolContracts["EBORequestModule"],
protocolContracts["BondedResponseModule"],
protocolContracts["BondEscalationModule"],
]);

const blockNumberService = new BlockNumberService(
new Map<Caip2ChainId, string[]>([[PROTOCOL_L2_CHAIN_ID, [PROTOCOL_L2_LOCAL_URL]]]),
{
baseUrl: new URL("http://not.needed/"),
bearerToken: "not.needed",
bearerTokenExpirationWindow: 1000,
servicePaths: {
block: "/block",
blockByTime: "/blockByTime",
},
},
logger,
);

const actorsManager = new EboActorsManager();

const processor = new EboProcessor(
{
requestModule: protocolContracts["EBORequestModule"],
responseModule: protocolContracts["BondedResponseModule"],
escalationModule: protocolContracts["BondEscalationModule"],
},
protocolProvider,
blockNumberService,
actorsManager,
logger,
{
notifyError: vi.fn(),
} as unknown as NotificationService,
);

const anvilClient = createTestClient({
mode: "anvil",
account: GRT_HOLDER,
chain: PROTOCOL_L2_CHAIN,
transport: http(PROTOCOL_L2_LOCAL_URL),
})
.extend(publicActions)
.extend(walletActions);

// Set epoch length to a big enough epoch length as in sepolia is way too short at the moment
await setEpochLength({
length: 100_000n,
client: anvilClient,
epochManagerAddress: EPOCH_MANAGER_ADDRESS,
governorAddress: GOVERNOR_ADDRESS,
});

const initBlock = await anvilClient.getBlockNumber();
const currentEpoch = await protocolProvider.getCurrentEpoch();

const correctResponse = await blockNumberService.getEpochBlockNumber(
currentEpoch.startTimestamp,
PROTOCOL_L2_CHAIN_ID,
);

await protocolProvider.createRequest(currentEpoch.number, PROTOCOL_L2_CHAIN_ID);

const requestCreatedEvent = await waitForEvent({
client: anvilClient,
filter: {
address: protocolContracts["Oracle"],
fromBlock: initBlock,
event: getAbiItem({ abi: oracleAbi, name: "RequestCreated" }),
strict: true,
},
pollingIntervalMs: 100,
blockTimeout: initBlock + 1000n,
});

expect(requestCreatedEvent).toBeDefined();

await protocolProvider.proposeResponse(requestCreatedEvent.args._request, {
proposer: accounts[0].account.address,
requestId: requestCreatedEvent.args._requestId as RequestId,
response: ProphetCodec.encodeResponse({ block: correctResponse - 1n }),
});

const badResponseProposedEvent = await waitForEvent({
client: anvilClient,
filter: {
address: protocolContracts["Oracle"],
fromBlock: initBlock,
event: getAbiItem({ abi: oracleAbi, name: "ResponseProposed" }),
strict: true,
},
pollingIntervalMs: 100,
blockTimeout: initBlock + 1000n,
});

processor.start(3000);

const badResponseDisputedEvent = await waitForEvent({
client: anvilClient,
filter: {
address: protocolContracts["Oracle"],
fromBlock: initBlock,
event: getAbiItem({ abi: oracleAbi, name: "ResponseDisputed" }),
strict: true,
},
matcher: (log) => {
return log.args._responseId === badResponseProposedEvent.args._responseId;
},
pollingIntervalMs: 100,
blockTimeout: initBlock + 1000n,
});

expect(badResponseDisputedEvent).toBeDefined();

const correctResponseProposedEvent = await waitForEvent({
client: anvilClient,
filter: {
address: protocolContracts["Oracle"],
fromBlock: initBlock,
event: getAbiItem({ abi: oracleAbi, name: "ResponseProposed" }),
strict: true,
},
matcher: (log) => {
const responseBlock = ProphetCodec.decodeResponse(
log.args._response.response,
).block;

return (
log.args._requestId === requestCreatedEvent.args._requestId &&
log.args._responseId !== badResponseProposedEvent.args._responseId &&
responseBlock === correctResponse
);
},
pollingIntervalMs: 100,
blockTimeout: initBlock + 1000n,
});

expect(correctResponseProposedEvent).toBeDefined();

await anvilClient.increaseTime({ seconds: 60 * 60 * 24 * 7 * 4 });

const disputeSettledEvent = await waitForEvent({
client: anvilClient,
filter: {
address: protocolContracts["Oracle"],
fromBlock: initBlock,
event: getAbiItem({ abi: oracleAbi, name: "DisputeStatusUpdated" }),
strict: true,
},
matcher: (log) => {
const status = ProphetCodec.decodeDisputeStatus(log.args._status);

return (
log.args._disputeId === badResponseDisputedEvent.args._disputeId &&
status === "Won"
);
},
pollingIntervalMs: 100,
blockTimeout: initBlock + 1000n,
});

expect(disputeSettledEvent).toBeDefined();

const [requestFinalizedEvent, newEpochEvent] = await Promise.all([
waitForEvent({
client: anvilClient,
filter: {
address: protocolContracts["Oracle"],
fromBlock: initBlock,
event: getAbiItem({ abi: oracleAbi, name: "OracleRequestFinalized" }),
strict: true,
},
matcher: (log) => {
return (
log.args._requestId === requestCreatedEvent.args._requestId &&
log.args._responseId === correctResponseProposedEvent.args._responseId
);
},
pollingIntervalMs: 100,
blockTimeout: initBlock + 1000n,
}),
waitForEvent({
client: anvilClient,
filter: {
address: protocolContracts["EBOFinalityModule"],
fromBlock: initBlock,
event: newEventAbi,
strict: true,
},
matcher: (log) => {
return (
log.args._chainId === keccak256(toHex(ARBITRUM_SEPOLIA_ID)) &&
log.args._epoch === currentEpoch.number
);
},
pollingIntervalMs: 100,
blockTimeout: initBlock + 1000n,
}),
]);

expect(requestFinalizedEvent).toBeDefined();
expect(newEpochEvent).toBeDefined();
});
});
16 changes: 10 additions & 6 deletions apps/agent/test/e2e/utils/prophet-e2e-scaffold/waitForEvent.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { AbiEvent, GetLogsParameters, Log } from "viem";
import { AbiEvent, GetLogsParameters, Log, MaybeAbiEventName } from "viem";

import { AnvilClient } from "./anvil.js";

interface WaitForEventInput<abi extends AbiEvent, client extends AnvilClient> {
/** Client to use for event polling */
client: client;
/** Event filtering */
filter: GetLogsParameters<abi, never, true>;
filter: GetLogsParameters<abi, [abi], true>;
/** Matcher to apply to filtered events */
matcher: (log: Log<bigint, number, boolean, abi, true>) => boolean;
matcher?: (
log: Log<bigint, number, false, abi, true, [abi], MaybeAbiEventName<abi>>,
) => boolean;

/** Event polling interval in milliseconds */
pollingIntervalMs: number;
/** Block number to time out after the polled chain has reached the specified block */
Expand All @@ -31,12 +34,13 @@ export async function waitForEvent<abi extends AbiEvent, client extends AnvilCli
do {
currentBlock = (await client.getBlock({ blockTag: "latest" })).number;

const logs = await client.getLogs<abi, never, true>(filter);
const logs = await client.getLogs<abi, [abi], true>(filter);
const matchingLogs = matcher ? logs.filter(matcher) : logs;

if (logs.some(matcher)) return true;
if (matchingLogs && matchingLogs.length > 0) return matchingLogs[0];

await new Promise((r) => setTimeout(r, pollingInterval));
} while (currentBlock < blockTimeout);

return false;
throw new Error(`Event ${filter.event?.name} not found.`);
}
1 change: 1 addition & 0 deletions apps/scripts/test/approveAccountingModules.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe("approveModules script", () => {
"Approved module: Bonded Response Module at address 0xBondedResponseModule",
),
);

expect(console.log).toHaveBeenCalledWith(
expect.stringContaining(
"Approved module: Bond Escalation Module at address 0xBondEscalationModule",
Expand Down
4 changes: 2 additions & 2 deletions packages/automated-dispute/src/exceptions/errorFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,15 +307,15 @@ const errorStrategiesEntries: [ErrorName, ErrorHandlingStrategy][] = [
{
shouldNotify: false,
shouldTerminate: false,
shouldReenqueue: true,
shouldReenqueue: false,
},
],
[
"BondEscalationModule_DisputeWindowOver",
{
shouldNotify: false,
shouldTerminate: false,
shouldReenqueue: true,
shouldReenqueue: false,
},
],
[
Expand Down
2 changes: 2 additions & 0 deletions packages/automated-dispute/src/exceptions/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export class ErrorHandler {

if (strategy.shouldTerminate && context.terminateActor) {
context.terminateActor();
} else {
this.logger.warn(`Event handling caused an error`);
}
}
}
Expand Down
Loading

0 comments on commit f6379bf

Please sign in to comment.