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

fix: handle DisputeEscalated events for not first disputes #81

Merged
merged 4 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
32 changes: 32 additions & 0 deletions apps/agent/config.tenderly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
protocolProvider:
rpcsConfig:
l1:
chainId: eip155:11155111
transactionReceiptConfirmations: 1
timeout: 10000
retryInterval: 150
l2:
chainId: eip155:421614
transactionReceiptConfirmations: 1
timeout: 10000
retryInterval: 150
contracts:
oracle: "0x10224eff6B1Caaf5daC49B2e7104b7161484B128"
epochManager: "0x7975475801BEf845f10Ce7784DC69aB1e0344f11"
eboRequestCreator: "0xa13318684281a820304C164427396385C306d870"
bondEscalationModule: "0x52d7728fE87826FfF51b21b303e2FF7cB04F6Aec"
horizonAccountingExtension: "0xbDAB27D1903da4e18B0D1BE873E18924514E52eC"

blockNumberService:
blockmetaConfig:
baseUrl: "localhost:443"
Copy link
Collaborator

Choose a reason for hiding this comment

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

just a double check that it's the correct port since its HTTPS one

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's kinda a placeholder for Tenderly config tbh, we are not indexing any chain that needs the blockmeta service.

servicePaths:
blockByTime: /sf.blockmeta.v2.BlockByTime
block: /sf.blockmeta.v2.Block
bearerTokenExpirationWindow: 31536000000

processor:
msBetweenChecks: 7500
accountingModules:
responseModule: "0xb97C59331F89a852Ae21aee215Da28820c533649"
escalationModule: "0x52d7728fE87826FfF51b21b303e2FF7cB04F6Aec"
19 changes: 9 additions & 10 deletions packages/automated-dispute/src/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
export * from "./blockNumberServiceRequired.exception.js";
export * from "./customContractError.js";
export * from "./decodeLogDataFailure.js";
export * from "./eboActor/index.js";
export * from "./eboProcessor/index.js";
export * from "./eboRegistry/index.js";

export * from "./errorFactory.js";
export * from "./invalidAccountOnClient.exception.js";
export * from "./invalidActorState.exception.js";
export * from "./invalidBlockHash.exception.js";
export * from "./invalidBlockRangeError.exception.js";
export * from "./invalidDisputeStatus.exception.js";
export * from "./prophetDecodingError.exception.js";
export * from "./requestAlreadyHandled.exception.js";
export * from "./requestMismatch.exception.js";
export * from "./responseAlreadyProposed.exception.js";
export * from "./rpcUrlsEmpty.exception.js";
export * from "./transactionExecutionError.exception.js";
export * from "./invalidAccountOnClient.exception.js";
export * from "./unsupportedEvent.exception.js";
export * from "./decodeLogDataFailure.js";
export * from "./invalidBlockRangeError.exception.js";
export * from "./unknownCustomError.exception.js";
export * from "./invalidBlockHash.exception.js";
export * from "./unknownDisputeStatus.exception.js";
export * from "./blockNumberServiceRequired.exception.js";
export * from "./customContractError.js";
export * from "./errorFactory.js";
export * from "./unsupportedEvent.exception.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ByteArray, Hex } from "viem";

export class ProphetDecodingError extends Error {
constructor(
public readonly id: string,
public readonly data: ByteArray | Hex,
public readonly err?: Error,
) {
super(`Failed to decode ${id} with data ${data}.`);

this.name = "ProphetDecodingError";
}
}

This file was deleted.

