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

feat: build onRequestCreated #18

Merged
merged 14 commits into from
Aug 8, 2024
Merged
5 changes: 3 additions & 2 deletions packages/automated-dispute/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"viem": "2.17.11",
"@ebo-agent/blocknumber": "workspace:*"
"@ebo-agent/blocknumber": "workspace:*",
"@ebo-agent/shared": "workspace:*",
"viem": "2.17.11"
}
}
112 changes: 105 additions & 7 deletions packages/automated-dispute/src/eboActor.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,121 @@
import { BlockNumberService } from "@ebo-agent/blocknumber";
import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js";
import { ILogger } from "@ebo-agent/shared";
import { ContractFunctionRevertedError } from "viem";

import { InvalidActorState } from "./exceptions/invalidActorState.exception.js";
import { RequestMismatch } from "./exceptions/requestMismatch.js";
import { EboRegistry } from "./interfaces/eboRegistry.js";
import { ProtocolProvider } from "./protocolProvider.js";
import { EboEvent } from "./types/events.js";
import { Dispute, Response } from "./types/prophet.js";

export class EboActor {
private requestActivity: unknown[];

constructor(
private readonly protocolProvider: ProtocolProvider,
private readonly blockNumberService: BlockNumberService,
private readonly registry: EboRegistry,
private readonly requestId: string,
) {
this.requestActivity = [];
private readonly logger: ILogger,
) {}

/**
* Handle 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 (this.registry.getRequest(event.metadata.requestId)) {
this.logger.error(
`The request ${event.metadata.requestId} was already being handled by an actor.`,
);

throw new InvalidActorState();
}

this.registry.addRequest(event.metadata.requestId, event.metadata.request);

if (this.anyActiveProposal()) {
// 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...`,
);

return;
}
Comment on lines +41 to +49
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@0xkenj1 included this check prior starting to build the Response (any response will be ignored if there's an active proposal)


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

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

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

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

throw err;
}
}
}

public async onRequestCreated(_event: EboEvent<"RequestCreated">): Promise<void> {
// TODO: implement
return;
/**
* Check if there's at least one proposal that has not received any dispute yet.
*
* @returns
*/
private anyActiveProposal() {
// TODO: implement this function
return false;
}

/**
* Check if the same proposal has already been made in the past.
*
* @param epoch epoch of the request
* @param chainId chain id of the request
* @param blockNumber proposed block number
* @returns true if there's a registry of a proposal with the same attributes, false otherwise
*/
private alreadyProposed(epoch: bigint, chainId: Caip2ChainId, blockNumber: bigint) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

would call this activePropose. We can have a propose on the registry that was already discarded

Copy link
Collaborator

Choose a reason for hiding this comment

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

also , we should add an isActive field to response object

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Does it make sense to have a new propose with the same block? I'd think that if there has already been a proposal with the block N, rejected or not, the agent wouldn't want to propose the same block N again. I might be missing something though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Left this as alreadyProposed, this will be the last thing to check on the agent side; it'd save it of trying to propose a response that has already been created. The activePropose check is done at the beginning of this method.

const responses = this.registry.getResponses();

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...`,
);

return true;
}

return false;
}

public async onResponseProposed(_event: EboEvent<"ResponseDisputed">): Promise<void> {
Expand Down
25 changes: 25 additions & 0 deletions packages/automated-dispute/src/eboMemoryRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { EboRegistry } from "./interfaces/eboRegistry.js";
import { Dispute, Request, Response } from "./types/prophet.js";

export class EboMemoryRegistry implements EboRegistry {
constructor(
private requests: Map<string, Request> = new Map(),
private responses: Map<string, Response> = new Map(),
private dispute: Map<string, Dispute> = new Map(),
) {}

/** @inheritdoc */
public addRequest(requestId: string, request: Request) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
public addRequest(requestId: string, request: Request) {
/** @inheritdoc */
public addRequest(requestId: string, request: Request) {

Copy link
Collaborator

Choose a reason for hiding this comment

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

ooaa nice, didn't know that. however, vscode is so magical that doesn't need this tag xd

this.requests.set(requestId, request);
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/** @inheritdoc */

/** @inheritdoc */
public getRequest(requestId: string) {
return this.requests.get(requestId);
}

/** @inheritdoc */
public getResponses() {
return this.responses;
}
}
1 change: 1 addition & 0 deletions packages/automated-dispute/src/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./rpcUrlsEmpty.exception.js";
export * from "./invalidActorState.exception.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export class InvalidActorState extends Error {
constructor() {
// TODO: we'll want to dump the Actor state into stderr at this point
super("The actor is in an invalid state.");

this.name = "InvalidActorState";
}
}
6 changes: 6 additions & 0 deletions packages/automated-dispute/src/exceptions/requestMismatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class RequestMismatch extends Error {
constructor(requestId: string, eventRequestId: string) {
super(`Actor handling request ${requestId} received a request ${eventRequestId} event.`);
this.name = "RequestMismatch";
}
}
27 changes: 27 additions & 0 deletions packages/automated-dispute/src/interfaces/eboRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Request, Response } from "../types/prophet.js";

/** Registry that stores Prophet entities (ie. requests, responses and disputes) */
export interface EboRegistry {
Copy link
Collaborator

Choose a reason for hiding this comment

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

lets add a high level description here

/**
* Add a `Request` by ID.
*
* @param requestId the ID of the `Request`
* @param request the `Request`
*/
addRequest(requestId: string, request: Request): void;

/**
* Get a `Request` by ID.
*
* @param requestId request ID
* @returns the request if already added into registry, `undefined` otherwise
*/
getRequest(requestId: string): Request | undefined;

/**
* Return all responses
*
* @returns responses map
*/
getResponses(): Map<string, Response>;
}
38 changes: 31 additions & 7 deletions packages/automated-dispute/src/protocolProvider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js";
import { Timestamp } from "@ebo-agent/shared";
import {
Address,
createPublicClient,
Expand Down Expand Up @@ -53,17 +55,28 @@ export class ProtocolProvider {
}

/**
* Gets the current epoch and the block number of the current epoch
* @returns The current epoch and the block number of the current epoch
* Gets the current epoch, the block number and its timestamp of the current epoch
*
* @returns The current epoch, its block number and its timestamp
*/
async getCurrentEpoch(): Promise<{ currentEpoch: bigint; currentEpochBlock: bigint }> {
const [currentEpoch, currentEpochBlock] = await Promise.all([
async getCurrentEpoch(): Promise<{
currentEpoch: bigint;
currentEpochBlockNumber: bigint;
currentEpochTimestamp: Timestamp;
}> {
const [currentEpoch, currentEpochBlockNumber] = await Promise.all([
this.epochManagerContract.read.currentEpoch(),
this.epochManagerContract.read.currentEpochBlock(),
]);

const currentEpochBlock = await this.client.getBlock({
blockNumber: currentEpochBlockNumber,
});

return {
currentEpoch,
currentEpochBlock,
currentEpochBlockNumber,
currentEpochTimestamp: currentEpochBlock.timestamp,
};
}

Expand All @@ -79,6 +92,8 @@ export class ProtocolProvider {
logIndex: 1,
metadata: {
requestId: "0x01",
chainId: "eip155:1",
epoch: 1n,
request: {
requester: "0x12345678901234567890123456789012",
requestModule: "0x12345678901234567890123456789012",
Expand All @@ -102,7 +117,11 @@ export class ProtocolProvider {
response: {
proposer: "0x12345678901234567890123456789012",
requestId: "0x01",
response: "0x01234",
response: {
block: 1n,
chainId: "eip155:1",
epoch: 20n,
},
},
},
} as EboEvent<"ResponseProposed">,
Expand Down Expand Up @@ -173,7 +192,12 @@ export class ProtocolProvider {
return;
}

async proposeResponse(_request: Request, _response: Response): Promise<void> {
async proposeResponse(
_requestId: string,
_epoch: bigint,
_chainId: Caip2ChainId,
_blockNumber: bigint,
): Promise<void> {
// TODO: implement actual method
return;
}
Expand Down
16 changes: 10 additions & 6 deletions packages/automated-dispute/src/types/events.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js";
import { Log } from "viem";

import { Dispute, Request } from "./prophet.js";
import { Dispute, Request, Response } from "./prophet.js";

export type EboEventName =
| "NewEpoch"
Expand All @@ -17,14 +18,17 @@ export interface NewEpoch {
epochBlockNumber: bigint;
}

export interface ResponseCreated {
export interface ResponseProposed {
requestId: string;
request: Request;
responseId: string;
response: Response;
}

export interface RequestCreated {
requestId: string;
epoch: bigint;
chainId: Caip2ChainId;
request: Request;
requestId: string;
}

export interface ResponseDisputed {
Expand Down Expand Up @@ -60,8 +64,8 @@ export type EboEventData<E extends EboEventName> = E extends "NewEpoch"
? NewEpoch
: E extends "RequestCreated"
? RequestCreated
: E extends "ResponseCreated"
? ResponseCreated
: E extends "ResponseProposed"
? ResponseProposed
Copy link
Collaborator

Choose a reason for hiding this comment

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

🤣

: E extends "ResponseDisputed"
? ResponseDisputed
: E extends "DisputeStatusChanged"
Expand Down
10 changes: 8 additions & 2 deletions packages/automated-dispute/src/types/prophet.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js";
import { Address } from "viem";

export interface Request {
Expand All @@ -7,13 +8,18 @@ export interface Request {
disputeModule: Address;
resolutionModule: Address;
finalityModule: Address;
// We might need here modules' data too
}

export interface Response {
proposer: Address;
requestId: string;
response: Uint8Array;

// To be byte-encode when sending it to Prophet
response: {
chainId: Caip2ChainId; // TODO: Pending on-chain definition on CAIP-2 usage
block: bigint;
epoch: bigint;
};
}

export interface Dispute {
Expand Down
Loading