Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: e2e dispute #72

Merged
merged 22 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8cd31c2
docs: add error types for ProphetCodec functions
0xyaco Oct 22, 2024
65ff4c3
chore: fix linters
0xyaco Oct 22, 2024
30857c1
fix: handle chains with blocks with same timestamp
0xyaco Oct 22, 2024
23ec54e
fix: add BigNumber to binary search
0xyaco Oct 23, 2024
e7d8b23
docs: add error types for ProphetCodec functions
0xyaco Oct 22, 2024
f34d525
fix: structs abi fields definition
0xyaco Oct 22, 2024
8cbcbcc
fix: avoid duplicating events during getEvents
0xyaco Oct 23, 2024
73c6351
fix: skip past events trying to be enqueued
0xyaco Oct 23, 2024
8b74a67
fix: handle request already created error appropriately
0xyaco Oct 23, 2024
1f47682
test: happy path working
0xyaco Oct 23, 2024
7885d9d
chore: remove caret from package.json
0xyaco Oct 23, 2024
0d0ba6b
Merge branch 'dev' into fix/blocknumber-binsearch
0xyaco Oct 23, 2024
b494c94
fix: candidate block number maths
0xyaco Oct 24, 2024
d1e2c55
refactor: normalize search methods naming
0xyaco Oct 24, 2024
4e9c335
Merge branch 'fix/blocknumber-binsearch' into fix/e2e-scenario-1
0xyaco Oct 24, 2024
635ac60
fix: fix blocks interval bounds during events sync
0xyaco Oct 24, 2024
86fb47c
test: test E2E response disputal
0xyaco Oct 24, 2024
c8b7399
fix: fix logging and comment
0xyaco Oct 25, 2024
e711311
fix: wait for disputestatus event
0xyaco Oct 25, 2024
d3ba273
Merge remote-tracking branch 'origin/dev' into test/e2e-scenario-2
0xyaco Oct 25, 2024
913649c
docs: finalize request docs
0xyaco Oct 25, 2024
a1b0fe6
fix: use Promise.all during pledging for dispute
0xyaco Oct 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice explanation

* - 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 () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will the test be enabled later?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the idea is to enable every test to be run sequentially later

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