From f3fa4c4adb472d2a1c27856c95af30a63c97c07a Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Wed, 1 May 2024 09:15:57 +0000 Subject: [PATCH 01/43] feat: Bounty evaluation (untested) --- config.example.yaml | 21 ++- package.json | 11 +- pnpm-lock.yaml | 42 ++++++ src/app.module.ts | 2 + src/config/config.schema.ts | 21 +++ src/config/config.service.ts | 30 +++- src/config/config.types.ts | 18 +++ src/pricing/pricing.interface.ts | 39 +++++ src/pricing/pricing.module.ts | 9 ++ src/pricing/pricing.provider.ts | 146 +++++++++++++++++++ src/pricing/pricing.service.ts | 161 +++++++++++++++++++++ src/pricing/pricing.types.ts | 28 ++++ src/pricing/pricing.worker.ts | 97 +++++++++++++ src/pricing/providers/coin-gecko.ts | 46 ++++++ src/pricing/providers/fixed.ts | 23 +++ src/store/store.lib.ts | 37 +++++ src/store/types/store.types.ts | 2 + src/submitter/queues/eval-queue.ts | 204 +++++++++++++++++++++++---- src/submitter/queues/submit-queue.ts | 17 +++ src/submitter/submitter.service.ts | 48 ++++++- src/submitter/submitter.types.ts | 10 ++ src/submitter/submitter.worker.ts | 30 +++- 22 files changed, 1003 insertions(+), 39 deletions(-) create mode 100644 src/pricing/pricing.interface.ts create mode 100644 src/pricing/pricing.module.ts create mode 100644 src/pricing/pricing.provider.ts create mode 100644 src/pricing/pricing.service.ts create mode 100644 src/pricing/pricing.types.ts create mode 100644 src/pricing/pricing.worker.ts create mode 100644 src/pricing/providers/coin-gecko.ts create mode 100644 src/pricing/providers/fixed.ts diff --git a/config.example.yaml b/config.example.yaml index eeebb1c..e8defd6 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -18,10 +18,20 @@ global: maxTries: 3 # Maximum tries for a transaction maxPendingTransactions: 50 # Maximum number of transactions within the 'submit' pipeline. - # Gas properties + # Evaluation properties gasLimitBuffer: # Extra gasLimit buffer. Customizable per AMB. default: 10000 mock: 50000 + minDeliveryReward: 0.001 # In the 'pricingDenomination' specified below + relativeMinDeliveryReward: 0.001 + minAckReward: 0.001 # In the 'pricingDenomination' specified below + relativeMinAckReward: 0.001 + + pricing: + provider: 'coin-gecko' + coinDecimals: 18 + pricingDenomination: 'usd' + wallet: retryInterval: 30000 # Time to wait before retrying a failed transaction @@ -85,6 +95,9 @@ chains: blockDelay: 2 interval: 5000 + pricing: + coinId: 'ethereum' # coin-gecko pricing provider specific configuration + # AMB configuration wormhole: wormholeChainId: 10002 @@ -100,6 +113,8 @@ chains: blockDelay: 5 getter: maxBlocks: 5000 + pricing: + coinId: 'ethereum' wormhole: wormholeChainId: 10003 incentivesAddress: '0xdF25f1BdE09Cee5ac1e6ef8dFA7113addBd58B28' @@ -108,6 +123,8 @@ chains: - chainId: 11155420 name: 'OP Sepolia' rpc: 'https://sepolia.optimism.io' + pricing: + coinId: 'ethereum' wormhole: wormholeChainId: 10005 incentivesAddress: '0xbDFD9163d8Cee1368698B023369f9A5Fd319A40F' @@ -119,6 +136,8 @@ chains: - chainId: 84532 name: 'Base Sepolia' rpc: 'https://sepolia.base.org' + pricing: + coinId: 'ethereum' polymer: incentivesAddress: '0xE106643739deB1879CcD8E3ffe2736D8B489bC2F' bridgeAddress: '0x9fcd52449261F732d017F8BD1CaCDc3dFbcD0361' diff --git a/package.json b/package.json index 70def9e..0c398c8 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "ethers6": "npm:ethers@^6.11.1", "ioredis": "^5.3.2", "js-yaml": "^4.1.0", + "node-fetch": "^3.3.2", "pg": "^8.11.3", "pino": "^8.15.1", "reflect-metadata": "^0.1.13", @@ -70,13 +71,19 @@ "typescript": "^5.1.3" }, "jest": { - "moduleFileExtensions": ["js", "json", "ts"], + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, - "collectCoverageFrom": ["**/*.(t|j)s"], + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], "coverageDirectory": "../coverage", "testEnvironment": "node" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eee239a..f799fb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ dependencies: js-yaml: specifier: ^4.1.0 version: 4.1.0 + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 pg: specifier: ^8.11.3 version: 8.11.3 @@ -5394,6 +5397,11 @@ packages: type: 2.7.2 dev: false + /data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + dev: false + /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -6431,6 +6439,14 @@ packages: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} dev: false + /fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + dev: false + /figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -6570,6 +6586,13 @@ packages: combined-stream: 1.0.8 mime-types: 2.1.35 + /formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + dependencies: + fetch-blob: 3.2.0 + dev: false + /formidable@2.1.2: resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} dependencies: @@ -8564,6 +8587,11 @@ packages: dev: false optional: true + /node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + dev: false + /node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} dependencies: @@ -8581,6 +8609,15 @@ packages: dependencies: whatwg-url: 5.0.0 + /node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + dev: false + /node-gyp-build-optional-packages@5.0.7: resolution: {integrity: sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==} hasBin: true @@ -10521,6 +10558,11 @@ packages: dependencies: defaults: 1.0.4 + /web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + dev: false + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} diff --git a/src/app.module.ts b/src/app.module.ts index cb1f8e5..61d2d30 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { SubmitterModule } from './submitter/submitter.module'; import { PersisterModule } from './store/persister/persister.module'; import { StoreModule } from './store/store.module'; import { MonitorModule } from './monitor/monitor.module'; +import { PricingModule } from './pricing/pricing.module'; @Module({ imports: [ @@ -17,6 +18,7 @@ import { MonitorModule } from './monitor/monitor.module'; GetterModule, EvaluatorModule, CollectorModule, + PricingModule, SubmitterModule, PersisterModule, StoreModule, diff --git a/src/config/config.schema.ts b/src/config/config.schema.ts index 8563d2a..b51c991 100644 --- a/src/config/config.schema.ts +++ b/src/config/config.schema.ts @@ -75,6 +75,7 @@ const GLOBAL_SCHEMA = { monitor: { $ref: "monitor-schema" }, getter: { $ref: "getter-schema" }, + pricing: { $ref: "pricing-schema" }, submitter: { $ref: "submitter-schema" }, persister: { $ref: "persister-schema" }, wallet: { $ref: "wallet-schema" }, @@ -112,6 +113,20 @@ const GETTER_SCHEMA = { additionalProperties: false } +export const PRICING_SCHEMA = { + $id: "pricing-schema", + type: "object", + properties: { + provider: { $ref: "non-empty-string-schema" }, + coinDecimals: { $ref: "positive-non-zero-integer-schema" }, + pricingDenomination: { $ref: "non-empty-string-schema" }, + cacheDuration: { $ref: "positive-number-schema" }, + retryInterval: { $ref: "positive-number-schema" }, + maxTries: { $ref: "positive-non-zero-integer-schema" }, + }, + additionalProperties: true // Allow for provider-specific configurations +} + const SUBMITTER_SCHEMA = { $id: "submitter-schema", type: "object", @@ -125,6 +140,7 @@ const SUBMITTER_SCHEMA = { maxTries: { $ref: "positive-number-schema" }, maxPendingTransactions: { $ref: "positive-number-schema" }, + //TODO define 'evaluation' configuration somewhere else? gasLimitBuffer: { type: "object", patternProperties: { @@ -133,6 +149,10 @@ const SUBMITTER_SCHEMA = { }, additionalProperties: false }, + minDeliveryReward: { $ref: "positive-number-schema" }, + relativeMinDeliveryReward: { $ref: "positive-number-schema" }, + minAckReward: { $ref: "positive-number-schema" }, + relativeMinAckReward: { $ref: "positive-number-schema" }, }, additionalProperties: false } @@ -239,6 +259,7 @@ export function getConfigValidator(): AnyValidateFunction { ajv.addSchema(GLOBAL_SCHEMA); ajv.addSchema(MONITOR_SCHEMA); ajv.addSchema(GETTER_SCHEMA); + ajv.addSchema(PRICING_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 ef2d8fe..ab53d26 100644 --- a/src/config/config.service.ts +++ b/src/config/config.service.ts @@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common'; import { readFileSync } from 'fs'; import * as yaml from 'js-yaml'; import dotenv from 'dotenv'; -import { getConfigValidator } from './config.schema'; -import { GlobalConfig, ChainConfig, AMBConfig, GetterGlobalConfig, SubmitterGlobalConfig, PersisterConfig, WalletGlobalConfig, GetterConfig, SubmitterConfig, WalletConfig, MonitorConfig, MonitorGlobalConfig } from './config.types'; +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'; @Injectable() export class ConfigService { @@ -87,6 +87,7 @@ export class ConfigService { logLevel: rawGlobalConfig.logLevel, monitor: this.formatMonitorGlobalConfig(rawGlobalConfig.monitor), getter: this.formatGetterGlobalConfig(rawGlobalConfig.getter), + pricing: this.formatPricingGlobalConfig(rawGlobalConfig.pricing), submitter: this.formatSubmitterGlobalConfig(rawGlobalConfig.submitter), persister: this.formatPersisterGlobalConfig(rawGlobalConfig.persister), wallet: this.formatWalletGlobalConfig(rawGlobalConfig.wallet), @@ -107,6 +108,7 @@ export class ConfigService { stoppingBlock: rawChainConfig.stoppingBlock, monitor: this.formatMonitorConfig(rawChainConfig.monitor), getter: this.formatGetterConfig(rawChainConfig.getter), + pricing: this.formatPricingConfig(rawChainConfig.pricing), submitter: this.formatSubmitterConfig(rawChainConfig.submitter), wallet: this.formatWalletConfig(rawChainConfig.wallet), }); @@ -164,6 +166,26 @@ export class ConfigService { return { ...rawConfig } as GetterGlobalConfig; } + private formatPricingGlobalConfig(rawConfig: any): PricingGlobalConfig { + const commonKeys = Object.keys(PRICING_SCHEMA.properties); + + const formattedConfig: Record = {}; + formattedConfig['providerSpecificConfig'] = {} + + // Any configuration keys that do not form part of the 'PRICING_SCHEMA' definition are + // assumed to be provider-specific configuration options. + for (const [key, value] of Object.entries(rawConfig)) { + if (commonKeys.includes(key)) { + formattedConfig[key] = value; + } + else { + formattedConfig['providerSpecificConfig'][key] = value; + } + } + + return formattedConfig as PricingGlobalConfig; + } + private formatSubmitterGlobalConfig(rawConfig: any): SubmitterGlobalConfig { return { ...rawConfig } as SubmitterGlobalConfig; } @@ -198,6 +220,10 @@ export class ConfigService { return this.formatGetterGlobalConfig(rawConfig); } + private formatPricingConfig(rawConfig: any): PricingConfig { + return this.formatPricingGlobalConfig(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 ffea932..d8057a9 100644 --- a/src/config/config.types.ts +++ b/src/config/config.types.ts @@ -4,6 +4,7 @@ export interface GlobalConfig { logLevel?: string; monitor: MonitorGlobalConfig; getter: GetterGlobalConfig; + pricing: PricingGlobalConfig; submitter: SubmitterGlobalConfig; persister: PersisterConfig; wallet: WalletGlobalConfig; @@ -24,6 +25,18 @@ export interface GetterGlobalConfig { export interface GetterConfig extends GetterGlobalConfig {} +export interface PricingGlobalConfig { + provider?: string; + coinDecimals?: number; + pricingDenomination?: string; + cacheDuration?: number; + retryInterval?: number; + maxTries?: number; + providerSpecificConfig: Record; +}; + +export interface PricingConfig extends PricingGlobalConfig {} + export interface SubmitterGlobalConfig { enabled?: boolean; newOrdersDelay?: number; @@ -33,6 +46,10 @@ export interface SubmitterGlobalConfig { maxPendingTransactions?: number; gasLimitBuffer?: Record & { default?: number }; + minDeliveryReward?: number; + relativeMinDeliveryReward?: number; + minAckReward?: number; + relativeMinAckReward?: number; } export interface SubmitterConfig extends SubmitterGlobalConfig {} @@ -78,6 +95,7 @@ export interface ChainConfig { stoppingBlock?: number; monitor: MonitorConfig; getter: GetterConfig; + pricing: PricingConfig; submitter: SubmitterConfig; wallet: WalletConfig; } diff --git a/src/pricing/pricing.interface.ts b/src/pricing/pricing.interface.ts new file mode 100644 index 0000000..95ebe35 --- /dev/null +++ b/src/pricing/pricing.interface.ts @@ -0,0 +1,39 @@ +import { MessagePort } from 'worker_threads'; +import { GetPriceMessage, GetPriceResponse } from './pricing.types'; + +export class PricingInterface { + private portMessageId = 0; + + constructor(private readonly port: MessagePort) {} + + private getNextPortMessageId(): number { + return this.portMessageId++; + } + + async getPrice( + chainId: string, + amount: bigint, + ): Promise { + + const messageId = this.getNextPortMessageId(); + + const resultPromise = new Promise(resolve => { + const listener = (data: GetPriceResponse) => { + if (data.messageId === messageId) { + this.port.off("message", listener); + resolve(data.price); + } + }; + this.port.on("message", listener); + + const request: GetPriceMessage = { + messageId, + chainId, + amount + }; + this.port.postMessage(request); + }); + + return resultPromise; + } +} diff --git a/src/pricing/pricing.module.ts b/src/pricing/pricing.module.ts new file mode 100644 index 0000000..9b876ac --- /dev/null +++ b/src/pricing/pricing.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PricingService } from './pricing.service'; + +@Global() +@Module({ + providers: [PricingService], + exports: [PricingService], +}) +export class PricingModule {} diff --git a/src/pricing/pricing.provider.ts b/src/pricing/pricing.provider.ts new file mode 100644 index 0000000..c6c29b2 --- /dev/null +++ b/src/pricing/pricing.provider.ts @@ -0,0 +1,146 @@ +import pino from "pino"; +import { tryErrorToString, wait } from "src/common/utils"; + +export const MAX_CACHE_DURATION = 1 * 60 * 60 * 1000; // 1 hour + +export interface PricingProviderConfig { + provider: string; + coinDecimals: number; + pricingDenomination: string; + cacheDuration: number; + retryInterval: number; + maxTries: number; + [key: string]: any; // Allow for additional provider-specific options +} + +export function loadPricingProvider( + config: Config, + logger: pino.Logger +): PricingProvider { + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const module = require(`./providers/${config.provider}`); + const providerClass: typeof BasePricingProvider = module.default; + return new providerClass( + config, + logger, + ) as unknown as PricingProvider; +} + +export async function loadPricingProviderAsync( + config: Config, + logger: pino.Logger +): Promise> { + + const module = await import(`./providers/${config.provider}`); + const providerClass: typeof BasePricingProvider = module.default; + return new providerClass( + config, + logger, + ) as unknown as PricingProvider; +} + + +export abstract class PricingProvider { + readonly abstract pricingProviderType: string; + + protected lastPriceUpdateTimestamp: number = 0; + protected cachedCoinPrice: number = 0; + + constructor( + protected readonly config: Config, + protected readonly logger: pino.Logger, + ) { + this.validateConfig(this.config); + } + + + + // Initialization helpers + // ******************************************************************************************** + + private validateConfig(config: Config): void { + if (config.cacheDuration > MAX_CACHE_DURATION) { + throw new Error( + `Invalid 'cacheDuration': exceeds maximum allowed (${config.cacheDuration} exceeds ${MAX_CACHE_DURATION}).` + ); + } + } + + + + // Pricing functions + // ******************************************************************************************** + + abstract queryCoinPrice(): Promise; + + async getPrice(amount: bigint): Promise { + const cacheValidUntilTimestamp = this.lastPriceUpdateTimestamp + this.config.cacheDuration; + const isCacheValid = Date.now() < cacheValidUntilTimestamp; + if (!isCacheValid) { + await this.updateCoinPrice(); + } + + return this.cachedCoinPrice * Number(amount) / this.config.coinDecimals; + } + + private async updateCoinPrice(): Promise { + + let latestPrice: number | undefined; + + let tryCount = 0; + while (latestPrice == undefined) { + try { + latestPrice = await this.queryCoinPrice(); + } + catch (error) { + this.logger.warn( + { + error: tryErrorToString(error), + try: ++tryCount, + }, + `Failed to query coin price. Retrying if possible.` + ); + + // Skip update and continue with 'stale' pricing info if 'maxTries' is reached, unless + // the price has never been successfully queried from the provider. + if (tryCount >= this.config.maxTries && this.lastPriceUpdateTimestamp != 0) { + this.logger.warn( + { + try: tryCount, + maxTries: this.config.maxTries, + price: this.cachedCoinPrice, + }, + `Failed to query coin price. Max tries reached. Continuing with stale data.` + ); + return this.cachedCoinPrice; + } + + await wait(this.config.retryInterval); + } + } + + this.lastPriceUpdateTimestamp = Date.now(); + this.cachedCoinPrice = latestPrice; + + this.logger.info( + { + price: latestPrice, + pricingDenomination: this.config.pricingDenomination + }, + 'Coin price updated.' + ) + return latestPrice; + } + +} + +// ! The following class should not be used, rather it is only provided for typing purposes. +export class BasePricingProvider extends PricingProvider { + readonly pricingProviderType: string = 'basePricingProvider'; + + async queryCoinPrice(): Promise { + throw new Error("Method not implemented."); + } + +} diff --git a/src/pricing/pricing.service.ts b/src/pricing/pricing.service.ts new file mode 100644 index 0000000..56f63dc --- /dev/null +++ b/src/pricing/pricing.service.ts @@ -0,0 +1,161 @@ +import { LoggerService, STATUS_LOG_INTERVAL } from './../logger/logger.service'; +import { ConfigService } from './../config/config.service'; +import { Global, Injectable, OnModuleInit } from "@nestjs/common"; +import { join } from 'path'; +import { Worker, MessagePort } from 'worker_threads'; +import { tryErrorToString } from 'src/common/utils'; +import { PricingProviderConfig } from './pricing.provider'; +import { LoggerOptions } from 'pino'; +import { PricingGetPortMessage, PricingGetPortResponse } from './pricing.types'; + +export const PRICING_DEFAULT_COIN_DECIMALS = 18; +export const PRICING_DEFAULT_CACHE_DURATION = 5 * 60 * 1000; +export const PRICING_DEFAULT_PRICING_DENOMINATION = 'usd'; +export const PRICING_DEFAULT_RETRY_INTERVAL = 2000; +export const PRICING_DEFAULT_MAX_TRIES = 3; + +export interface PricingWorkerData { + chainPricingProvidersConfig: Record; + loggerOptions: LoggerOptions; +} + +@Global() +@Injectable() +export class PricingService implements OnModuleInit { + private worker: Worker | null = null; + private requestPortMessageId = 0; + + constructor( + private readonly configService: ConfigService, + private readonly loggerService: LoggerService, + ) {} + + onModuleInit() { + this.loggerService.info(`Starting Pricing worker...`); + + this.initializeWorker(); + + this.initiateIntervalStatusLog(); + } + + private initializeWorker(): void { + const workerData = this.loadWorkerConfig(); + + this.worker = new Worker(join(__dirname, 'pricing.worker.js'), { + workerData + }); + + this.worker.on('error', (error) => { + this.loggerService.fatal( + { error: tryErrorToString(error) }, + `Error on pricing worker.`, + ); + }); + + this.worker.on('exit', (exitCode) => { + this.worker = null; + this.loggerService.fatal( + { exitCode }, + `Pricing worker exited.`, + ); + }); + } + + private loadWorkerConfig(): PricingWorkerData { + const globalPricingConfig = this.configService.globalConfig.pricing; + + const chainPricingProvidersConfig: Record = {}; + + for (const [chainId, chainConfig] of this.configService.chainsConfig) { + const chainPricingConfig = chainConfig.pricing; + + const provider = chainPricingConfig.provider ?? globalPricingConfig.provider; + if (provider == undefined) { + this.loggerService.warn( + { chainId }, + `No pricing provider specified. Skipping chain. (CHAIN WILL NOT BE ABLE TO RELAY PACKETS)` + ); + continue; + } + + // Set the 'common' pricing configuration + const pricingProviderConfig: PricingProviderConfig = { + provider, + coinDecimals: chainPricingConfig.coinDecimals + ?? globalPricingConfig.coinDecimals + ?? PRICING_DEFAULT_COIN_DECIMALS, + pricingDenomination: chainPricingConfig.pricingDenomination + ?? globalPricingConfig.pricingDenomination + ?? PRICING_DEFAULT_PRICING_DENOMINATION, + cacheDuration: chainPricingConfig.cacheDuration + ?? globalPricingConfig.cacheDuration + ?? PRICING_DEFAULT_CACHE_DURATION, + retryInterval: chainPricingConfig.retryInterval + ?? globalPricingConfig.retryInterval + ?? PRICING_DEFAULT_RETRY_INTERVAL, + maxTries: chainPricingConfig.maxTries + ?? globalPricingConfig.maxTries + ?? PRICING_DEFAULT_MAX_TRIES, + } + + // Set the 'default' provider-specific options (if the chain provider matches the + // default one) + if (provider === globalPricingConfig.provider) { + for (const [key, value] of Object.entries(globalPricingConfig.providerSpecificConfig)) { + pricingProviderConfig[key] = value; + } + } + + // Set the chain provider-specific options (will override any 'default' options set + // that have matching keys) + for (const [key, value] of Object.entries(chainPricingConfig.providerSpecificConfig)) { + pricingProviderConfig[key] = value; + } + } + + return { + chainPricingProvidersConfig: chainPricingProvidersConfig, + loggerOptions: this.loggerService.loggerOptions + } + } + + private initiateIntervalStatusLog(): void { + const logStatus = () => { + const isActive = this.worker != null; + this.loggerService.info( + { isActive }, + 'Pricing worker status.' + ); + }; + setInterval(logStatus, STATUS_LOG_INTERVAL); + } + + + private getNextRequestPortMessageId(): number { + return this.requestPortMessageId++; + } + + async attachToPricing(): Promise { + + const worker = this.worker; + if (worker == undefined) { + throw new Error(`Pricing worker is null.`); + } + + const messageId = this.getNextRequestPortMessageId(); + const portPromise = new Promise((resolve) => { + const listener = (data: PricingGetPortResponse) => { + if (data.messageId === messageId) { + worker.off("message", listener); + resolve(data.port); + } + }; + worker.on("message", listener); + + const portMessage: PricingGetPortMessage = { messageId }; + worker.postMessage(portMessage); + }); + + return portPromise; + } +} \ No newline at end of file diff --git a/src/pricing/pricing.types.ts b/src/pricing/pricing.types.ts new file mode 100644 index 0000000..71f74eb --- /dev/null +++ b/src/pricing/pricing.types.ts @@ -0,0 +1,28 @@ +import { MessagePort } from "worker_threads"; + + + +// Port Channels Types +// ************************************************************************************************ +export interface PricingGetPortMessage { + messageId: number; +} + +export interface PricingGetPortResponse { + messageId: number; + port: MessagePort; +} + + +export interface GetPriceMessage { + messageId: number; + chainId: string; + amount: bigint; +} + +export interface GetPriceResponse { + messageId: number; + chainId: string; + amount: bigint; + price: number | null; +} \ No newline at end of file diff --git a/src/pricing/pricing.worker.ts b/src/pricing/pricing.worker.ts new file mode 100644 index 0000000..05c235e --- /dev/null +++ b/src/pricing/pricing.worker.ts @@ -0,0 +1,97 @@ +import pino from "pino"; +import { BasePricingProvider, PricingProviderConfig, loadPricingProvider } from "./pricing.provider"; +import { PricingWorkerData } from "./pricing.service"; +import { parentPort, workerData, MessagePort, MessageChannel } from "worker_threads"; +import { GetPriceResponse, PricingGetPortMessage, PricingGetPortResponse, GetPriceMessage } from "./pricing.types"; + + +class PricingWorker { + private readonly config: PricingWorkerData; + + private readonly providers = new Map(); + + private readonly logger: pino.Logger; + + + private portsCount = 0; + private readonly ports: Record = {}; + + constructor() { + this.config = workerData as PricingWorkerData; + this.logger = this.initializeLogger(); + + this.initializeProviders(); + + this.initializePorts(); + } + + + + // Initialization helpers + // ******************************************************************************************** + + private initializeLogger(): pino.Logger { + return pino(this.config.loggerOptions).child({ + worker: 'pricing' + }); + } + + private initializeProviders() { + for (const [chainId, config] of Object.entries(this.config.chainPricingProvidersConfig)) { + this.initializeProvider(chainId, config); + } + } + + private initializeProvider(chainId: string, config: PricingProviderConfig) { + const logger = this.logger.child({ chainId }); + const provider = loadPricingProvider(config, logger); + this.providers.set(chainId, provider); + } + + private initializePorts(): void { + parentPort!.on('message', (message: PricingGetPortMessage) => { + const port = this.registerNewPort(); + const response: PricingGetPortResponse = { + messageId: message.messageId, + port + }; + parentPort!.postMessage(response, [port]); + }); + } + + private registerNewPort(): MessagePort { + + const portId = this.portsCount++; + + const { port1, port2 } = new MessageChannel(); + + port1.on('message', (request: GetPriceMessage) => { + const pricePromise = this.getPrice(request.chainId, request.amount); + void pricePromise.then((price) => { + const response: GetPriceResponse = { + messageId: request.messageId, + chainId: request.chainId, + amount: request.amount, + price + }; + port1.emit('message', response); + }); + }) + + this.ports[portId] = port1; + + return port2; + } + + private async getPrice(chainId: string, amount: bigint): Promise { + const provider = this.providers.get(chainId); + if (provider == undefined) { + return null; + } + + return provider.getPrice(amount); + } + +} + +new PricingWorker(); diff --git a/src/pricing/providers/coin-gecko.ts b/src/pricing/providers/coin-gecko.ts new file mode 100644 index 0000000..86bd4d0 --- /dev/null +++ b/src/pricing/providers/coin-gecko.ts @@ -0,0 +1,46 @@ +import pino from "pino"; +import fetch from "node-fetch"; +import { PricingProviderConfig, PricingProvider } from "../pricing.provider"; + +export const PRICING_TYPE_COIN_GECKO = 'coin-gecko'; +export const BASE_COIN_GECKO_URL = 'https://api.coingecko.com/api/v3'; + +//TODO add support for an api key + +export interface CoinGeckoPricingConfig extends PricingProviderConfig { + coinId: string; +} + +export class FixedPricingProvider extends PricingProvider { + readonly pricingProviderType = PRICING_TYPE_COIN_GECKO; + + constructor( + config: CoinGeckoPricingConfig, + logger: pino.Logger, + ) { + super(config, logger); + } + + async queryCoinPrice(): Promise { + + const coinId = this.config.coinId; + const denom = this.config.pricingDenomination.toLowerCase(); + const endpoint = `${BASE_COIN_GECKO_URL}/simple/price?ids=${coinId}&vs_currencies=${denom}`; + + const response = await fetch(endpoint, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }); + + const parsedResponse: any = await response.json(); + const price = Number(parsedResponse[coinId]?.[denom]); + + if (isNaN(price)) { + throw new Error( + `Failed to parse api query response (endpoint ${endpoint}, response ${JSON.stringify(parsedResponse)})` + ); + } + + return price; + } +} diff --git a/src/pricing/providers/fixed.ts b/src/pricing/providers/fixed.ts new file mode 100644 index 0000000..78e36fb --- /dev/null +++ b/src/pricing/providers/fixed.ts @@ -0,0 +1,23 @@ +import pino from "pino"; +import { PricingProviderConfig, PricingProvider } from "../pricing.provider"; + +export const PRICING_TYPE_FIXED = 'fixed'; + +export interface FixedPricingConfig extends PricingProviderConfig { + value: number; +} + +export class FixedPricingProvider extends PricingProvider { + readonly pricingProviderType = PRICING_TYPE_FIXED; + + constructor( + config: FixedPricingConfig, + logger: pino.Logger, + ) { + super(config, logger); + } + + async queryCoinPrice(): Promise { + return this.config.value; + } +} diff --git a/src/store/store.lib.ts b/src/store/store.lib.ts index fd90351..25c6f95 100644 --- a/src/store/store.lib.ts +++ b/src/store/store.lib.ts @@ -218,6 +218,11 @@ export class Store { // TODO: handle this case better. return null; } + + if (bounty.deliveryGasCost != undefined) { + bounty.deliveryGasCost = BigInt(bounty.deliveryGasCost); + } + return bounty; } @@ -304,6 +309,38 @@ export class Store { return this.set(key, JSON.stringify(bounty)); } + /** + * Register how much gas was used to delivery the message associated with the Bounty. + */ + async registerDeliveryCost(event: { + messageIdentifier: string; + deliveryGasCost: bigint; + }): Promise { + const chainId = this.chainId; + if (chainId === null) + throw new Error('ChainId is not set: This connection is readonly'); + + const messageIdentifier = event.messageIdentifier; + + // Get the bounty + const key = Store.combineString( + Store.relayerStorePrefix, + Store.bountyMidfix, + messageIdentifier, + ); + const existingValue = await this.redis.get(key); + if (!existingValue) { + return; //TODO This case should never be reached. Add log. + } + + // Update the bounty information + const bounty: BountyJson = JSON.parse(existingValue); + // It's fine to override the 'BountyJson' type to 'Bounty', as the newly added information + // will be converted on JSON.stringify() (i.e. bigint => string). + (bounty as unknown as Bounty).deliveryGasCost = event.deliveryGasCost; + await this.set(key, JSON.stringify(bounty)); + } + /** * @dev This is generally assumed to be the first time the event is seen and the second time that a bounty is seen. * However, we also wanna be able to run the relayer in a way where the event is seen for the second time AND/OR diff --git a/src/store/types/store.types.ts b/src/store/types/store.types.ts index 2ba1fad..9ffc41f 100644 --- a/src/store/types/store.types.ts +++ b/src/store/types/store.types.ts @@ -48,6 +48,7 @@ export type Bounty = { submitTransactionHash?: string; execTransactionHash?: string; ackTransactionHash?: string; + deliveryGasCost?: bigint; }; export type BountyJson = { @@ -67,4 +68,5 @@ export type BountyJson = { submitTransactionHash?: string; execTransactionHash?: string; ackTransactionHash?: string; + deliveryGasCost?: string; }; diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 541ac90..469b19d 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -2,41 +2,64 @@ import { HandleOrderResult, ProcessingQueue, } from '../../processing-queue/processing-queue'; -import { EvalOrder, SubmitOrder } from '../submitter.types'; +import { BountyEvaluationConfig, EvalOrder, SubmitOrder } from '../submitter.types'; import pino from 'pino'; import { Store } from 'src/store/store.lib'; import { Bounty } from 'src/store/types/store.types'; import { BountyStatus } from 'src/store/types/bounty.enum'; import { IncentivizedMessageEscrow } from 'src/contracts'; -import { tryErrorToString } from 'src/common/utils'; -import { zeroPadValue } from 'ethers6'; +import { tryErrorToString, wait } from 'src/common/utils'; +import { BytesLike, FeeData, JsonRpcProvider, MaxUint256, zeroPadValue } from 'ethers6'; +import { ParsePayload, MessageContext } from 'src/payload/decode.payload'; +import { PricingInterface } from 'src/pricing/pricing.interface'; + export class EvalQueue extends ProcessingQueue { readonly relayerAddress: string; + private feeData: FeeData | undefined; + constructor( retryInterval: number, maxTries: number, + relayerAddress: string, private readonly store: Store, private readonly incentivesContracts: Map, private readonly chainId: string, - private readonly gasLimitBuffer: Record, - relayerAddress: string, + private readonly evaluationcConfig: BountyEvaluationConfig, + private readonly pricing: PricingInterface, + private readonly provider: JsonRpcProvider, private readonly logger: pino.Logger, ) { super(retryInterval, maxTries); this.relayerAddress = zeroPadValue(relayerAddress, 32); } + override async init(): Promise { + await this.initializeFeeData(); + } + + protected override async onProcessOrders(): Promise { + await this.updateFeeData(); + } + protected async handleOrder( order: EvalOrder, _retryCount: number, ): Promise | null> { - const gasLimit = await this.evaluateBounty(order); + const bounty = await this.queryBountyInfo(order.messageIdentifier); + if (bounty === null || bounty === undefined) { + throw Error( + `Bounty of message not found on evaluation (message ${order.messageIdentifier})`, + ); + } + + const gasLimit = await this.evaluateBounty(order, bounty); + const isDelivery = bounty.fromChainId != this.chainId; if (gasLimit > 0) { // Move the order to the submit queue - return { result: { ...order, gasLimit } }; + return { result: { ...order, gasLimit, isDelivery } }; } else { return null; } @@ -114,14 +137,8 @@ export class EvalQueue extends ProcessingQueue { return this.store.getBounty(messageIdentifier); } - private async evaluateBounty(order: EvalOrder): Promise { + private async evaluateBounty(order: EvalOrder, bounty: Bounty): Promise { const messageIdentifier = order.messageIdentifier; - const bounty = await this.queryBountyInfo(messageIdentifier); - if (bounty === null || bounty === undefined) { - throw Error( - `Bounty of message not found on evaluation (message ${messageIdentifier})`, - ); - } // Check if the bounty has already been submitted/is in process of being submitted const isDelivery = bounty.fromChainId != this.chainId; @@ -154,8 +171,8 @@ export class EvalQueue extends ProcessingQueue { const gasLimitBuffer = this.getGasLimitBuffer(order.amb); - //TODO gas prices are not being considered at this point if (isDelivery) { + //TODO is this correct? Is this desired? // Source to Destination const gasLimit = BigInt(bounty.maxGasDelivery + gasLimitBuffer); @@ -170,14 +187,38 @@ export class EvalQueue extends ProcessingQueue { `Bounty evaluation (source to destination).`, ); + if (order.priority) { + return gasEstimation; + } + + //TODO do we want this check? Should the tx always be submitted regardless of maxGasDelivery? const isGasLimitEnough = gasLimit >= gasEstimation; - const relayDelivery = order.priority || isGasLimitEnough; - return relayDelivery - ? (isGasLimitEnough ? gasLimit : gasEstimation) // Return the largest of gasLimit and gasEstimation - : 0n; + if (!isGasLimitEnough) { + // Do not relay packet + return 0n; + } + + // Evaluate the cost of packet relaying + const gasCostEstimate = this.getGasCost(gasEstimation); + const deliveryFiatCost = await this.getGasCostFiatPrice(gasCostEstimate, this.chainId); + + const maxGasDelivery = BigInt(bounty.maxGasDelivery); + const gasRewardEstimate = bounty.priceOfDeliveryGas * ( + gasEstimation > maxGasDelivery ? maxGasDelivery : gasEstimation + ); + const deliveryFiatReward = await this.getGasCostFiatPrice(gasRewardEstimate, bounty.fromChainId); + + const deliveryProfit = deliveryFiatReward - deliveryFiatCost; + + const relayDelivery = ( + deliveryProfit > this.evaluationcConfig.minDeliveryReward || + deliveryProfit / deliveryFiatCost > this.evaluationcConfig.relativeMinDeliveryReward + ); + + return relayDelivery ? gasEstimation : 0n; } else { // Destination to Source - const gasLimit = BigInt(bounty.maxGasAck + gasLimitBuffer); + const gasLimit = BigInt(bounty.maxGasAck + gasLimitBuffer); //TODO do we care about this? this.logger.debug( { @@ -190,17 +231,128 @@ export class EvalQueue extends ProcessingQueue { `Bounty evaluation (destination to source).`, ); - const isGasLimitEnough = gasLimit >= gasEstimation; - const relayAck = order.priority || isGasLimitEnough; - return relayAck - ? (isGasLimitEnough ? gasLimit : gasEstimation) // Return the largest of gasLimit and gasEstimation - : 0n; + if (order.priority) { + return gasEstimation; + } + + // Evaluate the cost of packet relaying + const gasCostEstimate = this.getGasCost(gasEstimation); + const ackFiatCost = await this.getGasCostFiatPrice(gasCostEstimate, this.chainId); + + const maxGasAck = BigInt(bounty.maxGasAck); + const gasRewardEstimate = bounty.priceOfAckGas * ( + gasEstimation > maxGasAck ? maxGasAck : gasEstimation + ); + const ackFiatReward = await this.getGasCostFiatPrice(gasRewardEstimate, this.chainId); + + const ackProfit = ackFiatReward - ackFiatCost; + const deliveryCost = bounty.deliveryGasCost ?? 0n; // This is only present if *this* relayer submitted the message delivery. + + let relayAck: boolean; + if (deliveryCost != 0n) { + // 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. + + // Recalculate the delivery reward using the latest pricing info + const usedGasDelivery = await this.getGasUsedForDelivery(order.message) ?? 0n; // 'gasUsed' should not be 'undefined', but if it is, continue as if it where 0 + const maxGasDelivery = BigInt(bounty.maxGasDelivery); + const deliveryGasReward = bounty.priceOfDeliveryGas * ( + usedGasDelivery > maxGasDelivery ? maxGasDelivery : usedGasDelivery + ); + const deliveryFiatReward = await this.getGasCostFiatPrice(deliveryGasReward, this.chainId); + + + relayAck = (ackProfit + deliveryFiatReward) > 0n; + } + else { + relayAck = ( + ackProfit > this.evaluationcConfig.minAckReward || + ackProfit / ackFiatCost > this.evaluationcConfig.relativeMinAckReward + ); + } + + return relayAck ? gasEstimation : 0n; } return 0n; // Do not relay packet } private getGasLimitBuffer(amb: string): number { - return this.gasLimitBuffer[amb] ?? this.gasLimitBuffer['default'] ?? 0; + return this.evaluationcConfig.gasLimitBuffer[amb] + ?? this.evaluationcConfig.gasLimitBuffer['default'] + ?? 0; + } + + + private async initializeFeeData(): Promise { + let tryCount = 0; + while (this.feeData == undefined) { + try { + this.feeData = await this.provider.getFeeData(); + } catch { + this.logger.warn( + { try: ++tryCount }, + 'Failed to initialize feeData on submitter eval-queue. Worker locked until successful update.' + ); + await wait(this.retryInterval); + } + } + } + + private async updateFeeData(): Promise { + try { + this.feeData = await this.provider.getFeeData(); + } catch { + // Continue with stale fee data. + } + } + + private getGasCost(gas: bigint): bigint { + // TODO! this should depend on the wallet's latest gas info AND on the config adjustments! + // TODO! OR the gas price should be sent to the wallet! + // If gas fee data is missing or incomplete, default the gas price to an extremely high + // value. + const gasPrice = this.feeData?.maxFeePerGas + ?? this.feeData?.maxFeePerGas + ?? MaxUint256; + + return gas * gasPrice; + } + + private async getGasCostFiatPrice(amount: bigint, chainId: string): Promise { + 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; } } diff --git a/src/submitter/queues/submit-queue.ts b/src/submitter/queues/submit-queue.ts index 738d1bd..73e9d8d 100644 --- a/src/submitter/queues/submit-queue.ts +++ b/src/submitter/queues/submit-queue.ts @@ -7,6 +7,7 @@ import { TransactionRequest, zeroPadValue } from 'ethers6'; import pino from 'pino'; import { tryErrorToString } from 'src/common/utils'; import { IncentivizedMessageEscrow } from 'src/contracts'; +import { Store } from 'src/store/store.lib'; import { WalletInterface } from 'src/wallet/wallet.interface'; export class SubmitQueue extends ProcessingQueue< @@ -18,6 +19,7 @@ export class SubmitQueue extends ProcessingQueue< constructor( retryInterval: number, maxTries: number, + private readonly store: Store, private readonly incentivesContracts: Map, relayerAddress: string, private readonly wallet: WalletInterface, @@ -138,6 +140,8 @@ export class SubmitQueue extends ProcessingQueue< orderDescription, `Successful submit order: message submitted.`, ); + + void this.registerSubmissionCost(order, result.txReceipt.gasUsed); } else { this.logger.debug( orderDescription, @@ -158,4 +162,17 @@ export class SubmitQueue extends ProcessingQueue< } } } + + private async registerSubmissionCost( + order: SubmitOrder, + gasUsed: bigint, + ): Promise { + // Currently the 'ack' submission cost is not registered. + if (order.isDelivery) { + void this.store.registerDeliveryCost({ + messageIdentifier: order.messageIdentifier, + deliveryGasCost: gasUsed + }); + } + } } diff --git a/src/submitter/submitter.service.ts b/src/submitter/submitter.service.ts index ab8df54..150bfae 100644 --- a/src/submitter/submitter.service.ts +++ b/src/submitter/submitter.service.ts @@ -8,12 +8,17 @@ 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'; const RETRY_INTERVAL_DEFAULT = 30000; const PROCESSING_INTERVAL_DEFAULT = 100; const MAX_TRIES_DEFAULT = 3; const MAX_PENDING_TRANSACTIONS = 50; const NEW_ORDERS_DELAY_DEFAULT = 0; +const MIN_DELIVERY_REWARD_DEFAULT = 0; +const RELATIVE_MIN_DELIVERY_REWARD_DEFAULT = 0; +const MIN_ACK_REWARD_DEFAULT = 0; +const RELATIVE_MIN_ACK_REWARD_DEFAULT = 0; interface GlobalSubmitterConfig { enabled: boolean; @@ -23,6 +28,10 @@ interface GlobalSubmitterConfig { maxTries: number; maxPendingTransactions: number; gasLimitBuffer: Record & { default?: number }; + minDeliveryReward: number; + relativeMinDeliveryReward: number; + minAckReward: number; + relativeMinAckReward: number; walletPublicKey: string; } @@ -37,6 +46,11 @@ export interface SubmitterWorkerData { maxTries: number; maxPendingTransactions: number; gasLimitBuffer: Record; + minDeliveryReward: number; + relativeMinDeliveryReward: number; + minAckReward: number; + relativeMinAckReward: number; + pricingPort: MessagePort; walletPublicKey: string; walletPort: MessagePort; loggerOptions: LoggerOptions; @@ -48,6 +62,7 @@ export class SubmitterService { constructor( private readonly configService: ConfigService, + private readonly pricingService: PricingService, private readonly walletService: WalletService, private readonly loggerService: LoggerService, ) {} @@ -73,7 +88,7 @@ export class SubmitterService { const worker = new Worker(join(__dirname, 'submitter.worker.js'), { workerData, - transferList: [workerData.walletPort] + transferList: [workerData.pricingPort, workerData.walletPort] }); worker.on('error', (error) => @@ -117,6 +132,14 @@ export class SubmitterService { if (!('default' in gasLimitBuffer)) { gasLimitBuffer['default'] = 0; } + const minDeliveryReward = + submitterConfig.minDeliveryReward ?? MIN_DELIVERY_REWARD_DEFAULT; + const relativeMinDeliveryReward = + submitterConfig.relativeMinDeliveryReward ?? RELATIVE_MIN_DELIVERY_REWARD_DEFAULT; + const minAckReward = + submitterConfig.minAckReward ?? MIN_ACK_REWARD_DEFAULT; + const relativeMinAckReward = + submitterConfig.relativeMinAckReward ?? RELATIVE_MIN_ACK_REWARD_DEFAULT; const walletPublicKey = (new Wallet(this.configService.globalConfig.privateKey)).address; @@ -129,6 +152,10 @@ export class SubmitterService { maxPendingTransactions, gasLimitBuffer, walletPublicKey, + minDeliveryReward, + relativeMinDeliveryReward, + minAckReward, + relativeMinAckReward, }; } @@ -177,6 +204,25 @@ export class SubmitterService { globalConfig.gasLimitBuffer, chainConfig.submitter.gasLimitBuffer ?? {}, ), + + minDeliveryReward: + chainConfig.submitter.minDeliveryReward ?? + globalConfig.minDeliveryReward, + + relativeMinDeliveryReward: + chainConfig.submitter.relativeMinDeliveryReward ?? + globalConfig.relativeMinDeliveryReward, + + minAckReward: + chainConfig.submitter.minAckReward ?? + globalConfig.minAckReward, + + relativeMinAckReward: + chainConfig.submitter.relativeMinAckReward ?? + globalConfig.relativeMinAckReward, + + + pricingPort: await this.pricingService.attachToPricing(), walletPublicKey: globalConfig.walletPublicKey, walletPort: await this.walletService.attachToWallet(chainId), diff --git a/src/submitter/submitter.types.ts b/src/submitter/submitter.types.ts index 9c40270..c8d3558 100644 --- a/src/submitter/submitter.types.ts +++ b/src/submitter/submitter.types.ts @@ -12,6 +12,7 @@ export interface EvalOrder extends Order { } export interface SubmitOrder extends Order { + isDelivery: boolean; priority: boolean; gasLimit: bigint | undefined; requeueCount?: number; @@ -26,3 +27,12 @@ export interface NewOrder { order: OrderType; processAt: number; } + + +export interface BountyEvaluationConfig { + gasLimitBuffer: Record; + minDeliveryReward: number; + relativeMinDeliveryReward: number, + minAckReward: number; + relativeMinAckReward: number; +} \ No newline at end of file diff --git a/src/submitter/submitter.worker.ts b/src/submitter/submitter.worker.ts index 3fb2ffd..ea47793 100644 --- a/src/submitter/submitter.worker.ts +++ b/src/submitter/submitter.worker.ts @@ -10,12 +10,13 @@ import { IncentivizedMessageEscrow__factory } from 'src/contracts/factories/Ince import { workerData } from 'worker_threads'; import { AmbPayload } from 'src/store/types/store.types'; import { STATUS_LOG_INTERVAL } from 'src/logger/logger.service'; -import { EvalOrder, NewOrder } from './submitter.types'; +import { BountyEvaluationConfig, EvalOrder, NewOrder } 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'; class SubmitterWorker { private readonly store: Store; @@ -28,6 +29,7 @@ class SubmitterWorker { private readonly chainId: string; + private readonly pricing: PricingInterface; private readonly wallet: WalletInterface; private readonly newOrdersQueue: NewOrder[] = []; @@ -51,18 +53,27 @@ class SubmitterWorker { }); this.signer = new Wallet(this.config.relayerPrivateKey, this.provider); + this.pricing = new PricingInterface(this.config.pricingPort); this.wallet = new WalletInterface(this.config.walletPort); [this.evalQueue, this.submitQueue] = this.initializeQueues( this.config.retryInterval, this.config.maxTries, + this.config.walletPublicKey, this.store, this.loadIncentivesContracts(this.config.incentivesAddresses), this.config.chainId, - this.config.gasLimitBuffer, - this.config.walletPublicKey, + { + gasLimitBuffer: this.config.gasLimitBuffer, + minDeliveryReward: this.config.minDeliveryReward, + relativeMinDeliveryReward: this.config.relativeMinDeliveryReward, + minAckReward: this.config.minAckReward, + relativeMinAckReward: this.config.relativeMinAckReward, + }, + this.pricing, this.wallet, + this.provider, this.logger, ); @@ -84,28 +95,33 @@ class SubmitterWorker { private initializeQueues( retryInterval: number, maxTries: number, + walletPublicKey: string, store: Store, incentivesContracts: Map, chainId: string, - gasLimitBuffer: Record, - walletPublicKey: string, + bountyEvaluationConfig: BountyEvaluationConfig, + pricing: PricingInterface, wallet: WalletInterface, + provider: JsonRpcProvider, logger: pino.Logger, ): [EvalQueue, SubmitQueue] { const evalQueue = new EvalQueue( retryInterval, maxTries, + walletPublicKey, store, incentivesContracts, chainId, - gasLimitBuffer, - walletPublicKey, + bountyEvaluationConfig, + pricing, + provider, logger, ); const submitQueue = new SubmitQueue( retryInterval, maxTries, + store, incentivesContracts, walletPublicKey, wallet, From 92a258cff381b21aaca69e428fd8c47444c34a2e Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Wed, 1 May 2024 10:11:20 +0000 Subject: [PATCH 02/43] fix: Pricing service config and loading --- src/config/config.schema.ts | 1 + src/config/config.service.ts | 2 +- src/pricing/pricing.service.ts | 8 +++++--- src/pricing/pricing.worker.ts | 2 +- src/submitter/submitter.module.ts | 3 ++- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/config/config.schema.ts b/src/config/config.schema.ts index b51c991..a0b12f4 100644 --- a/src/config/config.schema.ts +++ b/src/config/config.schema.ts @@ -237,6 +237,7 @@ const CHAINS_SCHEMA = { monitor: { $ref: "monitor-schema" }, getter: { $ref: "getter-schema" }, + pricing: { $ref: "pricing-schema" }, submitter: { $ref: "submitter-schema" }, wallet: { $ref: "wallet-schema" }, }, diff --git a/src/config/config.service.ts b/src/config/config.service.ts index ab53d26..760fa9b 100644 --- a/src/config/config.service.ts +++ b/src/config/config.service.ts @@ -174,7 +174,7 @@ export class ConfigService { // Any configuration keys that do not form part of the 'PRICING_SCHEMA' definition are // assumed to be provider-specific configuration options. - for (const [key, value] of Object.entries(rawConfig)) { + for (const [key, value] of Object.entries(rawConfig ?? {})) { if (commonKeys.includes(key)) { formattedConfig[key] = value; } diff --git a/src/pricing/pricing.service.ts b/src/pricing/pricing.service.ts index 56f63dc..7b46113 100644 --- a/src/pricing/pricing.service.ts +++ b/src/pricing/pricing.service.ts @@ -15,7 +15,7 @@ export const PRICING_DEFAULT_RETRY_INTERVAL = 2000; export const PRICING_DEFAULT_MAX_TRIES = 3; export interface PricingWorkerData { - chainPricingProvidersConfig: Record; + chainPricingProviderConfigs: Record; loggerOptions: LoggerOptions; } @@ -64,7 +64,7 @@ export class PricingService implements OnModuleInit { private loadWorkerConfig(): PricingWorkerData { const globalPricingConfig = this.configService.globalConfig.pricing; - const chainPricingProvidersConfig: Record = {}; + const chainPricingProviderConfigs: Record = {}; for (const [chainId, chainConfig] of this.configService.chainsConfig) { const chainPricingConfig = chainConfig.pricing; @@ -111,10 +111,12 @@ export class PricingService implements OnModuleInit { for (const [key, value] of Object.entries(chainPricingConfig.providerSpecificConfig)) { pricingProviderConfig[key] = value; } + + chainPricingProviderConfigs[chainId] = pricingProviderConfig; } return { - chainPricingProvidersConfig: chainPricingProvidersConfig, + chainPricingProviderConfigs: chainPricingProviderConfigs, loggerOptions: this.loggerService.loggerOptions } } diff --git a/src/pricing/pricing.worker.ts b/src/pricing/pricing.worker.ts index 05c235e..ab3f661 100644 --- a/src/pricing/pricing.worker.ts +++ b/src/pricing/pricing.worker.ts @@ -37,7 +37,7 @@ class PricingWorker { } private initializeProviders() { - for (const [chainId, config] of Object.entries(this.config.chainPricingProvidersConfig)) { + for (const [chainId, config] of Object.entries(this.config.chainPricingProviderConfigs)) { this.initializeProvider(chainId, config); } } diff --git a/src/submitter/submitter.module.ts b/src/submitter/submitter.module.ts index 03569ed..6537d1b 100644 --- a/src/submitter/submitter.module.ts +++ b/src/submitter/submitter.module.ts @@ -1,3 +1,4 @@ +import { PricingModule } from './../pricing/pricing.module'; import { Module } from '@nestjs/common'; import { SubmitterService } from './submitter.service'; import { WalletModule } from 'src/wallet/wallet.module'; @@ -5,6 +6,6 @@ import { WalletModule } from 'src/wallet/wallet.module'; @Module({ providers: [SubmitterService], exports: [SubmitterService], - imports: [WalletModule], + imports: [PricingModule, WalletModule], }) export class SubmitterModule {} From 0a49ee6b73d31afbf9bb91abebaa5a26715f42d3 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Wed, 1 May 2024 13:19:03 +0000 Subject: [PATCH 03/43] fix: Replace node-fetch with axios ('require' problem) --- package.json | 2 +- pnpm-lock.yaml | 69 +++++++++++------------------ src/pricing/providers/coin-gecko.ts | 18 ++++---- 3 files changed, 37 insertions(+), 52 deletions(-) diff --git a/package.json b/package.json index 0c398c8..8be1922 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@typechain/ethers-v6": "^0.5.1", "@wormhole-foundation/relayer-engine": "^0.3.2", "ajv": "^8.12.0", + "axios": "^1.6.8", "dotenv": "^16.3.1", "drizzle-kit": "^0.20.6", "drizzle-orm": "^0.29.1", @@ -38,7 +39,6 @@ "ethers6": "npm:ethers@^6.11.1", "ioredis": "^5.3.2", "js-yaml": "^4.1.0", - "node-fetch": "^3.3.2", "pg": "^8.11.3", "pino": "^8.15.1", "reflect-metadata": "^0.1.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f799fb7..3076cfd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ dependencies: ajv: specifier: ^8.12.0 version: 8.12.0 + axios: + specifier: ^1.6.8 + version: 1.6.8 dotenv: specifier: ^16.3.1 version: 16.4.5 @@ -53,9 +56,6 @@ dependencies: js-yaml: specifier: ^4.1.0 version: 4.1.0 - node-fetch: - specifier: ^3.3.2 - version: 3.3.2 pg: specifier: ^8.11.3 version: 8.11.3 @@ -4326,6 +4326,16 @@ packages: - debug dev: false + /axios@1.6.8: + resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} + dependencies: + follow-redirects: 1.15.6 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /babel-jest@29.7.0(@babel/core@7.24.0): resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5397,11 +5407,6 @@ packages: type: 2.7.2 dev: false - /data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - dev: false - /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -6439,14 +6444,6 @@ packages: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} dev: false - /fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - dev: false - /figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -6548,6 +6545,16 @@ packages: optional: true dev: false + /follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /foreground-child@3.1.1: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} @@ -6586,13 +6593,6 @@ packages: combined-stream: 1.0.8 mime-types: 2.1.35 - /formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - dependencies: - fetch-blob: 3.2.0 - dev: false - /formidable@2.1.2: resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} dependencies: @@ -8587,11 +8587,6 @@ packages: dev: false optional: true - /node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - dev: false - /node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} dependencies: @@ -8609,15 +8604,6 @@ packages: dependencies: whatwg-url: 5.0.0 - /node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - dev: false - /node-gyp-build-optional-packages@5.0.7: resolution: {integrity: sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==} hasBin: true @@ -9188,6 +9174,10 @@ packages: forwarded: 0.2.0 ipaddr.js: 1.9.1 + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -10558,11 +10548,6 @@ packages: dependencies: defaults: 1.0.4 - /web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - dev: false - /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} diff --git a/src/pricing/providers/coin-gecko.ts b/src/pricing/providers/coin-gecko.ts index 86bd4d0..e228d65 100644 --- a/src/pricing/providers/coin-gecko.ts +++ b/src/pricing/providers/coin-gecko.ts @@ -1,5 +1,5 @@ import pino from "pino"; -import fetch from "node-fetch"; +import axios from "axios"; import { PricingProviderConfig, PricingProvider } from "../pricing.provider"; export const PRICING_TYPE_COIN_GECKO = 'coin-gecko'; @@ -14,6 +14,10 @@ export interface CoinGeckoPricingConfig extends PricingProviderConfig { export class FixedPricingProvider extends PricingProvider { readonly pricingProviderType = PRICING_TYPE_COIN_GECKO; + private readonly client = axios.create({ + baseURL: BASE_COIN_GECKO_URL, + }) + constructor( config: CoinGeckoPricingConfig, logger: pino.Logger, @@ -25,19 +29,15 @@ export class FixedPricingProvider extends PricingProvider Date: Wed, 1 May 2024 13:19:38 +0000 Subject: [PATCH 04/43] fix: Pricing provider fixes --- src/pricing/pricing.provider.ts | 2 +- src/pricing/pricing.worker.ts | 2 +- src/pricing/providers/coin-gecko.ts | 4 +++- src/pricing/providers/fixed.ts | 2 ++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pricing/pricing.provider.ts b/src/pricing/pricing.provider.ts index c6c29b2..c2923b8 100644 --- a/src/pricing/pricing.provider.ts +++ b/src/pricing/pricing.provider.ts @@ -81,7 +81,7 @@ export abstract class PricingProvider { await this.updateCoinPrice(); } - return this.cachedCoinPrice * Number(amount) / this.config.coinDecimals; + return this.cachedCoinPrice * Number(amount) / 10**this.config.coinDecimals; } private async updateCoinPrice(): Promise { diff --git a/src/pricing/pricing.worker.ts b/src/pricing/pricing.worker.ts index ab3f661..06131db 100644 --- a/src/pricing/pricing.worker.ts +++ b/src/pricing/pricing.worker.ts @@ -74,7 +74,7 @@ class PricingWorker { amount: request.amount, price }; - port1.emit('message', response); + port1.postMessage(response); }); }) diff --git a/src/pricing/providers/coin-gecko.ts b/src/pricing/providers/coin-gecko.ts index e228d65..b8d4134 100644 --- a/src/pricing/providers/coin-gecko.ts +++ b/src/pricing/providers/coin-gecko.ts @@ -11,7 +11,7 @@ export interface CoinGeckoPricingConfig extends PricingProviderConfig { coinId: string; } -export class FixedPricingProvider extends PricingProvider { +export class CoinGeckoPricingProvider extends PricingProvider { readonly pricingProviderType = PRICING_TYPE_COIN_GECKO; private readonly client = axios.create({ @@ -44,3 +44,5 @@ export class FixedPricingProvider extends PricingProvider { return this.config.value; } } + +export default FixedPricingProvider; From 4fea8db695aceb17d103c0286ad78a1fe0189ef6 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Wed, 1 May 2024 13:25:35 +0000 Subject: [PATCH 05/43] fix: delivery 'gasUsed' logic for evaluation --- src/submitter/queues/eval-queue.ts | 5 ++++- src/submitter/submitter.types.ts | 1 + src/submitter/submitter.worker.ts | 8 ++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 469b19d..806198e 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -255,7 +255,9 @@ export class EvalQueue extends ProcessingQueue { // delivery. // Recalculate the delivery reward using the latest pricing info - const usedGasDelivery = await this.getGasUsedForDelivery(order.message) ?? 0n; // 'gasUsed' should not be 'undefined', but if it is, continue as if it where 0 + const usedGasDelivery = order.incentivesPayload + ? await this.getGasUsedForDelivery(order.incentivesPayload) ?? 0n + : 0n; // 'gasUsed' should not be 'undefined', but if it is, continue as if it was 0 const maxGasDelivery = BigInt(bounty.maxGasDelivery); const deliveryGasReward = bounty.priceOfDeliveryGas * ( usedGasDelivery > maxGasDelivery ? maxGasDelivery : usedGasDelivery @@ -321,6 +323,7 @@ export class EvalQueue extends ProcessingQueue { } 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.'); diff --git a/src/submitter/submitter.types.ts b/src/submitter/submitter.types.ts index c8d3558..db67c27 100644 --- a/src/submitter/submitter.types.ts +++ b/src/submitter/submitter.types.ts @@ -5,6 +5,7 @@ export interface Order { messageIdentifier: string; message: BytesLike; messageCtx: BytesLike; + incentivesPayload?: BytesLike; } export interface EvalOrder extends Order { diff --git a/src/submitter/submitter.worker.ts b/src/submitter/submitter.worker.ts index ea47793..cb1ddf1 100644 --- a/src/submitter/submitter.worker.ts +++ b/src/submitter/submitter.worker.ts @@ -218,8 +218,8 @@ class SubmitterWorker { { messageIdentifier: message.messageIdentifier, }, - `AMB message not found on submit order. Priority set to 'false'.` - ) + `AMB message not found on submit order. Submission evaluation will be less accurate.` + ); } return this.addSubmitOrder( @@ -228,6 +228,7 @@ class SubmitterWorker { message.message, message.messageCtx ?? '', ambMessage?.priority ?? false, // eval priority => undefined = false. + ambMessage?.payload, ); }) }); @@ -239,6 +240,7 @@ class SubmitterWorker { message: BytesLike, messageCtx: BytesLike, priority: boolean, + incentivesPayload?: BytesLike, ) { this.logger.debug( { messageIdentifier, priority }, @@ -252,6 +254,7 @@ class SubmitterWorker { message, messageCtx, priority: true, + incentivesPayload, }); } else { // Push into the evaluation queue @@ -263,6 +266,7 @@ class SubmitterWorker { message, messageCtx, priority: false, + incentivesPayload, }, }); } From 269702fc2d04085bc4b38de8cc48b071d69bb291 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Wed, 1 May 2024 13:44:29 +0000 Subject: [PATCH 06/43] chore: Improve evaluation logging --- src/submitter/queues/eval-queue.ts | 99 ++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 27 deletions(-) diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 806198e..5d3dc2a 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -176,24 +176,36 @@ export class EvalQueue extends ProcessingQueue { // Source to Destination const gasLimit = BigInt(bounty.maxGasDelivery + gasLimitBuffer); - this.logger.debug( - { - messageIdentifier, - gasLimit, - maxGasDelivery: bounty.maxGasDelivery, - gasLimitBuffer, - gasEstimation: gasEstimation.toString(), - }, - `Bounty evaluation (source to destination).`, - ); - if (order.priority) { + this.logger.debug( + { + messageIdentifier, + gasLimit, + maxGasDelivery: bounty.maxGasDelivery, + gasLimitBuffer, + gasEstimation: gasEstimation.toString(), + priority: true, + }, + `Bounty evaluation (source to destination).`, + ); + return gasEstimation; } //TODO do we want this check? Should the tx always be submitted regardless of maxGasDelivery? const isGasLimitEnough = gasLimit >= gasEstimation; if (!isGasLimitEnough) { + this.logger.debug( + { + messageIdentifier, + gasLimit, + maxGasDelivery: bounty.maxGasDelivery, + gasLimitBuffer, + gasEstimation: gasEstimation.toString(), + }, + `Bounty evaluation (source to destination). Gas limit exceeded.`, + ); + // Do not relay packet return 0n; } @@ -208,30 +220,47 @@ export class EvalQueue extends ProcessingQueue { ); const deliveryFiatReward = await this.getGasCostFiatPrice(gasRewardEstimate, bounty.fromChainId); - const deliveryProfit = deliveryFiatReward - deliveryFiatCost; + const deliveryFiatProfit = deliveryFiatReward - deliveryFiatCost; const relayDelivery = ( - deliveryProfit > this.evaluationcConfig.minDeliveryReward || - deliveryProfit / deliveryFiatCost > this.evaluationcConfig.relativeMinDeliveryReward + deliveryFiatProfit > this.evaluationcConfig.minDeliveryReward || + deliveryFiatProfit / deliveryFiatCost > this.evaluationcConfig.relativeMinDeliveryReward ); - return relayDelivery ? gasEstimation : 0n; - } else { - // Destination to Source - const gasLimit = BigInt(bounty.maxGasAck + gasLimitBuffer); //TODO do we care about this? - this.logger.debug( { messageIdentifier, gasLimit, - maxGasAck: bounty.maxGasAck, + maxGasDelivery: bounty.maxGasDelivery, gasLimitBuffer, gasEstimation: gasEstimation.toString(), + deliveryFiatCost, + deliveryFiatReward, + deliveryFiatProfit, + relayDelivery, }, - `Bounty evaluation (destination to source).`, + `Bounty evaluation (source to destination).`, ); + return relayDelivery ? gasEstimation : 0n; + } else { + // Destination to Source + const gasLimit = BigInt(bounty.maxGasAck + gasLimitBuffer); //TODO do we care about this? + + if (order.priority) { + this.logger.debug( + { + messageIdentifier, + gasLimit, + maxGasAck: bounty.maxGasAck, + gasLimitBuffer, + gasEstimation: gasEstimation.toString(), + priority: true, + }, + `Bounty evaluation (destination to source).`, + ); + return gasEstimation; } @@ -245,10 +274,11 @@ export class EvalQueue extends ProcessingQueue { ); const ackFiatReward = await this.getGasCostFiatPrice(gasRewardEstimate, this.chainId); - const ackProfit = ackFiatReward - ackFiatCost; + const ackFiatProfit = ackFiatReward - ackFiatCost; const deliveryCost = bounty.deliveryGasCost ?? 0n; // This is only present if *this* relayer submitted the message delivery. let relayAck: boolean; + let deliveryFiatReward = 0; if (deliveryCost != 0n) { // 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 @@ -262,18 +292,33 @@ export class EvalQueue extends ProcessingQueue { const deliveryGasReward = bounty.priceOfDeliveryGas * ( usedGasDelivery > maxGasDelivery ? maxGasDelivery : usedGasDelivery ); - const deliveryFiatReward = await this.getGasCostFiatPrice(deliveryGasReward, this.chainId); - + deliveryFiatReward = await this.getGasCostFiatPrice(deliveryGasReward, this.chainId); - relayAck = (ackProfit + deliveryFiatReward) > 0n; + relayAck = (ackFiatProfit + deliveryFiatReward) > 0n; } else { relayAck = ( - ackProfit > this.evaluationcConfig.minAckReward || - ackProfit / ackFiatCost > this.evaluationcConfig.relativeMinAckReward + ackFiatProfit > this.evaluationcConfig.minAckReward || + ackFiatProfit / ackFiatCost > this.evaluationcConfig.relativeMinAckReward ); } + this.logger.debug( + { + messageIdentifier, + gasLimit, + maxGasAck: bounty.maxGasAck, + gasLimitBuffer, + gasEstimation: gasEstimation.toString(), + ackFiatCost, + ackFiatReward, + ackFiatProfit, + deliveryFiatReward, + relayAck, + }, + `Bounty evaluation (destination to source).`, + ); + return relayAck ? gasEstimation : 0n; } From 16b1787fa59fac45339e85d2b5cf8fc4d5153bc4 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Wed, 1 May 2024 15:46:58 +0000 Subject: [PATCH 07/43] chore: Fix pricing 'coinId's --- config.example.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index e8defd6..2d27e15 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -114,7 +114,7 @@ chains: getter: maxBlocks: 5000 pricing: - coinId: 'ethereum' + coinId: 'arbitrum' wormhole: wormholeChainId: 10003 incentivesAddress: '0xdF25f1BdE09Cee5ac1e6ef8dFA7113addBd58B28' @@ -124,7 +124,7 @@ chains: name: 'OP Sepolia' rpc: 'https://sepolia.optimism.io' pricing: - coinId: 'ethereum' + coinId: 'optimism' wormhole: wormholeChainId: 10005 incentivesAddress: '0xbDFD9163d8Cee1368698B023369f9A5Fd319A40F' @@ -137,7 +137,7 @@ chains: name: 'Base Sepolia' rpc: 'https://sepolia.base.org' pricing: - coinId: 'ethereum' + coinId: 'ethereum' #TODO polymer: incentivesAddress: '0xE106643739deB1879CcD8E3ffe2736D8B489bC2F' bridgeAddress: '0x9fcd52449261F732d017F8BD1CaCDc3dFbcD0361' From dd811a6347fd1ca2122505ddf529e97c63b7edd1 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Thu, 2 May 2024 13:01:35 +0000 Subject: [PATCH 08/43] chore: Add comments --- src/submitter/queues/eval-queue.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 5d3dc2a..22a62ba 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -169,7 +169,7 @@ export class EvalQueue extends ProcessingQueue { this.relayerAddress, ); - const gasLimitBuffer = this.getGasLimitBuffer(order.amb); + const gasLimitBuffer = this.getGasLimitBuffer(order.amb); //TODO not needed if (isDelivery) { //TODO is this correct? Is this desired? @@ -211,12 +211,15 @@ export class EvalQueue extends ProcessingQueue { } // Evaluate the cost of packet relaying + //TODO for delivery, the ack reward must be taken into account + //TODO - Skip delivery if maxGasAck is too small? + //TODO - Take into account the ack gas price somehow? const gasCostEstimate = this.getGasCost(gasEstimation); const deliveryFiatCost = await this.getGasCostFiatPrice(gasCostEstimate, this.chainId); const maxGasDelivery = BigInt(bounty.maxGasDelivery); const gasRewardEstimate = bounty.priceOfDeliveryGas * ( - gasEstimation > maxGasDelivery ? maxGasDelivery : gasEstimation + gasEstimation > maxGasDelivery ? maxGasDelivery : gasEstimation //TODO gasEstimation is too large (it does not take into account that gas used by verification logic is not paid for) ); const deliveryFiatReward = await this.getGasCostFiatPrice(gasRewardEstimate, bounty.fromChainId); @@ -270,7 +273,7 @@ export class EvalQueue extends ProcessingQueue { const maxGasAck = BigInt(bounty.maxGasAck); const gasRewardEstimate = bounty.priceOfAckGas * ( - gasEstimation > maxGasAck ? maxGasAck : gasEstimation + gasEstimation > maxGasAck ? maxGasAck : gasEstimation //TODO gasEstimation is too large ); const ackFiatReward = await this.getGasCostFiatPrice(gasRewardEstimate, this.chainId); From 45f850c61232caa238cf3cd44dfbd4e0a4bd46f5 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Thu, 2 May 2024 20:11:09 +0000 Subject: [PATCH 09/43] fix: Revert pricing coinIds to 'ethereum' --- config.example.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 2d27e15..e8defd6 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -114,7 +114,7 @@ chains: getter: maxBlocks: 5000 pricing: - coinId: 'arbitrum' + coinId: 'ethereum' wormhole: wormholeChainId: 10003 incentivesAddress: '0xdF25f1BdE09Cee5ac1e6ef8dFA7113addBd58B28' @@ -124,7 +124,7 @@ chains: name: 'OP Sepolia' rpc: 'https://sepolia.optimism.io' pricing: - coinId: 'optimism' + coinId: 'ethereum' wormhole: wormholeChainId: 10005 incentivesAddress: '0xbDFD9163d8Cee1368698B023369f9A5Fd319A40F' @@ -137,7 +137,7 @@ chains: name: 'Base Sepolia' rpc: 'https://sepolia.base.org' pricing: - coinId: 'ethereum' #TODO + coinId: 'ethereum' polymer: incentivesAddress: '0xE106643739deB1879CcD8E3ffe2736D8B489bC2F' bridgeAddress: '0x9fcd52449261F732d017F8BD1CaCDc3dFbcD0361' From 89b74c35020344b016f617dd90c991b323b3ca78 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Thu, 2 May 2024 20:15:29 +0000 Subject: [PATCH 10/43] chore: Remove 'gasLimitBuffer' --- config.example.yaml | 3 --- src/config/config.schema.ts | 8 ------ src/config/config.types.ts | 1 - src/submitter/queues/eval-queue.ts | 39 ------------------------------ src/submitter/submitter.service.ts | 31 ------------------------ src/submitter/submitter.types.ts | 1 - src/submitter/submitter.worker.ts | 1 - 7 files changed, 84 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index e8defd6..645b032 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -19,9 +19,6 @@ global: maxPendingTransactions: 50 # Maximum number of transactions within the 'submit' pipeline. # Evaluation properties - gasLimitBuffer: # Extra gasLimit buffer. Customizable per AMB. - default: 10000 - mock: 50000 minDeliveryReward: 0.001 # In the 'pricingDenomination' specified below relativeMinDeliveryReward: 0.001 minAckReward: 0.001 # In the 'pricingDenomination' specified below diff --git a/src/config/config.schema.ts b/src/config/config.schema.ts index a0b12f4..b08b140 100644 --- a/src/config/config.schema.ts +++ b/src/config/config.schema.ts @@ -141,14 +141,6 @@ const SUBMITTER_SCHEMA = { maxPendingTransactions: { $ref: "positive-number-schema" }, //TODO define 'evaluation' configuration somewhere else? - gasLimitBuffer: { - type: "object", - patternProperties: { - default: { $ref: "positive-number-schema" }, - ["^[a-zA-Z0-9_-]+$"]: { $ref: "positive-number-schema" }, - }, - additionalProperties: false - }, minDeliveryReward: { $ref: "positive-number-schema" }, relativeMinDeliveryReward: { $ref: "positive-number-schema" }, minAckReward: { $ref: "positive-number-schema" }, diff --git a/src/config/config.types.ts b/src/config/config.types.ts index d8057a9..f4680dc 100644 --- a/src/config/config.types.ts +++ b/src/config/config.types.ts @@ -45,7 +45,6 @@ export interface SubmitterGlobalConfig { maxTries?: number; maxPendingTransactions?: number; - gasLimitBuffer?: Record & { default?: number }; minDeliveryReward?: number; relativeMinDeliveryReward?: number; minAckReward?: number; diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 22a62ba..9bf1293 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -169,20 +169,14 @@ export class EvalQueue extends ProcessingQueue { this.relayerAddress, ); - const gasLimitBuffer = this.getGasLimitBuffer(order.amb); //TODO not needed - if (isDelivery) { //TODO is this correct? Is this desired? // Source to Destination - const gasLimit = BigInt(bounty.maxGasDelivery + gasLimitBuffer); - if (order.priority) { this.logger.debug( { messageIdentifier, - gasLimit, maxGasDelivery: bounty.maxGasDelivery, - gasLimitBuffer, gasEstimation: gasEstimation.toString(), priority: true, }, @@ -191,24 +185,6 @@ export class EvalQueue extends ProcessingQueue { return gasEstimation; } - - //TODO do we want this check? Should the tx always be submitted regardless of maxGasDelivery? - const isGasLimitEnough = gasLimit >= gasEstimation; - if (!isGasLimitEnough) { - this.logger.debug( - { - messageIdentifier, - gasLimit, - maxGasDelivery: bounty.maxGasDelivery, - gasLimitBuffer, - gasEstimation: gasEstimation.toString(), - }, - `Bounty evaluation (source to destination). Gas limit exceeded.`, - ); - - // Do not relay packet - return 0n; - } // Evaluate the cost of packet relaying //TODO for delivery, the ack reward must be taken into account @@ -233,9 +209,7 @@ export class EvalQueue extends ProcessingQueue { this.logger.debug( { messageIdentifier, - gasLimit, maxGasDelivery: bounty.maxGasDelivery, - gasLimitBuffer, gasEstimation: gasEstimation.toString(), deliveryFiatCost, deliveryFiatReward, @@ -248,16 +222,11 @@ export class EvalQueue extends ProcessingQueue { return relayDelivery ? gasEstimation : 0n; } else { // Destination to Source - const gasLimit = BigInt(bounty.maxGasAck + gasLimitBuffer); //TODO do we care about this? - - if (order.priority) { this.logger.debug( { messageIdentifier, - gasLimit, maxGasAck: bounty.maxGasAck, - gasLimitBuffer, gasEstimation: gasEstimation.toString(), priority: true, }, @@ -309,9 +278,7 @@ export class EvalQueue extends ProcessingQueue { this.logger.debug( { messageIdentifier, - gasLimit, maxGasAck: bounty.maxGasAck, - gasLimitBuffer, gasEstimation: gasEstimation.toString(), ackFiatCost, ackFiatReward, @@ -328,12 +295,6 @@ export class EvalQueue extends ProcessingQueue { return 0n; // Do not relay packet } - private getGasLimitBuffer(amb: string): number { - return this.evaluationcConfig.gasLimitBuffer[amb] - ?? this.evaluationcConfig.gasLimitBuffer['default'] - ?? 0; - } - private async initializeFeeData(): Promise { let tryCount = 0; diff --git a/src/submitter/submitter.service.ts b/src/submitter/submitter.service.ts index 150bfae..d31df37 100644 --- a/src/submitter/submitter.service.ts +++ b/src/submitter/submitter.service.ts @@ -27,7 +27,6 @@ interface GlobalSubmitterConfig { processingInterval: number; maxTries: number; maxPendingTransactions: number; - gasLimitBuffer: Record & { default?: number }; minDeliveryReward: number; relativeMinDeliveryReward: number; minAckReward: number; @@ -45,7 +44,6 @@ export interface SubmitterWorkerData { processingInterval: number; maxTries: number; maxPendingTransactions: number; - gasLimitBuffer: Record; minDeliveryReward: number; relativeMinDeliveryReward: number; minAckReward: number; @@ -128,10 +126,6 @@ export class SubmitterService { const maxPendingTransactions = submitterConfig.maxPendingTransactions ?? MAX_PENDING_TRANSACTIONS; - const gasLimitBuffer = submitterConfig.gasLimitBuffer ?? {}; - if (!('default' in gasLimitBuffer)) { - gasLimitBuffer['default'] = 0; - } const minDeliveryReward = submitterConfig.minDeliveryReward ?? MIN_DELIVERY_REWARD_DEFAULT; const relativeMinDeliveryReward = @@ -150,7 +144,6 @@ export class SubmitterService { processingInterval, maxTries, maxPendingTransactions, - gasLimitBuffer, walletPublicKey, minDeliveryReward, relativeMinDeliveryReward, @@ -199,11 +192,6 @@ export class SubmitterService { maxPendingTransactions: chainConfig.submitter.maxPendingTransactions ?? globalConfig.maxPendingTransactions, - - gasLimitBuffer: this.getChainGasLimitBufferConfig( - globalConfig.gasLimitBuffer, - chainConfig.submitter.gasLimitBuffer ?? {}, - ), minDeliveryReward: chainConfig.submitter.minDeliveryReward ?? @@ -229,23 +217,4 @@ export class SubmitterService { loggerOptions: this.loggerService.loggerOptions, }; } - - private getChainGasLimitBufferConfig( - defaultGasLimitBufferConfig: Record, - chainGasLimitBufferConfig: Record, - ): Record { - const gasLimitBuffers: Record = {}; - - // Apply defaults - for (const key in defaultGasLimitBufferConfig) { - gasLimitBuffers[key] = defaultGasLimitBufferConfig[key]!; - } - - // Apply chain overrides - for (const key in chainGasLimitBufferConfig) { - gasLimitBuffers[key] = chainGasLimitBufferConfig[key]!; - } - - return gasLimitBuffers; - } } diff --git a/src/submitter/submitter.types.ts b/src/submitter/submitter.types.ts index db67c27..081843c 100644 --- a/src/submitter/submitter.types.ts +++ b/src/submitter/submitter.types.ts @@ -31,7 +31,6 @@ export interface NewOrder { export interface BountyEvaluationConfig { - gasLimitBuffer: Record; minDeliveryReward: number; relativeMinDeliveryReward: number, minAckReward: number; diff --git a/src/submitter/submitter.worker.ts b/src/submitter/submitter.worker.ts index cb1ddf1..73389b2 100644 --- a/src/submitter/submitter.worker.ts +++ b/src/submitter/submitter.worker.ts @@ -65,7 +65,6 @@ class SubmitterWorker { this.loadIncentivesContracts(this.config.incentivesAddresses), this.config.chainId, { - gasLimitBuffer: this.config.gasLimitBuffer, minDeliveryReward: this.config.minDeliveryReward, relativeMinDeliveryReward: this.config.relativeMinDeliveryReward, minAckReward: this.config.minAckReward, From 818ee7be3a1e99f96ad24d444a20a89ea0a4d755 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Fri, 3 May 2024 11:19:31 +0000 Subject: [PATCH 11/43] fix: gas cost calculation --- src/submitter/queues/eval-queue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 9bf1293..93933d2 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -325,7 +325,7 @@ export class EvalQueue extends ProcessingQueue { // If gas fee data is missing or incomplete, default the gas price to an extremely high // value. const gasPrice = this.feeData?.maxFeePerGas - ?? this.feeData?.maxFeePerGas + ?? this.feeData?.gasPrice ?? MaxUint256; return gas * gasPrice; From 513d22b29756eb7bb8d2666153edf7bd5da88f97 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Fri, 3 May 2024 11:25:36 +0000 Subject: [PATCH 12/43] chore: Improve pricing evaluation logging --- src/submitter/queues/eval-queue.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 93933d2..26d197d 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -211,6 +211,8 @@ export class EvalQueue extends ProcessingQueue { messageIdentifier, maxGasDelivery: bounty.maxGasDelivery, gasEstimation: gasEstimation.toString(), + gasCostEstimate: gasCostEstimate.toString(), + gasRewardEstimate: gasRewardEstimate.toString(), deliveryFiatCost, deliveryFiatReward, deliveryFiatProfit, @@ -250,6 +252,7 @@ export class EvalQueue extends ProcessingQueue { const deliveryCost = bounty.deliveryGasCost ?? 0n; // This is only present if *this* relayer submitted the message delivery. let relayAck: boolean; + let deliveryGasReward = 0n; let deliveryFiatReward = 0; if (deliveryCost != 0n) { // If the delivery was submitted by *this* relayer, always submit the ack *unless* @@ -261,7 +264,7 @@ export class EvalQueue extends ProcessingQueue { ? await this.getGasUsedForDelivery(order.incentivesPayload) ?? 0n : 0n; // 'gasUsed' should not be 'undefined', but if it is, continue as if it was 0 const maxGasDelivery = BigInt(bounty.maxGasDelivery); - const deliveryGasReward = bounty.priceOfDeliveryGas * ( + deliveryGasReward = bounty.priceOfDeliveryGas * ( usedGasDelivery > maxGasDelivery ? maxGasDelivery : usedGasDelivery ); deliveryFiatReward = await this.getGasCostFiatPrice(deliveryGasReward, this.chainId); @@ -280,9 +283,12 @@ export class EvalQueue extends ProcessingQueue { messageIdentifier, maxGasAck: bounty.maxGasAck, gasEstimation: gasEstimation.toString(), + gasCostEstimate: gasCostEstimate.toString(), + gasRewardEstimate: gasRewardEstimate.toString(), ackFiatCost, ackFiatReward, ackFiatProfit, + deliveryGasReward: deliveryGasReward.toString(), deliveryFiatReward, relayAck, }, From 058a62fb072c8b668019e5f38798ef0d9003b678 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Mon, 20 May 2024 14:57:02 +0000 Subject: [PATCH 13/43] feat: Mainnet example configuration --- config.example.yaml | 77 +++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 49 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 00df956..fa5a2d7 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -40,17 +40,17 @@ global: maxAllowedPriorityFeePerGas: # Upper bound to the 'maxPriorityFeePerGas' set on transactions (for chains that support eip-1559) '100000000000' maxPriorityFeeAdjustmentFactor: # Decimal factor used to adjust the 'maxPriorityFeePerGas' returned by 'getFeeData()'. - 1.05 # The resulting value is set as the 'maxPriorityFeePerGas' property of the transaction + 1.01 # The resulting value is set as the 'maxPriorityFeePerGas' property of the transaction # if it is smaller than the configuration property 'maxAllowedPriorityFeePerGas' (if set). # Legacy Transactions maxAllowedGasPrice: '200000000000' # Upper bound to the 'gasPrice' set on transactions (for chains that do not support eip-1559) - gasPriceAdjustmentFactor: 1.05 # Decimal factor used to adjust the 'gasPrice' returned by 'getFeeData()'. The resulting + gasPriceAdjustmentFactor: 1.01 # Decimal factor used to adjust the 'gasPrice' returned by 'getFeeData()'. The resulting # value is set as the 'gasPrice' property of the transaction if it is smaller than the # configuration property 'maxAllowedGasPrice' (if set). # All Transactions - priorityAdjustmentFactor: 1.2 # Decimal factor used to adjust **all** the gas prices (including 'maxFeePerGas') for + priorityAdjustmentFactor: 1.05 # Decimal factor used to adjust **all** the gas prices (including 'maxFeePerGas') for # priority transactions. persister: @@ -59,58 +59,37 @@ global: # AMBs configuration ambs: - # Mock is used for internal testnets. For production it should never be used. - - name: mock - enabled: false # Defaults to 'true' if the key is missing - incentivesAddress: '0x0000000000000000000000000000000000000000' - privateKey: '' - - # While we can't relay packages for Polymer, we to still collect the packages for the underwriter to work. - - name: polymer - - name: wormhole - isTestnet: true + isTestnet: false # Chain configuration chains: - - chainId: 11155111 - name: 'Sepolia' - rpc: 'https://eth-sepolia-public.unifra.io' - - # startingBlock # The block number at which to start Relaying (not all AMB collectors may support this property) - # stoppingBlock # The block number at which to stop Relaying (not all AMB collectors may support this property) - - # Overrides + - chainId: 10 + name: 'OP Mainnet' + rpc: 'https://mainnet.optimism.io' monitor: - blockDelay: 2 - interval: 5000 - - # AMB configuration - wormhole: - wormholeChainId: 10002 - incentivesAddress: '0x294F41D30D058C9e5A71810A6C758E595b7aC170' - bridgeAddress: '0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78' - - - chainId: 11155420 - name: 'OP Sepolia' - rpc: 'https://sepolia.optimism.io' - wormhole: - wormholeChainId: 10005 - incentivesAddress: '0x198cDD55d90277726f3222D5A8111AdB8b0af9ee' - bridgeAddress: '0x31377888146f3253211EFEf5c676D41ECe7D58Fe' - - - chainId: 84532 - name: 'Base Sepolia' - rpc: 'https://sepolia.base.org' + interval: 1000 wormhole: - wormholeChainId: 10004 - incentivesAddress: '0x63B4E24DC9814fAcDe241fB4dEFcA04d5fc6d763' - bridgeAddress: '0x79A1027a6A159502049F10906D333EC57E95F083' + wormholeChainId: 24 + incentivesAddress: '0x8C8727276725b7Da11fDA6e2646B2d2448E5B3c5' + bridgeAddress: '0xEe91C335eab126dF5fDB3797EA9d6aD93aeC9722' - - chainId: 168587773 - name: 'Blast Testnet' - rpc: 'https://sepolia.blast.io' + - chainId: 81457 + name: 'Blast Mainnet' + rpc: 'https://rpc.blast.io' + monitor: + interval: 1000 wormhole: wormholeChainId: 36 - incentivesAddress: '0x9524ACA1fF46fAd177160F0a803189Cb552A3780' - bridgeAddress: '0x473e002D7add6fB67a4964F13bFd61280Ca46886' \ No newline at end of file + incentivesAddress: '0x3C5C5436BCa59042cBC835276E51428781366d85' + bridgeAddress: '0xbebdb6C8ddC678FfA9f8748f85C815C556Dd8ac6' + + - chainId: 8453 + name: 'Base Mainnet' + rpc: 'https://mainnet.base.org' + monitor: + interval: 1000 + wormhole: + wormholeChainId: 30 + incentivesAddress: '0x3C5C5436BCa59042cBC835276E51428781366d85' + bridgeAddress: '0xbebdb6C8ddC678FfA9f8748f85C815C556Dd8ac6' From 487bb64f4a503828f3f911cd20541334e5af32db Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Mon, 20 May 2024 15:23:54 +0000 Subject: [PATCH 14/43] feat: Update docker compose for mainnet --- docker-compose.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 835c99b..4e83f21 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -22,10 +22,8 @@ services: - /node.key - --spyRPC - '[::]:${SPY_PORT}' - - --network - - /wormhole/testnet/2/1 - - --bootstrap - - '/dns4/t-guardian-01.nodes.stable.io/udp/8999/quic/p2p/12D3KooWCW3LGUtkCVkHZmVSZHzL3C4WRKWfqAiJPz1NR7dT9Bxh,/dns4/t-guardian-02.nodes.stable.io/udp/8999/quic/p2p/12D3KooWJXA6goBCiWM8ucjzc4jVUBSqL9Rri6UpjHbkMPErz5zK' + - --env + - mainnet attach: false profiles: ['wormhole'] logging: From 5f9686e049f3955b8eb252033d093078014888b1 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Mon, 20 May 2024 15:25:05 +0000 Subject: [PATCH 15/43] Revert "feat: Update docker compose for mainnet" This reverts commit 487bb64f4a503828f3f911cd20541334e5af32db. --- docker-compose.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 4e83f21..835c99b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -22,8 +22,10 @@ services: - /node.key - --spyRPC - '[::]:${SPY_PORT}' - - --env - - mainnet + - --network + - /wormhole/testnet/2/1 + - --bootstrap + - '/dns4/t-guardian-01.nodes.stable.io/udp/8999/quic/p2p/12D3KooWCW3LGUtkCVkHZmVSZHzL3C4WRKWfqAiJPz1NR7dT9Bxh,/dns4/t-guardian-02.nodes.stable.io/udp/8999/quic/p2p/12D3KooWJXA6goBCiWM8ucjzc4jVUBSqL9Rri6UpjHbkMPErz5zK' attach: false profiles: ['wormhole'] logging: From c969fe78a32c14b264f69283719d48bf7735633d Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Mon, 20 May 2024 15:27:18 +0000 Subject: [PATCH 16/43] feat: Update docker compose for mainnet --- docker-compose.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 835c99b..4e83f21 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -22,10 +22,8 @@ services: - /node.key - --spyRPC - '[::]:${SPY_PORT}' - - --network - - /wormhole/testnet/2/1 - - --bootstrap - - '/dns4/t-guardian-01.nodes.stable.io/udp/8999/quic/p2p/12D3KooWCW3LGUtkCVkHZmVSZHzL3C4WRKWfqAiJPz1NR7dT9Bxh,/dns4/t-guardian-02.nodes.stable.io/udp/8999/quic/p2p/12D3KooWJXA6goBCiWM8ucjzc4jVUBSqL9Rri6UpjHbkMPErz5zK' + - --env + - mainnet attach: false profiles: ['wormhole'] logging: From 793eff3bbe815d53f3c8dafa66fbf0d238bc611e Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Mon, 20 May 2024 16:21:48 +0000 Subject: [PATCH 17/43] chore: Add some comments back to config.example.yaml --- config.example.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config.example.yaml b/config.example.yaml index fa5a2d7..347c36c 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -67,8 +67,15 @@ chains: - chainId: 10 name: 'OP Mainnet' rpc: 'https://mainnet.optimism.io' + + # startingBlock # The block number at which to start Relaying (not all AMB collectors may support this property) + # stoppingBlock # The block number at which to stop Relaying (not all AMB collectors may support this property) + + # Overrides monitor: interval: 1000 + + # AMB configuration wormhole: wormholeChainId: 24 incentivesAddress: '0x8C8727276725b7Da11fDA6e2646B2d2448E5B3c5' From 4c73a4c4fb93cbde7888f106e64c49c8c7eafa06 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Mon, 20 May 2024 18:06:16 +0000 Subject: [PATCH 18/43] fix: Relayer engine wormhole-sdk dependency --- package.json | 5 +++++ pnpm-lock.yaml | 21 ++++++++++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 70def9e..92077ce 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,11 @@ "typechain": "^8.3.2", "typescript": "^5.1.3" }, + "pnpm": { + "overrides": { + "@wormhole-foundation/relayer-engine>@certusone/wormhole-sdk": "0.10.15" + } + }, "jest": { "moduleFileExtensions": ["js", "json", "ts"], "rootDir": "src", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eee239a..691fe23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + '@wormhole-foundation/relayer-engine>@certusone/wormhole-sdk': 0.10.15 + dependencies: '@nestjs/common': specifier: ^10.0.0 @@ -684,8 +687,8 @@ packages: '@types/node': 18.19.22 dev: false - /@certusone/wormhole-sdk@0.10.11(fastestsmallesttextencoderdecoder@1.0.22)(google-protobuf@3.21.2): - resolution: {integrity: sha512-WG2gmVqc5t5T6WeSkh6aDPMToN0whTJ1WQv9Kke3/bk4ssR6Gn6gv2OXHL0ZzYcUS8hft5EQBTNr1HGNQT2gMQ==} + /@certusone/wormhole-sdk@0.10.15(fastestsmallesttextencoderdecoder@1.0.22)(google-protobuf@3.21.2): + resolution: {integrity: sha512-XECfrvdYjsGPZWyR1bqWCPOiRw7+6upszpSvAXjKIqEnTNXOCYRkt5ae8TVh5oZxPjpts2X3t4Oi9WGcEssHpQ==} dependencies: '@certusone/wormhole-sdk-proto-web': 0.0.7(google-protobuf@3.21.2) '@certusone/wormhole-sdk-wasm': 0.0.1 @@ -2108,7 +2111,7 @@ packages: dependencies: '@injectivelabs/exceptions': 1.14.6(google-protobuf@3.21.2) '@injectivelabs/ts-types': 1.14.6 - '@injectivelabs/utils': 1.10.12(google-protobuf@3.21.2) + '@injectivelabs/utils': 1.14.6(google-protobuf@3.21.2) link-module-alias: 1.2.0 shx: 0.3.4 transitivePeerDependencies: @@ -2148,11 +2151,11 @@ packages: '@injectivelabs/grpc-web-react-native-transport': 0.0.2(@injectivelabs/grpc-web@0.0.1) '@injectivelabs/indexer-proto-ts': 1.10.8-rc.4 '@injectivelabs/mito-proto-ts': 1.0.9 - '@injectivelabs/networks': 1.10.12(google-protobuf@3.21.2) + '@injectivelabs/networks': 1.14.6(google-protobuf@3.21.2) '@injectivelabs/test-utils': 1.14.4 '@injectivelabs/token-metadata': 1.14.7(google-protobuf@3.21.2) '@injectivelabs/ts-types': 1.14.6 - '@injectivelabs/utils': 1.10.12(google-protobuf@3.21.2) + '@injectivelabs/utils': 1.14.6(google-protobuf@3.21.2) '@metamask/eth-sig-util': 4.0.1 axios: 0.27.2 bech32: 2.0.0 @@ -3841,7 +3844,7 @@ packages: dependencies: '@bull-board/api': 5.14.2(@bull-board/ui@5.14.2) '@bull-board/koa': 5.14.2 - '@certusone/wormhole-sdk': 0.10.11(fastestsmallesttextencoderdecoder@1.0.22)(google-protobuf@3.21.2) + '@certusone/wormhole-sdk': 0.10.15(fastestsmallesttextencoderdecoder@1.0.22)(google-protobuf@3.21.2) '@certusone/wormhole-spydk': 0.0.1 '@datastructures-js/queue': 4.2.3 '@improbable-eng/grpc-web-node-http-transport': 0.15.0(@improbable-eng/grpc-web@0.15.0) @@ -7129,7 +7132,7 @@ packages: is-extglob: 2.1.1 /is-hex-prefixed@1.0.0: - resolution: {integrity: sha1-fY035q135dEnFIkTxXPggtd39VQ=} + resolution: {integrity: sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==} engines: {node: '>=6.5.0', npm: '>=3'} requiresBuild: true dev: false @@ -9508,7 +9511,7 @@ packages: create-hash: 1.2.0 drbg.js: 1.0.1 elliptic: 6.5.5 - nan: 2.14.0 + nan: 2.19.0 safe-buffer: 5.2.1 dev: false optional: true @@ -9839,7 +9842,7 @@ packages: dev: true /strip-hex-prefix@1.0.0: - resolution: {integrity: sha1-DF8VX+8RUTczd96du1iNoFUA428=} + resolution: {integrity: sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==} engines: {node: '>=6.5.0', npm: '>=3'} requiresBuild: true dependencies: From 2c5e0f06f6c26bb70d9d201257f3c3a9e0db7596 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Tue, 21 May 2024 10:21:05 +0000 Subject: [PATCH 19/43] fix: relayer-engine import --- src/collector/wormhole/wormhole-engine.worker.ts | 2 +- src/collector/wormhole/wormhole-recovery.worker.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/collector/wormhole/wormhole-engine.worker.ts b/src/collector/wormhole/wormhole-engine.worker.ts index 7026e52..be36bd9 100644 --- a/src/collector/wormhole/wormhole-engine.worker.ts +++ b/src/collector/wormhole/wormhole-engine.worker.ts @@ -3,7 +3,7 @@ import { ParsedVaaWithBytes, StandardRelayerApp, StandardRelayerContext, -} from '@catalabs/relayer-engine'; +} from '@wormhole-foundation/relayer-engine'; import { decodeWormholeMessage } from 'src/collector/wormhole/wormhole.utils'; import { add0X } from 'src/common/utils'; import { workerData } from 'worker_threads'; diff --git a/src/collector/wormhole/wormhole-recovery.worker.ts b/src/collector/wormhole/wormhole-recovery.worker.ts index 108a1f5..4d9bbeb 100644 --- a/src/collector/wormhole/wormhole-recovery.worker.ts +++ b/src/collector/wormhole/wormhole-recovery.worker.ts @@ -4,7 +4,7 @@ import { workerData } from 'worker_threads'; import { ParsedVaaWithBytes, parseVaaWithBytes, -} from '@catalabs/relayer-engine'; +} from '@wormhole-foundation/relayer-engine'; import { decodeWormholeMessage } from './wormhole.utils'; import { add0X } from 'src/common/utils'; import { AmbPayload } from 'src/store/types/store.types'; From a9c4006e6a3938708f0a62f56fa3bbc11aa07a9d Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Tue, 21 May 2024 11:15:20 +0000 Subject: [PATCH 20/43] fix: Wormhole recovery api endpoint --- src/collector/wormhole/api-utils.ts | 7 +++++-- src/collector/wormhole/wormhole-engine.worker.ts | 1 + src/collector/wormhole/wormhole-recovery.ts | 1 + src/collector/wormhole/wormhole-recovery.worker.ts | 1 + src/collector/wormhole/wormhole.types.ts | 1 + 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/collector/wormhole/api-utils.ts b/src/collector/wormhole/api-utils.ts index 0a20e79..a217b46 100644 --- a/src/collector/wormhole/api-utils.ts +++ b/src/collector/wormhole/api-utils.ts @@ -1,11 +1,13 @@ import pino from "pino"; import { wait } from "src/common/utils"; -export const WORMHOLESCAN_API_ENDPOINT = 'https://api.testnet.wormholescan.io'; +export const WORMHOLESCAN_API_ENDPOINT = 'https://api.wormholescan.io'; +export const WORMHOLESCAN_API_ENDPOINT_TESTNET = 'https://api.testnet.wormholescan.io'; export async function fetchVAAs( womrholeChainId: number, emitterAddress: string, + isTestnet: boolean, pageIndex: number, logger: pino.Logger, pageSize = 1000, @@ -14,8 +16,9 @@ export async function fetchVAAs( ): Promise { for (let tryCount = 0; tryCount < maxTries; tryCount++) { try { + const apiEndpoint = isTestnet ? WORMHOLESCAN_API_ENDPOINT_TESTNET : WORMHOLESCAN_API_ENDPOINT; const response = await fetch( - `${WORMHOLESCAN_API_ENDPOINT}/api/v1/vaas/${womrholeChainId}/${emitterAddress}?page=${pageIndex}&pageSize=${pageSize}`, + `${apiEndpoint}/api/v1/vaas/${womrholeChainId}/${emitterAddress}?page=${pageIndex}&pageSize=${pageSize}`, ); const body = await response.text(); diff --git a/src/collector/wormhole/wormhole-engine.worker.ts b/src/collector/wormhole/wormhole-engine.worker.ts index be36bd9..b8e8cdd 100644 --- a/src/collector/wormhole/wormhole-engine.worker.ts +++ b/src/collector/wormhole/wormhole-engine.worker.ts @@ -143,6 +143,7 @@ class WormholeEngineWorker { const mostRecentVAAs = await fetchVAAs( wormholeChainId, wormholeConfig.incentivesAddress, + this.config.isTestnet, 0, this.logger, 1, diff --git a/src/collector/wormhole/wormhole-recovery.ts b/src/collector/wormhole/wormhole-recovery.ts index 83dd48e..c3dbcf2 100644 --- a/src/collector/wormhole/wormhole-recovery.ts +++ b/src/collector/wormhole/wormhole-recovery.ts @@ -19,6 +19,7 @@ function loadRecoveryWorkerData( } return { + isTestnet: wormholeConfig.isTestnet, ...wormholeChainConfig, startingBlock, wormholeChainIdMap: wormholeConfig.wormholeChainIdMap, diff --git a/src/collector/wormhole/wormhole-recovery.worker.ts b/src/collector/wormhole/wormhole-recovery.worker.ts index 4d9bbeb..8dff720 100644 --- a/src/collector/wormhole/wormhole-recovery.worker.ts +++ b/src/collector/wormhole/wormhole-recovery.worker.ts @@ -267,6 +267,7 @@ class WormholeRecoveryWorker { const pageVAAs: any[] = await fetchVAAs( womrholeChainId, emitterAddress, + this.config.isTestnet, pageIndex, this.logger, pageSize, diff --git a/src/collector/wormhole/wormhole.types.ts b/src/collector/wormhole/wormhole.types.ts index f72d636..eafbac6 100644 --- a/src/collector/wormhole/wormhole.types.ts +++ b/src/collector/wormhole/wormhole.types.ts @@ -38,6 +38,7 @@ export interface WormholeMessageSnifferWorkerData extends WormholeChainConfig { } export interface WormholeRecoveryWorkerData extends WormholeChainConfig { + isTestnet: boolean; startingBlock: number; wormholeChainIdMap: Map; loggerOptions: LoggerOptions; From 152e02c4921cc30bc8c973c29391ec940c8359bc Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Thu, 23 May 2024 14:40:54 +0000 Subject: [PATCH 21/43] chore: Refactor wallet ports (route via wallet service) --- src/wallet/wallet.service.ts | 49 +++++++++++++++++++++--------------- src/wallet/wallet.types.ts | 11 +++----- src/wallet/wallet.worker.ts | 37 ++++++++++++++------------- 3 files changed, 51 insertions(+), 46 deletions(-) diff --git a/src/wallet/wallet.service.ts b/src/wallet/wallet.service.ts index 12cca2e..2cf7b12 100644 --- a/src/wallet/wallet.service.ts +++ b/src/wallet/wallet.service.ts @@ -1,10 +1,10 @@ import { Global, Injectable, OnModuleInit } from '@nestjs/common'; import { join } from 'path'; import { LoggerOptions } from 'pino'; -import { Worker, MessagePort } from 'worker_threads'; +import { Worker, MessagePort, MessageChannel } from 'worker_threads'; import { ConfigService } from 'src/config/config.service'; import { LoggerService, STATUS_LOG_INTERVAL } from 'src/logger/logger.service'; -import { WalletGetPortMessage, WalletGetPortResponse } from './wallet.types'; +import { WalletServiceRoutingMessage, WalletTransactionRequestMessage } from './wallet.types'; import { Wallet } from 'ethers6'; import { tryErrorToString } from 'src/common/utils'; @@ -60,7 +60,8 @@ export interface WalletWorkerData { @Injectable() export class WalletService implements OnModuleInit { private workers: Record = {}; - private requestPortMessageId = 0; + private portsCount = 0; + private readonly ports: Record = {}; readonly publicKey: string; @@ -105,6 +106,19 @@ export class WalletService implements OnModuleInit { `Wallet worker exited.`, ); }); + + worker.on('message', (message: WalletServiceRoutingMessage) => { + const port = this.ports[message.portId]; + if (port == undefined) { + this.loggerService.error( + message, + `Unable to route transaction response on wallet: port id not found.` + ); + return; + } + + port.postMessage(message.data); + }) } // Add a small delay to wait for the workers to be initialized @@ -232,32 +246,27 @@ export class WalletService implements OnModuleInit { setInterval(logStatus, STATUS_LOG_INTERVAL); } - - private getNextRequestPortMessageId(): number { - return this.requestPortMessageId++; - } - async attachToWallet(chainId: string): Promise { const worker = this.workers[chainId]; if (worker == undefined) { throw new Error(`Wallet does not exist for chain ${chainId}`); } + + const portId = this.portsCount++; - const messageId = this.getNextRequestPortMessageId(); - const portPromise = new Promise((resolve) => { - const listener = (data: WalletGetPortResponse) => { - if (data.messageId == messageId) { - worker.off("message", listener); - resolve(data.port); - } - }; - worker.on("message", listener); + const { port1, port2 } = new MessageChannel(); - const portMessage: WalletGetPortMessage = { messageId }; - worker.postMessage(portMessage); + port1.on('message', (message: WalletTransactionRequestMessage) => { + const routingMessage: WalletServiceRoutingMessage = { + portId, + data: message + }; + worker.postMessage(routingMessage); }); - return portPromise; + this.ports[portId] = port1; + + return port2; } } diff --git a/src/wallet/wallet.types.ts b/src/wallet/wallet.types.ts index 0de3580..a800af5 100644 --- a/src/wallet/wallet.types.ts +++ b/src/wallet/wallet.types.ts @@ -1,17 +1,12 @@ import { TransactionRequest, TransactionReceipt, TransactionResponse } from "ethers6"; -import { MessagePort } from "worker_threads"; // Port Channels Types // ************************************************************************************************ -export interface WalletGetPortMessage { - messageId: number; -} - -export interface WalletGetPortResponse { - messageId: number; - port: MessagePort; +export interface WalletServiceRoutingMessage { + portId: number; + data: T; } //TODO add 'priority' diff --git a/src/wallet/wallet.worker.ts b/src/wallet/wallet.worker.ts index edf107f..21b8b00 100644 --- a/src/wallet/wallet.worker.ts +++ b/src/wallet/wallet.worker.ts @@ -6,7 +6,7 @@ import { STATUS_LOG_INTERVAL } from "src/logger/logger.service"; import { TransactionHelper } from "./transaction-helper"; import { ConfirmQueue } from "./queues/confirm-queue"; import { WalletWorkerData } from "./wallet.service"; -import { ConfirmedTransaction, GasFeeConfig, WalletGetPortMessage, WalletGetPortResponse, PendingTransaction, WalletTransactionOptions, WalletTransactionRequest, WalletTransactionRequestMessage, WalletTransactionRequestResponse, BalanceConfig } from "./wallet.types"; +import { ConfirmedTransaction, GasFeeConfig, PendingTransaction, WalletTransactionOptions, WalletTransactionRequest, WalletTransactionRequestMessage, WalletTransactionRequestResponse, BalanceConfig, WalletServiceRoutingMessage } from "./wallet.types"; import { SubmitQueue } from "./queues/submit-queue"; @@ -66,7 +66,7 @@ class WalletWorker { this.logger ); - this.initializePorts(); + this.initializePort(); this.initiateIntervalStatusLog(); } @@ -149,14 +149,15 @@ class WalletWorker { }; } - private initializePorts(): void { - parentPort!.on('message', (message: WalletGetPortMessage) => { - const port = this.registerNewPort(); - const response: WalletGetPortResponse = { - messageId: message.messageId, - port - }; - parentPort!.postMessage(response, [port]) + private initializePort(): void { + parentPort!.on('message', (message: WalletServiceRoutingMessage) => { + this.addTransaction( + message.portId, + message.data.messageId, + message.data.txRequest, + message.data.metadata, + message.data.options + ); }); } @@ -477,13 +478,7 @@ class WalletWorker { confirmationError?: any, ): void { - const port = this.ports[request.portId]; - if (port == undefined) { - this.logger.error({ request }, 'Failed to send transaction result: invalid portId.'); - return; - } - - const response: WalletTransactionRequestResponse = { + const transactionResponse: WalletTransactionRequestResponse = { messageId: request.messageId, txRequest: request.txRequest, metadata: request.metadata, @@ -492,7 +487,13 @@ class WalletWorker { submissionError: tryErrorToString(submissionError), confirmationError: tryErrorToString(confirmationError), } - port.postMessage(response); + + const routingResponse: WalletServiceRoutingMessage = { + portId: request.portId, + data: transactionResponse, + } + + parentPort!.postMessage(routingResponse); } private isNonceExpiredError(error: any, includeUnderpricedError?: boolean): boolean { From 775a19495922d2bc0fb76313d5140cc92a1af743 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Sat, 25 May 2024 17:56:57 +0000 Subject: [PATCH 22/43] feat: Wallet worker restart mechanism --- src/wallet/wallet.interface.ts | 30 ++++-- src/wallet/wallet.service.ts | 188 ++++++++++++++++++++++++--------- src/wallet/wallet.worker.ts | 24 +---- 3 files changed, 163 insertions(+), 79 deletions(-) diff --git a/src/wallet/wallet.interface.ts b/src/wallet/wallet.interface.ts index f962688..7df2c29 100644 --- a/src/wallet/wallet.interface.ts +++ b/src/wallet/wallet.interface.ts @@ -1,7 +1,7 @@ import { TransactionReceipt, TransactionRequest, TransactionResponse } from 'ethers6'; import { MessagePort } from 'worker_threads'; import { WalletTransactionOptions, WalletTransactionRequestMessage, WalletTransactionRequestResponse } from './wallet.types'; -import { tryErrorToString } from 'src/common/utils'; +import { WALLET_WORKER_CRASHED_MESSAGE_ID } from './wallet.service'; export interface TransactionResult { txRequest: TransactionRequest; @@ -30,19 +30,31 @@ export class WalletInterface { const messageId = this.getNextPortMessageId(); const resultPromise = new Promise>(resolve => { - const listener = (data: WalletTransactionRequestResponse) => { - if (data.messageId == messageId) { + const listener = (data: any) => { + if (data.messageId === messageId) { this.port.off("message", listener); + const walletResponse = data as WalletTransactionRequestResponse; + const result = { - txRequest: data.txRequest, - metadata: data.metadata, - tx: data.tx, - txReceipt: data.txReceipt, - submissionError: tryErrorToString(data.submissionError), - confirmationError: tryErrorToString(data.confirmationError) + txRequest: walletResponse.txRequest, + metadata: walletResponse.metadata, + tx: walletResponse.tx, + txReceipt: walletResponse.txReceipt, + submissionError: data.submissionError, + confirmationError: data.confirmationError }; resolve(result); + } else if (data.messageId === WALLET_WORKER_CRASHED_MESSAGE_ID) { + this.port.off("message", listener); + + const result = { + txRequest: transaction, + metadata, + submissionError: new Error('Wallet crashed.'), //TODO use a custom error type? + confirmationError: new Error('Wallet crashed.'), //TODO use a custom error type? + }; + resolve(result); } }; this.port.on("message", listener); diff --git a/src/wallet/wallet.service.ts b/src/wallet/wallet.service.ts index 2cf7b12..566a5d9 100644 --- a/src/wallet/wallet.service.ts +++ b/src/wallet/wallet.service.ts @@ -8,6 +8,8 @@ import { WalletServiceRoutingMessage, WalletTransactionRequestMessage } from './ import { Wallet } from 'ethers6'; import { tryErrorToString } from 'src/common/utils'; +export const WALLET_WORKER_CRASHED_MESSAGE_ID = -1; + const DEFAULT_WALLET_RETRY_INTERVAL = 30000; const DEFAULT_WALLET_PROCESSING_INTERVAL = 100; const DEFAULT_WALLET_MAX_TRIES = 3; @@ -56,12 +58,21 @@ export interface WalletWorkerData { } +interface PortDescription { + chainId: string; + port: MessagePort; +} + @Global() @Injectable() export class WalletService implements OnModuleInit { + private readonly defaultWorkerConfig: DefaultWalletWorkerData; + private workers: Record = {}; private portsCount = 0; - private readonly ports: Record = {}; + private readonly ports: Record = {}; + + private readonly queuedRequests: Record = {}; readonly publicKey: string; @@ -69,6 +80,7 @@ export class WalletService implements OnModuleInit { private readonly configService: ConfigService, private readonly loggerService: LoggerService, ) { + this.defaultWorkerConfig = this.loadDefaultWorkerConfig(); this.publicKey = (new Wallet(this.configService.globalConfig.privateKey)).address; } @@ -81,44 +93,9 @@ export class WalletService implements OnModuleInit { } private async initializeWorkers(): Promise { - const defaultWorkerConfig = this.loadDefaultWorkerConfig(); for (const [chainId,] of this.configService.chainsConfig) { - - const workerData = this.loadWorkerConfig(chainId, defaultWorkerConfig); - - const worker = new Worker(join(__dirname, 'wallet.worker.js'), { - workerData - }); - this.workers[chainId] = worker; - - worker.on('error', (error) => - this.loggerService.fatal( - { error: tryErrorToString(error), chainId }, - `Error on wallet worker.`, - ), - ); - - worker.on('exit', (exitCode) => { - this.workers[chainId] = null; - this.loggerService.fatal( - { exitCode, chainId }, - `Wallet worker exited.`, - ); - }); - - worker.on('message', (message: WalletServiceRoutingMessage) => { - const port = this.ports[message.portId]; - if (port == undefined) { - this.loggerService.error( - message, - `Unable to route transaction response on wallet: port id not found.` - ); - return; - } - - port.postMessage(message.data); - }) + this.spawnWorker(chainId); } // Add a small delay to wait for the workers to be initialized @@ -166,9 +143,10 @@ export class WalletService implements OnModuleInit { private loadWorkerConfig( chainId: string, - defaultConfig: DefaultWalletWorkerData ): WalletWorkerData { + const defaultConfig = this.defaultWorkerConfig; + const chainConfig = this.configService.chainsConfig.get(chainId); if (chainConfig == undefined) { throw new Error(`Unable to load config for chain ${chainId}`); @@ -229,6 +207,56 @@ export class WalletService implements OnModuleInit { }; } + private spawnWorker( + chainId: string + ): void { + const workerData = this.loadWorkerConfig(chainId); + this.loggerService.info( + { + chainId, + workerData, + }, + `Spawning wallet worker.` + ); + + const worker = new Worker(join(__dirname, 'wallet.worker.js'), { + workerData + }); + this.workers[chainId] = worker; + + worker.on('error', (error) => + this.loggerService.error( + { error: tryErrorToString(error), chainId }, + `Error on wallet worker.`, + ), + ); + + worker.on('exit', (exitCode) => { + this.workers[chainId] = null; + this.loggerService.error( + { exitCode, chainId }, + `Wallet worker exited.`, + ); + + this.abortPendingRequests(chainId); + this.spawnWorker(chainId); + this.recoverQueuedMessages(chainId); + }); + + worker.on('message', (message: WalletServiceRoutingMessage) => { + const portDescription = this.ports[message.portId]; + if (portDescription == undefined) { + this.loggerService.error( + message, + `Unable to route transaction response on wallet: port id not found.` + ); + return; + } + + portDescription.port.postMessage(message.data); + }); + } + private initiateIntervalStatusLog(): void { const logStatus = () => { const activeWorkers = []; @@ -247,26 +275,90 @@ export class WalletService implements OnModuleInit { } async attachToWallet(chainId: string): Promise { - const worker = this.workers[chainId]; - - if (worker == undefined) { - throw new Error(`Wallet does not exist for chain ${chainId}`); - } const portId = this.portsCount++; const { port1, port2 } = new MessageChannel(); port1.on('message', (message: WalletTransactionRequestMessage) => { - const routingMessage: WalletServiceRoutingMessage = { + this.handleTransactionRequestMessage( + chainId, portId, - data: message - }; - worker.postMessage(routingMessage); + message, + ); }); - this.ports[portId] = port1; + this.ports[portId] = { + chainId, + port: port1, + }; return port2; } + + private handleTransactionRequestMessage( + chainId: string, + portId: number, + message: WalletTransactionRequestMessage + ): void { + const worker = this.workers[chainId]; + + const routingMessage: WalletServiceRoutingMessage = { + portId, + data: message + }; + + if (worker == undefined) { + this.loggerService.warn( + { + chainId, + portId, + message + }, + `Wallet does not exist for the requested chain. Queueing message.` + ); + + if (!(chainId in this.queuedRequests)) { + this.queuedRequests[chainId] = []; + } + this.queuedRequests[chainId]!.push(routingMessage); + } else { + worker.postMessage(routingMessage); + } + } + + private abortPendingRequests( + chainId: string, + ): void { + for (const portDescription of Object.values(this.ports)) { + if (portDescription.chainId === chainId) { + portDescription.port.postMessage({ + messageId: WALLET_WORKER_CRASHED_MESSAGE_ID + }); + } + } + } + + private recoverQueuedMessages( + chainId: string, + ): void { + const queuedRequests = this.queuedRequests[chainId] ?? []; + this.queuedRequests[chainId] = []; + + this.loggerService.info( + { + chainId, + count: queuedRequests.length, + }, + `Recovering queued wallet requests.` + ); + + for (const request of queuedRequests) { + this.handleTransactionRequestMessage( + chainId, + request.portId, + request.data, + ); + } + } } diff --git a/src/wallet/wallet.worker.ts b/src/wallet/wallet.worker.ts index 21b8b00..70b0605 100644 --- a/src/wallet/wallet.worker.ts +++ b/src/wallet/wallet.worker.ts @@ -1,12 +1,12 @@ import { JsonRpcProvider, Wallet, Provider, AbstractProvider, ZeroAddress, TransactionResponse, TransactionReceipt, TransactionRequest } from "ethers6"; import pino, { LoggerOptions } from "pino"; -import { workerData, parentPort, MessageChannel, MessagePort } from 'worker_threads'; +import { workerData, parentPort, MessagePort } from 'worker_threads'; import { tryErrorToString, wait } from "src/common/utils"; import { STATUS_LOG_INTERVAL } from "src/logger/logger.service"; import { TransactionHelper } from "./transaction-helper"; import { ConfirmQueue } from "./queues/confirm-queue"; import { WalletWorkerData } from "./wallet.service"; -import { ConfirmedTransaction, GasFeeConfig, PendingTransaction, WalletTransactionOptions, WalletTransactionRequest, WalletTransactionRequestMessage, WalletTransactionRequestResponse, BalanceConfig, WalletServiceRoutingMessage } from "./wallet.types"; +import { ConfirmedTransaction, GasFeeConfig, PendingTransaction, WalletTransactionOptions, WalletTransactionRequest, WalletTransactionRequestResponse, BalanceConfig, WalletServiceRoutingMessage } from "./wallet.types"; import { SubmitQueue } from "./queues/submit-queue"; @@ -161,26 +161,6 @@ class WalletWorker { }); } - private registerNewPort(): MessagePort { - - const portId = this.portsCount++; - - const { port1, port2 } = new MessageChannel(); - - port1.on('message', (message: WalletTransactionRequestMessage) => { - this.addTransaction( - portId, - message.messageId, - message.txRequest, - message.metadata, - message.options - ); - }) - this.ports[portId] = port1; - - return port2; - } - private addTransaction( portId: number, messageId: number, From 67e11b213fb4bd4a7c02ffbc9de55f18275dcdcc Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Fri, 31 May 2024 11:32:34 +0000 Subject: [PATCH 23/43] fix: pnpm-lock after merge --- pnpm-lock.yaml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f1d1f9..9c80e96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ dependencies: ajv: specifier: ^8.12.0 version: 8.12.0 + axios: + specifier: ^1.6.8 + version: 1.7.2 dotenv: specifier: ^16.3.1 version: 16.4.5 @@ -4327,6 +4330,16 @@ packages: - debug dev: false + /axios@1.7.2: + resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==} + dependencies: + follow-redirects: 1.15.6 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /babel-jest@29.7.0(@babel/core@7.24.0): resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6536,6 +6549,16 @@ packages: optional: true dev: false + /follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /foreground-child@3.1.1: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} @@ -9155,6 +9178,10 @@ packages: forwarded: 0.2.0 ipaddr.js: 1.9.1 + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} From 92bdfad1c0a4f923e034fc635d78caed5da19d40 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:50:04 +0000 Subject: [PATCH 24/43] feat: Take into account 'unrewarded' gas on bounty evaluation --- config.example.yaml | 2 ++ src/config/config.schema.ts | 2 ++ src/config/config.service.ts | 9 ++++++++- src/config/config.types.ts | 2 ++ src/submitter/queues/eval-queue.ts | 10 ++++++++-- src/submitter/submitter.service.ts | 20 ++++++++++++++++++++ src/submitter/submitter.types.ts | 2 ++ src/submitter/submitter.worker.ts | 2 ++ 8 files changed, 46 insertions(+), 3 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index a704a13..4f7db8e 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -19,8 +19,10 @@ global: maxPendingTransactions: 50 # Maximum number of transactions within the 'submit' pipeline. # Evaluation properties + unrewardedDeliveryGas: '100000' # Gas amount that will be unrewarded on delivery submission. minDeliveryReward: 0.001 # In the 'pricingDenomination' specified below relativeMinDeliveryReward: 0.001 + unrewardedAckGas: '50000' # Gas amount that will be unrewarded on ack submission. minAckReward: 0.001 # In the 'pricingDenomination' specified below relativeMinAckReward: 0.001 diff --git a/src/config/config.schema.ts b/src/config/config.schema.ts index 53e13f5..f809f91 100644 --- a/src/config/config.schema.ts +++ b/src/config/config.schema.ts @@ -147,8 +147,10 @@ const SUBMITTER_SCHEMA = { maxPendingTransactions: { $ref: "positive-number-schema" }, //TODO define 'evaluation' configuration somewhere else? + unrewardedDeliveryGas: { $ref: "gas-field-schema" }, minDeliveryReward: { $ref: "positive-number-schema" }, relativeMinDeliveryReward: { $ref: "positive-number-schema" }, + unrewardedAckGas: { $ref: "gas-field-schema" }, minAckReward: { $ref: "positive-number-schema" }, relativeMinAckReward: { $ref: "positive-number-schema" }, }, diff --git a/src/config/config.service.ts b/src/config/config.service.ts index 760fa9b..a59dc96 100644 --- a/src/config/config.service.ts +++ b/src/config/config.service.ts @@ -187,7 +187,14 @@ export class ConfigService { } private formatSubmitterGlobalConfig(rawConfig: any): SubmitterGlobalConfig { - return { ...rawConfig } as SubmitterGlobalConfig; + const config = { ...rawConfig }; + if (config.unrewardedDeliveryGas != undefined) { + config.unrewardedDeliveryGas = BigInt(config.unrewardedDeliveryGas); + } + if (config.unrewardedAckGas != undefined) { + config.unrewardedAckGas = BigInt(config.unrewardedAckGas); + } + return config as SubmitterGlobalConfig; } private formatPersisterGlobalConfig(rawConfig: any): PersisterConfig { diff --git a/src/config/config.types.ts b/src/config/config.types.ts index f4680dc..1bf3315 100644 --- a/src/config/config.types.ts +++ b/src/config/config.types.ts @@ -45,8 +45,10 @@ export interface SubmitterGlobalConfig { maxTries?: number; maxPendingTransactions?: number; + unrewardedDeliveryGas?: bigint; minDeliveryReward?: number; relativeMinDeliveryReward?: number; + unrewardedAckGas?: bigint; minAckReward?: number; relativeMinAckReward?: number; } diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 26d197d..9be157a 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -194,8 +194,11 @@ export class EvalQueue extends ProcessingQueue { const deliveryFiatCost = await this.getGasCostFiatPrice(gasCostEstimate, this.chainId); const maxGasDelivery = BigInt(bounty.maxGasDelivery); + const rewardableGasEstimation = gasEstimation > this.evaluationcConfig.unrewardedDeliveryGas + ? gasEstimation - this.evaluationcConfig.unrewardedDeliveryGas + : 0n; const gasRewardEstimate = bounty.priceOfDeliveryGas * ( - gasEstimation > maxGasDelivery ? maxGasDelivery : gasEstimation //TODO gasEstimation is too large (it does not take into account that gas used by verification logic is not paid for) + rewardableGasEstimation > maxGasDelivery ? maxGasDelivery : rewardableGasEstimation ); const deliveryFiatReward = await this.getGasCostFiatPrice(gasRewardEstimate, bounty.fromChainId); @@ -243,8 +246,11 @@ export class EvalQueue extends ProcessingQueue { const ackFiatCost = await this.getGasCostFiatPrice(gasCostEstimate, this.chainId); const maxGasAck = BigInt(bounty.maxGasAck); + const rewardableGasEstimation = gasEstimation > this.evaluationcConfig.unrewardedAckGas + ? gasEstimation - this.evaluationcConfig.unrewardedAckGas + : 0n; const gasRewardEstimate = bounty.priceOfAckGas * ( - gasEstimation > maxGasAck ? maxGasAck : gasEstimation //TODO gasEstimation is too large + rewardableGasEstimation > maxGasAck ? maxGasAck : rewardableGasEstimation ); const ackFiatReward = await this.getGasCostFiatPrice(gasRewardEstimate, this.chainId); diff --git a/src/submitter/submitter.service.ts b/src/submitter/submitter.service.ts index d31df37..066c86f 100644 --- a/src/submitter/submitter.service.ts +++ b/src/submitter/submitter.service.ts @@ -15,8 +15,10 @@ const PROCESSING_INTERVAL_DEFAULT = 100; const MAX_TRIES_DEFAULT = 3; const MAX_PENDING_TRANSACTIONS = 50; const NEW_ORDERS_DELAY_DEFAULT = 0; +const UNREWARDED_DELIVERY_GAS_DEFAULT = 0n; const MIN_DELIVERY_REWARD_DEFAULT = 0; const RELATIVE_MIN_DELIVERY_REWARD_DEFAULT = 0; +const UNREWARDED_ACK_GAS_DEFAULT = 0n; const MIN_ACK_REWARD_DEFAULT = 0; const RELATIVE_MIN_ACK_REWARD_DEFAULT = 0; @@ -27,8 +29,10 @@ interface GlobalSubmitterConfig { processingInterval: number; maxTries: number; maxPendingTransactions: number; + unrewardedDeliveryGas: bigint; minDeliveryReward: number; relativeMinDeliveryReward: number; + unrewardedAckGas: bigint; minAckReward: number; relativeMinAckReward: number; walletPublicKey: string; @@ -44,8 +48,10 @@ export interface SubmitterWorkerData { processingInterval: number; maxTries: number; maxPendingTransactions: number; + unrewardedDeliveryGas: bigint; minDeliveryReward: number; relativeMinDeliveryReward: number; + unrewardedAckGas: bigint; minAckReward: number; relativeMinAckReward: number; pricingPort: MessagePort; @@ -126,10 +132,14 @@ export class SubmitterService { const maxPendingTransactions = submitterConfig.maxPendingTransactions ?? MAX_PENDING_TRANSACTIONS; + const unrewardedDeliveryGas = + submitterConfig.unrewardedDeliveryGas ?? UNREWARDED_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 minAckReward = submitterConfig.minAckReward ?? MIN_ACK_REWARD_DEFAULT; const relativeMinAckReward = @@ -145,8 +155,10 @@ export class SubmitterService { maxTries, maxPendingTransactions, walletPublicKey, + unrewardedDeliveryGas, minDeliveryReward, relativeMinDeliveryReward, + unrewardedAckGas, minAckReward, relativeMinAckReward, }; @@ -193,6 +205,10 @@ export class SubmitterService { chainConfig.submitter.maxPendingTransactions ?? globalConfig.maxPendingTransactions, + unrewardedDeliveryGas: + chainConfig.submitter.unrewardedDeliveryGas ?? + globalConfig.unrewardedDeliveryGas, + minDeliveryReward: chainConfig.submitter.minDeliveryReward ?? globalConfig.minDeliveryReward, @@ -200,6 +216,10 @@ export class SubmitterService { relativeMinDeliveryReward: chainConfig.submitter.relativeMinDeliveryReward ?? globalConfig.relativeMinDeliveryReward, + + unrewardedAckGas: + chainConfig.submitter.unrewardedAckGas ?? + globalConfig.unrewardedAckGas, minAckReward: chainConfig.submitter.minAckReward ?? diff --git a/src/submitter/submitter.types.ts b/src/submitter/submitter.types.ts index 081843c..cabb8d7 100644 --- a/src/submitter/submitter.types.ts +++ b/src/submitter/submitter.types.ts @@ -31,8 +31,10 @@ export interface NewOrder { export interface BountyEvaluationConfig { + unrewardedDeliveryGas: bigint; minDeliveryReward: number; relativeMinDeliveryReward: number, + unrewardedAckGas: bigint; minAckReward: number; relativeMinAckReward: number; } \ No newline at end of file diff --git a/src/submitter/submitter.worker.ts b/src/submitter/submitter.worker.ts index 73389b2..98f96ef 100644 --- a/src/submitter/submitter.worker.ts +++ b/src/submitter/submitter.worker.ts @@ -65,8 +65,10 @@ class SubmitterWorker { this.loadIncentivesContracts(this.config.incentivesAddresses), this.config.chainId, { + unrewardedDeliveryGas: this.config.unrewardedDeliveryGas, minDeliveryReward: this.config.minDeliveryReward, relativeMinDeliveryReward: this.config.relativeMinDeliveryReward, + unrewardedAckGas: this.config.unrewardedAckGas, minAckReward: this.config.minAckReward, relativeMinAckReward: this.config.relativeMinAckReward, }, From db017918f3387ccf437b31092774a52dc71d9338 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Mon, 3 Jun 2024 11:06:44 +0000 Subject: [PATCH 25/43] fix: Variable name --- src/submitter/queues/eval-queue.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 9be157a..7e79094 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -26,7 +26,7 @@ export class EvalQueue extends ProcessingQueue { private readonly store: Store, private readonly incentivesContracts: Map, private readonly chainId: string, - private readonly evaluationcConfig: BountyEvaluationConfig, + private readonly evaluationConfig: BountyEvaluationConfig, private readonly pricing: PricingInterface, private readonly provider: JsonRpcProvider, private readonly logger: pino.Logger, @@ -194,8 +194,8 @@ export class EvalQueue extends ProcessingQueue { const deliveryFiatCost = await this.getGasCostFiatPrice(gasCostEstimate, this.chainId); const maxGasDelivery = BigInt(bounty.maxGasDelivery); - const rewardableGasEstimation = gasEstimation > this.evaluationcConfig.unrewardedDeliveryGas - ? gasEstimation - this.evaluationcConfig.unrewardedDeliveryGas + const rewardableGasEstimation = gasEstimation > this.evaluationConfig.unrewardedDeliveryGas + ? gasEstimation - this.evaluationConfig.unrewardedDeliveryGas : 0n; const gasRewardEstimate = bounty.priceOfDeliveryGas * ( rewardableGasEstimation > maxGasDelivery ? maxGasDelivery : rewardableGasEstimation @@ -205,8 +205,8 @@ export class EvalQueue extends ProcessingQueue { const deliveryFiatProfit = deliveryFiatReward - deliveryFiatCost; const relayDelivery = ( - deliveryFiatProfit > this.evaluationcConfig.minDeliveryReward || - deliveryFiatProfit / deliveryFiatCost > this.evaluationcConfig.relativeMinDeliveryReward + deliveryFiatProfit > this.evaluationConfig.minDeliveryReward || + deliveryFiatProfit / deliveryFiatCost > this.evaluationConfig.relativeMinDeliveryReward ); this.logger.debug( @@ -246,8 +246,8 @@ export class EvalQueue extends ProcessingQueue { const ackFiatCost = await this.getGasCostFiatPrice(gasCostEstimate, this.chainId); const maxGasAck = BigInt(bounty.maxGasAck); - const rewardableGasEstimation = gasEstimation > this.evaluationcConfig.unrewardedAckGas - ? gasEstimation - this.evaluationcConfig.unrewardedAckGas + const rewardableGasEstimation = gasEstimation > this.evaluationConfig.unrewardedAckGas + ? gasEstimation - this.evaluationConfig.unrewardedAckGas : 0n; const gasRewardEstimate = bounty.priceOfAckGas * ( rewardableGasEstimation > maxGasAck ? maxGasAck : rewardableGasEstimation @@ -279,8 +279,8 @@ export class EvalQueue extends ProcessingQueue { } else { relayAck = ( - ackFiatProfit > this.evaluationcConfig.minAckReward || - ackFiatProfit / ackFiatCost > this.evaluationcConfig.relativeMinAckReward + ackFiatProfit > this.evaluationConfig.minAckReward || + ackFiatProfit / ackFiatCost > this.evaluationConfig.relativeMinAckReward ); } From a235165c7899759fbe7207245997229ccc208774 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Mon, 3 Jun 2024 13:56:32 +0000 Subject: [PATCH 26/43] fix: Variable name --- src/wallet/wallet.interface.ts | 4 ++-- src/wallet/wallet.types.ts | 2 +- src/wallet/wallet.worker.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wallet/wallet.interface.ts b/src/wallet/wallet.interface.ts index 7df2c29..84f42d9 100644 --- a/src/wallet/wallet.interface.ts +++ b/src/wallet/wallet.interface.ts @@ -1,6 +1,6 @@ import { TransactionReceipt, TransactionRequest, TransactionResponse } from 'ethers6'; import { MessagePort } from 'worker_threads'; -import { WalletTransactionOptions, WalletTransactionRequestMessage, WalletTransactionRequestResponse } from './wallet.types'; +import { WalletTransactionOptions, WalletTransactionRequestMessage, WalletTransactionRequestResponseMessage } from './wallet.types'; import { WALLET_WORKER_CRASHED_MESSAGE_ID } from './wallet.service'; export interface TransactionResult { @@ -34,7 +34,7 @@ export class WalletInterface { if (data.messageId === messageId) { this.port.off("message", listener); - const walletResponse = data as WalletTransactionRequestResponse; + const walletResponse = data as WalletTransactionRequestResponseMessage; const result = { txRequest: walletResponse.txRequest, diff --git a/src/wallet/wallet.types.ts b/src/wallet/wallet.types.ts index a800af5..0bb44d8 100644 --- a/src/wallet/wallet.types.ts +++ b/src/wallet/wallet.types.ts @@ -17,7 +17,7 @@ export interface WalletTransactionRequestMessage { options?: WalletTransactionOptions; } -export interface WalletTransactionRequestResponse { +export interface WalletTransactionRequestResponseMessage { messageId: number; txRequest: TransactionRequest; metadata: T; diff --git a/src/wallet/wallet.worker.ts b/src/wallet/wallet.worker.ts index 70b0605..7cba0f0 100644 --- a/src/wallet/wallet.worker.ts +++ b/src/wallet/wallet.worker.ts @@ -6,7 +6,7 @@ import { STATUS_LOG_INTERVAL } from "src/logger/logger.service"; import { TransactionHelper } from "./transaction-helper"; import { ConfirmQueue } from "./queues/confirm-queue"; import { WalletWorkerData } from "./wallet.service"; -import { ConfirmedTransaction, GasFeeConfig, PendingTransaction, WalletTransactionOptions, WalletTransactionRequest, WalletTransactionRequestResponse, BalanceConfig, WalletServiceRoutingMessage } from "./wallet.types"; +import { ConfirmedTransaction, GasFeeConfig, PendingTransaction, WalletTransactionOptions, WalletTransactionRequest, WalletTransactionRequestResponseMessage, BalanceConfig, WalletServiceRoutingMessage } from "./wallet.types"; import { SubmitQueue } from "./queues/submit-queue"; @@ -458,7 +458,7 @@ class WalletWorker { confirmationError?: any, ): void { - const transactionResponse: WalletTransactionRequestResponse = { + const transactionResponse: WalletTransactionRequestResponseMessage = { messageId: request.messageId, txRequest: request.txRequest, metadata: request.metadata, From 4790da261d96d1870db3754e99bbd4e22044ed2a Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Tue, 4 Jun 2024 07:50:45 +0000 Subject: [PATCH 27/43] feat: Better structure wallet communication --- src/submitter/queues/submit-queue.ts | 2 + src/submitter/submitter.service.ts | 2 +- src/submitter/submitter.worker.ts | 1 + src/wallet/wallet.interface.ts | 29 +++++--- src/wallet/wallet.service.ts | 101 ++++++++++++++------------- src/wallet/wallet.types.ts | 31 ++++++-- src/wallet/wallet.worker.ts | 39 +++++++---- 7 files changed, 130 insertions(+), 75 deletions(-) diff --git a/src/submitter/queues/submit-queue.ts b/src/submitter/queues/submit-queue.ts index 738d1bd..70dda61 100644 --- a/src/submitter/queues/submit-queue.ts +++ b/src/submitter/queues/submit-queue.ts @@ -20,6 +20,7 @@ export class SubmitQueue extends ProcessingQueue< maxTries: number, private readonly incentivesContracts: Map, relayerAddress: string, + private readonly chainId: string, private readonly wallet: WalletInterface, private readonly logger: pino.Logger, ) { @@ -65,6 +66,7 @@ export class SubmitQueue extends ProcessingQueue< }; const txPromise = this.wallet.submitTransaction( + this.chainId, txRequest, order, ).then((transactionResult): SubmitOrderResult => { diff --git a/src/submitter/submitter.service.ts b/src/submitter/submitter.service.ts index ab8df54..4b6500b 100644 --- a/src/submitter/submitter.service.ts +++ b/src/submitter/submitter.service.ts @@ -179,7 +179,7 @@ export class SubmitterService { ), walletPublicKey: globalConfig.walletPublicKey, - walletPort: await this.walletService.attachToWallet(chainId), + walletPort: await this.walletService.attachToWallet(), loggerOptions: this.loggerService.loggerOptions, }; } diff --git a/src/submitter/submitter.worker.ts b/src/submitter/submitter.worker.ts index 3fb2ffd..ce776cf 100644 --- a/src/submitter/submitter.worker.ts +++ b/src/submitter/submitter.worker.ts @@ -108,6 +108,7 @@ class SubmitterWorker { maxTries, incentivesContracts, walletPublicKey, + chainId, wallet, logger, ); diff --git a/src/wallet/wallet.interface.ts b/src/wallet/wallet.interface.ts index 84f42d9..4d8b12d 100644 --- a/src/wallet/wallet.interface.ts +++ b/src/wallet/wallet.interface.ts @@ -1,7 +1,6 @@ import { TransactionReceipt, TransactionRequest, TransactionResponse } from 'ethers6'; import { MessagePort } from 'worker_threads'; -import { WalletTransactionOptions, WalletTransactionRequestMessage, WalletTransactionRequestResponseMessage } from './wallet.types'; -import { WALLET_WORKER_CRASHED_MESSAGE_ID } from './wallet.service'; +import { WALLET_WORKER_CRASHED_MESSAGE_ID, WalletMessageType, WalletPortData, WalletTransactionOptions, WalletTransactionRequestMessage, WalletTransactionRequestResponseMessage } from './wallet.types'; export interface TransactionResult { txRequest: TransactionRequest; @@ -22,6 +21,7 @@ export class WalletInterface { } async submitTransaction( + chainId: string, transaction: TransactionRequest, metadata?: T, options?: WalletTransactionOptions @@ -30,22 +30,25 @@ export class WalletInterface { const messageId = this.getNextPortMessageId(); const resultPromise = new Promise>(resolve => { - const listener = (data: any) => { + const listener = (data: WalletPortData) => { if (data.messageId === messageId) { this.port.off("message", listener); - const walletResponse = data as WalletTransactionRequestResponseMessage; + const walletResponse = data.message as WalletTransactionRequestResponseMessage; const result = { txRequest: walletResponse.txRequest, metadata: walletResponse.metadata, tx: walletResponse.tx, txReceipt: walletResponse.txReceipt, - submissionError: data.submissionError, - confirmationError: data.confirmationError + submissionError: walletResponse.submissionError, + confirmationError: walletResponse.confirmationError }; resolve(result); - } else if (data.messageId === WALLET_WORKER_CRASHED_MESSAGE_ID) { + } else if ( + data.messageId === WALLET_WORKER_CRASHED_MESSAGE_ID + && data.chainId == chainId + ) { this.port.off("message", listener); const result = { @@ -59,13 +62,19 @@ export class WalletInterface { }; this.port.on("message", listener); - const request: WalletTransactionRequestMessage = { - messageId, + const message: WalletTransactionRequestMessage = { + type: WalletMessageType.TransactionRequest, txRequest: transaction, metadata, options }; - this.port.postMessage(request); + + const portData: WalletPortData = { + chainId, + messageId, + message, + } + this.port.postMessage(portData); }); return resultPromise; diff --git a/src/wallet/wallet.service.ts b/src/wallet/wallet.service.ts index 566a5d9..02417a2 100644 --- a/src/wallet/wallet.service.ts +++ b/src/wallet/wallet.service.ts @@ -4,12 +4,10 @@ import { LoggerOptions } from 'pino'; import { Worker, MessagePort, MessageChannel } from 'worker_threads'; import { ConfigService } from 'src/config/config.service'; import { LoggerService, STATUS_LOG_INTERVAL } from 'src/logger/logger.service'; -import { WalletServiceRoutingMessage, WalletTransactionRequestMessage } from './wallet.types'; +import { WALLET_WORKER_CRASHED_MESSAGE_ID, WalletCrashedMessage, WalletMessageType, WalletPortData, WalletServiceRoutingData } from './wallet.types'; import { Wallet } from 'ethers6'; import { tryErrorToString } from 'src/common/utils'; -export const WALLET_WORKER_CRASHED_MESSAGE_ID = -1; - const DEFAULT_WALLET_RETRY_INTERVAL = 30000; const DEFAULT_WALLET_PROCESSING_INTERVAL = 100; const DEFAULT_WALLET_MAX_TRIES = 3; @@ -58,11 +56,6 @@ export interface WalletWorkerData { } -interface PortDescription { - chainId: string; - port: MessagePort; -} - @Global() @Injectable() export class WalletService implements OnModuleInit { @@ -70,9 +63,9 @@ export class WalletService implements OnModuleInit { private workers: Record = {}; private portsCount = 0; - private readonly ports: Record = {}; + private readonly ports: Record = {}; - private readonly queuedRequests: Record = {}; + private readonly queuedMessages: Record = {}; readonly publicKey: string; @@ -243,17 +236,23 @@ export class WalletService implements OnModuleInit { this.recoverQueuedMessages(chainId); }); - worker.on('message', (message: WalletServiceRoutingMessage) => { - const portDescription = this.ports[message.portId]; - if (portDescription == undefined) { + worker.on('message', (routingData: WalletServiceRoutingData) => { + const port = this.ports[routingData.portId]; + if (port == undefined) { this.loggerService.error( - message, + { routingData }, `Unable to route transaction response on wallet: port id not found.` ); return; } - portDescription.port.postMessage(message.data); + const portData: WalletPortData = { + chainId, + messageId: routingData.messageId, + message: routingData.message, + }; + + port.postMessage(portData); }); } @@ -274,38 +273,35 @@ export class WalletService implements OnModuleInit { setInterval(logStatus, STATUS_LOG_INTERVAL); } - async attachToWallet(chainId: string): Promise { + async attachToWallet(): Promise { const portId = this.portsCount++; const { port1, port2 } = new MessageChannel(); - port1.on('message', (message: WalletTransactionRequestMessage) => { - this.handleTransactionRequestMessage( - chainId, + port1.on('message', (portData: WalletPortData) => { + this.handleWalletPortData( portId, - message, + portData, ); }); - this.ports[portId] = { - chainId, - port: port1, - }; + this.ports[portId] = port1; return port2; } - private handleTransactionRequestMessage( - chainId: string, + private handleWalletPortData( portId: number, - message: WalletTransactionRequestMessage + portData: WalletPortData, ): void { + const chainId = portData.chainId; const worker = this.workers[chainId]; - const routingMessage: WalletServiceRoutingMessage = { + const routingData: WalletServiceRoutingData = { portId, - data: message + messageId: portData.messageId, + message: portData.message, }; if (worker == undefined) { @@ -313,51 +309,60 @@ export class WalletService implements OnModuleInit { { chainId, portId, - message + message: portData.message }, `Wallet does not exist for the requested chain. Queueing message.` ); - if (!(chainId in this.queuedRequests)) { - this.queuedRequests[chainId] = []; + if (!(chainId in this.queuedMessages)) { + this.queuedMessages[chainId] = []; } - this.queuedRequests[chainId]!.push(routingMessage); + this.queuedMessages[chainId]!.push(routingData); } else { - worker.postMessage(routingMessage); + worker.postMessage(routingData); } } private abortPendingRequests( chainId: string, ): void { - for (const portDescription of Object.values(this.ports)) { - if (portDescription.chainId === chainId) { - portDescription.port.postMessage({ - messageId: WALLET_WORKER_CRASHED_MESSAGE_ID - }); - } + const message: WalletCrashedMessage = { + type: WalletMessageType.WalletCrashed, + }; + + const walletCrashBroadcast: WalletPortData = { + chainId, + messageId: WALLET_WORKER_CRASHED_MESSAGE_ID, + message, + } + + for (const port of Object.values(this.ports)) { + port.postMessage(walletCrashBroadcast); } } private recoverQueuedMessages( chainId: string, ): void { - const queuedRequests = this.queuedRequests[chainId] ?? []; - this.queuedRequests[chainId] = []; + const queuedMessages = this.queuedMessages[chainId] ?? []; + this.queuedMessages[chainId] = []; this.loggerService.info( { chainId, - count: queuedRequests.length, + count: queuedMessages.length, }, `Recovering queued wallet requests.` ); - for (const request of queuedRequests) { - this.handleTransactionRequestMessage( - chainId, - request.portId, - request.data, + for (const queuedMessage of queuedMessages) { + this.handleWalletPortData( + queuedMessage.portId, + { + chainId, + messageId: queuedMessage.messageId, + message: queuedMessage.message, + }, ); } } diff --git a/src/wallet/wallet.types.ts b/src/wallet/wallet.types.ts index 0bb44d8..445bf55 100644 --- a/src/wallet/wallet.types.ts +++ b/src/wallet/wallet.types.ts @@ -4,21 +4,40 @@ import { TransactionRequest, TransactionReceipt, TransactionResponse } from "eth // Port Channels Types // ************************************************************************************************ -export interface WalletServiceRoutingMessage { + +export const WALLET_WORKER_CRASHED_MESSAGE_ID = -1; + +export interface WalletPortData { + chainId: string; + messageId: number; + message: WalletMessage; +} + +export interface WalletServiceRoutingData { portId: number; - data: T; + messageId: number; + message: WalletMessage; +} + +export enum WalletMessageType { + TransactionRequest, + TransactionRequestResponse, + WalletCrashed, } +export type WalletMessage = WalletTransactionRequestMessage | WalletTransactionRequestResponseMessage | WalletCrashedMessage; + + //TODO add 'priority' export interface WalletTransactionRequestMessage { - messageId: number; + type: WalletMessageType.TransactionRequest, txRequest: TransactionRequest; metadata: T; options?: WalletTransactionOptions; } export interface WalletTransactionRequestResponseMessage { - messageId: number; + type: WalletMessageType.TransactionRequestResponse, txRequest: TransactionRequest; metadata: T; tx?: TransactionResponse; @@ -27,6 +46,10 @@ export interface WalletTransactionRequestResponseMessage { confirmationError?: any; } +export interface WalletCrashedMessage { + type: WalletMessageType.WalletCrashed, +} + // Processing Types diff --git a/src/wallet/wallet.worker.ts b/src/wallet/wallet.worker.ts index 7cba0f0..30d4622 100644 --- a/src/wallet/wallet.worker.ts +++ b/src/wallet/wallet.worker.ts @@ -6,7 +6,7 @@ import { STATUS_LOG_INTERVAL } from "src/logger/logger.service"; import { TransactionHelper } from "./transaction-helper"; import { ConfirmQueue } from "./queues/confirm-queue"; import { WalletWorkerData } from "./wallet.service"; -import { ConfirmedTransaction, GasFeeConfig, PendingTransaction, WalletTransactionOptions, WalletTransactionRequest, WalletTransactionRequestResponseMessage, BalanceConfig, WalletServiceRoutingMessage } from "./wallet.types"; +import { ConfirmedTransaction, GasFeeConfig, PendingTransaction, WalletTransactionOptions, WalletTransactionRequest, WalletTransactionRequestResponseMessage, BalanceConfig, WalletServiceRoutingData, WalletMessageType } from "./wallet.types"; import { SubmitQueue } from "./queues/submit-queue"; @@ -150,17 +150,31 @@ class WalletWorker { } private initializePort(): void { - parentPort!.on('message', (message: WalletServiceRoutingMessage) => { - this.addTransaction( - message.portId, - message.data.messageId, - message.data.txRequest, - message.data.metadata, - message.data.options - ); + parentPort!.on('message', (message: WalletServiceRoutingData) => { + this.processRequest(message); }); } + private processRequest(data: WalletServiceRoutingData): void { + const messageType = data.message.type; + switch(messageType) { + case WalletMessageType.TransactionRequest: + this.addTransaction( + data.portId, + data.messageId, + data.message.txRequest, + data.message.metadata, + data.message.options + ) + break; + default: + this.logger.error( + data, + 'Unable to process request: wallet message type unsupported.' + ); + } + } + private addTransaction( portId: number, messageId: number, @@ -459,7 +473,7 @@ class WalletWorker { ): void { const transactionResponse: WalletTransactionRequestResponseMessage = { - messageId: request.messageId, + type: WalletMessageType.TransactionRequestResponse, txRequest: request.txRequest, metadata: request.metadata, tx, @@ -468,9 +482,10 @@ class WalletWorker { confirmationError: tryErrorToString(confirmationError), } - const routingResponse: WalletServiceRoutingMessage = { + const routingResponse: WalletServiceRoutingData = { portId: request.portId, - data: transactionResponse, + messageId: request.messageId, + message: transactionResponse, } parentPort!.postMessage(routingResponse); From 74e3ab082e3b4d82f95e334b32ca627c73fcf9e8 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:26:39 +0000 Subject: [PATCH 28/43] feat: Add 'getFeeData' query to wallet worker and overhaul gas calculation logic --- src/wallet/transaction-helper.ts | 104 +++++++++++++++++++------------ src/wallet/wallet.interface.ts | 52 +++++++++++++++- src/wallet/wallet.types.ts | 21 ++++++- src/wallet/wallet.worker.ts | 33 +++++++++- 4 files changed, 166 insertions(+), 44 deletions(-) diff --git a/src/wallet/transaction-helper.ts b/src/wallet/transaction-helper.ts index 016b8b9..760e4a4 100644 --- a/src/wallet/transaction-helper.ts +++ b/src/wallet/transaction-helper.ts @@ -224,78 +224,102 @@ export class TransactionHelper { } } - getFeeDataForTransaction(priority?: boolean): GasFeeOverrides { - const queriedFeeData = this.feeData; - if (queriedFeeData == undefined) { - return {}; + getCachedFeeData(): FeeData | undefined { + return this.feeData; + } + + getAdjustedFeeData( + priority?: boolean, + ): FeeData | undefined { + const feeData = {...this.feeData}; + if (feeData == undefined) { + return undefined; } - const queriedMaxPriorityFeePerGas = queriedFeeData.maxPriorityFeePerGas; - if (queriedMaxPriorityFeePerGas != null) { - // Set fee data for an EIP 1559 transactions - let maxFeePerGas = this.maxFeePerGas; + // Override 'maxFeePerGas' if it is specified on the config. + if (this.maxFeePerGas) { + feeData.maxFeePerGas = this.maxFeePerGas; + } + // Adjust the 'maxPriorityFeePerGas' if present. + if (feeData.maxPriorityFeePerGas != undefined) { // Adjust the 'maxPriorityFeePerGas' by the adjustment factor - let maxPriorityFeePerGas; if (this.maxPriorityFeeAdjustmentFactor != undefined) { - maxPriorityFeePerGas = queriedMaxPriorityFeePerGas + feeData.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas * this.maxPriorityFeeAdjustmentFactor / DECIMAL_BASE_BIG_INT; } // Apply the max allowed 'maxPriorityFeePerGas' if ( - maxPriorityFeePerGas != undefined && this.maxAllowedPriorityFeePerGas != undefined && - this.maxAllowedPriorityFeePerGas < maxPriorityFeePerGas + this.maxAllowedPriorityFeePerGas < feeData.maxPriorityFeePerGas ) { - maxPriorityFeePerGas = this.maxAllowedPriorityFeePerGas; - } - - if (priority) { - if (maxFeePerGas != undefined) { - maxFeePerGas = maxFeePerGas * this.priorityAdjustmentFactor / DECIMAL_BASE_BIG_INT; - } - - if (maxPriorityFeePerGas != undefined) { - maxPriorityFeePerGas = maxPriorityFeePerGas * this.priorityAdjustmentFactor / DECIMAL_BASE_BIG_INT; - } + feeData.maxPriorityFeePerGas = this.maxAllowedPriorityFeePerGas; } + } - return { - maxFeePerGas, - maxPriorityFeePerGas, - }; - } else { - // Set traditional gasPrice - const queriedGasPrice = queriedFeeData.gasPrice; - if (queriedGasPrice == null) return {}; - + // Adjust the 'gasPrice' if present. + if (feeData.gasPrice) { // Adjust the 'gasPrice' by the adjustment factor - let gasPrice; if (this.gasPriceAdjustmentFactor != undefined) { - gasPrice = queriedGasPrice + feeData.gasPrice = feeData.gasPrice * this.gasPriceAdjustmentFactor / DECIMAL_BASE_BIG_INT; } // Apply the max allowed 'gasPrice' if ( - gasPrice != undefined && this.maxAllowedGasPrice != undefined && - this.maxAllowedGasPrice < gasPrice + this.maxAllowedGasPrice < feeData.gasPrice ) { - gasPrice = this.maxAllowedGasPrice; + feeData.gasPrice = this.maxAllowedGasPrice; } + } - if (priority && gasPrice != undefined) { - gasPrice = gasPrice + // Apply the 'priority' adjustment factor + if (priority) { + if (feeData.maxFeePerGas != undefined) { + feeData.maxFeePerGas = feeData.maxFeePerGas + *this.priorityAdjustmentFactor + / DECIMAL_BASE_BIG_INT; + } + + if (feeData.maxPriorityFeePerGas != undefined) { + feeData.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas + *this.priorityAdjustmentFactor + / DECIMAL_BASE_BIG_INT; + } + + if (feeData.gasPrice != undefined) { + feeData.gasPrice = feeData.gasPrice * this.priorityAdjustmentFactor / DECIMAL_BASE_BIG_INT; } + } + + return new FeeData( + feeData.gasPrice, + feeData.maxFeePerGas, + feeData.maxPriorityFeePerGas + ); + } + getFeeDataForTransaction(priority?: boolean): GasFeeOverrides { + const adjustedFeeData = this.getAdjustedFeeData(priority); + if (adjustedFeeData == undefined) { + return {}; + } + + if (adjustedFeeData.maxPriorityFeePerGas != undefined) { + // Set fee data for EIP 1559 transactions + return { + maxFeePerGas: adjustedFeeData.maxFeePerGas ?? undefined, + maxPriorityFeePerGas: adjustedFeeData.maxPriorityFeePerGas, + }; + } else { return { - gasPrice, + gasPrice: adjustedFeeData.gasPrice ?? undefined, }; } } diff --git a/src/wallet/wallet.interface.ts b/src/wallet/wallet.interface.ts index 4d8b12d..eaf837c 100644 --- a/src/wallet/wallet.interface.ts +++ b/src/wallet/wallet.interface.ts @@ -1,6 +1,6 @@ -import { TransactionReceipt, TransactionRequest, TransactionResponse } from 'ethers6'; +import { FeeData, TransactionReceipt, TransactionRequest, TransactionResponse } from 'ethers6'; import { MessagePort } from 'worker_threads'; -import { WALLET_WORKER_CRASHED_MESSAGE_ID, WalletMessageType, WalletPortData, WalletTransactionOptions, WalletTransactionRequestMessage, WalletTransactionRequestResponseMessage } from './wallet.types'; +import { WALLET_WORKER_CRASHED_MESSAGE_ID, WalletFeeDataMessage, WalletGetFeeDataMessage, WalletMessageType, WalletPortData, WalletTransactionOptions, WalletTransactionRequestMessage, WalletTransactionRequestResponseMessage } from './wallet.types'; export interface TransactionResult { txRequest: TransactionRequest; @@ -79,4 +79,52 @@ export class WalletInterface { return resultPromise; } + + async getFeeData( + chainId: string, + priority?: boolean, + ): Promise { + + const messageId = this.getNextPortMessageId(); + + const resultPromise = new Promise(resolve => { + const listener = (data: WalletPortData) => { + if (data.messageId === messageId) { + this.port.off("message", listener); + + const walletResponse = data.message as WalletFeeDataMessage; + + const result = { + gasPrice: walletResponse.gasPrice, + maxFeePerGas: walletResponse.maxFeePerGas, + maxPriorityFeePerGas: walletResponse.maxPriorityFeePerGas, + } as FeeData; + resolve(result); + } else if ( + data.messageId === WALLET_WORKER_CRASHED_MESSAGE_ID + && data.chainId == chainId + ) { + this.port.off("message", listener); + + const result = {} as FeeData; + resolve(result); + } + }; + this.port.on("message", listener); + + const message: WalletGetFeeDataMessage = { + type: WalletMessageType.GetFeeData, + priority: priority ?? false, + }; + + const portData: WalletPortData = { + chainId, + messageId, + message, + } + this.port.postMessage(portData); + }); + + return resultPromise; + } } diff --git a/src/wallet/wallet.types.ts b/src/wallet/wallet.types.ts index 445bf55..0fc461b 100644 --- a/src/wallet/wallet.types.ts +++ b/src/wallet/wallet.types.ts @@ -22,10 +22,16 @@ export interface WalletServiceRoutingData { export enum WalletMessageType { TransactionRequest, TransactionRequestResponse, + GetFeeData, + FeeData, WalletCrashed, } -export type WalletMessage = WalletTransactionRequestMessage | WalletTransactionRequestResponseMessage | WalletCrashedMessage; +export type WalletMessage = WalletTransactionRequestMessage + | WalletTransactionRequestResponseMessage + | WalletGetFeeDataMessage + | WalletFeeDataMessage + | WalletCrashedMessage; //TODO add 'priority' @@ -46,6 +52,19 @@ export interface WalletTransactionRequestResponseMessage { confirmationError?: any; } +export interface WalletGetFeeDataMessage { + type: WalletMessageType.GetFeeData, + priority: boolean, +} + +export interface WalletFeeDataMessage { + type: WalletMessageType.FeeData, + priority: boolean, + gasPrice?: bigint; + maxFeePerGas?: bigint; + maxPriorityFeePerGas?: bigint; +} + export interface WalletCrashedMessage { type: WalletMessageType.WalletCrashed, } diff --git a/src/wallet/wallet.worker.ts b/src/wallet/wallet.worker.ts index 30d4622..fb137cd 100644 --- a/src/wallet/wallet.worker.ts +++ b/src/wallet/wallet.worker.ts @@ -6,7 +6,7 @@ import { STATUS_LOG_INTERVAL } from "src/logger/logger.service"; import { TransactionHelper } from "./transaction-helper"; import { ConfirmQueue } from "./queues/confirm-queue"; import { WalletWorkerData } from "./wallet.service"; -import { ConfirmedTransaction, GasFeeConfig, PendingTransaction, WalletTransactionOptions, WalletTransactionRequest, WalletTransactionRequestResponseMessage, BalanceConfig, WalletServiceRoutingData, WalletMessageType } from "./wallet.types"; +import { ConfirmedTransaction, GasFeeConfig, PendingTransaction, WalletTransactionOptions, WalletTransactionRequest, WalletTransactionRequestResponseMessage, BalanceConfig, WalletServiceRoutingData, WalletMessageType, WalletFeeDataMessage } from "./wallet.types"; import { SubmitQueue } from "./queues/submit-queue"; @@ -167,6 +167,13 @@ class WalletWorker { data.message.options ) break; + case WalletMessageType.GetFeeData: + this.handleGetFeeDataRequest( + data.portId, + data.messageId, + data.message.priority, + ); + break; default: this.logger.error( data, @@ -196,6 +203,30 @@ class WalletWorker { this.newRequestsQueue.push(request); } + private handleGetFeeDataRequest( + portId: number, + messageId: number, + priority: boolean, + ): void { + const adjustedFeeData = this.transactionHelper.getAdjustedFeeData(priority); + + const feeDataMessage: WalletFeeDataMessage = { + type: WalletMessageType.FeeData, + priority, + maxFeePerGas: adjustedFeeData?.maxFeePerGas ?? undefined, + maxPriorityFeePerGas: adjustedFeeData?.maxPriorityFeePerGas ?? undefined, + gasPrice: adjustedFeeData?.gasPrice ?? undefined, + }; + + const routingResponse: WalletServiceRoutingData = { + portId, + messageId, + message: feeDataMessage, + }; + + parentPort!.postMessage(routingResponse); + } + private initiateIntervalStatusLog(): void { const logStatus = () => { const status = { From da8e8b0cddb4acc01dea3bfa10607c068c3b6c7c Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:24:43 +0000 Subject: [PATCH 29/43] chore: Overhaul relay eval (WIP) --- src/submitter/queues/eval-queue.ts | 245 +++++++++++++++++------------ src/submitter/submitter.worker.ts | 4 +- 2 files changed, 148 insertions(+), 101 deletions(-) diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 7e79094..0dc1375 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -8,17 +8,16 @@ import { Store } from 'src/store/store.lib'; import { Bounty } from 'src/store/types/store.types'; import { BountyStatus } from 'src/store/types/bounty.enum'; import { IncentivizedMessageEscrow } from 'src/contracts'; -import { tryErrorToString, wait } from 'src/common/utils'; -import { BytesLike, FeeData, JsonRpcProvider, MaxUint256, zeroPadValue } from 'ethers6'; +import { tryErrorToString } from 'src/common/utils'; +import { BytesLike, MaxUint256, 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'; export class EvalQueue extends ProcessingQueue { readonly relayerAddress: string; - private feeData: FeeData | undefined; - constructor( retryInterval: number, maxTries: number, @@ -28,21 +27,13 @@ export class EvalQueue extends ProcessingQueue { private readonly chainId: string, private readonly evaluationConfig: BountyEvaluationConfig, private readonly pricing: PricingInterface, - private readonly provider: JsonRpcProvider, + private readonly wallet: WalletInterface, private readonly logger: pino.Logger, ) { super(retryInterval, maxTries); this.relayerAddress = zeroPadValue(relayerAddress, 32); } - override async init(): Promise { - await this.initializeFeeData(); - } - - protected override async onProcessOrders(): Promise { - await this.updateFeeData(); - } - protected async handleOrder( order: EvalOrder, _retryCount: number, @@ -185,40 +176,62 @@ export class EvalQueue extends ProcessingQueue { return gasEstimation; } - - // Evaluate the cost of packet relaying - //TODO for delivery, the ack reward must be taken into account - //TODO - Skip delivery if maxGasAck is too small? - //TODO - Take into account the ack gas price somehow? - const gasCostEstimate = this.getGasCost(gasEstimation); - const deliveryFiatCost = await this.getGasCostFiatPrice(gasCostEstimate, this.chainId); - - const maxGasDelivery = BigInt(bounty.maxGasDelivery); - const rewardableGasEstimation = gasEstimation > this.evaluationConfig.unrewardedDeliveryGas - ? gasEstimation - this.evaluationConfig.unrewardedDeliveryGas - : 0n; - const gasRewardEstimate = bounty.priceOfDeliveryGas * ( - rewardableGasEstimation > maxGasDelivery ? maxGasDelivery : rewardableGasEstimation + + const destinationGasPrice = await this.getGasPrice(this.chainId); + const sourceGasPrice = await this.getGasPrice(bounty.fromChainId); + + const deliveryCostEstimate = this.calcGasCost( // ! In destination chain gas value + gasEstimation, + destinationGasPrice + ); + + const deliveryRewardEstimate = this.calcGasReward( // ! In source chain gas value + gasEstimation, + this.evaluationConfig.unrewardedDeliveryGas, + BigInt(bounty.maxGasDelivery), + bounty.priceOfDeliveryGas ); - const deliveryFiatReward = await this.getGasCostFiatPrice(gasRewardEstimate, bounty.fromChainId); - const deliveryFiatProfit = deliveryFiatReward - deliveryFiatCost; + // Consider the worst 'ack' submission profit, as in that case no will desire to submit + // the 'ack', and this relayer will have to submit it in order to get the payment + // for the delivery. + const maxAckLossEstimate = this.calcMaxGasLoss( // ! In source chain gas value + sourceGasPrice, + this.evaluationConfig.unrewardedAckGas, + BigInt(bounty.maxGasAck), + bounty.priceOfAckGas, + ); + + + // Compute the cost and reward of the message delivery (in Fiat) and evaluate the message delivery profit + const deliveryFiatCostEstimate = await this.getGasCostFiatPrice(deliveryCostEstimate, this.chainId); + + const correctedDeliveryRewardEstimate = deliveryRewardEstimate - maxAckLossEstimate; + const deliveryFiatRewardEstimate = await this.getGasCostFiatPrice(correctedDeliveryRewardEstimate, bounty.fromChainId); + + const deliveryFiatProfit = deliveryFiatRewardEstimate - deliveryFiatCostEstimate; const relayDelivery = ( deliveryFiatProfit > this.evaluationConfig.minDeliveryReward || - deliveryFiatProfit / deliveryFiatCost > this.evaluationConfig.relativeMinDeliveryReward + deliveryFiatProfit / deliveryFiatCostEstimate > this.evaluationConfig.relativeMinDeliveryReward ); this.logger.debug( { messageIdentifier, maxGasDelivery: bounty.maxGasDelivery, - gasEstimation: gasEstimation.toString(), - gasCostEstimate: gasCostEstimate.toString(), - gasRewardEstimate: gasRewardEstimate.toString(), - deliveryFiatCost, - deliveryFiatReward, - deliveryFiatProfit, + maxGasAck: bounty.maxGasAck, + deliveryGasEstimation: gasEstimation.toString(), + destinationGasPrice: destinationGasPrice.toString(), + sourceGasPrice: sourceGasPrice.toString(), + deliveryCostEstimate: deliveryCostEstimate.toString(), + deliveryRewardEstimate: deliveryRewardEstimate.toString(), + maxAckLossEstimate: maxAckLossEstimate.toString(), + deliveryFiatCostEstimate: deliveryFiatCostEstimate.toString(), + deliveryFiatRewardEstimate: deliveryFiatRewardEstimate.toString(), + deliveryFiatProfit: deliveryFiatProfit.toString(), + minDeliveryReward: this.evaluationConfig.minDeliveryReward, + relativeMinDeliveryReward: this.evaluationConfig.relativeMinDeliveryReward, relayDelivery, }, `Bounty evaluation (source to destination).`, @@ -241,61 +254,68 @@ export class EvalQueue extends ProcessingQueue { return gasEstimation; } - // Evaluate the cost of packet relaying - const gasCostEstimate = this.getGasCost(gasEstimation); - const ackFiatCost = await this.getGasCostFiatPrice(gasCostEstimate, this.chainId); + // Evaluate the cost of the 'ack' relaying + const sourceGasPrice = await this.getGasPrice(bounty.fromChainId); + + const ackCostEstimate = this.calcGasCost( // ! In source chain gas value + gasEstimation, + sourceGasPrice + ); - const maxGasAck = BigInt(bounty.maxGasAck); - const rewardableGasEstimation = gasEstimation > this.evaluationConfig.unrewardedAckGas - ? gasEstimation - this.evaluationConfig.unrewardedAckGas - : 0n; - const gasRewardEstimate = bounty.priceOfAckGas * ( - rewardableGasEstimation > maxGasAck ? maxGasAck : rewardableGasEstimation + const ackRewardEstimate = this.calcGasReward( // ! In source chain gas value + gasEstimation, + this.evaluationConfig.unrewardedAckGas, + BigInt(bounty.maxGasAck), + bounty.priceOfAckGas ); - const ackFiatReward = await this.getGasCostFiatPrice(gasRewardEstimate, this.chainId); - const ackFiatProfit = ackFiatReward - ackFiatCost; const deliveryCost = bounty.deliveryGasCost ?? 0n; // This is only present if *this* relayer submitted the message delivery. - - let relayAck: boolean; - let deliveryGasReward = 0n; - let deliveryFiatReward = 0; + + let deliveryReward = 0n; if (deliveryCost != 0n) { - // 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. // Recalculate the delivery reward using the latest pricing info const usedGasDelivery = order.incentivesPayload ? await this.getGasUsedForDelivery(order.incentivesPayload) ?? 0n : 0n; // 'gasUsed' should not be 'undefined', but if it is, continue as if it was 0 - const maxGasDelivery = BigInt(bounty.maxGasDelivery); - deliveryGasReward = bounty.priceOfDeliveryGas * ( - usedGasDelivery > maxGasDelivery ? maxGasDelivery : usedGasDelivery - ); - deliveryFiatReward = await this.getGasCostFiatPrice(deliveryGasReward, this.chainId); - relayAck = (ackFiatProfit + deliveryFiatReward) > 0n; - } - else { - relayAck = ( - ackFiatProfit > this.evaluationConfig.minAckReward || - ackFiatProfit / ackFiatCost > this.evaluationConfig.relativeMinAckReward + deliveryReward = this.calcGasReward( // ! In source chain gas value + usedGasDelivery, + 0n, + BigInt(bounty.maxGasDelivery), + bounty.priceOfDeliveryGas ); } + const ackProfit = ackRewardEstimate - ackCostEstimate; + const ackFiatProfit = await this.getGasCostFiatPrice(ackProfit, this.chainId); + const relativeProfit = Number(ackProfit) / Number(ackCostEstimate); + + // 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 || + relativeProfit > this.evaluationConfig.relativeMinAckReward + ); + this.logger.debug( { messageIdentifier, + maxGasDelivery: bounty.maxGasDelivery, maxGasAck: bounty.maxGasAck, - gasEstimation: gasEstimation.toString(), - gasCostEstimate: gasCostEstimate.toString(), - gasRewardEstimate: gasRewardEstimate.toString(), - ackFiatCost, - ackFiatReward, - ackFiatProfit, - deliveryGasReward: deliveryGasReward.toString(), - deliveryFiatReward, + ackGasEstimation: gasEstimation.toString(), + sourceGasPrice: sourceGasPrice.toString(), + deliveryCost: deliveryCost.toString(), + deliveryReward: deliveryReward.toString(), + ackCostEstimate: ackCostEstimate.toString(), + ackRewardEstimate: ackRewardEstimate.toString(), + ackFiatProfit: ackFiatProfit.toString(), + minAckReward: this.evaluationConfig.minAckReward, + relativeMinAckReward: this.evaluationConfig.relativeMinAckReward, relayAck, }, `Bounty evaluation (destination to source).`, @@ -307,40 +327,69 @@ export class EvalQueue extends ProcessingQueue { return 0n; // Do not relay packet } + private calcGasCost( + gas: bigint, + gasPrice: bigint, + ): bigint { + return gas * gasPrice; + } + + private calcGasReward( + gas: bigint, + unrewardedGas: bigint, + bountyMaxGas: bigint, + bountyPriceOfGas: bigint, + ): bigint { + + // Subtract an estimate of the amount of 'unrewardable' gas from the gas usage estimation. + const rewardableDeliveryGasEstimation = gas > unrewardedGas + ? gas - unrewardedGas + : 0n; + + const deliveryRewardEstimate = bountyPriceOfGas * ( + rewardableDeliveryGasEstimation > bountyMaxGas + ? bountyMaxGas + : rewardableDeliveryGasEstimation + ); - private async initializeFeeData(): Promise { - let tryCount = 0; - while (this.feeData == undefined) { - try { - this.feeData = await this.provider.getFeeData(); - } catch { - this.logger.warn( - { try: ++tryCount }, - 'Failed to initialize feeData on submitter eval-queue. Worker locked until successful update.' - ); - await wait(this.retryInterval); - } - } + return deliveryRewardEstimate; } - private async updateFeeData(): Promise { - try { - this.feeData = await this.provider.getFeeData(); - } catch { - // Continue with stale fee data. - } + private calcMaxGasLoss( + gasPrice: bigint, + unrewardedGas: bigint, + bountyMaxGas: bigint, + bountyPriceOfGas: bigint, + ): bigint { + // Evaluate the worst possible loss of a submission. There are 2 possible scenarios: + // - The provided `bountyPriceOfGas` covers the current gas price: the worst loss + // will occur if no gas is used for the submission logic (and hence there is no bounty + // reward). + // - The provided `bountyPriceOfGas' does *not* cover the current gas price: the + // worst loss will occur if the maximum allowed amount of gas is used for the + // submission logic. + + const fixedCost = unrewardedGas * gasPrice; + + const worstCaseVariableCost = gasPrice > bountyPriceOfGas + ? bountyMaxGas * (gasPrice - bountyPriceOfGas) + : 0n; + + return fixedCost + worstCaseVariableCost; } - private getGasCost(gas: bigint): bigint { - // TODO! this should depend on the wallet's latest gas info AND on the config adjustments! - // TODO! OR the gas price should be sent to the wallet! + 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. - const gasPrice = this.feeData?.maxFeePerGas - ?? this.feeData?.gasPrice + // ! 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 gas * gasPrice; + return gasPrice; } private async getGasCostFiatPrice(amount: bigint, chainId: string): Promise { diff --git a/src/submitter/submitter.worker.ts b/src/submitter/submitter.worker.ts index 86b25e3..ea46bef 100644 --- a/src/submitter/submitter.worker.ts +++ b/src/submitter/submitter.worker.ts @@ -74,7 +74,6 @@ class SubmitterWorker { }, this.pricing, this.wallet, - this.provider, this.logger, ); @@ -103,7 +102,6 @@ class SubmitterWorker { bountyEvaluationConfig: BountyEvaluationConfig, pricing: PricingInterface, wallet: WalletInterface, - provider: JsonRpcProvider, logger: pino.Logger, ): [EvalQueue, SubmitQueue] { const evalQueue = new EvalQueue( @@ -115,7 +113,7 @@ class SubmitterWorker { chainId, bountyEvaluationConfig, pricing, - provider, + wallet, logger, ); From d65a56d3340b4572819578109ff598e0bf03bb95 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Wed, 5 Jun 2024 11:45:25 +0000 Subject: [PATCH 30/43] chore: Overhaul and refactor the bounty evaluation logic --- src/submitter/queues/eval-queue.ts | 355 ++++++++++++++++------------- 1 file changed, 192 insertions(+), 163 deletions(-) diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 0dc1375..446648b 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -45,12 +45,40 @@ export class EvalQueue extends ProcessingQueue { ); } - const gasLimit = await this.evaluateBounty(order, bounty); + // Check if the message has already been submitted. const isDelivery = bounty.fromChainId != this.chainId; + if (isDelivery) { + // Source to Destination + if (bounty.status >= BountyStatus.MessageDelivered) { + this.logger.info( + { messageIdentifier: bounty.messageIdentifier }, + `Bounty evaluation (source to destination). Message already delivered.`, + ); + return null; // Do not relay packet + } + } else { + // Destination to Source + if (bounty.status >= BountyStatus.BountyClaimed) { + this.logger.info( + { messageIdentifier: bounty.messageIdentifier }, + `Bounty evaluation (destination to source). Ack already delivered.`, + ); + return null; // Do not relay packet + } + } + + const contract = this.incentivesContracts.get(order.amb)!; //TODO handle undefined case + const gasEstimation = await contract.processPacket.estimateGas( + order.messageCtx, + order.message, + this.relayerAddress, + ); - if (gasLimit > 0) { + const submitRelay = await this.evaluateRelaySubmission(gasEstimation, bounty, order); + + if (submitRelay) { // Move the order to the submit queue - return { result: { ...order, gasLimit, isDelivery } }; + return { result: { ...order, gasLimit: gasEstimation, isDelivery } }; } else { return null; } @@ -94,12 +122,12 @@ export class EvalQueue extends ProcessingQueue { if (success) { if (result?.gasLimit != null && result.gasLimit > 0) { - this.logger.debug( + this.logger.info( orderDescription, `Successful bounty evaluation: submit order.`, ); } else { - this.logger.debug( + this.logger.info( orderDescription, `Successful bounty evaluation: drop order.`, ); @@ -128,40 +156,16 @@ export class EvalQueue extends ProcessingQueue { return this.store.getBounty(messageIdentifier); } - private async evaluateBounty(order: EvalOrder, bounty: Bounty): Promise { + private async evaluateRelaySubmission( + gasEstimation: bigint, + bounty: Bounty, + order: EvalOrder, + ): Promise { const messageIdentifier = order.messageIdentifier; - // Check if the bounty has already been submitted/is in process of being submitted const isDelivery = bounty.fromChainId != this.chainId; - if (isDelivery) { - // Source to Destination - if (bounty.status >= BountyStatus.MessageDelivered) { - this.logger.debug( - { messageIdentifier }, - `Bounty evaluation (source to destination). Bounty already delivered.`, - ); - return 0n; // Do not relay packet - } - } else { - // Destination to Source - if (bounty.status >= BountyStatus.BountyClaimed) { - this.logger.debug( - { messageIdentifier }, - `Bounty evaluation (destination to source). Bounty already acked.`, - ); - return 0n; // Do not relay packet - } - } - - const contract = this.incentivesContracts.get(order.amb)!; //TODO handle undefined case - const gasEstimation = await contract.processPacket.estimateGas( - order.messageCtx, - order.message, - this.relayerAddress, - ); if (isDelivery) { - //TODO is this correct? Is this desired? // Source to Destination if (order.priority) { this.logger.debug( @@ -171,73 +175,13 @@ export class EvalQueue extends ProcessingQueue { gasEstimation: gasEstimation.toString(), priority: true, }, - `Bounty evaluation (source to destination).`, + `Bounty evaluation (source to destination): submit delivery (priority order).`, ); - return gasEstimation; + return true; } - const destinationGasPrice = await this.getGasPrice(this.chainId); - const sourceGasPrice = await this.getGasPrice(bounty.fromChainId); - - const deliveryCostEstimate = this.calcGasCost( // ! In destination chain gas value - gasEstimation, - destinationGasPrice - ); - - const deliveryRewardEstimate = this.calcGasReward( // ! In source chain gas value - gasEstimation, - this.evaluationConfig.unrewardedDeliveryGas, - BigInt(bounty.maxGasDelivery), - bounty.priceOfDeliveryGas - ); - - // Consider the worst 'ack' submission profit, as in that case no will desire to submit - // the 'ack', and this relayer will have to submit it in order to get the payment - // for the delivery. - const maxAckLossEstimate = this.calcMaxGasLoss( // ! In source chain gas value - sourceGasPrice, - this.evaluationConfig.unrewardedAckGas, - BigInt(bounty.maxGasAck), - bounty.priceOfAckGas, - ); - - - // Compute the cost and reward of the message delivery (in Fiat) and evaluate the message delivery profit - const deliveryFiatCostEstimate = await this.getGasCostFiatPrice(deliveryCostEstimate, this.chainId); - - const correctedDeliveryRewardEstimate = deliveryRewardEstimate - maxAckLossEstimate; - const deliveryFiatRewardEstimate = await this.getGasCostFiatPrice(correctedDeliveryRewardEstimate, bounty.fromChainId); - - const deliveryFiatProfit = deliveryFiatRewardEstimate - deliveryFiatCostEstimate; - - const relayDelivery = ( - deliveryFiatProfit > this.evaluationConfig.minDeliveryReward || - deliveryFiatProfit / deliveryFiatCostEstimate > this.evaluationConfig.relativeMinDeliveryReward - ); - - this.logger.debug( - { - messageIdentifier, - maxGasDelivery: bounty.maxGasDelivery, - maxGasAck: bounty.maxGasAck, - deliveryGasEstimation: gasEstimation.toString(), - destinationGasPrice: destinationGasPrice.toString(), - sourceGasPrice: sourceGasPrice.toString(), - deliveryCostEstimate: deliveryCostEstimate.toString(), - deliveryRewardEstimate: deliveryRewardEstimate.toString(), - maxAckLossEstimate: maxAckLossEstimate.toString(), - deliveryFiatCostEstimate: deliveryFiatCostEstimate.toString(), - deliveryFiatRewardEstimate: deliveryFiatRewardEstimate.toString(), - deliveryFiatProfit: deliveryFiatProfit.toString(), - minDeliveryReward: this.evaluationConfig.minDeliveryReward, - relativeMinDeliveryReward: this.evaluationConfig.relativeMinDeliveryReward, - relayDelivery, - }, - `Bounty evaluation (source to destination).`, - ); - - return relayDelivery ? gasEstimation : 0n; + return this.evaluateDeliverySubmission(gasEstimation, bounty); } else { // Destination to Source if (order.priority) { @@ -248,83 +192,168 @@ export class EvalQueue extends ProcessingQueue { gasEstimation: gasEstimation.toString(), priority: true, }, - `Bounty evaluation (destination to source).`, + `Bounty evaluation (destination to source): submit ack (priority order).`, ); - return gasEstimation; + return true; } - // Evaluate the cost of the 'ack' relaying - const sourceGasPrice = await this.getGasPrice(bounty.fromChainId); + return this.evaluateAckSubmission(gasEstimation, bounty, order.incentivesPayload); + } + } - const ackCostEstimate = this.calcGasCost( // ! In source chain gas value - gasEstimation, - sourceGasPrice - ); + private async evaluateDeliverySubmission( + deliveryGasEstimation: bigint, + bounty: Bounty + ): Promise { + + const destinationGasPrice = await this.getGasPrice(this.chainId); + const sourceGasPrice = await this.getGasPrice(bounty.fromChainId); - const ackRewardEstimate = this.calcGasReward( // ! In source chain gas value - gasEstimation, - this.evaluationConfig.unrewardedAckGas, - BigInt(bounty.maxGasAck), - bounty.priceOfAckGas - ); + const deliveryCost = this.calcGasCost( // ! In destination chain gas value + deliveryGasEstimation, + destinationGasPrice + ); - const deliveryCost = bounty.deliveryGasCost ?? 0n; // This is only present if *this* relayer submitted the message delivery. + const deliveryReward = this.calcGasReward( // ! In source chain gas value + deliveryGasEstimation, + this.evaluationConfig.unrewardedDeliveryGas, + BigInt(bounty.maxGasDelivery), + bounty.priceOfDeliveryGas + ); - let deliveryReward = 0n; - if (deliveryCost != 0n) { + // Consider the worst 'ack' submission loss, as in that case no one will desire to submit + // the 'ack', and this relayer will have to submit it in order to get the payment for the + // delivery. + const maxAckLoss = this.calcMaxGasLoss( // ! In source chain gas value + sourceGasPrice, + this.evaluationConfig.unrewardedAckGas, + BigInt(bounty.maxGasAck), + bounty.priceOfAckGas, + ); - // Recalculate the delivery reward using the latest pricing info - const usedGasDelivery = order.incentivesPayload - ? await this.getGasUsedForDelivery(order.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, - BigInt(bounty.maxGasDelivery), - bounty.priceOfDeliveryGas - ); - } + // 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 ackProfit = ackRewardEstimate - ackCostEstimate; - const ackFiatProfit = await this.getGasCostFiatPrice(ackProfit, this.chainId); - const relativeProfit = Number(ackProfit) / Number(ackCostEstimate); + const correctedDeliveryReward = deliveryReward - maxAckLoss; + const deliveryFiatReward = await this.getGasCostFiatPrice( + correctedDeliveryReward, + bounty.fromChainId + ); - // 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 deliveryFiatProfit = deliveryFiatReward - deliveryFiatCost; + const deliveryRelativeProfit = deliveryFiatProfit / deliveryFiatCost; - const relayAck = ( - relayAckForDeliveryBounty || - ackFiatProfit > this.evaluationConfig.minAckReward || - relativeProfit > this.evaluationConfig.relativeMinAckReward - ); + const relayDelivery = ( + deliveryFiatProfit > this.evaluationConfig.minDeliveryReward || + deliveryRelativeProfit > this.evaluationConfig.relativeMinDeliveryReward + ); - this.logger.debug( - { - messageIdentifier, - maxGasDelivery: bounty.maxGasDelivery, - maxGasAck: bounty.maxGasAck, - ackGasEstimation: gasEstimation.toString(), - sourceGasPrice: sourceGasPrice.toString(), - deliveryCost: deliveryCost.toString(), - deliveryReward: deliveryReward.toString(), - ackCostEstimate: ackCostEstimate.toString(), - ackRewardEstimate: ackRewardEstimate.toString(), - ackFiatProfit: ackFiatProfit.toString(), - minAckReward: this.evaluationConfig.minAckReward, - relativeMinAckReward: this.evaluationConfig.relativeMinAckReward, - relayAck, - }, - `Bounty evaluation (destination to source).`, - ); + this.logger.debug( + { + messageIdentifier: bounty.messageIdentifier, + maxGasDelivery: bounty.maxGasDelivery, + maxGasAck: bounty.maxGasAck, + deliveryGasEstimation: deliveryGasEstimation.toString(), + destinationGasPrice: destinationGasPrice.toString(), + sourceGasPrice: sourceGasPrice.toString(), + deliveryCost: deliveryCost.toString(), + deliveryReward: deliveryReward.toString(), + maxAckLoss: maxAckLoss.toString(), + deliveryFiatCost: deliveryFiatCost.toString(), + deliveryFiatReward: deliveryFiatReward.toString(), + deliveryFiatProfit: deliveryFiatProfit, + deliveryRelativeProfit: deliveryRelativeProfit, + minDeliveryReward: this.evaluationConfig.minDeliveryReward, + relativeMinDeliveryReward: this.evaluationConfig.relativeMinDeliveryReward, + relayDelivery, + }, + `Bounty evaluation (source to destination).`, + ); + + return relayDelivery; + } + + private async evaluateAckSubmission( + ackGasEstimation: bigint, + bounty: Bounty, + incentivesPayload?: BytesLike, + ): Promise { + + // Evaluate the cost of the 'ack' relaying + const sourceGasPrice = await this.getGasPrice(this.chainId); + + const ackCost = this.calcGasCost( // ! In source chain gas value + ackGasEstimation, + sourceGasPrice + ); + + const ackReward = this.calcGasReward( // ! In source chain gas value + ackGasEstimation, + this.evaluationConfig.unrewardedAckGas, + BigInt(bounty.maxGasAck), + bounty.priceOfAckGas + ); - return relayAck ? gasEstimation : 0n; + const ackProfit = ackReward - 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 + ); } - return 0n; // Do not relay packet + // 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.debug( + { + messageIdentifier: bounty.messageIdentifier, + maxGasDelivery: bounty.maxGasDelivery, + maxGasAck: bounty.maxGasAck, + ackGasEstimation: ackGasEstimation.toString(), + sourceGasPrice: sourceGasPrice.toString(), + ackCost: ackCost.toString(), + ackReward: ackReward.toString(), + ackFiatProfit: ackFiatProfit.toString(), + ackRelativeProfit: ackRelativeProfit, + minAckReward: this.evaluationConfig.minAckReward, + relativeMinAckReward: this.evaluationConfig.relativeMinAckReward, + deliveryCost: deliveryCost.toString(), + deliveryReward: deliveryReward.toString(), + relayAckForDeliveryBounty, + relayAck, + }, + `Bounty evaluation (destination to source).`, + ); + + return relayAck; } private calcGasCost( @@ -341,18 +370,18 @@ export class EvalQueue extends ProcessingQueue { bountyPriceOfGas: bigint, ): bigint { - // Subtract an estimate of the amount of 'unrewardable' gas from the gas usage estimation. - const rewardableDeliveryGasEstimation = gas > unrewardedGas + // Subtract the 'unrewardable' gas amount estimate from the gas usage estimation. + const rewardableGasEstimation = gas > unrewardedGas ? gas - unrewardedGas : 0n; - const deliveryRewardEstimate = bountyPriceOfGas * ( - rewardableDeliveryGasEstimation > bountyMaxGas + const rewardEstimate = bountyPriceOfGas * ( + rewardableGasEstimation > bountyMaxGas ? bountyMaxGas - : rewardableDeliveryGasEstimation + : rewardableGasEstimation ); - return deliveryRewardEstimate; + return rewardEstimate; } private calcMaxGasLoss( From 44bb35f5aff193fb54759eed27d11ea0f47f11f7 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Wed, 5 Jun 2024 13:08:23 +0000 Subject: [PATCH 31/43] chore: Remove the 'evaluator' module --- src/app.module.ts | 2 -- src/evaluator/evaluator.module.ts | 8 -------- src/evaluator/evaluator.service.ts | 25 ------------------------- 3 files changed, 35 deletions(-) delete mode 100644 src/evaluator/evaluator.module.ts delete mode 100644 src/evaluator/evaluator.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 61d2d30..ee80018 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,7 +1,6 @@ import { Module } from '@nestjs/common'; import { CollectorModule } from './collector/collector.module'; import { ConfigModule } from './config/config.module'; -import { EvaluatorModule } from './evaluator/evaluator.module'; import { GetterModule } from './getter/getter.module'; import { LoggerModule } from './logger/logger.module'; import { SubmitterModule } from './submitter/submitter.module'; @@ -16,7 +15,6 @@ import { PricingModule } from './pricing/pricing.module'; LoggerModule, MonitorModule, GetterModule, - EvaluatorModule, CollectorModule, PricingModule, SubmitterModule, diff --git a/src/evaluator/evaluator.module.ts b/src/evaluator/evaluator.module.ts deleted file mode 100644 index 8276e02..0000000 --- a/src/evaluator/evaluator.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { EvaluatorService } from './evaluator.service'; - -@Module({ - providers: [EvaluatorService], - exports: [EvaluatorService], -}) -export class EvaluatorModule {} diff --git a/src/evaluator/evaluator.service.ts b/src/evaluator/evaluator.service.ts deleted file mode 100644 index 9aa5e58..0000000 --- a/src/evaluator/evaluator.service.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Bounty } from 'src/store/types/store.types'; -import { LoggerService } from 'src/logger/logger.service'; - -@Injectable() -export class EvaluatorService { - constructor(private readonly logger: LoggerService) {} - - /** - * Evaluates bounties to gauge their profitability - * @param bounty - * @param chain - * @param address - * @returns The bounty mutation with the evaluation parameters - */ - async evaluateBounty(bounty: Bounty, _address: string): Promise { - this.logger.info( - `Checking gas price for bounty ${bounty.messageIdentifier}`, - ); - - //TODO implement evaluating currently it's just being forwarded - - return bounty; - } -} From a60e77fa385e2b13c5cbc505b89352701588635c Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:18:11 +0000 Subject: [PATCH 32/43] feat: Add order reevaluation logic --- config.example.yaml | 2 + src/config/config.schema.ts | 2 + src/config/config.types.ts | 2 + src/submitter/queues/eval-queue.ts | 3 + src/submitter/submitter.service.ts | 20 +++++++ src/submitter/submitter.types.ts | 6 +- src/submitter/submitter.worker.ts | 95 ++++++++++++++++++++++++------ 7 files changed, 111 insertions(+), 19 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 4f7db8e..6bf5ff2 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -19,6 +19,8 @@ global: 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. unrewardedDeliveryGas: '100000' # Gas amount that will be unrewarded on delivery submission. minDeliveryReward: 0.001 # In the 'pricingDenomination' specified below relativeMinDeliveryReward: 0.001 diff --git a/src/config/config.schema.ts b/src/config/config.schema.ts index f809f91..5e406cb 100644 --- a/src/config/config.schema.ts +++ b/src/config/config.schema.ts @@ -147,6 +147,8 @@ const SUBMITTER_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" }, minDeliveryReward: { $ref: "positive-number-schema" }, relativeMinDeliveryReward: { $ref: "positive-number-schema" }, diff --git a/src/config/config.types.ts b/src/config/config.types.ts index 1bf3315..8cfe56e 100644 --- a/src/config/config.types.ts +++ b/src/config/config.types.ts @@ -45,6 +45,8 @@ export interface SubmitterGlobalConfig { maxTries?: number; maxPendingTransactions?: number; + evaluationRetryInterval?: number; + maxEvaluationDuration?: number; unrewardedDeliveryGas?: bigint; minDeliveryReward?: number; relativeMinDeliveryReward?: number; diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 446648b..8cf6bcc 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -80,6 +80,9 @@ export class EvalQueue extends ProcessingQueue { // Move the order to the submit queue return { result: { ...order, gasLimit: gasEstimation, isDelivery } }; } else { + // Request the order to be retried in the future. + order.retryEvaluation = true; + return null; } } diff --git a/src/submitter/submitter.service.ts b/src/submitter/submitter.service.ts index c075972..3146fe0 100644 --- a/src/submitter/submitter.service.ts +++ b/src/submitter/submitter.service.ts @@ -15,6 +15,8 @@ const PROCESSING_INTERVAL_DEFAULT = 100; const MAX_TRIES_DEFAULT = 3; 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 MIN_DELIVERY_REWARD_DEFAULT = 0; const RELATIVE_MIN_DELIVERY_REWARD_DEFAULT = 0; @@ -29,6 +31,8 @@ interface GlobalSubmitterConfig { processingInterval: number; maxTries: number; maxPendingTransactions: number; + evaluationRetryInterval: number; + maxEvaluationDuration: number; unrewardedDeliveryGas: bigint; minDeliveryReward: number; relativeMinDeliveryReward: number; @@ -48,6 +52,8 @@ export interface SubmitterWorkerData { processingInterval: number; maxTries: number; maxPendingTransactions: number; + evaluationRetryInterval: number; + maxEvaluationDuration: number; unrewardedDeliveryGas: bigint; minDeliveryReward: number; relativeMinDeliveryReward: number; @@ -132,6 +138,10 @@ export class SubmitterService { const maxPendingTransactions = submitterConfig.maxPendingTransactions ?? MAX_PENDING_TRANSACTIONS; + const evaluationRetryInterval = + submitterConfig.evaluationRetryInterval ?? EVALUATION_RETRY_INTERVAL_DEFAULT; + const maxEvaluationDuration = + submitterConfig.maxEvaluationDuration ?? MAX_EVALUATION_DURATION_DEFAULT; const unrewardedDeliveryGas = submitterConfig.unrewardedDeliveryGas ?? UNREWARDED_DELIVERY_GAS_DEFAULT; const minDeliveryReward = @@ -155,6 +165,8 @@ export class SubmitterService { maxTries, maxPendingTransactions, walletPublicKey, + evaluationRetryInterval, + maxEvaluationDuration, unrewardedDeliveryGas, minDeliveryReward, relativeMinDeliveryReward, @@ -205,9 +217,17 @@ export class SubmitterService { chainConfig.submitter.maxPendingTransactions ?? globalConfig.maxPendingTransactions, + evaluationRetryInterval: + chainConfig.submitter.evaluationRetryInterval ?? + globalConfig.evaluationRetryInterval, + unrewardedDeliveryGas: chainConfig.submitter.unrewardedDeliveryGas ?? globalConfig.unrewardedDeliveryGas, + + maxEvaluationDuration: + chainConfig.submitter.maxEvaluationDuration ?? + globalConfig.maxEvaluationDuration, minDeliveryReward: chainConfig.submitter.minDeliveryReward ?? diff --git a/src/submitter/submitter.types.ts b/src/submitter/submitter.types.ts index cabb8d7..008eb18 100644 --- a/src/submitter/submitter.types.ts +++ b/src/submitter/submitter.types.ts @@ -10,6 +10,8 @@ export interface Order { export interface EvalOrder extends Order { priority: boolean; + evaluationDeadline: number; + retryEvaluation?: boolean; } export interface SubmitOrder extends Order { @@ -24,13 +26,15 @@ export interface SubmitOrderResult extends SubmitOrder { txReceipt: TransactionReceipt; } -export interface NewOrder { +export interface PendingOrder { order: OrderType; processAt: number; } export interface BountyEvaluationConfig { + evaluationRetryInterval: number, + maxEvaluationDuration: number, unrewardedDeliveryGas: bigint; minDeliveryReward: number; relativeMinDeliveryReward: number, diff --git a/src/submitter/submitter.worker.ts b/src/submitter/submitter.worker.ts index ea46bef..a734232 100644 --- a/src/submitter/submitter.worker.ts +++ b/src/submitter/submitter.worker.ts @@ -10,7 +10,7 @@ import { IncentivizedMessageEscrow__factory } from 'src/contracts/factories/Ince import { workerData } from 'worker_threads'; import { AmbPayload } from 'src/store/types/store.types'; import { STATUS_LOG_INTERVAL } from 'src/logger/logger.service'; -import { BountyEvaluationConfig, EvalOrder, NewOrder } from './submitter.types'; +import { BountyEvaluationConfig, EvalOrder, PendingOrder } from './submitter.types'; import { EvalQueue } from './queues/eval-queue'; import { SubmitQueue } from './queues/submit-queue'; import { wait } from 'src/common/utils'; @@ -32,7 +32,7 @@ class SubmitterWorker { private readonly pricing: PricingInterface; private readonly wallet: WalletInterface; - private readonly newOrdersQueue: NewOrder[] = []; + private readonly pendingQueue: PendingOrder[] = []; private readonly evalQueue: EvalQueue; private readonly submitQueue: SubmitQueue; @@ -65,6 +65,8 @@ class SubmitterWorker { this.loadIncentivesContracts(this.config.incentivesAddresses), this.config.chainId, { + evaluationRetryInterval: this.config.evaluationRetryInterval, + maxEvaluationDuration: this.config.maxEvaluationDuration, unrewardedDeliveryGas: this.config.unrewardedDeliveryGas, minDeliveryReward: this.config.minDeliveryReward, relativeMinDeliveryReward: this.config.relativeMinDeliveryReward, @@ -140,7 +142,7 @@ class SubmitterWorker { const logStatus = () => { const status = { capacity: this.getSubmitterCapacity(), - newOrdersQueue: this.newOrdersQueue.length, + pendingQueue: this.pendingQueue.length, evalQueue: this.evalQueue.size, evalRetryQueue: this.evalQueue.retryQueue.length, submitQueue: this.submitQueue.size, @@ -181,21 +183,57 @@ class SubmitterWorker { await this.listenForOrders(); while (true) { - const evalOrders = await this.processNewOrdersQueue(); + const evalOrders = await this.processPendingQueue(); await this.evalQueue.addOrders(...evalOrders); await this.evalQueue.processOrders(); - const [newSubmitOrders, ,] = this.evalQueue.getFinishedOrders(); + const [newSubmitOrders, noSubmitEvalOrders,] = this.evalQueue.getFinishedOrders(); + this.processNoSubmitEvalOrders(noSubmitEvalOrders); await this.submitQueue.addOrders(...newSubmitOrders); await this.submitQueue.processOrders(); + //TODO process failed submissions in any way? + this.submitQueue.getFinishedOrders(); // Flush the internal queues await wait(this.config.processingInterval); } } + private processNoSubmitEvalOrders(evalOrders: EvalOrder[]): void { + for (const order of evalOrders) { + if (order.retryEvaluation) { + + // Clear the 'retryEvaluation' flag to avoid unexpected retries. + order.retryEvaluation = undefined; + + const nextEvaluationTime = Date.now() + this.config.evaluationRetryInterval; + if (nextEvaluationTime > order.evaluationDeadline) { + this.logger.info( + { + messageIdentifier: order.messageIdentifier, + nextEvaluationTime, + evaluationDeadline: order.evaluationDeadline, + }, + 'Dropping evaluation order.' + ); + } + else { + this.logger.info( + { + messageIdentifier: order.messageIdentifier, + nextEvaluationTime, + evaluationDeadline: order.evaluationDeadline, + }, + 'Queueing order for reevaluation.' + ); + this.addPendingOrder(nextEvaluationTime, order); + } + } + } + } + /** * Subscribe to the Store to listen for relevant payloads to submit. */ @@ -247,48 +285,69 @@ class SubmitterWorker { `Submit order received.`, ); if (priority) { - // Push directly into the submit queue + // Push directly into the eval queue await this.evalQueue.addOrders({ amb, messageIdentifier, message, messageCtx, priority: true, + evaluationDeadline: Date.now() + this.config.maxEvaluationDuration, incentivesPayload, }); } else { - // Push into the evaluation queue - this.newOrdersQueue.push({ - processAt: Date.now() + this.config.newOrdersDelay, - order: { + // Push into the pending queue + this.addPendingOrder( + Date.now() + this.config.newOrdersDelay, + { amb, messageIdentifier, message, messageCtx, priority: false, + evaluationDeadline: Date.now() + this.config.maxEvaluationDuration, incentivesPayload, }, - }); + ); } } - /*************** New Order Queue ***************/ + /*************** Pending Orders Queue ***************/ + + private addPendingOrder(processAt: number, order: EvalOrder): void { + + // Insert the new order into the 'pendingQueue' keeping the queue order. + const insertIndex = this.pendingQueue.findIndex(order => { + return order.processAt > processAt; + }); + + const pendingOrder: PendingOrder = { + order, + processAt + }; + + if (insertIndex == -1) { + this.pendingQueue.push(pendingOrder); + } else { + this.pendingQueue.splice(insertIndex, 0, pendingOrder); + } + } - private async processNewOrdersQueue(): Promise { + private async processPendingQueue(): Promise { const currentTimestamp = Date.now(); const capacity = this.getSubmitterCapacity(); let i; - for (i = 0; i < this.newOrdersQueue.length; i++) { - const nextNewOrder = this.newOrdersQueue[i]!; + for (i = 0; i < this.pendingQueue.length; i++) { + const nextPendingOrder = this.pendingQueue[i]!; - if (nextNewOrder.processAt > currentTimestamp || i + 1 > capacity) { + if (nextPendingOrder.processAt > currentTimestamp || i + 1 > capacity) { break; } } - const ordersToEval = this.newOrdersQueue.splice(0, i).map((newOrder) => { - return newOrder.order; + const ordersToEval = this.pendingQueue.splice(0, i).map((pendingOrder) => { + return pendingOrder.order; }); return ordersToEval; From 0381090d9806b4e21588ada1223c4e66a755cb7c Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:10:15 +0000 Subject: [PATCH 33/43] chore: Fix confusing log statement --- src/submitter/queues/eval-queue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 8cf6bcc..655b889 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -132,7 +132,7 @@ export class EvalQueue extends ProcessingQueue { } else { this.logger.info( orderDescription, - `Successful bounty evaluation: drop order.`, + `Successful bounty evaluation: do not submit order.`, ); } } else { From 243dc8a5540cf0bd681fae372ba9a10461230f79 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Thu, 6 Jun 2024 07:11:14 +0000 Subject: [PATCH 34/43] feat: Implement a 'profitability factor' --- config.example.yaml | 2 ++ src/config/config.schema.ts | 1 + src/config/config.types.ts | 1 + src/submitter/queues/eval-queue.ts | 13 +++++++++++-- src/submitter/submitter.service.ts | 10 ++++++++++ src/submitter/submitter.types.ts | 1 + src/submitter/submitter.worker.ts | 1 + 7 files changed, 27 insertions(+), 2 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 6bf5ff2..93a2b39 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -27,6 +27,8 @@ global: unrewardedAckGas: '50000' # Gas amount that will be unrewarded on ack submission. minAckReward: 0.001 # In the 'pricingDenomination' specified below relativeMinAckReward: 0.001 + profitabilityFactor: 1.0 # Profitiability evaluation adjustment factor. A larger + # factor implies a larger profitability guarantee. pricing: provider: 'coin-gecko' diff --git a/src/config/config.schema.ts b/src/config/config.schema.ts index 5e406cb..50256d7 100644 --- a/src/config/config.schema.ts +++ b/src/config/config.schema.ts @@ -155,6 +155,7 @@ const SUBMITTER_SCHEMA = { unrewardedAckGas: { $ref: "gas-field-schema" }, minAckReward: { $ref: "positive-number-schema" }, relativeMinAckReward: { $ref: "positive-number-schema" }, + profitabilityFactor: { $ref: "positive-number-schema" }, }, additionalProperties: false } diff --git a/src/config/config.types.ts b/src/config/config.types.ts index 8cfe56e..b7c838e 100644 --- a/src/config/config.types.ts +++ b/src/config/config.types.ts @@ -53,6 +53,7 @@ export interface SubmitterGlobalConfig { unrewardedAckGas?: bigint; minAckReward?: number; relativeMinAckReward?: number; + profitabilityFactor?: number; } export interface SubmitterConfig extends SubmitterGlobalConfig {} diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 655b889..42860af 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -14,10 +14,14 @@ import { ParsePayload, MessageContext } from 'src/payload/decode.payload'; import { PricingInterface } from 'src/pricing/pricing.interface'; import { WalletInterface } from 'src/wallet/wallet.interface'; +const DECIMAL_BASE = 10000; +const DECIMAL_BASE_BIG_INT = BigInt(DECIMAL_BASE); export class EvalQueue extends ProcessingQueue { readonly relayerAddress: string; + private readonly profitabilityFactor: bigint; + constructor( retryInterval: number, maxTries: number, @@ -32,6 +36,7 @@ export class EvalQueue extends ProcessingQueue { ) { super(retryInterval, maxTries); this.relayerAddress = zeroPadValue(relayerAddress, 32); + this.profitabilityFactor = BigInt(this.evaluationConfig.profitabilityFactor * DECIMAL_BASE); } protected async handleOrder( @@ -249,7 +254,7 @@ export class EvalQueue extends ProcessingQueue { bounty.fromChainId ); - const deliveryFiatProfit = deliveryFiatReward - deliveryFiatCost; + const deliveryFiatProfit = (deliveryFiatReward - deliveryFiatCost) / this.evaluationConfig.profitabilityFactor; const deliveryRelativeProfit = deliveryFiatProfit / deliveryFiatCost; const relayDelivery = ( @@ -270,6 +275,7 @@ export class EvalQueue extends ProcessingQueue { maxAckLoss: maxAckLoss.toString(), deliveryFiatCost: deliveryFiatCost.toString(), deliveryFiatReward: deliveryFiatReward.toString(), + profitabilityFactor: this.evaluationConfig.profitabilityFactor, deliveryFiatProfit: deliveryFiatProfit, deliveryRelativeProfit: deliveryRelativeProfit, minDeliveryReward: this.evaluationConfig.minDeliveryReward, @@ -303,7 +309,9 @@ export class EvalQueue extends ProcessingQueue { bounty.priceOfAckGas ); - const ackProfit = ackReward - ackCost; // ! In source chain gas value + const ackProfit = (ackReward - ackCost) + * DECIMAL_BASE_BIG_INT + / this.profitabilityFactor; // ! In source chain gas value const ackFiatProfit = await this.getGasCostFiatPrice(ackProfit, this.chainId); const ackRelativeProfit = Number(ackProfit) / Number(ackCost); @@ -344,6 +352,7 @@ export class EvalQueue extends ProcessingQueue { sourceGasPrice: sourceGasPrice.toString(), ackCost: ackCost.toString(), ackReward: ackReward.toString(), + profitabilityFactor: this.evaluationConfig.profitabilityFactor, ackFiatProfit: ackFiatProfit.toString(), ackRelativeProfit: ackRelativeProfit, minAckReward: this.evaluationConfig.minAckReward, diff --git a/src/submitter/submitter.service.ts b/src/submitter/submitter.service.ts index 3146fe0..8b496c2 100644 --- a/src/submitter/submitter.service.ts +++ b/src/submitter/submitter.service.ts @@ -23,6 +23,7 @@ const RELATIVE_MIN_DELIVERY_REWARD_DEFAULT = 0; const UNREWARDED_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; @@ -39,6 +40,7 @@ interface GlobalSubmitterConfig { unrewardedAckGas: bigint; minAckReward: number; relativeMinAckReward: number; + profitabilityFactor: number; walletPublicKey: string; } @@ -60,6 +62,7 @@ export interface SubmitterWorkerData { unrewardedAckGas: bigint; minAckReward: number; relativeMinAckReward: number; + profitabilityFactor: number; pricingPort: MessagePort; walletPublicKey: string; walletPort: MessagePort; @@ -154,6 +157,8 @@ export class SubmitterService { 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; @@ -173,6 +178,7 @@ export class SubmitterService { unrewardedAckGas, minAckReward, relativeMinAckReward, + profitabilityFactor, }; } @@ -249,6 +255,10 @@ export class SubmitterService { chainConfig.submitter.relativeMinAckReward ?? globalConfig.relativeMinAckReward, + profitabilityFactor: + chainConfig.submitter.profitabilityFactor ?? + globalConfig.profitabilityFactor, + pricingPort: await this.pricingService.attachToPricing(), diff --git a/src/submitter/submitter.types.ts b/src/submitter/submitter.types.ts index 008eb18..4359c2e 100644 --- a/src/submitter/submitter.types.ts +++ b/src/submitter/submitter.types.ts @@ -41,4 +41,5 @@ export interface BountyEvaluationConfig { unrewardedAckGas: 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 a734232..c4f0a7f 100644 --- a/src/submitter/submitter.worker.ts +++ b/src/submitter/submitter.worker.ts @@ -73,6 +73,7 @@ class SubmitterWorker { unrewardedAckGas: this.config.unrewardedAckGas, minAckReward: this.config.minAckReward, relativeMinAckReward: this.config.relativeMinAckReward, + profitabilityFactor: this.config.profitabilityFactor, }, this.pricing, this.wallet, From bf7f0f6e676c5fc1b8c17712c91af2114416415d Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Thu, 6 Jun 2024 09:55:32 +0000 Subject: [PATCH 35/43] feat: Add CoinGecko config validation --- src/pricing/providers/coin-gecko.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pricing/providers/coin-gecko.ts b/src/pricing/providers/coin-gecko.ts index b8d4134..3305a65 100644 --- a/src/pricing/providers/coin-gecko.ts +++ b/src/pricing/providers/coin-gecko.ts @@ -23,6 +23,13 @@ export class CoinGeckoPricingProvider extends PricingProvider { From b014a8773dd4650d846c2cdf9cbbc90e6ab71db9 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Thu, 6 Jun 2024 10:02:05 +0000 Subject: [PATCH 36/43] chore: Recover testnet config --- config.example.yaml | 71 +++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 93a2b39..a529231 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -72,51 +72,58 @@ global: # AMBs configuration ambs: + # Mock is used for internal testnets. For production it should never be used. + - name: mock + enabled: false # Defaults to 'true' if the key is missing + incentivesAddress: '0x0000000000000000000000000000000000000000' + privateKey: '' + + # While we can't relay packages for Polymer, we to still collect the packages for the underwriter to work. + - name: polymer + - name: wormhole - isTestnet: false + isTestnet: true # Chain configuration chains: - - chainId: 10 - name: 'OP Mainnet' - rpc: 'https://mainnet.optimism.io' + - chainId: 11155111 + name: 'Sepolia' + rpc: 'https://eth-sepolia-public.unifra.io' # startingBlock # The block number at which to start Relaying (not all AMB collectors may support this property) # stoppingBlock # The block number at which to stop Relaying (not all AMB collectors may support this property) # Overrides monitor: - interval: 1000 - - pricing: - coinId: 'ethereum' # coin-gecko pricing provider specific configuration + blockDelay: 2 + interval: 5000 # AMB configuration wormhole: - wormholeChainId: 24 - incentivesAddress: '0x8C8727276725b7Da11fDA6e2646B2d2448E5B3c5' - bridgeAddress: '0xEe91C335eab126dF5fDB3797EA9d6aD93aeC9722' + wormholeChainId: 10002 + incentivesAddress: '0x294F41D30D058C9e5A71810A6C758E595b7aC170' + bridgeAddress: '0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78' - - chainId: 81457 - name: 'Blast Mainnet' - rpc: 'https://rpc.blast.io' - monitor: - interval: 1000 - pricing: - coinId: 'ethereum' # coin-gecko pricing provider specific configuration + - chainId: 11155420 + name: 'OP Sepolia' + rpc: 'https://sepolia.optimism.io' wormhole: - wormholeChainId: 36 - incentivesAddress: '0x3C5C5436BCa59042cBC835276E51428781366d85' - bridgeAddress: '0xbebdb6C8ddC678FfA9f8748f85C815C556Dd8ac6' - - - chainId: 8453 - name: 'Base Mainnet' - rpc: 'https://mainnet.base.org' - monitor: - interval: 1000 - pricing: - coinId: 'ethereum' # coin-gecko pricing provider specific configuration + wormholeChainId: 10005 + incentivesAddress: '0x198cDD55d90277726f3222D5A8111AdB8b0af9ee' + bridgeAddress: '0x31377888146f3253211EFEf5c676D41ECe7D58Fe' + + - chainId: 84532 + name: 'Base Sepolia' + rpc: 'https://sepolia.base.org' wormhole: - wormholeChainId: 30 - incentivesAddress: '0x3C5C5436BCa59042cBC835276E51428781366d85' - bridgeAddress: '0xbebdb6C8ddC678FfA9f8748f85C815C556Dd8ac6' + wormholeChainId: 10004 + incentivesAddress: '0x63B4E24DC9814fAcDe241fB4dEFcA04d5fc6d763' + bridgeAddress: '0x79A1027a6A159502049F10906D333EC57E95F083' + + - chainId: 168587773 + name: 'Blast Testnet' + rpc: 'https://sepolia.blast.io' + wormhole: + wormholeChainId: 36 + incentivesAddress: '0x9524ACA1fF46fAd177160F0a803189Cb552A3780' + bridgeAddress: '0x473e002D7add6fB67a4964F13bFd61280Ca46886' From b64a98bba46840dc388c32a72c72e3561d76c76e Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:38:21 +0000 Subject: [PATCH 37/43] chore: Add 'coinId' to example config --- config.example.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config.example.yaml b/config.example.yaml index a529231..f3db1b3 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -98,6 +98,9 @@ chains: blockDelay: 2 interval: 5000 + pricing: + coinId: 'ethereum' # coin-gecko pricing provider specific configuration + # AMB configuration wormhole: wormholeChainId: 10002 @@ -107,6 +110,8 @@ chains: - chainId: 11155420 name: 'OP Sepolia' rpc: 'https://sepolia.optimism.io' + pricing: + coinId: 'ethereum' # coin-gecko pricing provider specific configuration wormhole: wormholeChainId: 10005 incentivesAddress: '0x198cDD55d90277726f3222D5A8111AdB8b0af9ee' @@ -115,6 +120,8 @@ chains: - chainId: 84532 name: 'Base Sepolia' rpc: 'https://sepolia.base.org' + pricing: + coinId: 'ethereum' # coin-gecko pricing provider specific configuration wormhole: wormholeChainId: 10004 incentivesAddress: '0x63B4E24DC9814fAcDe241fB4dEFcA04d5fc6d763' @@ -123,6 +130,8 @@ chains: - chainId: 168587773 name: 'Blast Testnet' rpc: 'https://sepolia.blast.io' + pricing: + coinId: 'ethereum' # coin-gecko pricing provider specific configuration wormhole: wormholeChainId: 36 incentivesAddress: '0x9524ACA1fF46fAd177160F0a803189Cb552A3780' From 17397b72aef2ffbdd6110f84f1487078503b5683 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:44:12 +0000 Subject: [PATCH 38/43] fix: Overhaul the max ack gas loss --- config.example.yaml | 6 ++-- src/config/config.schema.ts | 2 ++ src/config/config.service.ts | 6 ++++ src/config/config.types.ts | 2 ++ src/submitter/queues/eval-queue.ts | 52 +++++++++++++++++++----------- src/submitter/submitter.service.ts | 20 ++++++++++++ src/submitter/submitter.types.ts | 2 ++ src/submitter/submitter.worker.ts | 2 ++ 8 files changed, 72 insertions(+), 20 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index f3db1b3..068d17c 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -21,10 +21,12 @@ global: # Evaluation properties evaluationRetryInterval: 3600000 # Interval at which to reevaluate whether to relay a message. maxEvaluationDuration: 86400000 # Time after which to drop an undelivered message. - unrewardedDeliveryGas: '100000' # Gas amount that will be unrewarded on delivery submission. + 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 relativeMinDeliveryReward: 0.001 - unrewardedAckGas: '50000' # Gas amount that will be unrewarded on ack submission. + verificationAckGas: '55000' # Gas amount used for packet verification upon ack. + unrewardedAckGas: '25000' # Gas amount that will be unrewarded on ack submission. minAckReward: 0.001 # In the 'pricingDenomination' specified below relativeMinAckReward: 0.001 profitabilityFactor: 1.0 # Profitiability evaluation adjustment factor. A larger diff --git a/src/config/config.schema.ts b/src/config/config.schema.ts index 50256d7..0ae31b4 100644 --- a/src/config/config.schema.ts +++ b/src/config/config.schema.ts @@ -150,9 +150,11 @@ const SUBMITTER_SCHEMA = { 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" }, diff --git a/src/config/config.service.ts b/src/config/config.service.ts index a59dc96..8a1064e 100644 --- a/src/config/config.service.ts +++ b/src/config/config.service.ts @@ -191,9 +191,15 @@ export class ConfigService { if (config.unrewardedDeliveryGas != undefined) { config.unrewardedDeliveryGas = BigInt(config.unrewardedDeliveryGas); } + if (config.verificationDeliveryGas != undefined) { + config.verificationDeliveryGas = BigInt(config.verificationDeliveryGas); + } if (config.unrewardedAckGas != undefined) { config.unrewardedAckGas = BigInt(config.unrewardedAckGas); } + if (config.verificationAckGas != undefined) { + config.verificationAckGas = BigInt(config.verificationAckGas); + } return config as SubmitterGlobalConfig; } diff --git a/src/config/config.types.ts b/src/config/config.types.ts index b7c838e..7cca08a 100644 --- a/src/config/config.types.ts +++ b/src/config/config.types.ts @@ -48,9 +48,11 @@ 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; diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 42860af..08b8eb9 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -230,12 +230,10 @@ export class EvalQueue extends ProcessingQueue { bounty.priceOfDeliveryGas ); - // Consider the worst 'ack' submission loss, as in that case no one will desire to submit - // the 'ack', and this relayer will have to submit it in order to get the payment for the - // delivery. const maxAckLoss = this.calcMaxGasLoss( // ! In source chain gas value sourceGasPrice, this.evaluationConfig.unrewardedAckGas, + this.evaluationConfig.verificationAckGas, BigInt(bounty.maxGasAck), bounty.priceOfAckGas, ); @@ -248,7 +246,7 @@ export class EvalQueue extends ProcessingQueue { this.chainId ); - const correctedDeliveryReward = deliveryReward - maxAckLoss; + const correctedDeliveryReward = deliveryReward + maxAckLoss; const deliveryFiatReward = await this.getGasCostFiatPrice( correctedDeliveryReward, bounty.fromChainId @@ -399,24 +397,42 @@ export class EvalQueue extends ProcessingQueue { private calcMaxGasLoss( gasPrice: bigint, unrewardedGas: bigint, + verificationGas: bigint, bountyMaxGas: bigint, bountyPriceOfGas: bigint, ): bigint { - // Evaluate the worst possible loss of a submission. There are 2 possible scenarios: - // - The provided `bountyPriceOfGas` covers the current gas price: the worst loss - // will occur if no gas is used for the submission logic (and hence there is no bounty - // reward). - // - The provided `bountyPriceOfGas' does *not* cover the current gas price: the - // worst loss will occur if the maximum allowed amount of gas is used for the - // submission logic. - - const fixedCost = unrewardedGas * gasPrice; - - const worstCaseVariableCost = gasPrice > bountyPriceOfGas - ? bountyMaxGas * (gasPrice - bountyPriceOfGas) - : 0n; - return fixedCost + worstCaseVariableCost; + // 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 { diff --git a/src/submitter/submitter.service.ts b/src/submitter/submitter.service.ts index 8b496c2..b3e07cd 100644 --- a/src/submitter/submitter.service.ts +++ b/src/submitter/submitter.service.ts @@ -18,9 +18,11 @@ 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; @@ -35,9 +37,11 @@ interface GlobalSubmitterConfig { evaluationRetryInterval: number; maxEvaluationDuration: number; unrewardedDeliveryGas: bigint; + verificationDeliveryGas: bigint; minDeliveryReward: number; relativeMinDeliveryReward: number; unrewardedAckGas: bigint; + verificationAckGas: bigint; minAckReward: number; relativeMinAckReward: number; profitabilityFactor: number; @@ -57,9 +61,11 @@ export interface SubmitterWorkerData { evaluationRetryInterval: number; maxEvaluationDuration: number; unrewardedDeliveryGas: bigint; + verificationDeliveryGas: bigint; minDeliveryReward: number; relativeMinDeliveryReward: number; unrewardedAckGas: bigint; + verificationAckGas: bigint; minAckReward: number; relativeMinAckReward: number; profitabilityFactor: number; @@ -147,12 +153,16 @@ export class SubmitterService { 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 = @@ -173,9 +183,11 @@ export class SubmitterService { evaluationRetryInterval, maxEvaluationDuration, unrewardedDeliveryGas, + verificationDeliveryGas, minDeliveryReward, relativeMinDeliveryReward, unrewardedAckGas, + verificationAckGas, minAckReward, relativeMinAckReward, profitabilityFactor, @@ -230,6 +242,10 @@ export class SubmitterService { unrewardedDeliveryGas: chainConfig.submitter.unrewardedDeliveryGas ?? globalConfig.unrewardedDeliveryGas, + + verificationDeliveryGas: + chainConfig.submitter.verificationDeliveryGas ?? + globalConfig.verificationDeliveryGas, maxEvaluationDuration: chainConfig.submitter.maxEvaluationDuration ?? @@ -246,6 +262,10 @@ export class SubmitterService { unrewardedAckGas: chainConfig.submitter.unrewardedAckGas ?? globalConfig.unrewardedAckGas, + + verificationAckGas: + chainConfig.submitter.verificationAckGas ?? + globalConfig.verificationAckGas, minAckReward: chainConfig.submitter.minAckReward ?? diff --git a/src/submitter/submitter.types.ts b/src/submitter/submitter.types.ts index 4359c2e..51e7188 100644 --- a/src/submitter/submitter.types.ts +++ b/src/submitter/submitter.types.ts @@ -36,9 +36,11 @@ 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; diff --git a/src/submitter/submitter.worker.ts b/src/submitter/submitter.worker.ts index c4f0a7f..2f7057b 100644 --- a/src/submitter/submitter.worker.ts +++ b/src/submitter/submitter.worker.ts @@ -68,9 +68,11 @@ class SubmitterWorker { 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, From 0a8750796a54c62a6b6a6249fa701069a90acd86 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Wed, 19 Jun 2024 14:52:45 +0000 Subject: [PATCH 39/43] feat: Implement addtional fee estimation via resolvers --- package.json | 3 +- pnpm-lock.yaml | 80 ++++++++++++++++++++++++++++ src/resolvers/base-sepolia.ts | 24 +++++++++ src/resolvers/op-stack.ts | 54 +++++++++++++++++++ src/resolvers/optimism-sepolia.ts | 24 +++++++++ src/resolvers/resolver.ts | 8 ++- src/submitter/queues/eval-queue.ts | 64 +++++++++++++++------- src/submitter/queues/submit-queue.ts | 35 ++---------- src/submitter/submitter.service.ts | 5 +- src/submitter/submitter.types.ts | 4 +- src/submitter/submitter.worker.ts | 50 +++++++---------- 11 files changed, 266 insertions(+), 85 deletions(-) create mode 100644 src/resolvers/base-sepolia.ts create mode 100644 src/resolvers/op-stack.ts create mode 100644 src/resolvers/optimism-sepolia.ts diff --git a/package.json b/package.json index a1149b3..f306219 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "pg": "^8.11.3", "pino": "^8.15.1", "reflect-metadata": "^0.1.13", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "viem": "^2.15.1" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c80e96..1448ac1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ dependencies: rxjs: specifier: ^7.8.1 version: 7.8.1 + viem: + specifier: ^2.15.1 + version: 2.15.1(typescript@5.4.2) devDependencies: '@nestjs/cli': @@ -150,6 +153,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /@adraffy/ens-normalize@1.10.0: + resolution: {integrity: sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==} + dev: false + /@adraffy/ens-normalize@1.10.1: resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} dev: false @@ -3041,6 +3048,14 @@ packages: resolution: {integrity: sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==} dev: false + /@scure/bip32@1.3.2: + resolution: {integrity: sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==} + dependencies: + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.3 + '@scure/base': 1.1.5 + dev: false + /@scure/bip32@1.3.3: resolution: {integrity: sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==} dependencies: @@ -3056,6 +3071,13 @@ packages: '@scure/base': 1.1.5 dev: false + /@scure/bip39@1.2.1: + resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} + dependencies: + '@noble/hashes': 1.3.3 + '@scure/base': 1.1.5 + dev: false + /@scure/bip39@1.2.2: resolution: {integrity: sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==} dependencies: @@ -4060,6 +4082,20 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: false + /abitype@1.0.0(typescript@5.4.2): + resolution: {integrity: sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + dependencies: + typescript: 5.4.2 + dev: false + /abort-controller-x@0.4.3: resolution: {integrity: sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==} dev: false @@ -7222,6 +7258,14 @@ packages: ws: 7.5.9(bufferutil@4.0.8)(utf-8-validate@5.0.10) dev: false + /isows@1.0.4(ws@8.17.1): + resolution: {integrity: sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ==} + peerDependencies: + ws: '*' + dependencies: + ws: 8.17.1 + dev: false + /istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -10529,6 +10573,29 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + /viem@2.15.1(typescript@5.4.2): + resolution: {integrity: sha512-Vrveen3vDOJyPf8Q8TDyWePG2pTdK6IpSi4P6qlvAP+rXkAeqRvwYBy9AmGm+BeYpCETAyTT0SrCP6458XSt+w==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@adraffy/ens-normalize': 1.10.0 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@scure/bip32': 1.3.2 + '@scure/bip39': 1.2.1 + abitype: 1.0.0(typescript@5.4.2) + isows: 1.0.4(ws@8.17.1) + typescript: 5.4.2 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + dev: false + /vlq@2.0.4: resolution: {integrity: sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA==} dev: false @@ -10782,6 +10849,19 @@ packages: utf-8-validate: 5.0.10 dev: false + /ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /ws@8.5.0: resolution: {integrity: sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==} engines: {node: '>=10.0.0'} diff --git a/src/resolvers/base-sepolia.ts b/src/resolvers/base-sepolia.ts new file mode 100644 index 0000000..2bbb034 --- /dev/null +++ b/src/resolvers/base-sepolia.ts @@ -0,0 +1,24 @@ +import pino from "pino"; +import { JsonRpcProvider } from "ethers6"; +import { ResolverConfig } from "./resolver"; +import OPStackResolver from "./op-stack"; + +export const BASE_SEPOLIA_CHAIN_NAME = 'baseSepolia'; + +export class BaseSepoliaResolver extends OPStackResolver { + + constructor( + config: ResolverConfig, + provider: JsonRpcProvider, + logger: pino.Logger, + ) { + super( + BASE_SEPOLIA_CHAIN_NAME, + config, + provider, + logger, + ); + } +} + +export default BaseSepoliaResolver; diff --git a/src/resolvers/op-stack.ts b/src/resolvers/op-stack.ts new file mode 100644 index 0000000..aa8f4c2 --- /dev/null +++ b/src/resolvers/op-stack.ts @@ -0,0 +1,54 @@ +import { JsonRpcProvider, TransactionRequest } from "ethers6"; +import { Resolver, ResolverConfig } from "./resolver"; +import pino from "pino"; +import { createPublicClient, http } from "viem"; +import { publicActionsL2 } from 'viem/op-stack' + +export const RESOLVER_TYPE_OP_STACK = 'op-stack'; + +export class OPStackResolver extends Resolver { + override readonly resolverType; + + private client: any; //TODO use VIEM types + + constructor( + chainName: string, + config: ResolverConfig, + provider: JsonRpcProvider, + logger: pino.Logger, + ) { + super( + config, + provider, + logger, + ); + + this.resolverType = RESOLVER_TYPE_OP_STACK; + + this.client = this.loadClient( + chainName, + this.provider._getConnection().url //TODO the 'rpc' url should be added to the ResolverConfig + ); + } + + private loadClient( + chainName: string, + rpc: string, + ): any { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const chain = require(`viem/chains`)[chainName]; + + return createPublicClient({ + chain, + transport: http(rpc), + }).extend(publicActionsL2()); + } + + override async estimateAdditionalFee( + transactionRequest: TransactionRequest + ): Promise { + return this.client.estimateL1Fee(transactionRequest) + } +} + +export default OPStackResolver; diff --git a/src/resolvers/optimism-sepolia.ts b/src/resolvers/optimism-sepolia.ts new file mode 100644 index 0000000..1fec480 --- /dev/null +++ b/src/resolvers/optimism-sepolia.ts @@ -0,0 +1,24 @@ +import pino from "pino"; +import { JsonRpcProvider } from "ethers6"; +import { ResolverConfig } from "./resolver"; +import OPStackResolver from "./op-stack"; + +export const OPTIMISM_SEPOLIA_CHAIN_NAME = 'optimismSepolia'; + +export class OptimismSepoliaResolver extends OPStackResolver { + + constructor( + config: ResolverConfig, + provider: JsonRpcProvider, + logger: pino.Logger, + ) { + super( + OPTIMISM_SEPOLIA_CHAIN_NAME, + config, + provider, + logger, + ); + } +} + +export default OptimismSepoliaResolver; diff --git a/src/resolvers/resolver.ts b/src/resolvers/resolver.ts index 1d1cb1e..93ddaca 100644 --- a/src/resolvers/resolver.ts +++ b/src/resolvers/resolver.ts @@ -1,4 +1,4 @@ -import { JsonRpcProvider } from "ethers6"; +import { JsonRpcProvider, TransactionRequest } from "ethers6"; import pino from "pino"; export const RESOLVER_TYPE_DEFAULT = 'default'; @@ -78,4 +78,10 @@ export class Resolver { ): Promise { return new Promise((resolve) => resolve(observedBlockNumber)); }; + + estimateAdditionalFee( + _transactionRequest: TransactionRequest + ): Promise { + return new Promise((resolve) => resolve(0n)); + } } diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 08b8eb9..57cfbee 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -7,35 +7,41 @@ import pino from 'pino'; import { Store } from 'src/store/store.lib'; import { Bounty } from 'src/store/types/store.types'; import { BountyStatus } from 'src/store/types/bounty.enum'; -import { IncentivizedMessageEscrow } from 'src/contracts'; +import { IncentivizedMockEscrow__factory } from 'src/contracts'; import { tryErrorToString } from 'src/common/utils'; -import { BytesLike, MaxUint256, zeroPadValue } from 'ethers6'; +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 { Resolver } from 'src/resolvers/resolver'; +import { IncentivizedMockEscrowInterface } from 'src/contracts/IncentivizedMockEscrow'; const DECIMAL_BASE = 10000; const DECIMAL_BASE_BIG_INT = BigInt(DECIMAL_BASE); export class EvalQueue extends ProcessingQueue { - readonly relayerAddress: string; + readonly paddedRelayerAddress: string; + private readonly escrowInterface: IncentivizedMockEscrowInterface; private readonly profitabilityFactor: bigint; constructor( retryInterval: number, maxTries: number, - relayerAddress: string, + private readonly relayerAddress: string, + private readonly resolver: Resolver, private readonly store: Store, - private readonly incentivesContracts: Map, + private readonly incentivesContracts: Map, private readonly chainId: string, private readonly evaluationConfig: BountyEvaluationConfig, private readonly pricing: PricingInterface, + private readonly provider: AbstractProvider, private readonly wallet: WalletInterface, private readonly logger: pino.Logger, ) { super(retryInterval, maxTries); - this.relayerAddress = zeroPadValue(relayerAddress, 32); + this.paddedRelayerAddress = zeroPadValue(relayerAddress, 32); + this.escrowInterface = IncentivizedMockEscrow__factory.createInterface(); this.profitabilityFactor = BigInt(this.evaluationConfig.profitabilityFactor * DECIMAL_BASE); } @@ -72,18 +78,34 @@ export class EvalQueue extends ProcessingQueue { } } - const contract = this.incentivesContracts.get(order.amb)!; //TODO handle undefined case - const gasEstimation = await contract.processPacket.estimateGas( + const contractAddress = this.incentivesContracts.get(order.amb)!; //TODO handle undefined case + + const transactionData = this.escrowInterface.encodeFunctionData("processPacket", [ order.messageCtx, order.message, - this.relayerAddress, - ); + this.paddedRelayerAddress, + ]); + + const transactionRequest: TransactionRequest = { + from: this.relayerAddress, + to: contractAddress, + data: transactionData, + } - const submitRelay = await this.evaluateRelaySubmission(gasEstimation, bounty, order); + const gasEstimation = await this.provider.estimateGas(transactionRequest); + const additionalFeeEstimation = await this.resolver.estimateAdditionalFee(transactionRequest); + + const submitRelay = await this.evaluateRelaySubmission( + gasEstimation, + additionalFeeEstimation, + bounty, + order + ); if (submitRelay) { // Move the order to the submit queue - return { result: { ...order, gasLimit: gasEstimation, isDelivery } }; + transactionRequest.gasLimit = gasEstimation; + return { result: { ...order, transactionRequest, isDelivery } }; } else { // Request the order to be retried in the future. order.retryEvaluation = true; @@ -129,7 +151,7 @@ export class EvalQueue extends ProcessingQueue { }; if (success) { - if (result?.gasLimit != null && result.gasLimit > 0) { + if (result != null) { this.logger.info( orderDescription, `Successful bounty evaluation: submit order.`, @@ -166,6 +188,7 @@ export class EvalQueue extends ProcessingQueue { private async evaluateRelaySubmission( gasEstimation: bigint, + additionalFeeEstimation: bigint, bounty: Bounty, order: EvalOrder, ): Promise { @@ -189,7 +212,7 @@ export class EvalQueue extends ProcessingQueue { return true; } - return this.evaluateDeliverySubmission(gasEstimation, bounty); + return this.evaluateDeliverySubmission(gasEstimation, additionalFeeEstimation, bounty); } else { // Destination to Source if (order.priority) { @@ -206,12 +229,13 @@ export class EvalQueue extends ProcessingQueue { return true; } - return this.evaluateAckSubmission(gasEstimation, bounty, order.incentivesPayload); + return this.evaluateAckSubmission(gasEstimation, additionalFeeEstimation, bounty, order.incentivesPayload); } } private async evaluateDeliverySubmission( deliveryGasEstimation: bigint, + additionalFeeEstimation: bigint, bounty: Bounty ): Promise { @@ -220,7 +244,8 @@ export class EvalQueue extends ProcessingQueue { const deliveryCost = this.calcGasCost( // ! In destination chain gas value deliveryGasEstimation, - destinationGasPrice + destinationGasPrice, + additionalFeeEstimation ); const deliveryReward = this.calcGasReward( // ! In source chain gas value @@ -288,6 +313,7 @@ export class EvalQueue extends ProcessingQueue { private async evaluateAckSubmission( ackGasEstimation: bigint, + additionalFeeEstimation: bigint, bounty: Bounty, incentivesPayload?: BytesLike, ): Promise { @@ -297,7 +323,8 @@ export class EvalQueue extends ProcessingQueue { const ackCost = this.calcGasCost( // ! In source chain gas value ackGasEstimation, - sourceGasPrice + sourceGasPrice, + additionalFeeEstimation ); const ackReward = this.calcGasReward( // ! In source chain gas value @@ -369,8 +396,9 @@ export class EvalQueue extends ProcessingQueue { private calcGasCost( gas: bigint, gasPrice: bigint, + additionalFee?: bigint, ): bigint { - return gas * gasPrice; + return gas * gasPrice + (additionalFee ?? 0n); } private calcGasReward( diff --git a/src/submitter/queues/submit-queue.ts b/src/submitter/queues/submit-queue.ts index dd1328e..40a5fc2 100644 --- a/src/submitter/queues/submit-queue.ts +++ b/src/submitter/queues/submit-queue.ts @@ -3,10 +3,9 @@ import { ProcessingQueue, } from '../../processing-queue/processing-queue'; import { SubmitOrder, SubmitOrderResult } from '../submitter.types'; -import { TransactionRequest, zeroPadValue } from 'ethers6'; +import { AbstractProvider } from 'ethers6'; import pino from 'pino'; import { tryErrorToString } from 'src/common/utils'; -import { IncentivizedMessageEscrow } from 'src/contracts'; import { Store } from 'src/store/store.lib'; import { WalletInterface } from 'src/wallet/wallet.interface'; @@ -14,20 +13,17 @@ export class SubmitQueue extends ProcessingQueue< SubmitOrder, SubmitOrderResult > { - readonly relayerAddress: string; constructor( retryInterval: number, maxTries: number, private readonly store: Store, - private readonly incentivesContracts: Map, - relayerAddress: string, private readonly chainId: string, + private readonly provider: AbstractProvider, private readonly wallet: WalletInterface, private readonly logger: pino.Logger, ) { super(retryInterval, maxTries); - this.relayerAddress = zeroPadValue(relayerAddress, 32); } protected async handleOrder( @@ -38,38 +34,17 @@ export class SubmitQueue extends ProcessingQueue< { messageIdentifier: order.messageIdentifier }, `Handling submit order`, ); - + // Simulate the packet submission as a static call. Skip if it's the first submission try, // as in that case the packet 'evaluation' will have been executed shortly before. - const contract = this.incentivesContracts.get(order.amb)!; //TODO handle undefined case - if (retryCount > 0 || (order.requeueCount ?? 0) > 0) { - await contract.processPacket.staticCall( - order.messageCtx, - order.message, - this.relayerAddress, - { - gasLimit: order.gasLimit, - }, - ); + await this.provider.call(order.transactionRequest) } // Execute the relay transaction if the static call did not fail. - const txData = contract.interface.encodeFunctionData("processPacket", [ - order.messageCtx, - order.message, - this.relayerAddress, - ]); - - const txRequest: TransactionRequest = { - to: await contract.getAddress(), - data: txData, - gasLimit: order.gasLimit, - }; - const txPromise = this.wallet.submitTransaction( this.chainId, - txRequest, + order.transactionRequest, order, ).then((transactionResult): SubmitOrderResult => { if (transactionResult.submissionError) { diff --git a/src/submitter/submitter.service.ts b/src/submitter/submitter.service.ts index b3e07cd..d8cbd66 100644 --- a/src/submitter/submitter.service.ts +++ b/src/submitter/submitter.service.ts @@ -51,7 +51,7 @@ interface GlobalSubmitterConfig { export interface SubmitterWorkerData { chainId: string; rpc: string; - relayerPrivateKey: string; + resolver: string | null; incentivesAddresses: Map; newOrdersDelay: number; retryInterval: number; @@ -200,7 +200,6 @@ export class SubmitterService { ): Promise { const chainId = chainConfig.chainId; const rpc = chainConfig.rpc; - const relayerPrivateKey = this.configService.globalConfig.privateKey; const incentivesAddresses = new Map(); this.configService.ambsConfig.forEach((amb) => { @@ -216,7 +215,7 @@ export class SubmitterService { return { chainId, rpc, - relayerPrivateKey, + resolver: chainConfig.resolver, incentivesAddresses, newOrdersDelay: diff --git a/src/submitter/submitter.types.ts b/src/submitter/submitter.types.ts index 51e7188..5f78e8c 100644 --- a/src/submitter/submitter.types.ts +++ b/src/submitter/submitter.types.ts @@ -1,4 +1,4 @@ -import { BytesLike, TransactionReceipt, TransactionResponse } from 'ethers6'; +import { BytesLike, TransactionReceipt, TransactionRequest, TransactionResponse } from 'ethers6'; export interface Order { amb: string; @@ -17,7 +17,7 @@ export interface EvalOrder extends Order { export interface SubmitOrder extends Order { isDelivery: boolean; priority: boolean; - gasLimit: bigint | undefined; + transactionRequest: TransactionRequest; requeueCount?: number; } diff --git a/src/submitter/submitter.worker.ts b/src/submitter/submitter.worker.ts index 2f7057b..3b4e29b 100644 --- a/src/submitter/submitter.worker.ts +++ b/src/submitter/submitter.worker.ts @@ -1,12 +1,6 @@ -import { - BytesLike, - JsonRpcProvider, - Wallet, -} from 'ethers6'; +import { BytesLike, JsonRpcProvider } from 'ethers6'; import pino, { LoggerOptions } from 'pino'; import { Store } from 'src/store/store.lib'; -import { IncentivizedMessageEscrow } from 'src/contracts'; -import { IncentivizedMessageEscrow__factory } from 'src/contracts/factories/IncentivizedMessageEscrow__factory'; import { workerData } from 'worker_threads'; import { AmbPayload } from 'src/store/types/store.types'; import { STATUS_LOG_INTERVAL } from 'src/logger/logger.service'; @@ -17,6 +11,7 @@ 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'; class SubmitterWorker { private readonly store: Store; @@ -25,10 +20,11 @@ class SubmitterWorker { private readonly config: SubmitterWorkerData; private readonly provider: JsonRpcProvider; - private readonly signer: Wallet; private readonly chainId: string; + private readonly resolver: Resolver; + private readonly pricing: PricingInterface; private readonly wallet: WalletInterface; @@ -51,7 +47,12 @@ class SubmitterWorker { this.provider = new JsonRpcProvider(this.config.rpc, undefined, { staticNetwork: true, }); - this.signer = new Wallet(this.config.relayerPrivateKey, this.provider); + + this.resolver = loadResolver( + this.config.resolver, + this.provider, + this.logger + ); this.pricing = new PricingInterface(this.config.pricingPort); this.wallet = new WalletInterface(this.config.walletPort); @@ -61,8 +62,9 @@ class SubmitterWorker { this.config.retryInterval, this.config.maxTries, this.config.walletPublicKey, + this.resolver, this.store, - this.loadIncentivesContracts(this.config.incentivesAddresses), + this.config.incentivesAddresses, this.config.chainId, { evaluationRetryInterval: this.config.evaluationRetryInterval, @@ -78,6 +80,7 @@ class SubmitterWorker { profitabilityFactor: this.config.profitabilityFactor, }, this.pricing, + this.provider, this.wallet, this.logger, ); @@ -101,11 +104,13 @@ class SubmitterWorker { retryInterval: number, maxTries: number, walletPublicKey: string, + resolver: Resolver, store: Store, - incentivesContracts: Map, + incentivesContracts: Map, chainId: string, bountyEvaluationConfig: BountyEvaluationConfig, pricing: PricingInterface, + provider: JsonRpcProvider, wallet: WalletInterface, logger: pino.Logger, ): [EvalQueue, SubmitQueue] { @@ -113,11 +118,13 @@ class SubmitterWorker { retryInterval, maxTries, walletPublicKey, + resolver, store, incentivesContracts, chainId, bountyEvaluationConfig, pricing, + provider, wallet, logger, ); @@ -126,9 +133,8 @@ class SubmitterWorker { retryInterval, maxTries, store, - incentivesContracts, - walletPublicKey, chainId, + provider, wallet, logger, ); @@ -157,26 +163,10 @@ class SubmitterWorker { setInterval(logStatus, STATUS_LOG_INTERVAL); } - private loadIncentivesContracts( - incentivesAddresses: Map, - ): Map { - const incentivesContracts = new Map(); - - incentivesAddresses.forEach((address: string, amb: string) => { - const contract = IncentivizedMessageEscrow__factory.connect( - address, - this.signer, - ); - incentivesContracts.set(amb, contract); - }); - - return incentivesContracts; - } - /*************** Main Logic Loop. ***************/ async run(): Promise { - this.logger.debug({ relayer: this.signer.address }, `Relaying messages.`); + this.logger.debug({ relayer: this.config.walletPublicKey }, `Relaying messages.`); // Initialize the queues await this.evalQueue.init(); From b84eec7b8b343f621a2e51c9cae607d1ca54ad7f Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Wed, 19 Jun 2024 15:02:44 +0000 Subject: [PATCH 40/43] feat: Add OP and Base resolvers --- src/resolvers/base.ts | 24 ++++++++++++++++++++++++ src/resolvers/optimism.ts | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 src/resolvers/base.ts create mode 100644 src/resolvers/optimism.ts diff --git a/src/resolvers/base.ts b/src/resolvers/base.ts new file mode 100644 index 0000000..ada6095 --- /dev/null +++ b/src/resolvers/base.ts @@ -0,0 +1,24 @@ +import pino from "pino"; +import { JsonRpcProvider } from "ethers6"; +import { ResolverConfig } from "./resolver"; +import OPStackResolver from "./op-stack"; + +export const BASE_CHAIN_NAME = 'base'; + +export class BaseResolver extends OPStackResolver { + + constructor( + config: ResolverConfig, + provider: JsonRpcProvider, + logger: pino.Logger, + ) { + super( + BASE_CHAIN_NAME, + config, + provider, + logger, + ); + } +} + +export default BaseResolver; diff --git a/src/resolvers/optimism.ts b/src/resolvers/optimism.ts new file mode 100644 index 0000000..c21e72d --- /dev/null +++ b/src/resolvers/optimism.ts @@ -0,0 +1,24 @@ +import pino from "pino"; +import { JsonRpcProvider } from "ethers6"; +import { ResolverConfig } from "./resolver"; +import OPStackResolver from "./op-stack"; + +export const OPTIMISM_CHAIN_NAME = 'optimism'; + +export class OptimismResolver extends OPStackResolver { + + constructor( + config: ResolverConfig, + provider: JsonRpcProvider, + logger: pino.Logger, + ) { + super( + OPTIMISM_CHAIN_NAME, + config, + provider, + logger, + ); + } +} + +export default OptimismResolver; From 5598c12db9f8d391ade7c937f29918db240a250d Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Wed, 19 Jun 2024 15:13:47 +0000 Subject: [PATCH 41/43] chore: Add 'additionalFeeEstimation' to eval logs --- src/submitter/queues/eval-queue.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 57cfbee..5e88839 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -291,6 +291,7 @@ export class EvalQueue extends ProcessingQueue { maxGasDelivery: bounty.maxGasDelivery, maxGasAck: bounty.maxGasAck, deliveryGasEstimation: deliveryGasEstimation.toString(), + deliveryAdditionalFeeEstimation: additionalFeeEstimation.toString(), destinationGasPrice: destinationGasPrice.toString(), sourceGasPrice: sourceGasPrice.toString(), deliveryCost: deliveryCost.toString(), @@ -374,6 +375,7 @@ export class EvalQueue extends ProcessingQueue { maxGasDelivery: bounty.maxGasDelivery, maxGasAck: bounty.maxGasAck, ackGasEstimation: ackGasEstimation.toString(), + ackAdditionalFeeEstimation: additionalFeeEstimation.toString(), sourceGasPrice: sourceGasPrice.toString(), ackCost: ackCost.toString(), ackReward: ackReward.toString(), From 4419a59794bfe1ad78da1b98605f04425c04e6b0 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:51:02 +0000 Subject: [PATCH 42/43] chore: Improve delivery evaluation logging --- src/submitter/queues/eval-queue.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 5e88839..2707499 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -271,18 +271,24 @@ export class EvalQueue extends ProcessingQueue { this.chainId ); - const correctedDeliveryReward = deliveryReward + maxAckLoss; - const deliveryFiatReward = await this.getGasCostFiatPrice( - correctedDeliveryReward, + const securedDeliveryReward = deliveryReward + maxAckLoss; + const securedDeliveryFiatReward = await this.getGasCostFiatPrice( + securedDeliveryReward, bounty.fromChainId ); - const deliveryFiatProfit = (deliveryFiatReward - deliveryFiatCost) / this.evaluationConfig.profitabilityFactor; - const deliveryRelativeProfit = deliveryFiatProfit / deliveryFiatCost; + // 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) / this.evaluationConfig.profitabilityFactor; + const securedDeliveryRelativeProfit = securedDeliveryFiatProfit / deliveryFiatCost; const relayDelivery = ( - deliveryFiatProfit > this.evaluationConfig.minDeliveryReward || - deliveryRelativeProfit > this.evaluationConfig.relativeMinDeliveryReward + securedDeliveryFiatProfit > this.evaluationConfig.minDeliveryReward || + securedDeliveryRelativeProfit > this.evaluationConfig.relativeMinDeliveryReward ); this.logger.debug( @@ -299,9 +305,10 @@ export class EvalQueue extends ProcessingQueue { maxAckLoss: maxAckLoss.toString(), deliveryFiatCost: deliveryFiatCost.toString(), deliveryFiatReward: deliveryFiatReward.toString(), + securedDeliveryFiatReward: securedDeliveryFiatReward.toString(), profitabilityFactor: this.evaluationConfig.profitabilityFactor, - deliveryFiatProfit: deliveryFiatProfit, - deliveryRelativeProfit: deliveryRelativeProfit, + securedDeliveryFiatProfit: securedDeliveryFiatProfit, + securedDeliveryRelativeProfit: securedDeliveryRelativeProfit, minDeliveryReward: this.evaluationConfig.minDeliveryReward, relativeMinDeliveryReward: this.evaluationConfig.relativeMinDeliveryReward, relayDelivery, From 502cceb1b67cd246288f375bba8ec2d0f0580da7 Mon Sep 17 00:00:00 2001 From: Jorge Sanmiguel <8038323+jsanmigimeno@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:53:25 +0000 Subject: [PATCH 43/43] feat: Improve bounty evaluation on arbitrum-like chains --- src/resolvers/arbitrum.ts | 57 +++++++++++++++++++++++++- src/resolvers/resolver.ts | 24 +++++++++++ src/submitter/queues/eval-queue.ts | 66 +++++++++++++++++------------- 3 files changed, 117 insertions(+), 30 deletions(-) diff --git a/src/resolvers/arbitrum.ts b/src/resolvers/arbitrum.ts index 89403ce..c9dfb39 100644 --- a/src/resolvers/arbitrum.ts +++ b/src/resolvers/arbitrum.ts @@ -1,13 +1,22 @@ -import { JsonRpcProvider } from "ethers6"; -import { Resolver, ResolverConfig } from "./resolver"; +import { Interface, JsonRpcProvider, TransactionRequest } from "ethers6"; +import { GasEstimateComponents, Resolver, ResolverConfig } from "./resolver"; import pino from "pino"; import { tryErrorToString, wait } from "src/common/utils"; export const RESOLVER_TYPE_ARBITRUM = 'arbitrum'; +// Arbitrum NodeInterface docs: +// https://docs.arbitrum.io/build-decentralized-apps/nodeinterface/reference +const ARBITRUM_NODE_INTERFACE_ADDRESS = '0x00000000000000000000000000000000000000c8'; +const GAS_ESTIMATE_COMPONENTS_SIGNATURE = 'function gasEstimateComponents(address to, bool contractCreation, bytes calldata data) returns (uint64, uint64, uint256, uint256)'; + export class ArbitrumResolver extends Resolver { override readonly resolverType; + private arbitrumNodeInterface = new Interface([ + GAS_ESTIMATE_COMPONENTS_SIGNATURE + ]); + constructor( config: ResolverConfig, provider: JsonRpcProvider, @@ -65,6 +74,50 @@ export class ArbitrumResolver extends Resolver { throw new Error(`Failed to map an 'observedBlockNumber' to an 'l1BlockNumber'. Max tries reached.`); }; + override async estimateGas( + transactionRequest: TransactionRequest + ): Promise { + + // This function relies on Arbitrum's 'gasEstimateComponents' to estimate the gas components. + // https://github.com/OffchainLabs/nitro-contracts/blob/1cab72ff3dfcfe06ceed371a9db7a54a527e3bfb/src/node-interface/NodeInterface.sol#L84 + + // Set the requested transaction's 'to' and 'data' fields within the + // 'gasEstimateComponents' function arguments, as these fields will be replaced when + // calling the `gasEstimateComponents` function. + const transactionData = this.arbitrumNodeInterface.encodeFunctionData( + "gasEstimateComponents", + [ + transactionRequest.to, + false, // 'contractCreation' + transactionRequest.data ?? "0x" + ] + ); + + const result = await this.provider.call({ + ...transactionRequest, + to: ARBITRUM_NODE_INTERFACE_ADDRESS, // Replace the 'to' address with Arbitrum's NodeInterface address. + data: transactionData // Replace the tx data with the encoded function arguments above. + }); + + const decodedResult = this.arbitrumNodeInterface.decodeFunctionResult( + "gasEstimateComponents", + result + ); + + const gasEstimate: bigint = decodedResult[0]; + const l1GasEstimate: bigint = decodedResult[1]; + + if (l1GasEstimate > gasEstimate) { + throw new Error(`Error on 'gasEstimateComponents' call (Arbitrum): returned 'l1GasEstimate' is larger than 'gasEstimate'.`); + } + + return { + gasEstimate, + observedGasEstimate: gasEstimate - l1GasEstimate, + additionalFeeEstimate: 0n, + } + + } } export default ArbitrumResolver; diff --git a/src/resolvers/resolver.ts b/src/resolvers/resolver.ts index 93ddaca..ba8b9d1 100644 --- a/src/resolvers/resolver.ts +++ b/src/resolvers/resolver.ts @@ -62,6 +62,12 @@ export async function loadResolverAsync( ); } +export interface GasEstimateComponents { + gasEstimate: bigint; // The overall gas usage (used to set the transaction 'gasLimit'). + observedGasEstimate: bigint; // The gas usage observed by the contract. + additionalFeeEstimate: bigint; // Any additional fee incurred by the transaction. +} + export class Resolver { readonly resolverType; @@ -79,6 +85,24 @@ export class Resolver { return new Promise((resolve) => resolve(observedBlockNumber)); }; + async estimateGas( + transactionRequest: TransactionRequest + ): Promise { + const gasEstimatePromise = this.provider.estimateGas(transactionRequest); + const additionalFeeEstimatePromise = this.estimateAdditionalFee(transactionRequest); + + const [ + gasEstimate, + additionalFeeEstimate + ] = await Promise.all([gasEstimatePromise, additionalFeeEstimatePromise]); + + return { + gasEstimate, + observedGasEstimate: gasEstimate, + additionalFeeEstimate, + }; + } + estimateAdditionalFee( _transactionRequest: TransactionRequest ): Promise { diff --git a/src/submitter/queues/eval-queue.ts b/src/submitter/queues/eval-queue.ts index 2707499..9663ec4 100644 --- a/src/submitter/queues/eval-queue.ts +++ b/src/submitter/queues/eval-queue.ts @@ -13,7 +13,7 @@ import { AbstractProvider, BytesLike, MaxUint256, TransactionRequest, zeroPadVal import { ParsePayload, MessageContext } from 'src/payload/decode.payload'; import { PricingInterface } from 'src/pricing/pricing.interface'; import { WalletInterface } from 'src/wallet/wallet.interface'; -import { Resolver } from 'src/resolvers/resolver'; +import { Resolver, GasEstimateComponents } from 'src/resolvers/resolver'; import { IncentivizedMockEscrowInterface } from 'src/contracts/IncentivizedMockEscrow'; const DECIMAL_BASE = 10000; @@ -92,19 +92,17 @@ export class EvalQueue extends ProcessingQueue { data: transactionData, } - const gasEstimation = await this.provider.estimateGas(transactionRequest); - const additionalFeeEstimation = await this.resolver.estimateAdditionalFee(transactionRequest); + const gasEstimateComponents = await this.resolver.estimateGas(transactionRequest); const submitRelay = await this.evaluateRelaySubmission( - gasEstimation, - additionalFeeEstimation, + gasEstimateComponents, bounty, order ); if (submitRelay) { // Move the order to the submit queue - transactionRequest.gasLimit = gasEstimation; + transactionRequest.gasLimit = gasEstimateComponents.gasEstimate; return { result: { ...order, transactionRequest, isDelivery } }; } else { // Request the order to be retried in the future. @@ -187,8 +185,7 @@ export class EvalQueue extends ProcessingQueue { } private async evaluateRelaySubmission( - gasEstimation: bigint, - additionalFeeEstimation: bigint, + gasEstimateComponents: GasEstimateComponents, bounty: Bounty, order: EvalOrder, ): Promise { @@ -203,7 +200,8 @@ export class EvalQueue extends ProcessingQueue { { messageIdentifier, maxGasDelivery: bounty.maxGasDelivery, - gasEstimation: gasEstimation.toString(), + gasEstimate: gasEstimateComponents.gasEstimate.toString(), + additionalFeeEstimate: gasEstimateComponents.additionalFeeEstimate.toString(), priority: true, }, `Bounty evaluation (source to destination): submit delivery (priority order).`, @@ -212,7 +210,7 @@ export class EvalQueue extends ProcessingQueue { return true; } - return this.evaluateDeliverySubmission(gasEstimation, additionalFeeEstimation, bounty); + return this.evaluateDeliverySubmission(gasEstimateComponents, bounty); } else { // Destination to Source if (order.priority) { @@ -220,7 +218,8 @@ export class EvalQueue extends ProcessingQueue { { messageIdentifier, maxGasAck: bounty.maxGasAck, - gasEstimation: gasEstimation.toString(), + gasEstimate: gasEstimateComponents.gasEstimate.toString(), + additionalFeeEstimate: gasEstimateComponents.additionalFeeEstimate.toString(), priority: true, }, `Bounty evaluation (destination to source): submit ack (priority order).`, @@ -229,27 +228,32 @@ export class EvalQueue extends ProcessingQueue { return true; } - return this.evaluateAckSubmission(gasEstimation, additionalFeeEstimation, bounty, order.incentivesPayload); + return this.evaluateAckSubmission(gasEstimateComponents, bounty, order.incentivesPayload); } } private async evaluateDeliverySubmission( - deliveryGasEstimation: bigint, - additionalFeeEstimation: bigint, + gasEstimateComponents: GasEstimateComponents, 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 - deliveryGasEstimation, + gasEstimate, destinationGasPrice, - additionalFeeEstimation + additionalFeeEstimate ); const deliveryReward = this.calcGasReward( // ! In source chain gas value - deliveryGasEstimation, + observedGasEstimate, this.evaluationConfig.unrewardedDeliveryGas, BigInt(bounty.maxGasDelivery), bounty.priceOfDeliveryGas @@ -296,8 +300,9 @@ export class EvalQueue extends ProcessingQueue { messageIdentifier: bounty.messageIdentifier, maxGasDelivery: bounty.maxGasDelivery, maxGasAck: bounty.maxGasAck, - deliveryGasEstimation: deliveryGasEstimation.toString(), - deliveryAdditionalFeeEstimation: additionalFeeEstimation.toString(), + gasEstimate: gasEstimate.toString(), + observedGasEstimate: observedGasEstimate.toString(), + additionalFeeEstimation: additionalFeeEstimate.toString(), destinationGasPrice: destinationGasPrice.toString(), sourceGasPrice: sourceGasPrice.toString(), deliveryCost: deliveryCost.toString(), @@ -320,23 +325,27 @@ export class EvalQueue extends ProcessingQueue { } private async evaluateAckSubmission( - ackGasEstimation: bigint, - additionalFeeEstimation: bigint, + gasEstimateComponents: GasEstimateComponents, bounty: Bounty, incentivesPayload?: BytesLike, ): Promise { - - // Evaluate the cost of the 'ack' relaying + + const { + gasEstimate, + observedGasEstimate, + additionalFeeEstimate + } = gasEstimateComponents; + const sourceGasPrice = await this.getGasPrice(this.chainId); const ackCost = this.calcGasCost( // ! In source chain gas value - ackGasEstimation, + gasEstimate, sourceGasPrice, - additionalFeeEstimation + additionalFeeEstimate ); const ackReward = this.calcGasReward( // ! In source chain gas value - ackGasEstimation, + observedGasEstimate, this.evaluationConfig.unrewardedAckGas, BigInt(bounty.maxGasAck), bounty.priceOfAckGas @@ -381,8 +390,9 @@ export class EvalQueue extends ProcessingQueue { messageIdentifier: bounty.messageIdentifier, maxGasDelivery: bounty.maxGasDelivery, maxGasAck: bounty.maxGasAck, - ackGasEstimation: ackGasEstimation.toString(), - ackAdditionalFeeEstimation: additionalFeeEstimation.toString(), + gasEstimate: gasEstimate.toString(), + observedGasEstimate: observedGasEstimate.toString(), + additionalFeeEstimation: additionalFeeEstimate.toString(), sourceGasPrice: sourceGasPrice.toString(), ackCost: ackCost.toString(), ackReward: ackReward.toString(),