Skip to content

Commit

Permalink
feat: implement EboActor.onResponseProposed handler (#19)
Browse files Browse the repository at this point in the history
# 🤖 Linear

Closes GRT-82

## Description

* Implemented `EboActor.onResponseProposed` handler
* Extracted some shared behavior inside `EboActor` to private methods
* Separated `EboActor` test suite inside a folder `eboActor/` with a
**single file per method**, as the set up/mocking for each handler +
scenario tends to be pretty loaded
* Mocked `logger` in some test files that were generating some stdout
noise during `vitest` execution
  • Loading branch information
0xyaco authored Aug 9, 2024
1 parent 3933c43 commit 647f044
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 107 deletions.
145 changes: 108 additions & 37 deletions packages/automated-dispute/src/eboActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,31 @@ import { ProtocolProvider } from "./protocolProvider.js";
import { EboEvent } from "./types/events.js";
import { Dispute, Response } from "./types/prophet.js";

/**
* Actor that handles a singular Prophet's request asking for the block number that corresponds
* to an instant on an indexed chain.
*/
export class EboActor {
constructor(
private readonly actorRequest: {
id: string;
epoch: bigint;
epochTimestamp: bigint;
},
private readonly protocolProvider: ProtocolProvider,
private readonly blockNumberService: BlockNumberService,
private readonly registry: EboRegistry,
private readonly requestId: string,
private readonly logger: ILogger,
) {}

/**
* Handle RequestCreated event.
* Handle `RequestCreated` event.
*
* @param event RequestCreated event
* @param event `RequestCreated` event
*/
public async onRequestCreated(event: EboEvent<"RequestCreated">): Promise<void> {
if (event.metadata.requestId != this.requestId)
throw new RequestMismatch(this.requestId, event.metadata.requestId);
if (event.metadata.requestId != this.actorRequest.id)
throw new RequestMismatch(this.actorRequest.id, event.metadata.requestId);

if (this.registry.getRequest(event.metadata.requestId)) {
this.logger.error(
Expand All @@ -42,39 +50,33 @@ export class EboActor {
// Skipping new proposal until the actor receives a ResponseDisputed event;
// at that moment, it will be possible to re-propose again.
this.logger.info(
`There is an active proposal for request ${this.requestId}. Skipping...`,
`There is an active proposal for request ${this.actorRequest.id}. Skipping...`,
);

return;
}

const { chainId } = event.metadata;
const { currentEpoch, currentEpochTimestamp } =
await this.protocolProvider.getCurrentEpoch();
const response = await this.buildResponse(chainId);

const epochBlockNumber = await this.blockNumberService.getEpochBlockNumber(
currentEpochTimestamp,
chainId,
);

if (this.alreadyProposed(currentEpoch, chainId, epochBlockNumber)) return;
if (this.alreadyProposed(response.epoch, response.chainId, response.block)) return;

try {
await this.protocolProvider.proposeResponse(
this.requestId,
currentEpoch,
chainId,
epochBlockNumber,
this.actorRequest.id,
response.epoch,
response.chainId,
response.block,
);
} catch (err) {
if (err instanceof ContractFunctionRevertedError) {
this.logger.warn(
`Block ${epochBlockNumber} for epoch ${currentEpoch} and ` +
`chain ${chainId} was not proposed. Skipping proposal...`,
`Block ${response.block} for epoch ${response.epoch} and ` +
`chain ${response.chainId} was not proposed. Skipping proposal...`,
);
} else {
this.logger.error(
`Actor handling request ${this.requestId} is not able to continue.`,
`Actor handling request ${this.actorRequest.id} is not able to continue.`,
);

throw err;
Expand Down Expand Up @@ -102,33 +104,102 @@ export class EboActor {
*/
private alreadyProposed(epoch: bigint, chainId: Caip2ChainId, blockNumber: bigint) {
const responses = this.registry.getResponses();
const newResponse: Response["response"] = {
epoch,
chainId,
block: blockNumber,
};

for (const [responseId, response] of responses) {
if (response.response.block != blockNumber) continue;
if (response.response.chainId != chainId) continue;
if (response.response.epoch != epoch) continue;

this.logger.info(
`Block ${blockNumber} for epoch ${epoch} and chain ${chainId} already proposed on response ${responseId}. Skipping...`,
);
for (const [responseId, proposedResponse] of responses) {
if (this.equalResponses(proposedResponse.response, newResponse)) {
this.logger.info(
`Block ${blockNumber} for epoch ${epoch} and chain ${chainId} already proposed on response ${responseId}. Skipping...`,
);

return true;
return true;
}
}

return false;
}

public async onResponseProposed(_event: EboEvent<"ResponseDisputed">): Promise<void> {
// TODO: implement
return;
/**
* Build a response body with an epoch, chain ID and block number.
*
* @param chainId chain ID to use in the response body
* @returns a response body
*/
private async buildResponse(chainId: Caip2ChainId): Promise<Response["response"]> {
const epochBlockNumber = await this.blockNumberService.getEpochBlockNumber(
this.actorRequest.epochTimestamp,
chainId,
);

return {
epoch: this.actorRequest.epoch,
chainId: chainId,
block: epochBlockNumber,
};
}

public async onResponseDisputed(_event: EboEvent<"ResponseDisputed">): Promise<void> {
// TODO: implement
return;
/**
* Handle `ResponseProposed` event.
*
* @param event a `ResponseProposed` event
* @returns void
*/
public async onResponseProposed(event: EboEvent<"ResponseProposed">): Promise<void> {
this.shouldHandleRequest(event.metadata.requestId);

this.registry.addResponse(event.metadata.responseId, event.metadata.response);

const eventResponse = event.metadata.response.response;
const actorResponse = await this.buildResponse(eventResponse.chainId);

if (this.equalResponses(actorResponse, eventResponse)) {
this.logger.info(`Response ${event.metadata.responseId} was validated. Skipping...`);

return;
}

await this.protocolProvider.disputeResponse(
event.metadata.requestId,
event.metadata.responseId,
event.metadata.response.proposer,
);
}

/**
* Check for deep equality between two responses
*
* @param a response
* @param b response
* @returns true if all attributes on `a` are equal to attributes on `b`, false otherwise
*/
private equalResponses(a: Response["response"], b: Response["response"]) {
if (a.block != b.block) return false;
if (a.chainId != b.chainId) return false;
if (a.epoch != b.epoch) return false;

return true;
}

/**
* Validate that the actor should handle the request by its ID.
*
* @param requestId request ID
*/
private shouldHandleRequest(requestId: string) {
if (this.actorRequest.id.toLowerCase() !== requestId.toLowerCase()) {
this.logger.error(`The request ${requestId} is not handled by this actor.`);

// We want to fail the actor as receiving events from other requests
// will most likely cause a corrupted state.
throw new InvalidActorState();
}
}

private async proposeResponse(_response: Response): Promise<void> {
public async onResponseDisputed(_event: EboEvent<"ResponseDisputed">): Promise<void> {
// TODO: implement
return;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/automated-dispute/src/eboMemoryRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export class EboMemoryRegistry implements EboRegistry {
return this.requests.get(requestId);
}

/** @inheritdoc */
public addResponse(responseId: string, response: Response): void {
this.responses.set(responseId, response);
}

/** @inheritdoc */
public getResponses() {
return this.responses;
Expand Down
10 changes: 9 additions & 1 deletion packages/automated-dispute/src/interfaces/eboRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export interface EboRegistry {
/**
* Add a `Request` by ID.
*
* @param requestId the ID of the `Request`
* @param requestId the ID of the `Request` to use as index
* @param request the `Request`
*/
addRequest(requestId: string, request: Request): void;
Expand All @@ -18,6 +18,14 @@ export interface EboRegistry {
*/
getRequest(requestId: string): Request | undefined;

/**
* Add a `Response` by ID.
*
* @param responseId the ID of the `Response` to use as index
* @param response the `Resopnse`
*/
addResponse(responseId: string, response: Response): void;

/**
* Return all responses
*
Expand Down
6 changes: 3 additions & 3 deletions packages/automated-dispute/src/protocolProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,9 @@ export class ProtocolProvider {
}

async disputeResponse(
_request: Request,
_response: Response,
_dispute: Dispute,
_requestId: string,
_responseId: string,
_proposer: Address,
): Promise<void> {
// TODO: implement actual method
return;
Expand Down
17 changes: 17 additions & 0 deletions packages/automated-dispute/tests/eboActor/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Address } from "viem";

import { Request } from "../../src/types/prophet";

export const DEFAULT_MOCKED_PROTOCOL_CONTRACTS = {
oracle: "0x123456" as Address,
epochManager: "0x654321" as Address,
};

export const DEFAULT_MOCKED_PROPHET_REQUEST: Request = {
disputeModule: "0x01" as Address,
finalityModule: "0x02" as Address,
requestModule: "0x03" as Address,
resolutionModule: "0x04" as Address,
responseModule: "0x05" as Address,
requester: "0x10" as Address,
};
Loading

0 comments on commit 647f044

Please sign in to comment.