4 changes: 1 addition & 3 deletions packages/automated-dispute/src/providers/protocolProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ import {
RpcUrlsEmpty,
TransactionExecutionError,
} from "../exceptions/index.js";
import { ProphetCodec } from "../external.js";
import {
IProtocolProvider,
IReadProvider,
Expand Down Expand Up @@ -467,7 +466,7 @@ export class ProtocolProvider implements IProtocolProvider {
responseId: HexUtils.normalize(_dispute.responseId) as ResponseId,
requestId: HexUtils.normalize(_dispute.requestId) as RequestId,
},
status: ProphetCodec.decodeDisputeStatus(_status),
status: _status,
blockNumber: event.blockNumber,
},
} as EboEvent<"DisputeStatusUpdated">;
Expand Down Expand Up @@ -525,7 +524,6 @@ export class ProtocolProvider implements IProtocolProvider {
requestId: HexUtils.normalize(_dispute.requestId) as RequestId,
},
caller: _caller as Address,
blockNumber: event.blockNumber,
},
} as EboEvent<"DisputeEscalated">;
}),
Expand Down
81 changes: 63 additions & 18 deletions packages/automated-dispute/src/services/eboActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
InvalidActorState,
InvalidDisputeStatus,
PastEventEnqueueError,
ProphetDecodingError,
RequestMismatch,
ResponseAlreadyProposed,
ResponseNotFound,
Expand All @@ -35,6 +36,7 @@ import {
AddRequest,
AddResponse,
FinalizeRequest,
NoOp,
ProphetCodec,
UpdateDisputeStatus,
} from "../services/index.js";
Expand Down Expand Up @@ -149,12 +151,27 @@ export class EboActor {
while ((event = this.eventsQueue.pop())) {
this.lastEventProcessed = event;

const updateStateCommand = this.buildUpdateStateCommand(event);
let updateStateCommand: EboRegistryCommand;

updateStateCommand.run();
try {
updateStateCommand = this.buildUpdateStateCommand(event);
updateStateCommand.run();
} catch (err) {
if (err instanceof ProphetDecodingError) {
// Skipping malformed entities
this.logger.warn(err.message);

continue;
} else {
throw err;
}
}

try {
if (this.eventsQueue.isEmpty()) {
const wasLastEvent = this.eventsQueue.isEmpty();
const isDisputeEscalatedEvent = event.name === "DisputeEscalated";

if (wasLastEvent || isDisputeEscalatedEvent) {
// `event` is the last and most recent event thus
// it needs to run some RPCs to keep Prophet's flow going on
await this.onLastEvent(event);
Expand Down Expand Up @@ -207,23 +224,44 @@ export class EboActor {
this.registry,
);

case "ResponseDisputed":
return AddDispute.buildFromEvent(
event as EboEvent<"ResponseDisputed">,
this.registry,
);
case "ResponseDisputed": {
const disputeId = (event as EboEvent<"ResponseDisputed">).metadata.disputeId;
const dispute = this.registry.getDispute(disputeId);

// Prophet's might emit the DisputeEscalated event prior the ResponseDisputed
// event (starting from the 2nd dispute within an EBO request due to the
// BondEscalationModule behavior).
//
// This force the agent to add the Dispute when processing the DisputeEscalated
// event, causing this event to be a no-op.
return dispute
? NoOp.build()
: AddDispute.buildFromEvent(
event as EboEvent<"ResponseDisputed">,
this.registry,
);
}

case "DisputeStatusUpdated":
return UpdateDisputeStatus.buildFromEvent(
event as EboEvent<"DisputeStatusUpdated">,
this.registry,
);

case "DisputeEscalated":
return UpdateDisputeStatus.buildFromEvent(
event as EboEvent<"DisputeEscalated">,
this.registry,
);
case "DisputeEscalated": {
const disputeId = (event as EboEvent<"DisputeEscalated">).metadata.disputeId;
const dispute = this.registry.getDispute(disputeId);

return dispute
? UpdateDisputeStatus.buildFromEvent(
event as EboEvent<"DisputeEscalated">,
this.registry,
)
: AddDispute.buildFromEvent(
event as EboEvent<"DisputeEscalated">,
this.registry,
);
}

case "OracleRequestFinalized":
return FinalizeRequest.buildFromEvent(
Expand Down Expand Up @@ -391,12 +429,12 @@ export class EboActor {
private getActiveDisputes(): Dispute[] {
const disputes = this.registry.getDisputes();

return disputes.filter((dispute) => dispute.status === "Active");
return disputes.filter((dispute) => dispute.decodedData.status === "Active");
}

// TODO: extract this into another service
private canBeSettled(request: Request, dispute: Dispute, atTimestamp: UnixTimestamp): boolean {
if (dispute.status !== "Active") return false;
if (dispute.decodedData.status !== "Active") return false;

const { bondEscalationDeadline, tyingBuffer } = request.decodedData.disputeModuleData;
const deadline = (dispute.createdAt.timestamp +
Expand Down Expand Up @@ -499,7 +537,7 @@ export class EboActor {
// Response is still able to be disputed
if (atTimestamp <= disputeWindow) return false;

return dispute ? ["Lost", "None"].includes(dispute.status) : true;
return dispute ? ["Lost", "None"].includes(dispute.decodedData.status) : true;
}

/**
Expand Down Expand Up @@ -548,7 +586,7 @@ export class EboActor {
// the proposal non-active.
const activeStatus: DisputeStatus[] = ["None", "Active"];

return activeStatus.includes(dispute.status);
return activeStatus.includes(dispute.decodedData.status);
});
}

Expand Down Expand Up @@ -809,6 +847,12 @@ export class EboActor {
`Dispute ${event.metadata.disputeId} needs to be added to the internal registry.`,
);

if (dispute.decodedData.status === "Escalated") {
this.logger.warn(`Skipping dispute ${dispute.id} as it's already been escalated`);

return;
}

const request = this.getActorRequest();
const proposedResponse = this.registry.getResponse(event.metadata.responseId);

Expand Down Expand Up @@ -941,7 +985,7 @@ export class EboActor {
private async onDisputeStatusChanged(event: EboEvent<"DisputeStatusUpdated">): Promise<void> {
const request = this.getActorRequest();
const disputeId = event.metadata.disputeId;
const disputeStatus = event.metadata.status;
const disputeStatus = ProphetCodec.decodeDisputeStatus(event.metadata.status);

this.logger.info(`Dispute ${disputeId} status changed to ${disputeStatus}.`);

Expand Down Expand Up @@ -971,6 +1015,7 @@ export class EboActor {

private async onDisputeEscalated(event: EboEvent<"DisputeEscalated">) {
const request = this.getActorRequest();

this.logger.info(
`Dispute ${event.metadata.disputeId} for request ${request.id} has been escalated.`,
);
Expand Down
1 change: 1 addition & 0 deletions packages/automated-dispute/src/services/eboProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export class EboProcessor {
const currentEpoch = await this.protocolProvider.getCurrentEpoch();

this.logger.info(`Current epoch fetched.`);
this.logger.debug(stringify(currentEpoch));

return currentEpoch;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class AddDispute implements EboRegistryCommand {
) {}

public static buildFromEvent(
event: EboEvent<"ResponseDisputed">,
event: EboEvent<"ResponseDisputed" | "DisputeEscalated">,
registry: EboRegistry,
): AddDispute {
const dispute: Dispute = {
Expand All @@ -21,7 +21,9 @@ export class AddDispute implements EboRegistryCommand {
blockNumber: event.blockNumber,
logIndex: event.logIndex,
},
status: "Active",
decodedData: {
status: event.name === "ResponseDisputed" ? "Active" : "Escalated",
},
prophetData: event.metadata.dispute,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from "./addDispute.js";
export * from "./addRequest.js";
export * from "./addResponse.js";
export * from "./finalizeRequest.js";
export * from "./noOp.js";
export * from "./updateDisputeStatus.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { CommandAlreadyRun, CommandNotRun } from "../../../exceptions/index.js";
import { EboRegistryCommand } from "../../../interfaces/index.js";

export class NoOp implements EboRegistryCommand {
Copy link
Collaborator

@jahabeebs jahabeebs Oct 31, 2024

Choose a reason for hiding this comment

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

Might be good to have some docs in this class as I'm curious what you mean by noop in this context---I know that it means "do nothing" usually but why it's not immediately clear to me why we need the build() command. Also, do you think we need some logging in build() would be useful for when it's invoked in ResponseDisputed so we know when a new noop instance is returned?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We could technically make the constructor public and use new NoOp(); I wanted to keep the same approach the other commands are using, just for the sake of things consistent.

Seems like a wise decision to log the no-op "execution", I agree!

private wasRun: boolean = false;

private constructor() {}

public static build(): NoOp {
return new NoOp();
}

run(): void {
if (this.wasRun) throw new CommandAlreadyRun(NoOp.name);

this.wasRun = true;
}

undo(): void {
if (!this.wasRun) throw new CommandNotRun(NoOp.name);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CommandAlreadyRun, CommandNotRun, DisputeNotFound } from "../../../exceptions/index.js";
import { EboRegistry, EboRegistryCommand } from "../../../interfaces/index.js";
import { DisputeStatus, EboEvent, EboEventName } from "../../../types/index.js";
import { ProphetCodec } from "../../prophetCodec.js";

export class UpdateDisputeStatus implements EboRegistryCommand {
private wasRun: boolean = false;
Expand All @@ -19,7 +20,7 @@ export class UpdateDisputeStatus implements EboRegistryCommand {
const disputeId = event.metadata.disputeId;

const status = this.isDisputeStatusChangedEvent(event)
? event.metadata.status
? ProphetCodec.decodeDisputeStatus(event.metadata.status)
: "Escalated";

return new UpdateDisputeStatus(registry, disputeId, status);
Expand All @@ -38,7 +39,7 @@ export class UpdateDisputeStatus implements EboRegistryCommand {

if (!dispute) throw new DisputeNotFound(this.disputeId);

this.previousStatus = dispute.status;
this.previousStatus = dispute.decodedData.status;

this.registry.updateDisputeStatus(this.disputeId, this.status);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ export class EboMemoryRegistry implements EboRegistry {

this.disputes.set(disputeId, {
...dispute,
status: status,
decodedData: {
status: status,
},
});
}

Expand Down
Loading