diff --git a/config.example.yaml b/config.example.yaml index 5744713..19d0fd2 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -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 diff --git a/src/config/config.schema.ts b/src/config/config.schema.ts index 7d3c861..8cd5449 100644 --- a/src/config/config.schema.ts +++ b/src/config/config.schema.ts @@ -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" }, @@ -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", @@ -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 } @@ -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" }, }, @@ -269,6 +278,7 @@ export function getConfigValidator(): AnyValidateFunction { 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); diff --git a/src/config/config.service.ts b/src/config/config.service.ts index f38a584..f403a3e 100644 --- a/src/config/config.service.ts +++ b/src/config/config.service.ts @@ -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() @@ -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), @@ -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), }); @@ -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); @@ -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 { @@ -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); } diff --git a/src/config/config.types.ts b/src/config/config.types.ts index 4312c44..0fb14b0 100644 --- a/src/config/config.types.ts +++ b/src/config/config.types.ts @@ -5,6 +5,7 @@ export interface GlobalConfig { monitor: MonitorGlobalConfig; getter: GetterGlobalConfig; pricing: PricingGlobalConfig; + evaluator: EvaluatorGlobalConfig; submitter: SubmitterGlobalConfig; persister: PersisterConfig; wallet: WalletGlobalConfig; @@ -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; @@ -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 {} @@ -103,6 +109,7 @@ export interface ChainConfig { monitor: MonitorConfig; getter: GetterConfig; pricing: PricingConfig; + evaluator: EvaluatorConfig; submitter: SubmitterConfig; wallet: WalletConfig; } diff --git a/src/evaluator/evaluator.controller.ts b/src/evaluator/evaluator.controller.ts new file mode 100644 index 0000000..dcc8253 --- /dev/null +++ b/src/evaluator/evaluator.controller.ts @@ -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 { + const port = await this.evaluatorService.attachToEvaluator(); + this.evaluator = new EvaluatorInterface(port); + } + + @Get('evaluateDelivery') + async evaluateDelivery(@Query() query: EvaluateDeliveryQuery): Promise { + + //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 { + + //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; + } +} diff --git a/src/evaluator/evaluator.interface.ts b/src/evaluator/evaluator.interface.ts new file mode 100644 index 0000000..daa14f3 --- /dev/null +++ b/src/evaluator/evaluator.interface.ts @@ -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 { + + const messageId = this.getNextPortMessageId(); + + const data: EvaluatorPortData = { + messageId, + message + }; + + const resultPromise = new Promise(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 { + + 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 { + + const message: EvaluateAckMessage = { + type: EvaluatorMessageType.EvaluateAck, + chainId, + messageIdentifier, + gasEstimateComponents, + value + }; + + return this.submitMessage(message); + } +} diff --git a/src/evaluator/evaluator.module.ts b/src/evaluator/evaluator.module.ts new file mode 100644 index 0000000..1f10d24 --- /dev/null +++ b/src/evaluator/evaluator.module.ts @@ -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 {} diff --git a/src/evaluator/evaluator.service.ts b/src/evaluator/evaluator.service.ts new file mode 100644 index 0000000..918f2a2 --- /dev/null +++ b/src/evaluator/evaluator.service.ts @@ -0,0 +1,174 @@ +import { Global, Injectable, OnModuleInit } from "@nestjs/common"; +import { join } from "path"; +import { Worker, MessagePort } from 'worker_threads'; +import { tryErrorToString } from "src/common/utils"; +import { ConfigService } from "src/config/config.service"; +import { LoggerService, STATUS_LOG_INTERVAL } from "src/logger/logger.service"; +import { EvaluatorGetPortResponse, EvaluatorGetPortMessage, EvaluationConfig, EvaluatorWorkerData, EVALUATOR_DEFAULT_MIN_ACK_REWARD, EVALUATOR_DEFAULT_MIN_DELIVERY_REWARD, EVALUATOR_DEFAULT_PROFITABILITY_FACTOR, EVALUATOR_DEFAULT_RELATIVE_MIN_ACK_REWARD, EVALUATOR_DEFAULT_RELATIVE_MIN_DELIVERY_REWARD, EVALUATOR_DEFAULT_UNREWARDED_ACK_GAS, EVALUATOR_DEFAULT_UNREWARDED_DELIVERY_GAS, EVALUATOR_DEFAULT_VERIFICATION_ACK_GAS, EVALUATOR_DEFAULT_VERIFICATION_DELIVERY_GAS } from "./evaluator.types"; +import { PricingService } from "src/pricing/pricing.service"; +import { WalletService } from "src/wallet/wallet.service"; + + +@Global() +@Injectable() +export class EvaluatorService implements OnModuleInit { + private worker: Worker | null = null; + private requestPortMessageId = 0; + + private setReady!: () => void; + readonly isReady: Promise; + + constructor( + private readonly configService: ConfigService, + private readonly pricingService: PricingService, + private readonly walletService: WalletService, + private readonly loggerService: LoggerService, + ) { + this.isReady = this.initializeIsReady(); + } + + async onModuleInit() { + this.loggerService.info(`Starting Evaluator worker...`); + + await this.initializeWorker(); + + this.initiateIntervalStatusLog(); + + this.setReady(); + } + + private initializeIsReady(): Promise { + return new Promise((resolve) => { + this.setReady = resolve; + }); + } + + private async initializeWorker(): Promise { + const workerData = await this.loadWorkerConfig(); + + this.worker = new Worker(join(__dirname, 'evaluator.worker.js'), { + workerData, + transferList: [ + workerData.pricingPort, + workerData.walletPort + ] + }); + + this.worker.on('error', (error) => { + this.loggerService.fatal( + { error: tryErrorToString(error) }, + `Error on evaluator worker.`, + ); + }); + + this.worker.on('exit', (exitCode) => { + this.worker = null; + this.loggerService.fatal( + { exitCode }, + `Evaluator worker exited.`, + ); + }); + } + + private async loadWorkerConfig(): Promise { + const globalEvaluatorConfig = this.configService.globalConfig.evaluator; + + const evaluationConfigs: Record = {}; + + for (const [chainId, chainConfig] of this.configService.chainsConfig) { + const chainEvaluatorConfig = chainConfig.evaluator; + + const chainEvaluationConfig: EvaluationConfig = { + + unrewardedDeliveryGas: chainEvaluatorConfig.unrewardedDeliveryGas + ?? globalEvaluatorConfig.unrewardedDeliveryGas + ?? EVALUATOR_DEFAULT_UNREWARDED_DELIVERY_GAS, + + verificationDeliveryGas: chainEvaluatorConfig.verificationDeliveryGas + ?? globalEvaluatorConfig.verificationDeliveryGas + ?? EVALUATOR_DEFAULT_VERIFICATION_DELIVERY_GAS, + + minDeliveryReward: chainEvaluatorConfig.minDeliveryReward + ?? globalEvaluatorConfig.minDeliveryReward + ?? EVALUATOR_DEFAULT_MIN_DELIVERY_REWARD, + + relativeMinDeliveryReward: chainEvaluatorConfig.relativeMinDeliveryReward + ?? globalEvaluatorConfig.relativeMinDeliveryReward + ?? EVALUATOR_DEFAULT_RELATIVE_MIN_DELIVERY_REWARD, + + unrewardedAckGas: chainEvaluatorConfig.unrewardedAckGas + ?? globalEvaluatorConfig.unrewardedAckGas + ?? EVALUATOR_DEFAULT_UNREWARDED_ACK_GAS, + + verificationAckGas: chainEvaluatorConfig.verificationAckGas + ?? globalEvaluatorConfig.verificationAckGas + ?? EVALUATOR_DEFAULT_VERIFICATION_ACK_GAS, + + minAckReward: chainEvaluatorConfig.minAckReward + ?? globalEvaluatorConfig.minAckReward + ?? EVALUATOR_DEFAULT_MIN_ACK_REWARD, + + relativeMinAckReward: chainEvaluatorConfig.relativeMinAckReward + ?? globalEvaluatorConfig.relativeMinAckReward + ?? EVALUATOR_DEFAULT_RELATIVE_MIN_ACK_REWARD, + + profitabilityFactor: chainEvaluatorConfig.profitabilityFactor + ?? globalEvaluatorConfig.profitabilityFactor + ?? EVALUATOR_DEFAULT_PROFITABILITY_FACTOR, + + } + + evaluationConfigs[chainId] = chainEvaluationConfig; + } + + return { + evaluationConfigs, + pricingPort: await this.pricingService.attachToPricing(), + walletPort: await this.walletService.attachToWallet(), + loggerOptions: this.loggerService.loggerOptions + } + } + + private initiateIntervalStatusLog(): void { + const logStatus = () => { + const isActive = this.worker != null; + this.loggerService.info( + { isActive }, + 'Evaluator worker status.' + ); + }; + setInterval(logStatus, STATUS_LOG_INTERVAL); + } + + + private getNextRequestPortMessageId(): number { + return this.requestPortMessageId++; + } + + async attachToEvaluator(): Promise { + + await this.isReady; + + const worker = this.worker; + if (worker == undefined) { + throw new Error(`Evaluator worker is null.`); + } + + const messageId = this.getNextRequestPortMessageId(); + const portPromise = new Promise((resolve) => { + const listener = (data: EvaluatorGetPortResponse) => { + if (data.messageId === messageId) { + worker.off("message", listener); + resolve(data.port); + } + }; + worker.on("message", listener); + + const portMessage: EvaluatorGetPortMessage = { messageId }; + worker.postMessage(portMessage); + }); + + return portPromise; + } + +} diff --git a/src/evaluator/evaluator.types.ts b/src/evaluator/evaluator.types.ts new file mode 100644 index 0000000..3be2697 --- /dev/null +++ b/src/evaluator/evaluator.types.ts @@ -0,0 +1,222 @@ +import { LoggerOptions } from "pino"; +import { GasEstimateComponents } from "src/resolvers/resolver"; +import { MessagePort } from "worker_threads"; + + + +// Constants +// ************************************************************************************************ + +export const EVALUATOR_DEFAULT_UNREWARDED_DELIVERY_GAS = 0n; +export const EVALUATOR_DEFAULT_VERIFICATION_DELIVERY_GAS = 0n; +export const EVALUATOR_DEFAULT_MIN_DELIVERY_REWARD = 0; +export const EVALUATOR_DEFAULT_RELATIVE_MIN_DELIVERY_REWARD = 0; +export const EVALUATOR_DEFAULT_UNREWARDED_ACK_GAS = 0n; +export const EVALUATOR_DEFAULT_VERIFICATION_ACK_GAS = 0n; +export const EVALUATOR_DEFAULT_MIN_ACK_REWARD = 0; +export const EVALUATOR_DEFAULT_RELATIVE_MIN_ACK_REWARD = 0; +export const EVALUATOR_DEFAULT_PROFITABILITY_FACTOR = 1; + + + +// Config and Worker Types +// ************************************************************************************************ + +export interface EvaluationConfig { + + unrewardedDeliveryGas: bigint; + verificationDeliveryGas: bigint; + minDeliveryReward: number; + relativeMinDeliveryReward: number, + + unrewardedAckGas: bigint; + verificationAckGas: bigint; + minAckReward: number; + relativeMinAckReward: number; + + profitabilityFactor: number; +} + +export interface EvaluatorWorkerData { + evaluationConfigs: Record; + pricingPort: MessagePort; + walletPort: MessagePort; + loggerOptions: LoggerOptions; +} + + + +// Port Channels Types +// ************************************************************************************************ +export interface EvaluatorGetPortMessage { + messageId: number; +} + +export interface EvaluatorGetPortResponse { + messageId: number; + port: MessagePort; +} + + +export interface EvaluatorPortData { + messageId: number; + message: EvaluatorMessage; +} + +export enum EvaluatorMessageType { + EvaluateDelivery, + EvaluateDeliveryResponse, + EvaluateAck, + EvaluateAckResponse, + EmptyResponse, +} + +export type EvaluatorMessage = EvaluateDeliveryMessage + | EvaluateDeliveryResponseMessage + | EvaluateAckMessage + | EvaluateAckResponseMessage + | EmptyResponseMessage; + + +export interface EvaluateDeliveryMessage { + type: EvaluatorMessageType.EvaluateDelivery; + chainId: string; + messageIdentifier: string; + gasEstimateComponents: GasEstimateComponents; + value: bigint; +} + +export interface EvaluateDeliveryResponseMessage { + type: EvaluatorMessageType.EvaluateDeliveryResponse; + chainId: string; + messageIdentifier: string; + evaluation: { + maxGasDelivery: bigint; + maxGasAck: bigint; + gasEstimate: bigint; + observedGasEstimate: bigint; + additionalFeeEstimate: bigint; + destinationGasPrice: bigint; + value: bigint; + sourceGasPrice: bigint; + deliveryCost: bigint; + deliveryReward: bigint; + maxAckLoss: bigint; + deliveryFiatCost: number; + deliveryFiatReward: number; + securedDeliveryFiatReward: number; + profitabilityFactor: number; + securedDeliveryFiatProfit: number; + securedDeliveryRelativeProfit: number; + minDeliveryReward: number; + relativeMinDeliveryReward: number; + relayDelivery: boolean; + } | null; +} + +export interface EvaluateAckMessage { + type: EvaluatorMessageType.EvaluateAck; + chainId: string; + messageIdentifier: string; + gasEstimateComponents: GasEstimateComponents; + value: bigint; +} + +export interface EvaluateAckResponseMessage { + type: EvaluatorMessageType.EvaluateAckResponse; + chainId: string; + messageIdentifier: string; + evaluation: { + maxGasDelivery: bigint; + maxGasAck: bigint; + gasEstimate: bigint; + observedGasEstimate: bigint; + additionalFeeEstimate: bigint; + sourceGasPrice: bigint; + ackCost: bigint; + ackReward: bigint; + profitabilityFactor: number; + ackFiatProfit: number; + ackRelativeProfit: number; + minAckReward: number; + relativeMinAckReward: number; + deliveryCost: bigint; + deliveryReward: bigint; + relayAckForDeliveryBounty: boolean; + relayAck: boolean; + } | null; +} + +export interface EmptyResponseMessage { + type: EvaluatorMessageType.EmptyResponse; +} + + + +// Controller Types +// ************************************************************************************************ + +export interface EvaluateDeliveryQuery { + chainId: string; + messageIdentifier: string; + gasEstimate: string; + observedGasEstimate: string; + additionalFeeEstimate: string; + value: string; +} + +export interface EvaluteDeliveryQueryResponse { + chainId: string; + messageIdentifier: string; + maxGasDelivery: string; + maxGasAck: string; + gasEstimate: string; + observedGasEstimate: string; + additionalFeeEstimate: string; + destinationGasPrice: string; + value: string; + sourceGasPrice: string; + deliveryCost: string; + deliveryReward: string; + maxAckLoss: string; + deliveryFiatCost: number; + deliveryFiatReward: number; + securedDeliveryFiatReward: number; + profitabilityFactor: number; + securedDeliveryFiatProfit: number; + securedDeliveryRelativeProfit: number; + minDeliveryReward: number; + relativeMinDeliveryReward: number; + relayDelivery: boolean; +} + +export interface EvaluateAckQuery { + chainId: string; + messageIdentifier: string; + gasEstimate: string; + observedGasEstimate: string; + additionalFeeEstimate: string; + value: string; +} + +export interface EvaluateAckQueryResponse { + chainId: string; + messageIdentifier: string; + maxGasDelivery: string; + maxGasAck: string; + gasEstimate: string; + observedGasEstimate: string; + additionalFeeEstimate: string; + sourceGasPrice: string; + ackCost: string; + ackReward: string; + profitabilityFactor: number; + ackFiatProfit: number; + ackRelativeProfit: number; + minAckReward: number; + relativeMinAckReward: number; + deliveryCost: string; + deliveryReward: string; + relayAckForDeliveryBounty: boolean; + relayAck: boolean; +} diff --git a/src/evaluator/evaluator.worker.ts b/src/evaluator/evaluator.worker.ts new file mode 100644 index 0000000..8516e71 --- /dev/null +++ b/src/evaluator/evaluator.worker.ts @@ -0,0 +1,549 @@ +import pino from "pino"; +import { parentPort, workerData, MessagePort, MessageChannel } from "worker_threads"; +import { EvaluateAckResponseMessage, EvaluateDeliveryResponseMessage, EvaluatorGetPortMessage, EvaluatorGetPortResponse, EvaluatorMessage, EvaluatorMessageType, EvaluatorPortData, EvaluatorWorkerData } from "./evaluator.types"; +import { Store } from "src/store/store.lib"; +import { GasEstimateComponents } from "src/resolvers/resolver"; +import { BytesLike, MaxUint256 } from "ethers6"; +import { WalletInterface } from "src/wallet/wallet.interface"; +import { PricingInterface } from "src/pricing/pricing.interface"; +import { MessageContext, ParsePayload } from "src/payload/decode.payload"; +import { tryErrorToString } from "src/common/utils"; + + +const DECIMAL_BASE = 10_000; +const DECIMAL_BASE_BIG_INT = BigInt(DECIMAL_BASE); + +class EvaluatorWorker { + private readonly config: EvaluatorWorkerData; + + private readonly logger: pino.Logger; + + private readonly store: Store; + + private readonly pricing: PricingInterface; + private readonly wallet: WalletInterface; + + private portsCount = 0; + private readonly ports: Record = {}; + + constructor() { + this.config = workerData as EvaluatorWorkerData; + + this.store = new Store(); + + this.pricing = new PricingInterface(this.config.pricingPort); + this.wallet = new WalletInterface(this.config.walletPort); + + this.logger = this.initializeLogger(); + + this.initializePorts(); + } + + + + // Initialization helpers + // ******************************************************************************************** + + private initializeLogger(): pino.Logger { + return pino(this.config.loggerOptions).child({ + worker: 'evaluator' + }); + } + + private initializePorts(): void { + parentPort!.on('message', (message: EvaluatorGetPortMessage) => { + const port = this.registerNewPort(); + const response: EvaluatorGetPortResponse = { + messageId: message.messageId, + port + }; + parentPort!.postMessage(response, [port]); + }); + } + + private registerNewPort(): MessagePort { + + const portId = this.portsCount++; + + const { port1, port2 } = new MessageChannel(); + + port1.on('message', (message: EvaluatorPortData) => { + void this.processRequest(message) + .then((response) => port1.postMessage(response)); + }) + + this.ports[portId] = port1; + + return port2; + } + + private async processRequest(data: EvaluatorPortData): Promise { + const messageType = data.message.type; + let returnData: EvaluatorMessage | null = null; + try { + switch (messageType) { + case EvaluatorMessageType.EvaluateDelivery: + returnData = await this.evaluateDelivery( + data.message.chainId, + data.message.messageIdentifier, + data.message.gasEstimateComponents, + data.message.value, + ); + break; + case EvaluatorMessageType.EvaluateAck: + returnData = await this.evaluateAck( + data.message.chainId, + data.message.messageIdentifier, + data.message.gasEstimateComponents, + data.message.value, + ); + break; + default: + this.logger.error( + { + messageType, + request: data + }, + `Unable to handle evaluator request: unknown message type` + ); + } + + } + catch (error) { + this.logger.error( + { + messageType, + request: data, + error: tryErrorToString(error), + }, + `Error on evaluator request processing.` + ); + } + + return { + messageId: data.messageId, + message: returnData ?? { type: EvaluatorMessageType.EmptyResponse }, + } + } + + + private async evaluateDelivery( + chainId: string, + messageIdentifier: string, + gasEstimateComponents: GasEstimateComponents, + value: bigint + ): Promise { + + const response: EvaluateDeliveryResponseMessage = { + type: EvaluatorMessageType.EvaluateDeliveryResponse, + chainId, + messageIdentifier, + evaluation: null, + }; + + const evaluationConfig = this.config.evaluationConfigs[chainId]; + if (evaluationConfig == null) { + this.logger.info( + { + chainId, + messageIdentifier, + }, + `Unable to perform delivery evaluation: no evaluation config found for the 'chainId' provided.` + ); + // Send a 'null' evaluation response + return response; + } + + const relayState = await this.store.getRelayState(messageIdentifier); + // TODO ideally the check `relayState.toChainId != chainId` would be performed at this point + // for extra precaution, but with the current implementation the `toChainId` field is not + // available until the message is delivered. + if (relayState?.bountyPlacedEvent == null) { + this.logger.info( + { + chainId, + messageIdentifier, + }, + `Unable to perform delivery evaluation: no BountyPlaced information found for the 'messageIdentifier' provided.` + ); + // Send a 'null' evaluation response + return response; + } + + const { + gasEstimate, + observedGasEstimate, + additionalFeeEstimate + } = gasEstimateComponents; + + const destinationGasPrice = await this.getGasPrice(chainId); + const sourceGasPrice = await this.getGasPrice(relayState.bountyPlacedEvent.fromChainId); + + const bountyPlacedEvent = relayState?.bountyPlacedEvent; + const priceOfDeliveryGas = relayState.bountyIncreasedEvent?.newDeliveryGasPrice + ?? bountyPlacedEvent.priceOfDeliveryGas; + const priceOfAckGas = relayState.bountyIncreasedEvent?.newAckGasPrice + ?? bountyPlacedEvent.priceOfAckGas; + + const deliveryCost = this.calcGasCost( // ! In destination chain gas value + gasEstimate, + destinationGasPrice, + additionalFeeEstimate + value + ); + + const deliveryReward = this.calcGasReward( // ! In source chain gas value + observedGasEstimate, + evaluationConfig.unrewardedDeliveryGas, + bountyPlacedEvent.maxGasDelivery, + priceOfDeliveryGas + ); + + const maxAckLoss = this.calcMaxGasLoss( // ! In source chain gas value + sourceGasPrice, + evaluationConfig.unrewardedAckGas, + evaluationConfig.verificationAckGas, + bountyPlacedEvent.maxGasAck, + priceOfAckGas, + ); + + + // Compute the cost and reward of the message delivery in Fiat and evaluate the message + // delivery profit. + const deliveryFiatCost = await this.getGasCostFiatPrice( + deliveryCost, + chainId + ); + + const adjustedDeliveryReward = evaluationConfig.profitabilityFactor == 0 + ? MaxUint256 + : deliveryReward * DECIMAL_BASE_BIG_INT + / BigInt(evaluationConfig.profitabilityFactor * DECIMAL_BASE); + + const securedDeliveryReward = adjustedDeliveryReward + maxAckLoss; + + const securedDeliveryFiatReward = await this.getGasCostFiatPrice( + securedDeliveryReward, + bountyPlacedEvent.fromChainId + ); + + // Compute the 'deliveryFiatReward' for logging purposes (i.e. without the 'maxAckLoss' factor) + // If `adjustedDeliveryReward` is 0, then `maxAckLoss` is the sole contributor to + // `securedDeliveryFiatReward`, and thus `deliveryFiatReward` is 0. + const securedRewardFactor = adjustedDeliveryReward == 0n + ? Infinity + : Number( + ((adjustedDeliveryReward + maxAckLoss) * DECIMAL_BASE_BIG_INT) / (adjustedDeliveryReward) + ) / DECIMAL_BASE; + const deliveryFiatReward = securedDeliveryFiatReward / securedRewardFactor; + + const securedDeliveryFiatProfit = securedDeliveryFiatReward - deliveryFiatCost; + const securedDeliveryRelativeProfit = securedDeliveryFiatProfit / deliveryFiatCost; + + const relayDelivery = ( + securedDeliveryFiatProfit > evaluationConfig.minDeliveryReward || + securedDeliveryRelativeProfit > evaluationConfig.relativeMinDeliveryReward + ); + + response.evaluation = { + maxGasDelivery: bountyPlacedEvent.maxGasDelivery, + maxGasAck: bountyPlacedEvent.maxGasAck, + gasEstimate, + observedGasEstimate, + additionalFeeEstimate, + value, + destinationGasPrice, + sourceGasPrice, + deliveryCost, + deliveryReward, + maxAckLoss, + deliveryFiatCost, + deliveryFiatReward, + securedDeliveryFiatReward, + profitabilityFactor: evaluationConfig.profitabilityFactor, + securedDeliveryFiatProfit, + securedDeliveryRelativeProfit, + minDeliveryReward: evaluationConfig.minDeliveryReward, + relativeMinDeliveryReward: evaluationConfig.relativeMinDeliveryReward, + relayDelivery, + }; + + return response; + } + + private async evaluateAck( + chainId: string, + messageIdentifier: string, + gasEstimateComponents: GasEstimateComponents, + value: bigint, + ): Promise { + + const response: EvaluateAckResponseMessage = { + type: EvaluatorMessageType.EvaluateAckResponse, + chainId, + messageIdentifier, + evaluation: null, + }; + + const evaluationConfig = this.config.evaluationConfigs[chainId]; + if (evaluationConfig == null) { + this.logger.info( + { + chainId, + messageIdentifier, + }, + `Unable to perform ack evaluation: no evaluation config found for the 'chainId' provided.` + ); + // Send a 'null' evaluation response + return response + } + + const relayState = await this.store.getRelayState(messageIdentifier); + if (relayState?.bountyPlacedEvent == null) { + this.logger.info( + { + chainId, + messageIdentifier, + }, + `Unable to perform ack evaluation: no BountyPlaced information found for the 'messageIdentifier' provided.` + ); + // Send a 'null' evaluation response + return response + } + + const bountyPlacedEvent = relayState.bountyPlacedEvent; + if (bountyPlacedEvent.fromChainId != chainId) { + this.logger.info( + { + chainId, + messageIdentifier, + }, + `Unable to perform ack evaluation: the specified 'chainId' does not match the 'fromChainId' stored on the bounty registry.` + ); + // Send a 'null' evaluation response + return response + } + + const toChainId = relayState.messageDeliveredEvent?.toChainId; + const ackAMBMessage = toChainId != undefined + ? await this.store.getAMBMessage(toChainId, messageIdentifier) + : undefined; + if (!ackAMBMessage) { + this.logger.info( + { + chainId, + messageIdentifier, + }, + `Message delivery data not found, ack evaluation will be less accurate.` + ); + } + const ackIncentivesPayload = ackAMBMessage?.incentivesPayload; + + const { + gasEstimate, + observedGasEstimate, + additionalFeeEstimate + } = gasEstimateComponents; + + const sourceGasPrice = await this.getGasPrice(chainId); + + const priceOfDeliveryGas = relayState.bountyIncreasedEvent?.newDeliveryGasPrice + ?? bountyPlacedEvent.priceOfDeliveryGas; + const priceOfAckGas = relayState.bountyIncreasedEvent?.newAckGasPrice + ?? bountyPlacedEvent.priceOfAckGas; + + const ackCost = this.calcGasCost( // ! In source chain gas value + gasEstimate, + sourceGasPrice, + additionalFeeEstimate + value + ); + + const ackReward = this.calcGasReward( // ! In source chain gas value + observedGasEstimate, + evaluationConfig.unrewardedAckGas, + bountyPlacedEvent.maxGasAck, + priceOfAckGas + ); + + const adjustedAckReward = evaluationConfig.profitabilityFactor == 0 + ? MaxUint256 + : ackReward * DECIMAL_BASE_BIG_INT + / BigInt(evaluationConfig.profitabilityFactor * DECIMAL_BASE); + + const ackProfit = adjustedAckReward - ackCost; // ! In source chain gas value + const ackFiatProfit = await this.getGasCostFiatPrice(ackProfit, chainId); + const ackRelativeProfit = Number(ackProfit) / Number(ackCost); + + let deliveryReward = 0n; + const deliveryCost = relayState.deliveryGasCost ?? 0n; // This is only present if *this* relayer submitted the message delivery. + if (deliveryCost != 0n) { + + // Recalculate the delivery reward using the latest pricing info + const usedGasDelivery = ackIncentivesPayload + ? await this.getGasUsedForDelivery(ackIncentivesPayload) ?? 0n + : 0n; // 'gasUsed' should not be 'undefined', but if it is, continue as if it was 0 + + deliveryReward = this.calcGasReward( // ! In source chain gas value + usedGasDelivery, + 0n, // No 'unrewarded' gas, as 'usedGasDelivery' is the exact value that is used to compute the reward. + bountyPlacedEvent.maxGasDelivery, + priceOfDeliveryGas + ); + } + + // If the delivery was submitted by *this* relayer, always submit the ack *unless* + // the net result of doing so is worse than not getting paid for the message + // delivery. + const relayAckForDeliveryBounty = deliveryCost != 0n && (ackProfit + deliveryReward > 0n); + + const relayAck = ( + relayAckForDeliveryBounty || + ackFiatProfit > evaluationConfig.minAckReward || + ackRelativeProfit > evaluationConfig.relativeMinAckReward + ); + + response.evaluation = { + maxGasDelivery: bountyPlacedEvent.maxGasDelivery, + maxGasAck: bountyPlacedEvent.maxGasAck, + gasEstimate, + observedGasEstimate, + additionalFeeEstimate, + sourceGasPrice, + ackCost, + ackReward, + profitabilityFactor: evaluationConfig.profitabilityFactor, + ackFiatProfit, + ackRelativeProfit, + minAckReward: evaluationConfig.minAckReward, + relativeMinAckReward: evaluationConfig.relativeMinAckReward, + deliveryCost, + deliveryReward, + relayAckForDeliveryBounty, + relayAck, + }; + + return response; + } + + private calcGasCost( + gas: bigint, + gasPrice: bigint, + additionalFee?: bigint, + ): bigint { + return gas * gasPrice + (additionalFee ?? 0n); + } + + private calcGasReward( + gas: bigint, + unrewardedGas: bigint, + bountyMaxGas: bigint, + bountyPriceOfGas: bigint, + ): bigint { + + // Subtract the 'unrewardable' gas amount estimate from the gas usage estimation. + const rewardableGasEstimation = gas > unrewardedGas + ? gas - unrewardedGas + : 0n; + + const rewardEstimate = bountyPriceOfGas * ( + rewardableGasEstimation > bountyMaxGas + ? bountyMaxGas + : rewardableGasEstimation + ); + + return rewardEstimate; + } + + private calcMaxGasLoss( + gasPrice: bigint, + unrewardedGas: bigint, + verificationGas: bigint, + bountyMaxGas: bigint, + bountyPriceOfGas: bigint, + ): bigint { + + // The gas used for the 'ack' submission is composed of 3 amounts: + // - Logic overhead: is never computed for the reward. + // - Verification logic: is only computed for the reward if the source application's + // 'ack' handler does not use all of the 'ack' gas allowance ('bountyMaxGas'). + // - Source application's 'ack' handler: it is always computed for the reward (up to a + // maximum of 'bountyMaxGas'). + + // Evaluate the minimum expected profit from the 'ack' delivery. There are 2 possible + // scenarios: + // - No gas is used by the source application's 'ack' handler. + // - The maximum allowed amount of gas is used by the source application's 'ack' handler. + + // NOTE: strictly speaking, 'verificationGas' should be upperbounded by 'bountyMaxGas' on + // the following line. However, this is not necessary, as in such a case + // 'maximumGasUsageProfit' will always return a smaller profit than 'minimumGasUsageProfit'. + const minimumGasUsageReward = verificationGas * bountyPriceOfGas; + const minimumGasUsageCost = (unrewardedGas + verificationGas) * gasPrice; + const minimumGasUsageProfit = minimumGasUsageReward - minimumGasUsageCost; + + const maximumGasUsageReward = bountyMaxGas * bountyPriceOfGas; + const maximumGasUsageCost = (unrewardedGas + verificationGas + bountyMaxGas) * gasPrice; + const maximumGasUsageProfit = maximumGasUsageReward - maximumGasUsageCost; + + const worstCaseProfit = minimumGasUsageProfit < maximumGasUsageProfit + ? minimumGasUsageProfit + : maximumGasUsageProfit; + + // Only return the 'worstCaseProfit' if it's negative. + return worstCaseProfit < 0n + ? worstCaseProfit + : 0n; + } + + private async getGasPrice(chainId: string): Promise { + const feeData = await this.wallet.getFeeData(chainId); + // If gas fee data is missing or incomplete, default the gas price to an extremely high + // value. + // ! Use 'gasPrice' over 'maxFeePerGas', as 'maxFeePerGas' defines the highest gas fee + // ! allowed, which does not necessarilly represent the real gas fee at which the + // ! transactions are going through. + const gasPrice = feeData?.gasPrice + ?? feeData?.maxFeePerGas + ?? MaxUint256; + + return gasPrice; + } + + private async getGasCostFiatPrice(amount: bigint, chainId: string): Promise { + //TODO add timeout? + const price = await this.pricing.getPrice(chainId, amount); + if (price == null) { + throw new Error('Unable to fetch price.'); + } + return price; + } + + private async getGasUsedForDelivery(message: BytesLike): Promise { + try { + const payload = ParsePayload(message.toString()); + + if (payload == undefined) { + return null; + } + + if (payload.context != MessageContext.CTX_DESTINATION_TO_SOURCE) { + this.logger.warn( + { payload }, + `Unable to extract the 'gasUsed' for delivery. Payload is not a 'destination-to-source' message.`, + ); + return null; + } + + return payload.gasSpent; + } + catch (error) { + this.logger.warn( + { message }, + `Failed to parse generalised incentives payload for 'gasSpent' (on delivery).` + ); + } + + return null; + } +} + +new EvaluatorWorker(); diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index f89b7fb..69b0f61 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -2,28 +2,21 @@ import { HandleOrderResult, ProcessingQueue, } from '../../processing-queue/processing-queue'; -import { Bounty, BountyEvaluationConfig, EvalOrder, SubmitOrder } from '../submitter.types'; +import { Bounty, EvalOrder, SubmitOrder } from '../submitter.types'; import pino from 'pino'; import { Store } from 'src/store/store.lib'; import { RelayState, RelayStatus } from 'src/store/store.types'; import { IncentivizedMockEscrow__factory } from 'src/contracts'; import { tryErrorToString } from 'src/common/utils'; -import { AbstractProvider, BytesLike, MaxUint256, TransactionRequest, zeroPadValue } from 'ethers6'; -import { ParsePayload, MessageContext } from 'src/payload/decode.payload'; -import { PricingInterface } from 'src/pricing/pricing.interface'; -import { WalletInterface } from 'src/wallet/wallet.interface'; +import { TransactionRequest, zeroPadValue } from 'ethers6'; import { Resolver, GasEstimateComponents } from 'src/resolvers/resolver'; import { IncentivizedMockEscrowInterface } from 'src/contracts/IncentivizedMockEscrow'; - -const DECIMAL_BASE = 10000; -const DECIMAL_BASE_BIG_INT = BigInt(DECIMAL_BASE); +import { EvaluatorInterface } from 'src/evaluator/evaluator.interface'; export class EvalQueue extends ProcessingQueue { readonly paddedRelayerAddress: string; private readonly escrowInterface: IncentivizedMockEscrowInterface; - private readonly profitabilityFactorBigInt: bigint; - constructor( retryInterval: number, maxTries: number, @@ -33,16 +26,12 @@ export class EvalQueue extends ProcessingQueue { private readonly incentivesContracts: Map, private readonly packetCosts: Map, private readonly chainId: string, - private readonly evaluationConfig: BountyEvaluationConfig, - private readonly pricing: PricingInterface, - private readonly provider: AbstractProvider, - private readonly wallet: WalletInterface, + private readonly evaluator: EvaluatorInterface, private readonly logger: pino.Logger, ) { super(retryInterval, maxTries); this.paddedRelayerAddress = zeroPadValue(relayerAddress, 32); this.escrowInterface = IncentivizedMockEscrow__factory.createInterface(); - this.profitabilityFactorBigInt = BigInt(this.evaluationConfig.profitabilityFactor * DECIMAL_BASE); } protected async handleOrder( @@ -240,7 +229,11 @@ export class EvalQueue extends ProcessingQueue { return true; } - return this.evaluateDeliverySubmission(gasEstimateComponents, value, bounty); + return this.evaluateDeliverySubmission( + bounty.messageIdentifier, + gasEstimateComponents, + value, + ); } else { // Destination to Source if (order.priority) { @@ -258,316 +251,68 @@ export class EvalQueue extends ProcessingQueue { return true; } - return this.evaluateAckSubmission(gasEstimateComponents, value, bounty, order.incentivesPayload); + return this.evaluateAckSubmission( + bounty.messageIdentifier, + gasEstimateComponents, + value + ); } } private async evaluateDeliverySubmission( + messageIdentifier: string, gasEstimateComponents: GasEstimateComponents, value: bigint, - bounty: Bounty ): Promise { - const { - gasEstimate, - observedGasEstimate, - additionalFeeEstimate - } = gasEstimateComponents; - - const destinationGasPrice = await this.getGasPrice(this.chainId); - const sourceGasPrice = await this.getGasPrice(bounty.fromChainId); - - const deliveryCost = this.calcGasCost( // ! In destination chain gas value - gasEstimate, - destinationGasPrice, - additionalFeeEstimate + value - ); - - const deliveryReward = this.calcGasReward( // ! In source chain gas value - observedGasEstimate, - this.evaluationConfig.unrewardedDeliveryGas, - BigInt(bounty.maxGasDelivery), - bounty.priceOfDeliveryGas - ); - - const maxAckLoss = this.calcMaxGasLoss( // ! In source chain gas value - sourceGasPrice, - this.evaluationConfig.unrewardedAckGas, - this.evaluationConfig.verificationAckGas, - BigInt(bounty.maxGasAck), - bounty.priceOfAckGas, - ); - - - // Compute the cost and reward of the message delivery in Fiat and evaluate the message - // delivery profit. - const deliveryFiatCost = await this.getGasCostFiatPrice( - deliveryCost, - this.chainId - ); - - const adjustedDevlieryReward = deliveryReward * DECIMAL_BASE_BIG_INT - / this.profitabilityFactorBigInt; - - const securedDeliveryReward = adjustedDevlieryReward + maxAckLoss; - - const securedDeliveryFiatReward = await this.getGasCostFiatPrice( - securedDeliveryReward, - bounty.fromChainId + const result = await this.evaluator.evaluateDelivery( + this.chainId, + messageIdentifier, + gasEstimateComponents, + value, ); - // Compute the 'deliveryFiatReward' for logging purposes (i.e. without the 'maxAckLoss' factor) - const securedRewardFactor = Number( - ((deliveryReward + maxAckLoss) * DECIMAL_BASE_BIG_INT) / (deliveryReward) - ) / DECIMAL_BASE; - const deliveryFiatReward = securedDeliveryFiatReward / securedRewardFactor; - - const securedDeliveryFiatProfit = securedDeliveryFiatReward - deliveryFiatCost; - const securedDeliveryRelativeProfit = securedDeliveryFiatProfit / deliveryFiatCost; - - const relayDelivery = ( - securedDeliveryFiatProfit > this.evaluationConfig.minDeliveryReward || - securedDeliveryRelativeProfit > this.evaluationConfig.relativeMinDeliveryReward - ); + if (result.evaluation == null) { + throw new Error('Failed to evaluate delivery submission: evaluation result is null.'); + } this.logger.info( { - messageIdentifier: bounty.messageIdentifier, - maxGasDelivery: bounty.maxGasDelivery, - maxGasAck: bounty.maxGasAck, - gasEstimate: gasEstimate.toString(), - observedGasEstimate: observedGasEstimate.toString(), - additionalFeeEstimation: additionalFeeEstimate.toString(), - destinationGasPrice: destinationGasPrice.toString(), - sourceGasPrice: sourceGasPrice.toString(), - deliveryCost: deliveryCost.toString(), - deliveryReward: deliveryReward.toString(), - maxAckLoss: maxAckLoss.toString(), - deliveryFiatCost: deliveryFiatCost.toString(), - deliveryFiatReward: deliveryFiatReward.toString(), - securedDeliveryFiatReward: securedDeliveryFiatReward.toString(), - profitabilityFactor: this.evaluationConfig.profitabilityFactor, - securedDeliveryFiatProfit: securedDeliveryFiatProfit, - securedDeliveryRelativeProfit: securedDeliveryRelativeProfit, - minDeliveryReward: this.evaluationConfig.minDeliveryReward, - relativeMinDeliveryReward: this.evaluationConfig.relativeMinDeliveryReward, - relayDelivery, + messageIdentifier, + ...result.evaluation, }, `Bounty evaluation (source to destination).`, ); - return relayDelivery; + return result.evaluation.relayDelivery; } private async evaluateAckSubmission( + messageIdentifier: string, gasEstimateComponents: GasEstimateComponents, value: bigint, - bounty: Bounty, - incentivesPayload?: BytesLike, ): Promise { - const { - gasEstimate, - observedGasEstimate, - additionalFeeEstimate - } = gasEstimateComponents; - - const sourceGasPrice = await this.getGasPrice(this.chainId); - - const ackCost = this.calcGasCost( // ! In source chain gas value - gasEstimate, - sourceGasPrice, - additionalFeeEstimate + value - ); - - const ackReward = this.calcGasReward( // ! In source chain gas value - observedGasEstimate, - this.evaluationConfig.unrewardedAckGas, - BigInt(bounty.maxGasAck), - bounty.priceOfAckGas + const result = await this.evaluator.evaluateAck( + this.chainId, + messageIdentifier, + gasEstimateComponents, + value, ); - const adjustedAckReward = ackReward * DECIMAL_BASE_BIG_INT - / this.profitabilityFactorBigInt; - - const ackProfit = adjustedAckReward - ackCost; // ! In source chain gas value - const ackFiatProfit = await this.getGasCostFiatPrice(ackProfit, this.chainId); - const ackRelativeProfit = Number(ackProfit) / Number(ackCost); - - let deliveryReward = 0n; - const deliveryCost = bounty.deliveryGasCost ?? 0n; // This is only present if *this* relayer submitted the message delivery. - if (deliveryCost != 0n) { - - // Recalculate the delivery reward using the latest pricing info - const usedGasDelivery = incentivesPayload - ? await this.getGasUsedForDelivery(incentivesPayload) ?? 0n - : 0n; // 'gasUsed' should not be 'undefined', but if it is, continue as if it was 0 - - deliveryReward = this.calcGasReward( // ! In source chain gas value - usedGasDelivery, - 0n, // No 'unrewarded' gas, as 'usedGasDelivery' is the exact value that is used to compute the reward. - BigInt(bounty.maxGasDelivery), - bounty.priceOfDeliveryGas - ); + if (result.evaluation == null) { + throw new Error('Failed to evaluate ack submission: evaluation result is null.'); } - // If the delivery was submitted by *this* relayer, always submit the ack *unless* - // the net result of doing so is worse than not getting paid for the message - // delivery. - const relayAckForDeliveryBounty = deliveryCost != 0n && (ackProfit + deliveryReward > 0n); - - const relayAck = ( - relayAckForDeliveryBounty || - ackFiatProfit > this.evaluationConfig.minAckReward || - ackRelativeProfit > this.evaluationConfig.relativeMinAckReward - ); - this.logger.info( { - messageIdentifier: bounty.messageIdentifier, - maxGasDelivery: bounty.maxGasDelivery, - maxGasAck: bounty.maxGasAck, - gasEstimate: gasEstimate.toString(), - observedGasEstimate: observedGasEstimate.toString(), - additionalFeeEstimation: additionalFeeEstimate.toString(), - sourceGasPrice: sourceGasPrice.toString(), - ackCost: ackCost.toString(), - ackReward: ackReward.toString(), - profitabilityFactor: this.evaluationConfig.profitabilityFactor, - ackFiatProfit: ackFiatProfit.toString(), - ackRelativeProfit: ackRelativeProfit, - minAckReward: this.evaluationConfig.minAckReward, - relativeMinAckReward: this.evaluationConfig.relativeMinAckReward, - deliveryCost: deliveryCost.toString(), - deliveryReward: deliveryReward.toString(), - relayAckForDeliveryBounty, - relayAck, + messageIdentifier, + ...result.evaluation, }, `Bounty evaluation (destination to source).`, ); - return relayAck; - } - - private calcGasCost( - gas: bigint, - gasPrice: bigint, - additionalFee?: bigint, - ): bigint { - return gas * gasPrice + (additionalFee ?? 0n); - } - - private calcGasReward( - gas: bigint, - unrewardedGas: bigint, - bountyMaxGas: bigint, - bountyPriceOfGas: bigint, - ): bigint { - - // Subtract the 'unrewardable' gas amount estimate from the gas usage estimation. - const rewardableGasEstimation = gas > unrewardedGas - ? gas - unrewardedGas - : 0n; - - const rewardEstimate = bountyPriceOfGas * ( - rewardableGasEstimation > bountyMaxGas - ? bountyMaxGas - : rewardableGasEstimation - ); - - return rewardEstimate; - } - - private calcMaxGasLoss( - gasPrice: bigint, - unrewardedGas: bigint, - verificationGas: bigint, - bountyMaxGas: bigint, - bountyPriceOfGas: bigint, - ): bigint { - - // The gas used for the 'ack' submission is composed of 3 amounts: - // - Logic overhead: is never computed for the reward. - // - Verification logic: is only computed for the reward if the source application's - // 'ack' handler does not use all of the 'ack' gas allowance ('bountyMaxGas'). - // - Source application's 'ack' handler: it is always computed for the reward (up to a - // maximum of 'bountyMaxGas'). - - // Evaluate the minimum expected profit from the 'ack' delivery. There are 2 possible - // scenarios: - // - No gas is used by the source application's 'ack' handler. - // - The maximum allowed amount of gas is used by the source application's 'ack' handler. - - // NOTE: strictly speaking, 'verificationGas' should be upperbounded by 'bountyMaxGas' on - // the following line. However, this is not necessary, as in such a case - // 'maximumGasUsageProfit' will always return a smaller profit than 'minimumGasUsageProfit'. - const minimumGasUsageReward = verificationGas * bountyPriceOfGas; - const minimumGasUsageCost = (unrewardedGas + verificationGas) * gasPrice; - const minimumGasUsageProfit = minimumGasUsageReward - minimumGasUsageCost; - - const maximumGasUsageReward = bountyMaxGas * bountyPriceOfGas; - const maximumGasUsageCost = (unrewardedGas + verificationGas + bountyMaxGas) * gasPrice; - const maximumGasUsageProfit = maximumGasUsageReward - maximumGasUsageCost; - - const worstCaseProfit = minimumGasUsageProfit < maximumGasUsageProfit - ? minimumGasUsageProfit - : maximumGasUsageProfit; - - // Only return the 'worstCaseProfit' if it's negative. - return worstCaseProfit < 0n - ? worstCaseProfit - : 0n; - } - - private async getGasPrice(chainId: string): Promise { - const feeData = await this.wallet.getFeeData(chainId); - // If gas fee data is missing or incomplete, default the gas price to an extremely high - // value. - // ! Use 'gasPrice' over 'maxFeePerGas', as 'maxFeePerGas' defines the highest gas fee - // ! allowed, which does not necessarilly represent the real gas fee at which the - // ! transactions are going through. - const gasPrice = feeData?.gasPrice - ?? feeData?.maxFeePerGas - ?? MaxUint256; - - return gasPrice; - } - - private async getGasCostFiatPrice(amount: bigint, chainId: string): Promise { - //TODO add timeout? - const price = await this.pricing.getPrice(chainId, amount); - if (price == null) { - throw new Error('Unable to fetch price.'); - } - return price; - } - - private async getGasUsedForDelivery(message: BytesLike): Promise { - try { - const payload = ParsePayload(message.toString()); - - if (payload == undefined) { - return null; - } - - if (payload.context != MessageContext.CTX_DESTINATION_TO_SOURCE) { - this.logger.warn( - { payload }, - `Unable to extract the 'gasUsed' for delivery. Payload is not a 'destination-to-source' message.`, - ); - return null; - } - - return payload.gasSpent; - } - catch (error) { - this.logger.warn( - { message }, - `Failed to parse generalised incentives payload for 'gasSpent' (on delivery).` - ); - } - - return null; + return result.evaluation.relayAck; } private getBountyFromRelayState(relayState: RelayState): Bounty | null { diff --git a/src/submitter/submitter.module.ts b/src/submitter/submitter.module.ts index 6537d1b..d876a54 100644 --- a/src/submitter/submitter.module.ts +++ b/src/submitter/submitter.module.ts @@ -1,4 +1,4 @@ -import { PricingModule } from './../pricing/pricing.module'; +import { EvaluatorModule } from './../evaluator/evaluator.module'; import { Module } from '@nestjs/common'; import { SubmitterService } from './submitter.service'; import { WalletModule } from 'src/wallet/wallet.module'; @@ -6,6 +6,6 @@ import { WalletModule } from 'src/wallet/wallet.module'; @Module({ providers: [SubmitterService], exports: [SubmitterService], - imports: [PricingModule, WalletModule], + imports: [EvaluatorModule, WalletModule], }) export class SubmitterModule {} diff --git a/src/submitter/submitter.service.ts b/src/submitter/submitter.service.ts index ad3ec1c..ccbbf05 100644 --- a/src/submitter/submitter.service.ts +++ b/src/submitter/submitter.service.ts @@ -8,7 +8,7 @@ import { LoggerOptions } from 'pino'; import { WalletService } from 'src/wallet/wallet.service'; import { Wallet } from 'ethers6'; import { tryErrorToString } from 'src/common/utils'; -import { PricingService } from 'src/pricing/pricing.service'; +import { EvaluatorService } from 'src/evaluator/evaluator.service'; const RETRY_INTERVAL_DEFAULT = 30000; const PROCESSING_INTERVAL_DEFAULT = 100; @@ -17,15 +17,6 @@ const MAX_PENDING_TRANSACTIONS = 50; const NEW_ORDERS_DELAY_DEFAULT = 0; const EVALUATION_RETRY_INTERVAL_DEFAULT = 60 * 60 * 1000; const MAX_EVALUATION_DURATION_DEFAULT = 24 * 60 * 60 * 1000; -const UNREWARDED_DELIVERY_GAS_DEFAULT = 0n; -const VERIFICATION_DELIVERY_GAS_DEFAULT = 0n; -const MIN_DELIVERY_REWARD_DEFAULT = 0; -const RELATIVE_MIN_DELIVERY_REWARD_DEFAULT = 0; -const UNREWARDED_ACK_GAS_DEFAULT = 0n; -const VERIFICATION_ACK_GAS_DEFAULT = 0n; -const MIN_ACK_REWARD_DEFAULT = 0; -const RELATIVE_MIN_ACK_REWARD_DEFAULT = 0; -const PROFITABILITY_FACTOR_DEFAULT = 1; interface GlobalSubmitterConfig { enabled: boolean; @@ -36,15 +27,6 @@ interface GlobalSubmitterConfig { maxPendingTransactions: number; evaluationRetryInterval: number; maxEvaluationDuration: number; - unrewardedDeliveryGas: bigint; - verificationDeliveryGas: bigint; - minDeliveryReward: number; - relativeMinDeliveryReward: number; - unrewardedAckGas: bigint; - verificationAckGas: bigint; - minAckReward: number; - relativeMinAckReward: number; - profitabilityFactor: number; walletPublicKey: string; } @@ -61,16 +43,7 @@ export interface SubmitterWorkerData { maxPendingTransactions: number; evaluationRetryInterval: number; maxEvaluationDuration: number; - unrewardedDeliveryGas: bigint; - verificationDeliveryGas: bigint; - minDeliveryReward: number; - relativeMinDeliveryReward: number; - unrewardedAckGas: bigint; - verificationAckGas: bigint; - minAckReward: number; - relativeMinAckReward: number; - profitabilityFactor: number; - pricingPort: MessagePort; + evaluatorPort: MessagePort; walletPublicKey: string; walletPort: MessagePort; loggerOptions: LoggerOptions; @@ -82,7 +55,7 @@ export class SubmitterService { constructor( private readonly configService: ConfigService, - private readonly pricingService: PricingService, + private readonly evaluatorService: EvaluatorService, private readonly walletService: WalletService, private readonly loggerService: LoggerService, ) {} @@ -108,7 +81,10 @@ export class SubmitterService { const worker = new Worker(join(__dirname, 'submitter.worker.js'), { workerData, - transferList: [workerData.pricingPort, workerData.walletPort] + transferList: [ + workerData.evaluatorPort, + workerData.walletPort + ] }); worker.on('error', (error) => @@ -152,24 +128,6 @@ export class SubmitterService { submitterConfig.evaluationRetryInterval ?? EVALUATION_RETRY_INTERVAL_DEFAULT; const maxEvaluationDuration = submitterConfig.maxEvaluationDuration ?? MAX_EVALUATION_DURATION_DEFAULT; - const unrewardedDeliveryGas = - submitterConfig.unrewardedDeliveryGas ?? UNREWARDED_DELIVERY_GAS_DEFAULT; - const verificationDeliveryGas = - submitterConfig.verificationDeliveryGas ?? VERIFICATION_DELIVERY_GAS_DEFAULT; - const minDeliveryReward = - submitterConfig.minDeliveryReward ?? MIN_DELIVERY_REWARD_DEFAULT; - const relativeMinDeliveryReward = - submitterConfig.relativeMinDeliveryReward ?? RELATIVE_MIN_DELIVERY_REWARD_DEFAULT; - const unrewardedAckGas = - submitterConfig.unrewardedAckGas ?? UNREWARDED_ACK_GAS_DEFAULT; - const verificationAckGas = - submitterConfig.verificationAckGas ?? VERIFICATION_ACK_GAS_DEFAULT; - const minAckReward = - submitterConfig.minAckReward ?? MIN_ACK_REWARD_DEFAULT; - const relativeMinAckReward = - submitterConfig.relativeMinAckReward ?? RELATIVE_MIN_ACK_REWARD_DEFAULT; - const profitabilityFactor = - submitterConfig.profitabilityFactor ?? PROFITABILITY_FACTOR_DEFAULT; const walletPublicKey = (new Wallet(this.configService.globalConfig.privateKey)).address; @@ -183,15 +141,6 @@ export class SubmitterService { walletPublicKey, evaluationRetryInterval, maxEvaluationDuration, - unrewardedDeliveryGas, - verificationDeliveryGas, - minDeliveryReward, - relativeMinDeliveryReward, - unrewardedAckGas, - verificationAckGas, - minAckReward, - relativeMinAckReward, - profitabilityFactor, }; } @@ -254,49 +203,13 @@ export class SubmitterService { evaluationRetryInterval: chainConfig.submitter.evaluationRetryInterval ?? globalConfig.evaluationRetryInterval, - - unrewardedDeliveryGas: - chainConfig.submitter.unrewardedDeliveryGas ?? - globalConfig.unrewardedDeliveryGas, - - verificationDeliveryGas: - chainConfig.submitter.verificationDeliveryGas ?? - globalConfig.verificationDeliveryGas, maxEvaluationDuration: chainConfig.submitter.maxEvaluationDuration ?? globalConfig.maxEvaluationDuration, - - minDeliveryReward: - chainConfig.submitter.minDeliveryReward ?? - globalConfig.minDeliveryReward, - - relativeMinDeliveryReward: - chainConfig.submitter.relativeMinDeliveryReward ?? - globalConfig.relativeMinDeliveryReward, - - unrewardedAckGas: - chainConfig.submitter.unrewardedAckGas ?? - globalConfig.unrewardedAckGas, - - verificationAckGas: - chainConfig.submitter.verificationAckGas ?? - globalConfig.verificationAckGas, - - minAckReward: - chainConfig.submitter.minAckReward ?? - globalConfig.minAckReward, - - relativeMinAckReward: - chainConfig.submitter.relativeMinAckReward ?? - globalConfig.relativeMinAckReward, - - profitabilityFactor: - chainConfig.submitter.profitabilityFactor ?? - globalConfig.profitabilityFactor, - pricingPort: await this.pricingService.attachToPricing(), + evaluatorPort: await this.evaluatorService.attachToEvaluator(), walletPublicKey: globalConfig.walletPublicKey, walletPort: await this.walletService.attachToWallet(), diff --git a/src/submitter/submitter.types.ts b/src/submitter/submitter.types.ts index 8855d17..0b10ef7 100644 --- a/src/submitter/submitter.types.ts +++ b/src/submitter/submitter.types.ts @@ -47,18 +47,3 @@ export interface Bounty { deliveryGasCost?: bigint; } - - -export interface BountyEvaluationConfig { - evaluationRetryInterval: number, - maxEvaluationDuration: number, - unrewardedDeliveryGas: bigint; - verificationDeliveryGas: bigint; - minDeliveryReward: number; - relativeMinDeliveryReward: number, - unrewardedAckGas: bigint; - verificationAckGas: bigint; - minAckReward: number; - relativeMinAckReward: number; - profitabilityFactor: number; -} \ No newline at end of file diff --git a/src/submitter/submitter.worker.ts b/src/submitter/submitter.worker.ts index ae6d299..4b92d99 100644 --- a/src/submitter/submitter.worker.ts +++ b/src/submitter/submitter.worker.ts @@ -4,14 +4,14 @@ import { Store } from 'src/store/store.lib'; import { workerData } from 'worker_threads'; import { AMBProof } from 'src/store/store.types'; import { STATUS_LOG_INTERVAL } from 'src/logger/logger.service'; -import { BountyEvaluationConfig, EvalOrder, PendingOrder } from './submitter.types'; +import { EvalOrder, PendingOrder } from './submitter.types'; import { EvalQueue } from './queues/eval-queue'; import { SubmitQueue } from './queues/submit-queue'; import { wait } from 'src/common/utils'; import { SubmitterWorkerData } from './submitter.service'; import { WalletInterface } from 'src/wallet/wallet.interface'; -import { PricingInterface } from 'src/pricing/pricing.interface'; import { Resolver, loadResolver } from 'src/resolvers/resolver'; +import { EvaluatorInterface } from 'src/evaluator/evaluator.interface'; class SubmitterWorker { private readonly store: Store; @@ -25,7 +25,7 @@ class SubmitterWorker { private readonly resolver: Resolver; - private readonly pricing: PricingInterface; + private readonly evaluator: EvaluatorInterface; private readonly wallet: WalletInterface; private readonly pendingQueue: PendingOrder[] = []; @@ -54,7 +54,7 @@ class SubmitterWorker { this.logger ); - this.pricing = new PricingInterface(this.config.pricingPort); + this.evaluator = new EvaluatorInterface(this.config.evaluatorPort); this.wallet = new WalletInterface(this.config.walletPort); [this.evalQueue, this.submitQueue] = @@ -67,20 +67,7 @@ class SubmitterWorker { this.config.incentivesAddresses, this.config.packetCosts, this.config.chainId, - { - evaluationRetryInterval: this.config.evaluationRetryInterval, - maxEvaluationDuration: this.config.maxEvaluationDuration, - unrewardedDeliveryGas: this.config.unrewardedDeliveryGas, - verificationDeliveryGas: this.config.verificationDeliveryGas, - minDeliveryReward: this.config.minDeliveryReward, - relativeMinDeliveryReward: this.config.relativeMinDeliveryReward, - unrewardedAckGas: this.config.unrewardedAckGas, - verificationAckGas: this.config.verificationAckGas, - minAckReward: this.config.minAckReward, - relativeMinAckReward: this.config.relativeMinAckReward, - profitabilityFactor: this.config.profitabilityFactor, - }, - this.pricing, + this.evaluator, this.provider, this.wallet, this.logger, @@ -110,8 +97,7 @@ class SubmitterWorker { incentivesContracts: Map, packetCosts: Map, chainId: string, - bountyEvaluationConfig: BountyEvaluationConfig, - pricing: PricingInterface, + evaluator: EvaluatorInterface, provider: JsonRpcProvider, wallet: WalletInterface, logger: pino.Logger, @@ -125,10 +111,7 @@ class SubmitterWorker { incentivesContracts, packetCosts, chainId, - bountyEvaluationConfig, - pricing, - provider, - wallet, + evaluator, logger, );