Skip to content

Commit

Permalink
[feat]: Implement an evaluator service and controller (#48)
Browse files Browse the repository at this point in the history
* refactor: Extract evaluation logic into a standalone module/worker

* fix: Port handling

* fix: Evaluator response handling

* fix: Remove chainId check

* chore: Update config.example.yaml with the new evaluator config

* feat: Implement evaluator controller

* feat: Return errors on failed evaluation queries

* refactor: Minor evaluator types refactor

* feat: Add logging statments to the evaluator

* chore: Misspelt variable correction

* feat: Effectively disable evaluation when profitabilityFactor is 0

* fix: Evaluation calculation 0 division
  • Loading branch information
jsanmigimeno authored Jul 23, 2024
1 parent be45584 commit 03a2e38
Show file tree
Hide file tree
Showing 15 changed files with 1,251 additions and 452 deletions.
4 changes: 2 additions & 2 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ global:
retryInterval: 30000 # Time to wait before retrying a failed transaction
maxTries: 3 # Maximum tries for a transaction
maxPendingTransactions: 50 # Maximum number of transactions within the 'submit' pipeline.

# Evaluation properties
evaluationRetryInterval: 3600000 # Interval at which to reevaluate whether to relay a message.
maxEvaluationDuration: 86400000 # Time after which to drop an undelivered message.

evaluator:
verificationDeliveryGas: '55000' # Gas amount used for packet verification upon delivery.
unrewardedDeliveryGas: '25000' # Gas amount that will be unrewarded on delivery submission.
minDeliveryReward: 0.001 # In the 'pricingDenomination' specified below
Expand Down
30 changes: 20 additions & 10 deletions src/config/config.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const GLOBAL_SCHEMA = {
monitor: { $ref: "monitor-schema" },
getter: { $ref: "getter-schema" },
pricing: { $ref: "pricing-schema" },
evaluator: { $ref: "evaluator-schema" },
submitter: { $ref: "submitter-schema" },
persister: { $ref: "persister-schema" },
wallet: { $ref: "wallet-schema" },
Expand Down Expand Up @@ -134,6 +135,23 @@ export const PRICING_SCHEMA = {
additionalProperties: true // Allow for provider-specific configurations
}

const EVALUATOR_SCHEMA = {
$id: "evaluator-schema",
type: "object",
properties: {
unrewardedDeliveryGas: { $ref: "gas-field-schema" },
verificationDeliveryGas: { $ref: "gas-field-schema" },
minDeliveryReward: { $ref: "positive-number-schema" },
relativeMinDeliveryReward: { $ref: "positive-number-schema" },
unrewardedAckGas: { $ref: "gas-field-schema" },
verificationAckGas: { $ref: "gas-field-schema" },
minAckReward: { $ref: "positive-number-schema" },
relativeMinAckReward: { $ref: "positive-number-schema" },
profitabilityFactor: { $ref: "positive-number-schema" },
},
additionalProperties: false
}

const SUBMITTER_SCHEMA = {
$id: "submitter-schema",
type: "object",
Expand All @@ -147,18 +165,8 @@ const SUBMITTER_SCHEMA = {
maxTries: { $ref: "positive-number-schema" },
maxPendingTransactions: { $ref: "positive-number-schema" },

//TODO define 'evaluation' configuration somewhere else?
evaluationRetryInterval: { $ref: "positive-number-schema" },
maxEvaluationDuration: { $ref: "positive-number-schema" },
unrewardedDeliveryGas: { $ref: "gas-field-schema" },
verificationDeliveryGas: { $ref: "gas-field-schema" },
minDeliveryReward: { $ref: "positive-number-schema" },
relativeMinDeliveryReward: { $ref: "positive-number-schema" },
unrewardedAckGas: { $ref: "gas-field-schema" },
verificationAckGas: { $ref: "gas-field-schema" },
minAckReward: { $ref: "positive-number-schema" },
relativeMinAckReward: { $ref: "positive-number-schema" },
profitabilityFactor: { $ref: "positive-number-schema" },
},
additionalProperties: false
}
Expand Down Expand Up @@ -245,6 +253,7 @@ const CHAINS_SCHEMA = {
monitor: { $ref: "monitor-schema" },
getter: { $ref: "getter-schema" },
pricing: { $ref: "pricing-schema" },
evaluator: { $ref: "evaluator-schema" },
submitter: { $ref: "submitter-schema" },
wallet: { $ref: "wallet-schema" },
},
Expand All @@ -269,6 +278,7 @@ export function getConfigValidator(): AnyValidateFunction<unknown> {
ajv.addSchema(MONITOR_SCHEMA);
ajv.addSchema(GETTER_SCHEMA);
ajv.addSchema(PRICING_SCHEMA);
ajv.addSchema(EVALUATOR_SCHEMA);
ajv.addSchema(SUBMITTER_SCHEMA);
ajv.addSchema(PERSISTER_SCHEMA);
ajv.addSchema(WALLET_SCHEMA);
Expand Down
16 changes: 13 additions & 3 deletions src/config/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { readFileSync } from 'fs';
import * as yaml from 'js-yaml';
import dotenv from 'dotenv';
import { PRICING_SCHEMA, getConfigValidator } from './config.schema';
import { GlobalConfig, ChainConfig, AMBConfig, GetterGlobalConfig, SubmitterGlobalConfig, PersisterConfig, WalletGlobalConfig, GetterConfig, SubmitterConfig, WalletConfig, MonitorConfig, MonitorGlobalConfig, PricingConfig, PricingGlobalConfig } from './config.types';
import { GlobalConfig, ChainConfig, AMBConfig, GetterGlobalConfig, SubmitterGlobalConfig, PersisterConfig, WalletGlobalConfig, GetterConfig, SubmitterConfig, WalletConfig, MonitorConfig, MonitorGlobalConfig, PricingConfig, PricingGlobalConfig, EvaluatorGlobalConfig, EvaluatorConfig } from './config.types';
import { JsonRpcProvider } from 'ethers6';

@Injectable()
Expand Down Expand Up @@ -101,6 +101,7 @@ export class ConfigService {
monitor: this.formatMonitorGlobalConfig(rawGlobalConfig.monitor),
getter: this.formatGetterGlobalConfig(rawGlobalConfig.getter),
pricing: this.formatPricingGlobalConfig(rawGlobalConfig.pricing),
evaluator: this.formatEvaluatorGlobalConfig(rawGlobalConfig.evaluator),
submitter: this.formatSubmitterGlobalConfig(rawGlobalConfig.submitter),
persister: this.formatPersisterGlobalConfig(rawGlobalConfig.persister),
wallet: this.formatWalletGlobalConfig(rawGlobalConfig.wallet),
Expand All @@ -122,6 +123,7 @@ export class ConfigService {
monitor: this.formatMonitorConfig(rawChainConfig.monitor),
getter: this.formatGetterConfig(rawChainConfig.getter),
pricing: this.formatPricingConfig(rawChainConfig.pricing),
evaluator: this.formatEvaluatorConfig(rawChainConfig.evaluator),
submitter: this.formatSubmitterConfig(rawChainConfig.submitter),
wallet: this.formatWalletConfig(rawChainConfig.wallet),
});
Expand Down Expand Up @@ -227,7 +229,7 @@ export class ConfigService {
return formattedConfig as PricingGlobalConfig;
}

private formatSubmitterGlobalConfig(rawConfig: any): SubmitterGlobalConfig {
private formatEvaluatorGlobalConfig(rawConfig: any): EvaluatorGlobalConfig {
const config = { ...rawConfig };
if (config.unrewardedDeliveryGas != undefined) {
config.unrewardedDeliveryGas = BigInt(config.unrewardedDeliveryGas);
Expand All @@ -241,7 +243,11 @@ export class ConfigService {
if (config.verificationAckGas != undefined) {
config.verificationAckGas = BigInt(config.verificationAckGas);
}
return config as SubmitterGlobalConfig;
return config as EvaluatorGlobalConfig;
}

private formatSubmitterGlobalConfig(rawConfig: any): SubmitterGlobalConfig {
return { ...rawConfig } as SubmitterGlobalConfig;
}

private formatPersisterGlobalConfig(rawConfig: any): PersisterConfig {
Expand Down Expand Up @@ -278,6 +284,10 @@ export class ConfigService {
return this.formatPricingGlobalConfig(rawConfig);
}

private formatEvaluatorConfig(rawConfig: any): EvaluatorConfig {
return this.formatEvaluatorGlobalConfig(rawConfig);
}

private formatSubmitterConfig(rawConfig: any): SubmitterConfig {
return this.formatSubmitterGlobalConfig(rawConfig);
}
Expand Down
25 changes: 16 additions & 9 deletions src/config/config.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface GlobalConfig {
monitor: MonitorGlobalConfig;
getter: GetterGlobalConfig;
pricing: PricingGlobalConfig;
evaluator: EvaluatorGlobalConfig;
submitter: SubmitterGlobalConfig;
persister: PersisterConfig;
wallet: WalletGlobalConfig;
Expand Down Expand Up @@ -38,6 +39,20 @@ export interface PricingGlobalConfig {

export interface PricingConfig extends PricingGlobalConfig {}

export interface EvaluatorGlobalConfig {
unrewardedDeliveryGas?: bigint;
verificationDeliveryGas?: bigint;
minDeliveryReward?: number;
relativeMinDeliveryReward?: number;
unrewardedAckGas?: bigint;
verificationAckGas?: bigint;
minAckReward?: number;
relativeMinAckReward?: number;
profitabilityFactor?: number;
}

export interface EvaluatorConfig extends EvaluatorGlobalConfig {}

export interface SubmitterGlobalConfig {
enabled?: boolean;
newOrdersDelay?: number;
Expand All @@ -48,15 +63,6 @@ export interface SubmitterGlobalConfig {

evaluationRetryInterval?: number;
maxEvaluationDuration?: number;
unrewardedDeliveryGas?: bigint;
verificationDeliveryGas?: bigint;
minDeliveryReward?: number;
relativeMinDeliveryReward?: number;
unrewardedAckGas?: bigint;
verificationAckGas?: bigint;
minAckReward?: number;
relativeMinAckReward?: number;
profitabilityFactor?: number;
}

export interface SubmitterConfig extends SubmitterGlobalConfig {}
Expand Down Expand Up @@ -103,6 +109,7 @@ export interface ChainConfig {
monitor: MonitorConfig;
getter: GetterConfig;
pricing: PricingConfig;
evaluator: EvaluatorConfig;
submitter: SubmitterConfig;
wallet: WalletConfig;
}
113 changes: 113 additions & 0 deletions src/evaluator/evaluator.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { BadRequestException, Controller, Get, OnModuleInit, Query } from "@nestjs/common";
import { EvaluatorInterface } from "./evaluator.interface";
import { EvaluatorService } from "./evaluator.service";
import { EvaluateAckQuery, EvaluateAckQueryResponse, EvaluateDeliveryQuery, EvaluteDeliveryQueryResponse } from "./evaluator.types";

@Controller()
export class EvaluatorController implements OnModuleInit {
private evaluator!: EvaluatorInterface;

constructor(
private readonly evaluatorService: EvaluatorService,
) {}

async onModuleInit() {
await this.initializeEvaluatorInterface();
}

private async initializeEvaluatorInterface(): Promise<void> {
const port = await this.evaluatorService.attachToEvaluator();
this.evaluator = new EvaluatorInterface(port);
}

@Get('evaluateDelivery')
async evaluateDelivery(@Query() query: EvaluateDeliveryQuery): Promise<any> {

//TODO validate query format
const result = await this.evaluator.evaluateDelivery(
query.chainId,
query.messageIdentifier,
{
gasEstimate: BigInt(query.gasEstimate),
observedGasEstimate: BigInt(query.observedGasEstimate),
additionalFeeEstimate: BigInt(query.additionalFeeEstimate),
},
BigInt(query.value)
);

if (result.evaluation == undefined) {
throw new BadRequestException('Failed to generate an evaluation output for the given parameters.');
}

const response: EvaluteDeliveryQueryResponse = {
chainId: result.chainId,
messageIdentifier: result.messageIdentifier,
maxGasDelivery: result.evaluation.maxGasDelivery.toString(),
maxGasAck: result.evaluation.maxGasAck.toString(),
gasEstimate: result.evaluation.gasEstimate.toString(),
observedGasEstimate: result.evaluation.observedGasEstimate.toString(),
additionalFeeEstimate: result.evaluation.additionalFeeEstimate.toString(),
destinationGasPrice: result.evaluation.destinationGasPrice.toString(),
value: result.evaluation.value.toString(),
sourceGasPrice: result.evaluation.sourceGasPrice.toString(),
deliveryCost: result.evaluation.deliveryCost.toString(),
deliveryReward: result.evaluation.deliveryReward.toString(),
maxAckLoss: result.evaluation.maxAckLoss.toString(),
deliveryFiatCost: result.evaluation.deliveryFiatCost,
deliveryFiatReward: result.evaluation.deliveryFiatReward,
securedDeliveryFiatReward: result.evaluation.securedDeliveryFiatReward,
profitabilityFactor: result.evaluation.profitabilityFactor,
securedDeliveryFiatProfit: result.evaluation.securedDeliveryFiatProfit,
securedDeliveryRelativeProfit: result.evaluation.securedDeliveryRelativeProfit,
minDeliveryReward: result.evaluation.minDeliveryReward,
relativeMinDeliveryReward: result.evaluation.relativeMinDeliveryReward,
relayDelivery: result.evaluation.relayDelivery,
}

return response;
}

@Get('evaluateAck')
async evaluateAck(@Query() query: EvaluateAckQuery): Promise<any> {

//TODO validate query format
const result = await this.evaluator.evaluateAck(
query.chainId,
query.messageIdentifier,
{
gasEstimate: BigInt(query.gasEstimate),
observedGasEstimate: BigInt(query.observedGasEstimate),
additionalFeeEstimate: BigInt(query.additionalFeeEstimate),
},
BigInt(query.value)
);

if (result.evaluation == undefined) {
throw new BadRequestException('Failed to generate an evaluation output for the given parameters.');
}

const response: EvaluateAckQueryResponse = {
chainId: result.chainId,
messageIdentifier: result.messageIdentifier,
maxGasDelivery: result.evaluation.maxGasDelivery.toString(),
maxGasAck: result.evaluation.maxGasAck.toString(),
gasEstimate: result.evaluation.gasEstimate.toString(),
observedGasEstimate: result.evaluation.observedGasEstimate.toString(),
additionalFeeEstimate: result.evaluation.additionalFeeEstimate.toString(),
sourceGasPrice: result.evaluation.sourceGasPrice.toString(),
ackCost: result.evaluation.ackCost.toString(),
ackReward: result.evaluation.ackReward.toString(),
profitabilityFactor: result.evaluation.profitabilityFactor,
ackFiatProfit: result.evaluation.ackFiatProfit,
ackRelativeProfit: result.evaluation.ackRelativeProfit,
minAckReward: result.evaluation.minAckReward,
relativeMinAckReward: result.evaluation.relativeMinAckReward,
deliveryCost: result.evaluation.deliveryCost.toString(),
deliveryReward: result.evaluation.deliveryReward.toString(),
relayAckForDeliveryBounty: result.evaluation.relayAckForDeliveryBounty,
relayAck: result.evaluation.relayAck,
}

return response;
}
}
74 changes: 74 additions & 0 deletions src/evaluator/evaluator.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { GasEstimateComponents } from "src/resolvers/resolver";
import { MessagePort } from "worker_threads";
import { EvaluateAckMessage, EvaluateAckResponseMessage, EvaluateDeliveryMessage, EvaluateDeliveryResponseMessage, EvaluatorMessage, EvaluatorMessageType, EvaluatorPortData } from "./evaluator.types";


export class EvaluatorInterface {
private portMessageId = 0;

constructor(private readonly port: MessagePort) {}

private getNextPortMessageId(): number {
return this.portMessageId++;
}

private async submitMessage(message: EvaluatorMessage): Promise<any> {

const messageId = this.getNextPortMessageId();

const data: EvaluatorPortData = {
messageId,
message
};

const resultPromise = new Promise<any>(resolve => {
const listener = (responseData: EvaluatorPortData) => {
if (responseData.messageId === messageId) {
this.port.off("message", listener);
resolve(responseData.message)
}
}
this.port.on("message", listener);

this.port.postMessage(data);
});

return resultPromise;
}

async evaluateDelivery(
chainId: string,
messageIdentifier: string,
gasEstimateComponents: GasEstimateComponents,
value: bigint,
): Promise<EvaluateDeliveryResponseMessage> {

const message: EvaluateDeliveryMessage = {
type: EvaluatorMessageType.EvaluateDelivery,
chainId,
messageIdentifier,
gasEstimateComponents,
value
};

return this.submitMessage(message);
}

async evaluateAck(
chainId: string,
messageIdentifier: string,
gasEstimateComponents: GasEstimateComponents,
value: bigint,
): Promise<EvaluateAckResponseMessage> {

const message: EvaluateAckMessage = {
type: EvaluatorMessageType.EvaluateAck,
chainId,
messageIdentifier,
gasEstimateComponents,
value
};

return this.submitMessage(message);
}
}
14 changes: 14 additions & 0 deletions src/evaluator/evaluator.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Global, Module } from '@nestjs/common';
import { PricingModule } from './../pricing/pricing.module';
import { WalletModule } from 'src/wallet/wallet.module';
import { EvaluatorService } from './evaluator.service';
import { EvaluatorController } from './evaluator.controller';

@Global()
@Module({
controllers: [EvaluatorController],
providers: [EvaluatorService],
exports: [EvaluatorService],
imports: [PricingModule, WalletModule],
})
export class EvaluatorModule {}
Loading

0 comments on commit 03a2e38

Please sign in to comment.