Merge the develop branch to the master branch, preparation to v3.4.0

This merge contains the following set of changes:
  * [Oracle, Improvement] Refetch old logs ranges to see if there are missed events (#627)
  * [Oracle, Improvement] Add support for EIP1559 gas price oracle (#631)
  * [Oracle, Improvement] CollectedSignatures AMB watcher for MEV bundling (#634)
  * [Oracle, Fix] Fix eip1559 transaction sending problems (#632)
This commit is contained in:
Alexander Kolotov 2022-02-11 10:24:38 +03:00 committed by GitHub
commit 5bc562e810
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1850 additions and 120 deletions

@ -8,11 +8,11 @@ COMMON_HOME_RPC_URL | The HTTPS URL(s) used to communicate to the RPC nodes in t
COMMON_FOREIGN_RPC_URL | The HTTPS URL(s) used to communicate to the RPC nodes in the Foreign network. Several URLs can be specified, delimited by spaces. If the connection to one of these nodes is lost the next URL is used for connection. | URL(s) COMMON_FOREIGN_RPC_URL | The HTTPS URL(s) used to communicate to the RPC nodes in the Foreign network. Several URLs can be specified, delimited by spaces. If the connection to one of these nodes is lost the next URL is used for connection. | URL(s)
COMMON_HOME_BRIDGE_ADDRESS | The address of the bridge contract address in the Home network. It is used to listen to events from and send validators' transactions to the Home network. | hexidecimal beginning with "0x" COMMON_HOME_BRIDGE_ADDRESS | The address of the bridge contract address in the Home network. It is used to listen to events from and send validators' transactions to the Home network. | hexidecimal beginning with "0x"
COMMON_FOREIGN_BRIDGE_ADDRESS | The address of the bridge contract address in the Foreign network. It is used to listen to events from and send validators' transactions to the Foreign network. | hexidecimal beginning with "0x" COMMON_FOREIGN_BRIDGE_ADDRESS | The address of the bridge contract address in the Foreign network. It is used to listen to events from and send validators' transactions to the Foreign network. | hexidecimal beginning with "0x"
COMMON_HOME_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Home network. The gas price provided by the oracle is used to send the validator's transactions to the RPC node. Since it is assumed that the Home network has a predefined gas price (e.g. the gas price in the Core of POA.Network is `1 GWei`), the gas price oracle parameter can be omitted for such networks. | URL COMMON_HOME_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Home network. The gas price provided by the oracle is used to send the validator's transactions to the RPC node. Since it is assumed that the Home network has a predefined gas price (e.g. the gas price in the Core of POA.Network is `1 GWei`), the gas price oracle parameter can be omitted for such networks. Set to `eip1559-gas-estimation` if you want to use EIP1559 RPC-based gas estimation. | URL
COMMON_HOME_GAS_PRICE_SPEED_TYPE | Assuming the gas price oracle responds with the following JSON structure: `{"fast": 20.0, "block_time": 12.834, "health": true, "standard": 6.0, "block_number": 6470469, "instant": 71.0, "slow": 1.889}`, this parameter specifies the desirable transaction speed. The speed type can be omitted when `COMMON_HOME_GAS_PRICE_SUPPLIER_URL` is not used. | `instant` / `fast` / `standard` / `slow` COMMON_HOME_GAS_PRICE_SPEED_TYPE | Assuming the gas price oracle responds with the following JSON structure: `{"fast": 20.0, "block_time": 12.834, "health": true, "standard": 6.0, "block_number": 6470469, "instant": 71.0, "slow": 1.889}`, this parameter specifies the desirable transaction speed. The speed type can be omitted when `COMMON_HOME_GAS_PRICE_SUPPLIER_URL` is not used. | `instant` / `fast` / `standard` / `slow`
COMMON_HOME_GAS_PRICE_FALLBACK | The gas price (in Wei) that is used if both the oracle and the fall back gas price specified in the Home Bridge contract are not available. | integer COMMON_HOME_GAS_PRICE_FALLBACK | The gas price (in Wei) that is used if both the oracle and the fall back gas price specified in the Home Bridge contract are not available. | integer
COMMON_HOME_GAS_PRICE_FACTOR | A value that will multiply the gas price of the oracle to convert it to gwei. If the oracle API returns gas prices in gwei then this can be set to `1`. Also, it could be used to intentionally pay more gas than suggested by the oracle to guarantee the transaction verification. E.g. `1.25` or `1.5`. | integer COMMON_HOME_GAS_PRICE_FACTOR | A value that will multiply the gas price of the oracle to convert it to gwei. If the oracle API returns gas prices in gwei then this can be set to `1`. Also, it could be used to intentionally pay more gas than suggested by the oracle to guarantee the transaction verification. E.g. `1.25` or `1.5`. | integer
COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Foreign network. The provided gas price is used to send the validator's transactions to the RPC node. If the Foreign network is Ethereum Foundation mainnet, the oracle URL can be: https://gasprice.poa.network. Otherwise this parameter can be omitted. Set to `gas-price-oracle` if you want to use npm `gas-price-oracle` package for retrieving gas price from multiple sources. | URL COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Foreign network. The provided gas price is used to send the validator's transactions to the RPC node. If the Foreign network is Ethereum Foundation mainnet, the oracle URL can be: https://gasprice.poa.network. Otherwise this parameter can be omitted. Set to `gas-price-oracle` if you want to use npm `gas-price-oracle` package for retrieving gas price from multiple sources. Set to `eip1559-gas-estimation` if you want to use EIP1559 RPC-based gas estimation. | URL
COMMON_FOREIGN_GAS_PRICE_SPEED_TYPE | Assuming the gas price oracle responds with the following JSON structure: `{"fast": 20.0, "block_time": 12.834, "health": true, "standard": 6.0, "block_number": 6470469, "instant": 71.0, "slow": 1.889}`, this parameter specifies the desirable transaction speed. The speed type can be omitted when `COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL`is not used. | `instant` / `fast` / `standard` / `slow` COMMON_FOREIGN_GAS_PRICE_SPEED_TYPE | Assuming the gas price oracle responds with the following JSON structure: `{"fast": 20.0, "block_time": 12.834, "health": true, "standard": 6.0, "block_number": 6470469, "instant": 71.0, "slow": 1.889}`, this parameter specifies the desirable transaction speed. The speed type can be omitted when `COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL`is not used. | `instant` / `fast` / `standard` / `slow`
COMMON_FOREIGN_GAS_PRICE_FALLBACK | The gas price (in Wei) used if both the oracle and fall back gas price specified in the Foreign Bridge contract are not available. | integer COMMON_FOREIGN_GAS_PRICE_FALLBACK | The gas price (in Wei) used if both the oracle and fall back gas price specified in the Foreign Bridge contract are not available. | integer
COMMON_FOREIGN_GAS_PRICE_FACTOR | A value that will multiply the gas price of the oracle to convert it to gwei. If the oracle API returns gas prices in gwei then this can be set to `1`. Also, it could be used to intentionally pay more gas than suggested by the oracle to guarantee the transaction verification. E.g. `1.25` or `1.5`. | integer COMMON_FOREIGN_GAS_PRICE_FACTOR | A value that will multiply the gas price of the oracle to convert it to gwei. If the oracle API returns gas prices in gwei then this can be set to `1`. Also, it could be used to intentionally pay more gas than suggested by the oracle to guarantee the transaction verification. E.g. `1.25` or `1.5`. | integer
@ -53,6 +53,12 @@ ORACLE_SHUTDOWN_CONTRACT_METHOD | Method signature to be used in the side chain
ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT | Max length for the block range used in `eth_getLogs` requests for polling contract events for the Foreign chain. Infinite, if not provided. | `integer` ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT | Max length for the block range used in `eth_getLogs` requests for polling contract events for the Foreign chain. Infinite, if not provided. | `integer`
ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT | Max length for the block range used in `eth_getLogs` requests for polling contract events for the Home chain. Infinite, if not provided. | `integer` ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT | Max length for the block range used in `eth_getLogs` requests for polling contract events for the Home chain. Infinite, if not provided. | `integer`
ORACLE_JSONRPC_ERROR_CODES | Override default JSON rpc error codes that can trigger RPC fallback to the next URL from the list (or a retry in case of a single RPC URL). Default is `-32603,-32002,-32005`. Should be a comma-separated list of negative integers. | `string` ORACLE_JSONRPC_ERROR_CODES | Override default JSON rpc error codes that can trigger RPC fallback to the next URL from the list (or a retry in case of a single RPC URL). Default is `-32603,-32002,-32005`. Should be a comma-separated list of negative integers. | `string`
ORACLE_HOME_EVENTS_REPROCESSING | If set to `true`, home events happened in the past will be refetched and processed once again, to ensure that nothing was missed on the first pass. | `bool`
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE | Batch size for one `eth_getLogs` request when reprocessing old logs in the home chain. Defaults to `1000` | `integer`
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY | Block confirmations number, after which old logs are being reprocessed in the home chain. Defaults to `500` | `integer`
ORACLE_FOREIGN_EVENTS_REPROCESSING | If set to `true`, foreign events happened in the past will be refetched and processed once again, to ensure that nothing was missed on the first pass. | `bool`
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE | Batch size for one `eth_getLogs` request when reprocessing old logs in the foreign chain. Defaults to `500` | `integer`
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY | Block confirmations number, after which old logs are being reprocessed in the foreign chain. Defaults to `250` | `integer`
## Monitor configuration ## Monitor configuration

@ -8,6 +8,7 @@
"test": "NODE_ENV=test mocha" "test": "NODE_ENV=test mocha"
}, },
"dependencies": { "dependencies": {
"@mycrypto/gas-estimation": "^1.1.0",
"gas-price-oracle": "^0.1.5", "gas-price-oracle": "^0.1.5",
"web3-utils": "^1.3.0", "web3-utils": "^1.3.0",
"node-fetch": "^2.1.2" "node-fetch": "^2.1.2"

@ -1,5 +1,6 @@
const { toWei, toBN, BN } = require('web3-utils') const { toWei, toBN, BN } = require('web3-utils')
const { GasPriceOracle } = require('gas-price-oracle') const { GasPriceOracle } = require('gas-price-oracle')
const { estimateFees } = require('@mycrypto/gas-estimation')
const fetch = require('node-fetch') const fetch = require('node-fetch')
const { BRIDGE_MODES } = require('./constants') const { BRIDGE_MODES } = require('./constants')
const { REWARDABLE_VALIDATORS_ABI } = require('./abis') const { REWARDABLE_VALIDATORS_ABI } = require('./abis')
@ -176,12 +177,20 @@ const gasPriceWithinLimits = (gasPrice, limits) => {
const normalizeGasPrice = (oracleGasPrice, factor, limits = null) => { const normalizeGasPrice = (oracleGasPrice, factor, limits = null) => {
let gasPrice = oracleGasPrice * factor let gasPrice = oracleGasPrice * factor
gasPrice = gasPriceWithinLimits(gasPrice, limits) gasPrice = gasPriceWithinLimits(gasPrice, limits)
return toBN(toWei(gasPrice.toFixed(2).toString(), 'gwei')) return toWei(gasPrice.toFixed(2).toString(), 'gwei')
} }
const gasPriceFromSupplier = async (url, options = {}) => { const gasPriceFromSupplier = async (web3, url, options = {}) => {
try { try {
let json let json
if (url === 'eip1559-gas-estimation') {
const { maxFeePerGas, maxPriorityFeePerGas } = await estimateFees(web3)
const res = { maxFeePerGas: maxFeePerGas.toString(10), maxPriorityFeePerGas: maxPriorityFeePerGas.toString(10) }
options.logger &&
options.logger.debug &&
options.logger.debug(res, 'Gas price updated using eip1559-gas-estimation')
return res
}
if (url === 'gas-price-oracle') { if (url === 'gas-price-oracle') {
json = await gasPriceOracle.fetchGasPricesOffChain() json = await gasPriceOracle.fetchGasPricesOffChain()
} else if (url) { } else if (url) {
@ -205,7 +214,7 @@ const gasPriceFromSupplier = async (url, options = {}) => {
options.logger.debug && options.logger.debug &&
options.logger.debug({ oracleGasPrice, normalizedGasPrice }, 'Gas price updated using the API') options.logger.debug({ oracleGasPrice, normalizedGasPrice }, 'Gas price updated using the API')
return normalizedGasPrice return { gasPrice: normalizedGasPrice }
} catch (e) { } catch (e) {
options.logger && options.logger.error && options.logger.error(`Gas Price API is not available. ${e.message}`) options.logger && options.logger.error && options.logger.error(`Gas Price API is not available. ${e.message}`)
} }
@ -214,11 +223,11 @@ const gasPriceFromSupplier = async (url, options = {}) => {
const gasPriceFromContract = async (bridgeContract, options = {}) => { const gasPriceFromContract = async (bridgeContract, options = {}) => {
try { try {
const gasPrice = await bridgeContract.methods.gasPrice().call() const gasPrice = (await bridgeContract.methods.gasPrice().call()).toString()
options.logger && options.logger &&
options.logger.debug && options.logger.debug &&
options.logger.debug({ gasPrice }, 'Gas price updated using the contracts') options.logger.debug({ gasPrice }, 'Gas price updated using the contracts')
return gasPrice return { gasPrice }
} catch (e) { } catch (e) {
options.logger && options.logger &&
options.logger.error && options.logger.error &&

@ -23,3 +23,9 @@ ORACLE_HOME_START_BLOCK=1
ORACLE_FOREIGN_START_BLOCK=1 ORACLE_FOREIGN_START_BLOCK=1
ORACLE_HOME_TO_FOREIGN_BLOCK_LIST=/mono/oracle/access-lists/block_list.txt ORACLE_HOME_TO_FOREIGN_BLOCK_LIST=/mono/oracle/access-lists/block_list.txt
ORACLE_FOREIGN_ARCHIVE_RPC_URL=http://parity2:8545 ORACLE_FOREIGN_ARCHIVE_RPC_URL=http://parity2:8545
ORACLE_HOME_EVENTS_REPROCESSING=false
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE=10
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY=10
ORACLE_FOREIGN_EVENTS_REPROCESSING=true
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE=10
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY=10

@ -22,3 +22,9 @@ ORACLE_ALLOW_HTTP_FOR_RPC=yes
ORACLE_HOME_START_BLOCK=1 ORACLE_HOME_START_BLOCK=1
ORACLE_FOREIGN_START_BLOCK=1 ORACLE_FOREIGN_START_BLOCK=1
ORACLE_HOME_TO_FOREIGN_BLOCK_LIST=/mono/oracle/access-lists/block_list.txt ORACLE_HOME_TO_FOREIGN_BLOCK_LIST=/mono/oracle/access-lists/block_list.txt
ORACLE_HOME_EVENTS_REPROCESSING=true
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE=10
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY=10
ORACLE_FOREIGN_EVENTS_REPROCESSING=true
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE=10
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY=10

@ -23,7 +23,13 @@ const {
ORACLE_HOME_START_BLOCK, ORACLE_HOME_START_BLOCK,
ORACLE_FOREIGN_START_BLOCK, ORACLE_FOREIGN_START_BLOCK,
ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT, ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT,
ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT,
ORACLE_HOME_EVENTS_REPROCESSING,
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE,
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY,
ORACLE_FOREIGN_EVENTS_REPROCESSING,
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE,
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY
} = process.env } = process.env
let homeAbi let homeAbi
@ -61,7 +67,12 @@ const homeConfig = {
blockPollingLimit: parseInt(ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT, 10), blockPollingLimit: parseInt(ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT, 10),
web3: web3Home, web3: web3Home,
bridgeContract: homeContract, bridgeContract: homeContract,
eventContract: homeContract eventContract: homeContract,
reprocessingOptions: {
enabled: ORACLE_HOME_EVENTS_REPROCESSING === 'true',
batchSize: parseInt(ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE, 10) || 1000,
blockDelay: parseInt(ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY, 10) || 500
}
} }
const foreignContract = new web3Foreign.eth.Contract(foreignAbi, COMMON_FOREIGN_BRIDGE_ADDRESS) const foreignContract = new web3Foreign.eth.Contract(foreignAbi, COMMON_FOREIGN_BRIDGE_ADDRESS)
@ -74,7 +85,12 @@ const foreignConfig = {
blockPollingLimit: parseInt(ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT, 10), blockPollingLimit: parseInt(ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT, 10),
web3: web3Foreign, web3: web3Foreign,
bridgeContract: foreignContract, bridgeContract: foreignContract,
eventContract: foreignContract eventContract: foreignContract,
reprocessingOptions: {
enabled: ORACLE_FOREIGN_EVENTS_REPROCESSING === 'true',
batchSize: parseInt(ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE, 10) || 500,
blockDelay: parseInt(ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY, 10) || 250
}
} }
const maxProcessingTime = const maxProcessingTime =

@ -0,0 +1,37 @@
const baseConfig = require('./base.config')
const { DEFAULT_TRANSACTION_RESEND_INTERVAL } = require('../src/utils/constants')
const { MEV_HELPER_ABI } = require('../src/utils/mev')
const { web3Foreign, getFlashbotsProvider } = require('../src/services/web3')
const {
ORACLE_FOREIGN_TX_RESEND_INTERVAL,
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS,
ORACLE_MEV_FOREIGN_BUNDLES_BLOCK_RANGE
} = process.env
const contract = new baseConfig.foreign.web3.eth.Contract(MEV_HELPER_ABI, ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS)
module.exports = {
...baseConfig,
pollingInterval: baseConfig.foreign.pollingInterval,
mevForeign: {
contractAddress: ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
contract,
minGasPrice: ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
flatMinerFee: ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
maxPriorityFeePerGas: ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
maxFeePerGas: ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS,
bundlesPerIteration: Math.max(parseInt(ORACLE_MEV_FOREIGN_BUNDLES_BLOCK_RANGE, 10) || 5, 1),
getFlashbotsProvider
},
mevJobsRedisKey: `${baseConfig.id}-collected-signatures-mev:mevJobs`,
id: 'mev-sender-foreign',
name: 'mev-sender-foreign',
web3: web3Foreign,
resendInterval: parseInt(ORACLE_FOREIGN_TX_RESEND_INTERVAL, 10) || DEFAULT_TRANSACTION_RESEND_INTERVAL
}

@ -8,7 +8,6 @@ const { ORACLE_FOREIGN_TX_RESEND_INTERVAL } = process.env
module.exports = { module.exports = {
...baseConfig, ...baseConfig,
queue: 'foreign-prioritized', queue: 'foreign-prioritized',
oldQueue: 'foreign',
id: 'foreign', id: 'foreign',
name: 'sender-foreign', name: 'sender-foreign',
web3: web3Foreign, web3: web3Foreign,

@ -8,7 +8,6 @@ const { ORACLE_HOME_TX_RESEND_INTERVAL } = process.env
module.exports = { module.exports = {
...baseConfig, ...baseConfig,
queue: 'home-prioritized', queue: 'home-prioritized',
oldQueue: 'home',
id: 'home', id: 'home',
name: 'sender-home', name: 'sender-home',
web3: web3Home, web3: web3Home,

@ -0,0 +1,30 @@
const baseConfig = require('./base.config')
const { MEV_HELPER_ABI } = require('../src/utils/mev')
const {
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS
} = process.env
const id = `${baseConfig.id}-collected-signatures-mev`
const contract = new baseConfig.foreign.web3.eth.Contract(MEV_HELPER_ABI, ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS)
module.exports = {
...baseConfig,
mevForeign: {
contractAddress: ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS,
contract,
minGasPrice: ORACLE_MEV_FOREIGN_MIN_GAS_PRICE,
flatMinerFee: ORACLE_MEV_FOREIGN_FLAT_MINER_FEE,
maxPriorityFeePerGas: ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS,
maxFeePerGas: ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS
},
main: baseConfig.home,
event: 'CollectedSignatures',
name: `watcher-${id}`,
id
}

@ -1,6 +1,14 @@
--- ---
version: '2.4' version: '2.4'
services: services:
redis:
cpus: 0.1
mem_limit: 500m
command: [ redis-server, --appendonly, 'yes' ]
hostname: redis
image: redis:4
restart: unless-stopped
volumes: [ '~/bridge_data/helpers/redis:/data' ]
interestFetcher: interestFetcher:
cpus: 0.1 cpus: 0.1
mem_limit: 500m mem_limit: 500m
@ -13,3 +21,41 @@ services:
INTERVAL: 300000 INTERVAL: 300000
restart: unless-stopped restart: unless-stopped
entrypoint: yarn helper:interestFether entrypoint: yarn helper:interestFether
mevWatcher:
cpus: 0.1
mem_limit: 500m
image: poanetwork/tokenbridge-oracle:latest
env_file: ./.env
environment:
NODE_ENV: production
ORACLE_VALIDATOR_ADDRESS: ${ORACLE_VALIDATOR_ADDRESS}
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS: 'TBD'
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE: '50000000000' # 50 gwei
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE: '1500000000000000' # 0.0015 eth = 300k gas * 5 gwei
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS: '0' # 0 gwei
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS: '1000000000000' # 1000 gwei
ORACLE_FOREIGN_RPC_POLLING_INTERVAL: '15000' # CollectedSignatures event polling interval
ORACLE_HOME_START_BLOCK: 'TBD'
ORACLE_HOME_SKIP_MANUAL_LANE: 'true'
restart: unless-stopped
entrypoint: yarn mev:watcher:collected-signatures
mevSender:
cpus: 0.1
mem_limit: 500m
image: poanetwork/tokenbridge-oracle:latest
env_file: ./.env
environment:
NODE_ENV: production
ORACLE_VALIDATOR_ADDRESS: ${ORACLE_VALIDATOR_ADDRESS}
ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY: ${ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY}
ORACLE_MEV_FOREIGN_HELPER_CONTRACT_ADDRESS: 'TBD'
ORACLE_MEV_FOREIGN_MIN_GAS_PRICE: '50000000000' # 50 gwei
ORACLE_MEV_FOREIGN_FLAT_MINER_FEE: '1500000000000000' # 0.0015 eth = 300k gas * 5 gwei
ORACLE_MEV_FOREIGN_MAX_PRIORITY_FEE_PER_GAS: '0' # 0 gwei
ORACLE_MEV_FOREIGN_MAX_FEE_PER_GAS: '1000000000000' # 1000 gwei
ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL: 'https://relay-goerli.flashbots.net'
ORACLE_MEV_FOREIGN_FLASHBOTS_AUTH_SIGNING_KEY: 82db7175932f4e6c8e45283b78b54fd5f195149378ec90d95b8fd0ec8bdadf1d
ORACLE_MEV_FOREIGN_BUNDLES_BLOCK_RANGE: '5'
ORACLE_FOREIGN_RPC_POLLING_INTERVAL: '70000' # time between sending different batches of MEV bundles (~= 5 blocks * 14 seconds)
restart: unless-stopped
entrypoint: yarn mev:sender:foreign

@ -19,6 +19,8 @@
"confirm:information-request": "./scripts/start-worker.sh confirmRelay information-request-watcher", "confirm:information-request": "./scripts/start-worker.sh confirmRelay information-request-watcher",
"manager:shutdown": "./scripts/start-worker.sh shutdownManager shutdown-manager", "manager:shutdown": "./scripts/start-worker.sh shutdownManager shutdown-manager",
"helper:interestFether": "node ./scripts/interestFetcher.js", "helper:interestFether": "node ./scripts/interestFetcher.js",
"mev:watcher:collected-signatures": "./scripts/start-worker.sh mevWatcher mev-collected-signatures-watcher",
"mev:sender:foreign": "./scripts/start-worker.sh mevSender foreign-mev-sender",
"dev": "concurrently -n 'watcher:signature-request,watcher:collected-signatures,watcher:affirmation-request,watcher:transfer, sender:home,sender:foreign' -c 'red,green,yellow,blue,magenta,cyan' 'yarn watcher:signature-request' 'yarn watcher:collected-signatures' 'yarn watcher:affirmation-request' 'yarn watcher:transfer' 'yarn sender:home' 'yarn sender:foreign'", "dev": "concurrently -n 'watcher:signature-request,watcher:collected-signatures,watcher:affirmation-request,watcher:transfer, sender:home,sender:foreign' -c 'red,green,yellow,blue,magenta,cyan' 'yarn watcher:signature-request' 'yarn watcher:collected-signatures' 'yarn watcher:affirmation-request' 'yarn watcher:transfer' 'yarn sender:home' 'yarn sender:foreign'",
"test": "NODE_ENV=test mocha", "test": "NODE_ENV=test mocha",
"test:watch": "NODE_ENV=test mocha --watch --reporter=min", "test:watch": "NODE_ENV=test mocha --watch --reporter=min",
@ -28,17 +30,19 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@flashbots/ethers-provider-bundle": "^0.4.3",
"amqp-connection-manager": "^2.0.0", "amqp-connection-manager": "^2.0.0",
"amqplib": "^0.5.2", "amqplib": "^0.5.2",
"bignumber.js": "^7.2.1", "bignumber.js": "^7.2.1",
"dotenv": "^5.0.1", "dotenv": "^5.0.1",
"ethers": "^5.5.3",
"ioredis": "^3.2.2", "ioredis": "^3.2.2",
"node-fetch": "^2.1.2", "node-fetch": "^2.1.2",
"pino": "^4.17.3", "pino": "^4.17.3",
"pino-pretty": "^2.0.1", "pino-pretty": "^2.0.1",
"promise-limit": "^2.7.0", "promise-limit": "^2.7.0",
"promise-retry": "^1.1.1", "promise-retry": "^1.1.1",
"web3": "^1.3.0" "web3": "^1.6.0"
}, },
"devDependencies": { "devDependencies": {
"bn-chai": "^1.0.1", "bn-chai": "^1.0.1",

@ -36,7 +36,7 @@ async function main() {
data, data,
nonce, nonce,
gasPrice: FOREIGN_TEST_TX_GAS_PRICE, gasPrice: FOREIGN_TEST_TX_GAS_PRICE,
amount: '0', value: '0',
gasLimit, gasLimit,
to: bridgeableTokenAddress, to: bridgeableTokenAddress,
web3: web3Foreign, web3: web3Foreign,

@ -29,7 +29,7 @@ async function main() {
data: '0x', data: '0x',
nonce, nonce,
gasPrice: HOME_TEST_TX_GAS_PRICE, gasPrice: HOME_TEST_TX_GAS_PRICE,
amount: HOME_MIN_AMOUNT_PER_TX, value: web3Home.utils.toWei(HOME_MIN_AMOUNT_PER_TX),
gasLimit: 100000, gasLimit: 100000,
to: COMMON_HOME_BRIDGE_ADDRESS, to: COMMON_HOME_BRIDGE_ADDRESS,
web3: web3Home, web3: web3Home,

@ -54,7 +54,7 @@ async function main() {
nonce, nonce,
gasPrice, gasPrice,
gasLimit: Math.round(gasLimit * 1.5), gasLimit: Math.round(gasLimit * 1.5),
amount: '0', value: '0',
chainId, chainId,
web3: web3Home web3: web3Home
}) })

@ -153,11 +153,11 @@ async function main({ sendJob, txHashes }) {
} }
async function sendJobTx(jobs) { async function sendJobTx(jobs) {
await GasPrice.start(chain, true)
const gasPrice = GasPrice.getPrice().toString(10)
const { web3 } = config.sender === 'foreign' ? config.foreign : config.home const { web3 } = config.sender === 'foreign' ? config.foreign : config.home
await GasPrice.start(chain, web3, true)
const gasPriceOptions = GasPrice.gasPriceOptions()
const chainId = await getChainId(web3) const chainId = await getChainId(web3)
let nonce = await getNonce(web3, config.validatorAddress) let nonce = await getNonce(web3, config.validatorAddress)
@ -174,13 +174,13 @@ async function sendJobTx(jobs) {
const txHash = await sendTx({ const txHash = await sendTx({
data: job.data, data: job.data,
nonce, nonce,
gasPrice, value: '0',
amount: '0',
gasLimit, gasLimit,
privateKey: config.validatorPrivateKey, privateKey: config.validatorPrivateKey,
to: job.to, to: job.to,
chainId, chainId,
web3 web3,
gasPriceOptions
}) })
nonce++ nonce++
@ -197,7 +197,7 @@ async function sendJobTx(jobs) {
if (e.message.toLowerCase().includes('insufficient funds')) { if (e.message.toLowerCase().includes('insufficient funds')) {
const currentBalance = await web3.eth.getBalance(config.validatorAddress) const currentBalance = await web3.eth.getBalance(config.validatorAddress)
const minimumBalance = gasLimit.multipliedBy(gasPrice) const minimumBalance = gasLimit.multipliedBy(gasPriceOptions.gasPrice || gasPriceOptions.maxFeePerGas)
logger.error( logger.error(
`Insufficient funds: ${currentBalance}. Stop processing messages until the balance is at least ${minimumBalance}.` `Insufficient funds: ${currentBalance}. Stop processing messages until the balance is at least ${minimumBalance}.`
) )

@ -4,7 +4,6 @@ const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError
const logger = require('../../services/logger').child({ const logger = require('../../services/logger').child({
module: 'processCollectedSignatures:estimateGas' module: 'processCollectedSignatures:estimateGas'
}) })
const { parseAMBHeader } = require('../../utils/message')
const web3 = new Web3() const web3 = new Web3()
const { toBN } = Web3.utils const { toBN } = Web3.utils
@ -22,15 +21,9 @@ async function estimateGas({
address address
}) { }) {
try { try {
const gasEstimate = await foreignBridge.methods.executeSignatures(message, signatures).estimateGas({ return await foreignBridge.methods.executeSignatures(message, signatures).estimateGas({
from: address from: address
}) })
const msgGasLimit = parseAMBHeader(message).gasLimit
// + estimateExtraGas(len)
// is not needed here, since estimateGas will already take into account gas
// needed for memory expansion, message processing, etc.
return gasEstimate + msgGasLimit
} catch (e) { } catch (e) {
if (e instanceof HttpListProviderError) { if (e instanceof HttpListProviderError) {
throw e throw e

@ -0,0 +1,183 @@
require('dotenv').config()
const promiseLimit = require('promise-limit')
const { HttpListProviderError } = require('../../services/HttpListProvider')
const { getValidatorContract } = require('../../tx/web3')
const rootLogger = require('../../services/logger')
const { signatureToVRS, packSignatures } = require('../../utils/message')
const { readAccessListFile, isRevertError } = require('../../utils/utils')
const { parseAMBMessage } = require('../../../../commons')
const estimateGas = require('../processAMBCollectedSignatures/estimateGas')
const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError } = require('../../utils/errors')
const { MAX_CONCURRENT_EVENTS, EXTRA_GAS_ABSOLUTE } = require('../../utils/constants')
const limit = promiseLimit(MAX_CONCURRENT_EVENTS)
const { ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST, ORACLE_HOME_TO_FOREIGN_BLOCK_LIST } = process.env
const ORACLE_HOME_SKIP_MANUAL_LANE = process.env.ORACLE_HOME_SKIP_MANUAL_LANE === 'true'
function processCollectedSignaturesBuilder(config) {
const { home, foreign, mevForeign } = config
let validatorContract = null
return async function processCollectedSignatures(signatures) {
const txToSend = []
if (validatorContract === null) {
validatorContract = await getValidatorContract(foreign.bridgeContract, foreign.web3)
}
rootLogger.debug(`Processing ${signatures.length} CollectedSignatures events`)
const callbacks = signatures
.map(colSignature => async () => {
const { messageHash, NumberOfCollectedSignatures } = colSignature.returnValues
const logger = rootLogger.child({
eventTransactionHash: colSignature.transactionHash
})
logger.info(`Processing CollectedSignatures ${colSignature.transactionHash}`)
const message = await home.bridgeContract.methods.message(messageHash).call()
const parsedMessage = parseAMBMessage(message)
if (ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST || ORACLE_HOME_TO_FOREIGN_BLOCK_LIST) {
const sender = parsedMessage.sender.toLowerCase()
const executor = parsedMessage.executor.toLowerCase()
if (ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST) {
const allowanceList = await readAccessListFile(ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST, logger)
if (!allowanceList.includes(executor) && !allowanceList.includes(sender)) {
logger.info(
{ sender, executor },
'Validator skips a message. Neither sender nor executor addresses are in the allowance list.'
)
return
}
} else if (ORACLE_HOME_TO_FOREIGN_BLOCK_LIST) {
const blockList = await readAccessListFile(ORACLE_HOME_TO_FOREIGN_BLOCK_LIST, logger)
if (blockList.includes(executor)) {
logger.info({ executor }, 'Validator skips a message. Executor address is in the block list.')
return
}
if (blockList.includes(sender)) {
logger.info({ sender }, 'Validator skips a message. Sender address is in the block list.')
return
}
}
}
if (ORACLE_HOME_SKIP_MANUAL_LANE && parsedMessage.decodedDataType.manualLane) {
logger.info(
{ dataType: parsedMessage.dataType },
'Validator skips a message. Message was forwarded to the manual lane by the extension'
)
return
}
logger.debug({ NumberOfCollectedSignatures }, 'Number of signatures to get')
const requiredSignatures = []
requiredSignatures.length = NumberOfCollectedSignatures
requiredSignatures.fill(0)
const signaturesArray = []
const [v, r, s] = [[], [], []]
logger.debug('Getting message signatures')
const signaturePromises = requiredSignatures.map(async (el, index) => {
logger.debug({ index }, 'Getting message signature')
const signature = await home.bridgeContract.methods.signature(messageHash, index).call()
const vrs = signatureToVRS(signature)
v.push(vrs.v)
r.push(vrs.r)
s.push(vrs.s)
signaturesArray.push(vrs)
})
await Promise.all(signaturePromises)
const signatures = packSignatures(signaturesArray)
logger.info(`Processing messageId: ${parsedMessage.messageId}`)
let gasEstimate
try {
logger.debug('Estimate gas')
gasEstimate = await estimateGas({
foreignBridge: foreign.bridgeContract,
validatorContract,
v,
r,
s,
signatures,
message,
numberOfCollectedSignatures: NumberOfCollectedSignatures,
messageId: parsedMessage.messageId,
address: config.validatorAddress
})
logger.debug({ gasEstimate }, 'Gas estimated')
} catch (e) {
if (e instanceof HttpListProviderError) {
throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.')
} else if (e instanceof AlreadyProcessedError) {
logger.info(`Already processed CollectedSignatures ${colSignature.transactionHash}`)
return
} else if (e instanceof IncompatibleContractError || e instanceof InvalidValidatorError) {
logger.error(`The message couldn't be processed; skipping: ${e.message}`)
return
} else {
logger.error(e, 'Unknown error while processing transaction')
throw e
}
}
const executeData = foreign.bridgeContract.methods.executeSignatures(message, signatures).encodeABI()
const profit = await estimateProfit(
mevForeign.contract,
mevForeign.minGasPrice,
executeData,
mevForeign.flatMinerFee
)
if (profit === '0') {
logger.error('No MEV opportunity found when testing with min gas price, skipping job')
return
}
logger.info(`Estimated profit of ${profit} when simulating with ${mevForeign.minGasPrice} gas price`)
txToSend.push({
profit,
executeData,
data: mevForeign.contract.methods.execute(executeData).encodeABI(),
gasEstimate,
extraGas: EXTRA_GAS_ABSOLUTE,
maxFeePerGas: mevForeign.maxFeePerGas,
maxPriorityFeePerGas: mevForeign.maxPriorityFeePerGas,
transactionReference: colSignature.transactionHash,
to: mevForeign.contractAddress,
value: mevForeign.flatMinerFee
})
})
.map(promise => limit(promise))
await Promise.all(callbacks)
return txToSend
}
}
async function estimateProfit(contract, gasPrice, data, minerFee) {
return contract.methods
.estimateProfit(gasPrice, data)
.call({ value: minerFee })
.then(
res => res.toString(),
e => {
if (isRevertError(e)) {
return '0'
}
throw e
}
)
}
module.exports = {
processCollectedSignaturesBuilder,
estimateProfit
}

@ -6,11 +6,9 @@ const logger = require('../../services/logger').child({
async function estimateGas({ web3, homeBridge, validatorContract, recipient, value, txHash, address }) { async function estimateGas({ web3, homeBridge, validatorContract, recipient, value, txHash, address }) {
try { try {
const gasEstimate = await homeBridge.methods.executeAffirmation(recipient, value, txHash).estimateGas({ return await homeBridge.methods.executeAffirmation(recipient, value, txHash).estimateGas({
from: address from: address
}) })
return gasEstimate
} catch (e) { } catch (e) {
if (e instanceof HttpListProviderError) { if (e instanceof HttpListProviderError) {
throw e throw e

@ -20,8 +20,7 @@ async function estimateGas({
signatures signatures
}) { }) {
try { try {
const gasEstimate = await foreignBridge.methods.executeSignatures(message, signatures).estimateGas() return await foreignBridge.methods.executeSignatures(message, signatures).estimateGas()
return gasEstimate
} catch (e) { } catch (e) {
if (e instanceof HttpListProviderError) { if (e instanceof HttpListProviderError) {
throw e throw e

@ -6,10 +6,9 @@ const logger = require('../../services/logger').child({
async function estimateGas({ web3, homeBridge, validatorContract, signature, message, address }) { async function estimateGas({ web3, homeBridge, validatorContract, signature, message, address }) {
try { try {
const gasEstimate = await homeBridge.methods.submitSignature(signature, message).estimateGas({ return await homeBridge.methods.submitSignature(signature, message).estimateGas({
from: address from: address
}) })
return gasEstimate
} catch (e) { } catch (e) {
if (e instanceof HttpListProviderError) { if (e instanceof HttpListProviderError) {
throw e throw e

159
oracle/src/mevSender.js Normal file

@ -0,0 +1,159 @@
require('../env')
const path = require('path')
const BigNumber = require('bignumber.js')
const { redis } = require('./services/redisClient')
const logger = require('./services/logger')
const { sendTx } = require('./tx/sendTx')
const { getNonce, getChainId, getBlock } = require('./tx/web3')
const { addExtraGas, checkHTTPS, watchdog } = require('./utils/utils')
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT } = require('./utils/constants')
const { estimateProfit } = require('./events/processAMBCollectedSignaturesMEV')
if (process.argv.length < 3) {
logger.error('Please check the number of arguments, config file was not provided')
process.exit(EXIT_CODES.GENERAL_ERROR)
}
const config = require(path.join('../config/', process.argv[2]))
const { web3, mevForeign, validatorAddress } = config
let chainId = 0
let flashbotsProvider
async function initialize() {
try {
const checkHttps = checkHTTPS(process.env.ORACLE_ALLOW_HTTP_FOR_RPC, logger)
web3.currentProvider.urls.forEach(checkHttps(config.id))
chainId = await getChainId(web3)
flashbotsProvider = await mevForeign.getFlashbotsProvider(chainId)
return runMain()
} catch (e) {
logger.error(e.message)
process.exit(EXIT_CODES.GENERAL_ERROR)
}
}
async function runMain() {
try {
if (redis.status === 'ready') {
if (config.maxProcessingTime) {
await watchdog(main, config.maxProcessingTime, () => {
logger.fatal('Max processing time reached')
process.exit(EXIT_CODES.MAX_TIME_REACHED)
})
} else {
await main()
}
}
} catch (e) {
logger.error(e)
}
setTimeout(runMain, config.pollingInterval)
}
async function main() {
try {
const jobs = Object.values(await redis.hgetall(config.mevJobsRedisKey)).map(JSON.parse)
const totalJobs = jobs.length
if (totalJobs === 0) {
logger.debug('Nothing to process')
return
}
const { baseFeePerGas: pendingBaseFee, number: pendingBlockNumber } = await getBlock(web3, 'pending')
const bestJob = pickBestJob(jobs, pendingBaseFee)
if (!bestJob) {
logger.info({ totalJobs, pendingBaseFee }, 'No suitable job was found, waiting for a lower gas price')
return
}
const jobLogger = logger.child({ eventTransactionHash: bestJob.transactionReference })
const maxProfit = await estimateProfit(
mevForeign.contract,
mevForeign.minGasPrice,
bestJob.executeData,
bestJob.value
)
if (maxProfit === '0') {
jobLogger.info(`No MEV opportunity found when testing with min gas price ${mevForeign.minGasPrice}, removing job`)
await redis.hdel(config.mevJobsRedisKey, bestJob.transactionReference)
return
}
jobLogger.info(`Estimated profit of ${maxProfit} when simulating with ${mevForeign.minGasPrice} gas price`)
bestJob.profit = maxProfit
if (new BigNumber(pendingBaseFee).gt(mevForeign.minGasPrice)) {
const profit = await estimateProfit(mevForeign.contract, pendingBaseFee, bestJob.executeData, bestJob.value)
if (profit === '0') {
jobLogger.info(
`No MEV opportunity found when testing with current gas price ${pendingBaseFee}, waiting for lower gas price`
)
bestJob.maxFeePerGas = pendingBaseFee
await redis.hset(config.mevJobsRedisKey, bestJob.transactionReference, JSON.stringify(bestJob))
return
}
jobLogger.info(`Estimated profit of ${profit} when simulating with ${pendingBaseFee} gas price`)
}
let gasLimit
if (typeof bestJob.extraGas === 'number') {
gasLimit = addExtraGas(bestJob.gasEstimate + bestJob.extraGas, 0, MAX_GAS_LIMIT)
} else {
gasLimit = addExtraGas(bestJob.gasEstimate, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT)
}
const nonce = await getNonce(web3, validatorAddress)
jobLogger.info(
{ nonce, fromBlock: pendingBlockNumber, toBlock: pendingBlockNumber + mevForeign.bundlesPerIteration - 1 },
'Sending MEV bundles'
)
const txHash = await sendTx({
data: bestJob.data,
nonce,
value: bestJob.value,
gasLimit,
privateKey: config.validatorPrivateKey,
to: bestJob.to,
chainId,
web3,
gasPriceOptions: {
maxFeePerGas: bestJob.maxFeePerGas,
maxPriorityFeePerGas: bestJob.maxPriorityFeePerGas
},
mevOptions: {
provider: flashbotsProvider,
fromBlock: pendingBlockNumber,
toBlock: pendingBlockNumber + mevForeign.bundlesPerIteration - 1,
logger
}
})
jobLogger.info({ txHash }, `Tx generated ${txHash} for event Tx ${bestJob.transactionReference}`)
await redis.hset(config.mevJobsRedisKey, bestJob.transactionReference, JSON.stringify(bestJob))
jobLogger.debug(`Finished processing msg`)
} catch (e) {
logger.error(e)
}
}
function pickBestJob(jobs, feePerGas) {
const feePerGasBN = new BigNumber(feePerGas)
let best = null
jobs.forEach(job => {
if (feePerGasBN.lt(job.maxFeePerGas) && (!best || new BigNumber(best.profit).lt(job.profit))) {
best = job
}
})
return best
}
initialize()

251
oracle/src/mevWatcher.js Normal file

@ -0,0 +1,251 @@
require('../env')
const path = require('path')
const { redis } = require('./services/redisClient')
const logger = require('./services/logger')
const { getBlockNumber, getRequiredBlockConfirmations, getEvents } = require('./tx/web3')
const { checkHTTPS, watchdog, syncForEach } = require('./utils/utils')
const { processCollectedSignaturesBuilder } = require('./events/processAMBCollectedSignaturesMEV')
const {
EXIT_CODES,
BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT,
MAX_HISTORY_BLOCK_TO_REPROCESS
} = require('./utils/constants')
if (process.argv.length < 3) {
logger.error('Please check the number of arguments, config file was not provided')
process.exit(EXIT_CODES.GENERAL_ERROR)
}
const config = require(path.join('../config/', process.argv[2]))
const processAMBCollectedSignaturesMEV = processCollectedSignaturesBuilder(config)
const {
web3,
bridgeContract,
eventContract,
startBlock,
pollingInterval,
chain,
reprocessingOptions,
blockPollingLimit
} = config.main
const lastBlockRedisKey = `${config.id}:lastProcessedBlock`
const lastReprocessedBlockRedisKey = `${config.id}:lastReprocessedBlock`
const seenEventsRedisKey = `${config.id}:seenEvents`
const mevJobsRedisKey = `${config.id}:mevJobs`
let lastProcessedBlock = Math.max(startBlock - 1, 0)
let lastReprocessedBlock
let lastSeenBlockNumber = 0
let sameBlockNumberCounter = 0
async function initialize() {
try {
const checkHttps = checkHTTPS(process.env.ORACLE_ALLOW_HTTP_FOR_RPC, logger)
web3.currentProvider.urls.forEach(checkHttps(chain))
await getLastProcessedBlock()
await getLastReprocessedBlock()
runMain({ sendToQueue: saveJobsToRedis })
} catch (e) {
logger.error(e)
process.exit(EXIT_CODES.GENERAL_ERROR)
}
}
async function runMain({ sendToQueue }) {
try {
if (redis.status === 'ready') {
if (config.maxProcessingTime) {
await watchdog(() => main({ sendToQueue }), config.maxProcessingTime, () => {
logger.fatal('Max processing time reached')
process.exit(EXIT_CODES.MAX_TIME_REACHED)
})
} else {
await main({ sendToQueue })
}
}
} catch (e) {
logger.error(e)
}
setTimeout(() => {
runMain({ sendToQueue })
}, pollingInterval)
}
async function saveJobsToRedis(jobs) {
return syncForEach(jobs, job => redis.hset(mevJobsRedisKey, job.transactionReference, JSON.stringify(job)))
}
async function getLastProcessedBlock() {
const result = await redis.get(lastBlockRedisKey)
logger.debug({ fromRedis: result, fromConfig: lastProcessedBlock }, 'Last Processed block obtained')
lastProcessedBlock = result ? parseInt(result, 10) : lastProcessedBlock
}
async function getLastReprocessedBlock() {
if (reprocessingOptions.enabled) {
const result = await redis.get(lastReprocessedBlockRedisKey)
if (result) {
lastReprocessedBlock = Math.max(parseInt(result, 10), lastProcessedBlock - MAX_HISTORY_BLOCK_TO_REPROCESS)
} else {
lastReprocessedBlock = lastProcessedBlock
}
logger.debug({ block: lastReprocessedBlock }, 'Last reprocessed block obtained')
} else {
// when reprocessing is being enabled not for the first time,
// we do not want to process blocks for which we didn't recorded seen events,
// instead, we want to start from the current block.
// Thus we should delete this reprocessing pointer once it is disabled.
await redis.del(lastReprocessedBlockRedisKey)
}
}
function updateLastProcessedBlock(lastBlockNumber) {
lastProcessedBlock = lastBlockNumber
return redis.set(lastBlockRedisKey, lastProcessedBlock)
}
function updateLastReprocessedBlock(lastBlockNumber) {
lastReprocessedBlock = lastBlockNumber
return redis.set(lastReprocessedBlockRedisKey, lastReprocessedBlock)
}
function processEvents(events) {
switch (config.id) {
case 'amb-collected-signatures-mev':
return processAMBCollectedSignaturesMEV(events)
default:
return []
}
}
const eventKey = e => `${e.transactionHash}-${e.logIndex}`
async function reprocessOldLogs(sendToQueue) {
const fromBlock = lastReprocessedBlock + 1
const toBlock = lastReprocessedBlock + reprocessingOptions.batchSize
const events = await getEvents({
contract: eventContract,
event: config.event,
fromBlock,
toBlock,
filter: config.eventFilter
})
const alreadySeenEvents = await getSeenEvents(fromBlock, toBlock)
const missingEvents = events.filter(e => !alreadySeenEvents[eventKey(e)])
if (missingEvents.length === 0) {
logger.debug('No missed events were found')
} else {
logger.info(`Found ${missingEvents.length} ${config.event} missed events`)
const job = await processEvents(missingEvents)
logger.info('Missed events transactions to send:', job.length)
if (job.length) {
await sendToQueue(job)
}
}
await updateLastReprocessedBlock(toBlock)
await deleteSeenEvents(0, toBlock)
}
async function getSeenEvents(fromBlock, toBlock) {
const keys = await redis.zrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
const res = {}
keys.forEach(k => {
res[k] = true
})
return res
}
function deleteSeenEvents(fromBlock, toBlock) {
return redis.zremrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
}
function addSeenEvents(events) {
return redis.zadd(seenEventsRedisKey, ...events.flatMap(e => [e.blockNumber, eventKey(e)]))
}
async function getLastBlockToProcess(web3, bridgeContract) {
const [lastBlockNumber, requiredBlockConfirmations] = await Promise.all([
getBlockNumber(web3),
getRequiredBlockConfirmations(bridgeContract)
])
if (lastBlockNumber < lastSeenBlockNumber) {
sameBlockNumberCounter = 0
logger.warn({ lastBlockNumber, lastSeenBlockNumber }, 'Received block number less than already seen block')
web3.currentProvider.switchToFallbackRPC()
} else if (lastBlockNumber === lastSeenBlockNumber) {
sameBlockNumberCounter++
if (sameBlockNumberCounter > 1) {
logger.info({ lastBlockNumber, sameBlockNumberCounter }, 'Received the same block number more than twice')
if (sameBlockNumberCounter >= BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT) {
sameBlockNumberCounter = 0
logger.warn(
{ lastBlockNumber, n: BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT },
'Received the same block number for too many times. Probably node is not synced anymore'
)
web3.currentProvider.switchToFallbackRPC()
}
}
} else {
sameBlockNumberCounter = 0
lastSeenBlockNumber = lastBlockNumber
}
return lastBlockNumber - requiredBlockConfirmations
}
async function main({ sendToQueue }) {
try {
const lastBlockToProcess = await getLastBlockToProcess(web3, bridgeContract)
if (reprocessingOptions.enabled) {
if (lastReprocessedBlock + reprocessingOptions.batchSize + reprocessingOptions.blockDelay < lastBlockToProcess) {
await reprocessOldLogs(sendToQueue)
return
}
}
if (lastBlockToProcess <= lastProcessedBlock) {
logger.debug('All blocks already processed')
return
}
const fromBlock = lastProcessedBlock + 1
const rangeEndBlock = blockPollingLimit ? fromBlock + blockPollingLimit : lastBlockToProcess
const toBlock = Math.min(lastBlockToProcess, rangeEndBlock)
const events = await getEvents({
contract: eventContract,
event: config.event,
fromBlock,
toBlock,
filter: config.eventFilter
})
logger.info(`Found ${events.length} ${config.event} events`)
if (events.length) {
const job = await processEvents(events)
logger.info('Transactions to send:', job.length)
if (job.length) {
await sendToQueue(job)
}
if (reprocessingOptions.enabled) {
await addSeenEvents(events)
}
}
logger.debug({ lastProcessedBlock: toBlock.toString() }, 'Updating last processed block')
await updateLastProcessedBlock(toBlock)
} catch (e) {
logger.error(e)
}
logger.debug('Finished')
}
initialize()

@ -9,6 +9,8 @@ const { sendTx } = require('./tx/sendTx')
const { getNonce, getChainId } = require('./tx/web3') const { getNonce, getChainId } = require('./tx/web3')
const { const {
addExtraGas, addExtraGas,
applyMinGasFeeBump,
chooseGasPriceOptions,
checkHTTPS, checkHTTPS,
syncForEach, syncForEach,
waitForFunds, waitForFunds,
@ -19,7 +21,7 @@ const {
isInsufficientBalanceError, isInsufficientBalanceError,
isNonceError isNonceError
} = require('./utils/utils') } = require('./utils/utils')
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT } = require('./utils/constants') const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT, MIN_GAS_PRICE_BUMP_FACTOR } = require('./utils/constants')
const { ORACLE_TX_REDUNDANCY } = process.env const { ORACLE_TX_REDUNDANCY } = process.env
@ -42,7 +44,7 @@ async function initialize() {
web3.currentProvider.urls.forEach(checkHttps(config.id)) web3.currentProvider.urls.forEach(checkHttps(config.id))
GasPrice.start(config.id) GasPrice.start(config.id, web3)
chainId = await getChainId(web3) chainId = await getChainId(web3)
connectQueue() connectQueue()
@ -55,7 +57,6 @@ async function initialize() {
function connectQueue() { function connectQueue() {
connectSenderToQueue({ connectSenderToQueue({
queueName: config.queue, queueName: config.queue,
oldQueueName: config.oldQueue,
resendInterval: config.resendInterval, resendInterval: config.resendInterval,
cb: options => { cb: options => {
if (config.maxProcessingTime) { if (config.maxProcessingTime) {
@ -121,7 +122,7 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
const txArray = JSON.parse(msg.content) const txArray = JSON.parse(msg.content)
logger.debug(`Msg received with ${txArray.length} Tx to send`) logger.debug(`Msg received with ${txArray.length} Tx to send`)
const gasPrice = GasPrice.getPrice().toString(10) const gasPriceOptions = GasPrice.gasPriceOptions()
let nonce let nonce
let insufficientFunds = false let insufficientFunds = false
@ -147,6 +148,7 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
} }
try { try {
const newGasPriceOptions = chooseGasPriceOptions(gasPriceOptions, job.gasPriceOptions)
if (isResend) { if (isResend) {
const tx = await web3Fallback.eth.getTransaction(job.txHash) const tx = await web3Fallback.eth.getTransaction(job.txHash)
@ -159,24 +161,26 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
nonce = await readNonce(true) nonce = await readNonce(true)
} }
logger.info(`Transaction ${job.txHash} was not mined, updating gasPrice: ${job.gasPrice} -> ${gasPrice}`) const oldGasPrice = JSON.stringify(job.gasPriceOptions)
const newGasPrice = JSON.stringify(newGasPriceOptions)
logger.info(`Transaction ${job.txHash} was not mined, updating gasPrice: ${oldGasPrice} -> ${newGasPrice}`)
} }
logger.info(`Sending transaction with nonce ${nonce}`) logger.info(`Sending transaction with nonce ${nonce}`)
const txHash = await sendTx({ const txHash = await sendTx({
data: job.data, data: job.data,
nonce, nonce,
gasPrice, value: '0',
amount: '0',
gasLimit, gasLimit,
privateKey: config.validatorPrivateKey, privateKey: config.validatorPrivateKey,
to: job.to, to: job.to,
chainId, chainId,
web3: web3Redundant web3: web3Redundant,
gasPriceOptions: newGasPriceOptions
}) })
const resendJob = { const resendJob = {
...job, ...job,
txHash, txHash,
gasPrice gasPriceOptions: newGasPriceOptions
} }
resendJobs.push(resendJob) resendJobs.push(resendJob)
@ -194,8 +198,8 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
if (isGasPriceError(e)) { if (isGasPriceError(e)) {
logger.info('Replacement transaction underpriced, forcing gas price update') logger.info('Replacement transaction underpriced, forcing gas price update')
GasPrice.start(config.id) GasPrice.start(config.id, web3)
failedTx.push(job) failedTx.push(applyMinGasFeeBump(job, MIN_GAS_PRICE_BUMP_FACTOR))
} else if (isResend || isSameTransactionError(e)) { } else if (isResend || isSameTransactionError(e)) {
resendJobs.push(job) resendJobs.push(job)
} else { } else {
@ -208,7 +212,7 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry, scheduleT
if (isInsufficientBalanceError(e)) { if (isInsufficientBalanceError(e)) {
insufficientFunds = true insufficientFunds = true
const currentBalance = await web3.eth.getBalance(config.validatorAddress) const currentBalance = await web3.eth.getBalance(config.validatorAddress)
minimumBalance = gasLimit.multipliedBy(gasPrice) minimumBalance = gasLimit.multipliedBy(gasPriceOptions.gasPrice || gasPriceOptions.maxFeePerGas)
logger.error( logger.error(
`Insufficient funds: ${currentBalance}. Stop processing messages until the balance is at least ${minimumBalance}.` `Insufficient funds: ${currentBalance}. Stop processing messages until the balance is at least ${minimumBalance}.`
) )

@ -40,23 +40,9 @@ function connectWatcherToQueue({ queueName, cb }) {
cb({ sendToQueue, channel: channelWrapper }) cb({ sendToQueue, channel: channelWrapper })
} }
function connectSenderToQueue({ queueName, oldQueueName, cb, resendInterval }) { function connectSenderToQueue({ queueName, cb, resendInterval }) {
const deadLetterExchange = `${queueName}-retry` const deadLetterExchange = `${queueName}-retry`
async function resendMessagesToNewQueue(channel) {
logger.info(`Trying to check messages in the old non-priority queue ${queueName}`)
while (true) {
const msg = await channel.get(oldQueueName)
if (msg === false) {
logger.info(`No messages in the old queue ${oldQueueName} left`)
break
}
logger.debug(`Message in the old queue ${oldQueueName} was found, redirecting it to the new queue ${queueName}`)
await channel.sendToQueue(queueName, msg.content, { persistent: true, priority: SENDER_QUEUE_SEND_PRIORITY })
await channel.ack(msg)
}
}
const channelWrapper = connection.createChannel({ const channelWrapper = connection.createChannel({
json: true json: true
}) })
@ -64,7 +50,6 @@ function connectSenderToQueue({ queueName, oldQueueName, cb, resendInterval }) {
channelWrapper.addSetup(async channel => { channelWrapper.addSetup(async channel => {
await channel.assertExchange(deadLetterExchange, 'fanout', { durable: true }) await channel.assertExchange(deadLetterExchange, 'fanout', { durable: true })
await channel.assertQueue(queueName, { durable: true, maxPriority: SENDER_QUEUE_MAX_PRIORITY }) await channel.assertQueue(queueName, { durable: true, maxPriority: SENDER_QUEUE_MAX_PRIORITY })
await channel.assertQueue(oldQueueName, { durable: true }).then(() => resendMessagesToNewQueue(channel))
await channel.bindQueue(queueName, deadLetterExchange) await channel.bindQueue(queueName, deadLetterExchange)
await channel.prefetch(1) await channel.prefetch(1)
await channel.consume(queueName, msg => await channel.consume(queueName, msg =>

@ -20,21 +20,21 @@ const {
COMMON_HOME_GAS_PRICE_FACTOR COMMON_HOME_GAS_PRICE_FACTOR
} = process.env } = process.env
let cachedGasPrice = null let cachedGasPriceOptions = null
let fetchGasPriceInterval = null let fetchGasPriceInterval = null
const fetchGasPrice = async (speedType, factor, bridgeContract, gasPriceSupplierUrl) => { const fetchGasPrice = async (speedType, factor, web3, bridgeContract, gasPriceSupplierUrl) => {
const contractOptions = { logger } const contractOptions = { logger }
const supplierOptions = { speedType, factor, limits: GAS_PRICE_BOUNDARIES, logger } const supplierOptions = { speedType, factor, limits: GAS_PRICE_BOUNDARIES, logger }
cachedGasPrice = cachedGasPriceOptions =
(await gasPriceFromSupplier(gasPriceSupplierUrl, supplierOptions)) || (await gasPriceFromSupplier(web3, gasPriceSupplierUrl, supplierOptions)) ||
(await gasPriceFromContract(bridgeContract, contractOptions)) || (await gasPriceFromContract(bridgeContract, contractOptions)) ||
cachedGasPrice cachedGasPriceOptions
return cachedGasPrice return cachedGasPriceOptions
} }
async function start(chainId, fetchOnce) { async function start(chainId, web3, fetchOnce) {
clearInterval(fetchGasPriceInterval) clearInterval(fetchGasPriceInterval)
let contract = null let contract = null
@ -49,7 +49,7 @@ async function start(chainId, fetchOnce) {
updateInterval = ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL || DEFAULT_UPDATE_INTERVAL updateInterval = ORACLE_HOME_GAS_PRICE_UPDATE_INTERVAL || DEFAULT_UPDATE_INTERVAL
factor = Number(COMMON_HOME_GAS_PRICE_FACTOR) || DEFAULT_GAS_PRICE_FACTOR factor = Number(COMMON_HOME_GAS_PRICE_FACTOR) || DEFAULT_GAS_PRICE_FACTOR
cachedGasPrice = COMMON_HOME_GAS_PRICE_FALLBACK cachedGasPriceOptions = { gasPrice: COMMON_HOME_GAS_PRICE_FALLBACK }
} else if (chainId === 'foreign') { } else if (chainId === 'foreign') {
contract = foreign.bridgeContract contract = foreign.bridgeContract
gasPriceSupplierUrl = COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL gasPriceSupplierUrl = COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL
@ -57,7 +57,7 @@ async function start(chainId, fetchOnce) {
updateInterval = ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL || DEFAULT_UPDATE_INTERVAL updateInterval = ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL || DEFAULT_UPDATE_INTERVAL
factor = Number(COMMON_FOREIGN_GAS_PRICE_FACTOR) || DEFAULT_GAS_PRICE_FACTOR factor = Number(COMMON_FOREIGN_GAS_PRICE_FACTOR) || DEFAULT_GAS_PRICE_FACTOR
cachedGasPrice = COMMON_FOREIGN_GAS_PRICE_FALLBACK cachedGasPriceOptions = { gasPrice: COMMON_FOREIGN_GAS_PRICE_FALLBACK }
} else { } else {
throw new Error(`Unrecognized chainId '${chainId}'`) throw new Error(`Unrecognized chainId '${chainId}'`)
} }
@ -67,21 +67,21 @@ async function start(chainId, fetchOnce) {
} }
if (fetchOnce) { if (fetchOnce) {
await fetchGasPrice(speedType, factor, contract, gasPriceSupplierUrl) await fetchGasPrice(speedType, factor, web3, contract, gasPriceSupplierUrl)
} else { } else {
fetchGasPriceInterval = await setIntervalAndRun( fetchGasPriceInterval = await setIntervalAndRun(
() => fetchGasPrice(speedType, factor, contract, gasPriceSupplierUrl), () => fetchGasPrice(speedType, factor, web3, contract, gasPriceSupplierUrl),
updateInterval updateInterval
) )
} }
} }
function getPrice() { function gasPriceOptions() {
return cachedGasPrice return cachedGasPriceOptions
} }
module.exports = { module.exports = {
start, start,
getPrice, gasPriceOptions,
fetchGasPrice fetchGasPrice
} }

@ -1,4 +1,6 @@
const Web3 = require('web3') const Web3 = require('web3')
const ethers = require('ethers')
const flashbots = require('@flashbots/ethers-provider-bundle')
const { HttpListProvider } = require('./HttpListProvider') const { HttpListProvider } = require('./HttpListProvider')
const { SafeEthLogsProvider } = require('./SafeEthLogsProvider') const { SafeEthLogsProvider } = require('./SafeEthLogsProvider')
const { RedundantHttpListProvider } = require('./RedundantHttpListProvider') const { RedundantHttpListProvider } = require('./RedundantHttpListProvider')
@ -9,6 +11,8 @@ const {
COMMON_FOREIGN_RPC_URL, COMMON_FOREIGN_RPC_URL,
ORACLE_SIDE_RPC_URL, ORACLE_SIDE_RPC_URL,
ORACLE_FOREIGN_ARCHIVE_RPC_URL, ORACLE_FOREIGN_ARCHIVE_RPC_URL,
ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL,
ORACLE_MEV_FOREIGN_FLASHBOTS_AUTH_SIGNING_KEY,
ORACLE_RPC_REQUEST_TIMEOUT, ORACLE_RPC_REQUEST_TIMEOUT,
ORACLE_HOME_RPC_POLLING_INTERVAL, ORACLE_HOME_RPC_POLLING_INTERVAL,
ORACLE_FOREIGN_RPC_POLLING_INTERVAL ORACLE_FOREIGN_RPC_POLLING_INTERVAL
@ -94,6 +98,15 @@ if (foreignUrls.length > 1) {
web3ForeignRedundant = new Web3(redundantProvider) web3ForeignRedundant = new Web3(redundantProvider)
} }
let getFlashbotsProvider
if (ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL) {
const provider = new ethers.providers.JsonRpcProvider(foreignUrls[0])
const authSigner = new ethers.Wallet(ORACLE_MEV_FOREIGN_FLASHBOTS_AUTH_SIGNING_KEY, provider)
getFlashbotsProvider = chainId =>
flashbots.FlashbotsBundleProvider.create(provider, authSigner, ORACLE_MEV_FOREIGN_FLASHBOTS_RPC_URL, chainId)
}
module.exports = { module.exports = {
web3Home, web3Home,
web3Foreign, web3Foreign,
@ -102,5 +115,6 @@ module.exports = {
web3HomeRedundant, web3HomeRedundant,
web3ForeignRedundant, web3ForeignRedundant,
web3HomeFallback, web3HomeFallback,
web3ForeignFallback web3ForeignFallback,
getFlashbotsProvider
} }

@ -1,25 +1,38 @@
const { toWei } = require('web3').utils async function sendTx(opts) {
const { privateKey, data, nonce, gasPrice, gasPriceOptions, value, gasLimit, to, chainId, web3, mevOptions } = opts
async function sendTx({ privateKey, data, nonce, gasPrice, amount, gasLimit, to, chainId, web3 }) { const gasOpts = gasPriceOptions || { gasPrice }
const serializedTx = await web3.eth.accounts.signTransaction( const serializedTx = await web3.eth.accounts.signTransaction(
{ {
nonce: Number(nonce), nonce: Number(nonce),
chainId, chainId,
to, to,
data, data,
value: toWei(amount), value,
gasPrice, gas: gasLimit,
gas: gasLimit ...gasOpts
}, },
privateKey privateKey
) )
return new Promise((res, rej) => if (!mevOptions) {
web3.eth return new Promise((res, rej) =>
.sendSignedTransaction(serializedTx.rawTransaction) web3.eth
.once('transactionHash', res) .sendSignedTransaction(serializedTx.rawTransaction)
.once('error', rej) .once('transactionHash', res)
.once('error', rej)
)
}
mevOptions.logger.debug(
{ rawTx: serializedTx.rawTransaction, txHash: serializedTx.transactionHash },
'Signed MEV helper transaction'
) )
for (let blockNumber = mevOptions.fromBlock; blockNumber <= mevOptions.toBlock; blockNumber++) {
mevOptions.logger.debug({ txHash: serializedTx.transactionHash, blockNumber }, 'Sending MEV bundle transaction')
await mevOptions.provider.sendRawBundle([serializedTx.rawTransaction], blockNumber)
}
return Promise.resolve(serializedTx.transactionHash)
} }
module.exports = { module.exports = {

@ -87,7 +87,7 @@ async function getEvents({ contract, event, fromBlock, toBlock, filter }) {
) )
const pastEvents = await contract.getPastEvents(event, { fromBlock, toBlock, filter }) const pastEvents = await contract.getPastEvents(event, { fromBlock, toBlock, filter })
logger.debug({ contractAddress, event, count: pastEvents.length }, 'Past events obtained') logger.debug({ contractAddress, event, count: pastEvents.length }, 'Past events obtained')
return pastEvents return pastEvents.sort((a, b) => a.blockNumber - b.blockNumber || a.transactionIndex - b.transactionIndex)
} catch (e) { } catch (e) {
logger.error(e.message) logger.error(e.message)
throw new Error(`${event} events cannot be obtained`) throw new Error(`${event} events cannot be obtained`)

@ -5,6 +5,7 @@ module.exports = {
MIN_AMB_HEADER_LENGTH: 32 + 20 + 20 + 4 + 2 + 1 + 2, MIN_AMB_HEADER_LENGTH: 32 + 20 + 20 + 4 + 2 + 1 + 2,
MAX_GAS_LIMIT: 10000000, MAX_GAS_LIMIT: 10000000,
MAX_CONCURRENT_EVENTS: 50, MAX_CONCURRENT_EVENTS: 50,
MAX_HISTORY_BLOCK_TO_REPROCESS: 10000,
RETRY_CONFIG: { RETRY_CONFIG: {
retries: 20, retries: 20,
factor: 1.4, factor: 1.4,
@ -23,6 +24,7 @@ module.exports = {
MIN: 1, MIN: 1,
MAX: 1000 MAX: 1000
}, },
MIN_GAS_PRICE_BUMP_FACTOR: 0.1,
DEFAULT_TRANSACTION_RESEND_INTERVAL: 20 * 60 * 1000, DEFAULT_TRANSACTION_RESEND_INTERVAL: 20 * 60 * 1000,
FALLBACK_RPC_URL_SWITCH_TIMEOUT: 60 * 60 * 1000, FALLBACK_RPC_URL_SWITCH_TIMEOUT: 60 * 60 * 1000,
BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT: 10, BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT: 10,

43
oracle/src/utils/mev.js Normal file

@ -0,0 +1,43 @@
const MEV_HELPER_ABI = [
{
constant: false,
inputs: [
{
name: '_data',
type: 'bytes'
}
],
name: 'execute',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_gasPrice',
type: 'uint256'
},
{
name: '_data',
type: 'bytes'
}
],
name: 'estimateProfit',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: true,
stateMutability: 'nonpayable',
type: 'function'
}
]
module.exports = {
MEV_HELPER_ABI
}

@ -2,6 +2,9 @@ const fs = require('fs')
const BigNumber = require('bignumber.js') const BigNumber = require('bignumber.js')
const promiseRetry = require('promise-retry') const promiseRetry = require('promise-retry')
const Web3 = require('web3') const Web3 = require('web3')
const { GAS_PRICE_BOUNDARIES } = require('./constants')
const { toBN, toWei } = Web3.utils
const retrySequence = [1, 2, 3, 5, 8, 13, 21, 34, 55, 60] const retrySequence = [1, 2, 3, 5, 8, 13, 21, 34, 55, 60]
@ -34,8 +37,8 @@ const promiseRetryForever = f => promiseRetry(f, { forever: true, factor: 1 })
async function waitForFunds(web3, address, minimumBalance, cb, logger) { async function waitForFunds(web3, address, minimumBalance, cb, logger) {
promiseRetryForever(async retry => { promiseRetryForever(async retry => {
logger.debug('Getting balance of validator account') logger.debug('Getting balance of validator account')
const newBalance = web3.utils.toBN(await web3.eth.getBalance(address)) const newBalance = toBN(await web3.eth.getBalance(address))
if (newBalance.gte(web3.utils.toBN(minimumBalance.toString(10)))) { if (newBalance.gte(toBN(minimumBalance.toString(10)))) {
logger.debug({ balance: newBalance, minimumBalance }, 'Validator has minimum necessary balance') logger.debug({ balance: newBalance, minimumBalance }, 'Validator has minimum necessary balance')
cb(newBalance) cb(newBalance)
} else { } else {
@ -64,6 +67,48 @@ function addExtraGas(gas, extraPercentage, maxGasLimit = Infinity) {
return BigNumber.min(maxGasLimit, gasWithExtra) return BigNumber.min(maxGasLimit, gasWithExtra)
} }
function applyMinGasFeeBump(job, bumpFactor = 0.1) {
if (!job.gasPriceOptions) {
return job
}
const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = job.gasPriceOptions
const maxGasPrice = toWei(GAS_PRICE_BOUNDARIES.MAX.toString(), 'gwei')
if (gasPrice) {
return {
...job,
gasPriceOptions: {
gasPrice: addExtraGas(gasPrice, bumpFactor, maxGasPrice).toString()
}
}
}
if (maxFeePerGas && maxPriorityFeePerGas) {
return {
...job,
gasPriceOptions: {
maxFeePerGas: addExtraGas(maxFeePerGas, bumpFactor, maxGasPrice).toString(),
maxPriorityFeePerGas: addExtraGas(maxPriorityFeePerGas, bumpFactor, maxGasPrice).toString()
}
}
}
return job
}
function chooseGasPriceOptions(a, b) {
if (!a) {
return b
}
if (a && b && a.gasPrice && b.gasPrice) {
return { gasPrice: BigNumber.max(a.gasPrice, b.gasPrice).toString() }
}
if (a && b && a.maxFeePerGas && b.maxFeePerGas && a.maxPriorityFeePerGas && b.maxPriorityFeePerGas) {
return {
maxFeePerGas: BigNumber.max(a.maxFeePerGas, b.maxFeePerGas).toString(),
maxPriorityFeePerGas: BigNumber.max(a.maxPriorityFeePerGas, b.maxPriorityFeePerGas).toString()
}
}
return a
}
async function setIntervalAndRun(f, interval) { async function setIntervalAndRun(f, interval) {
const handler = setInterval(f, interval) const handler = setInterval(f, interval)
await f() await f()
@ -183,6 +228,8 @@ module.exports = {
waitForFunds, waitForFunds,
waitForUnsuspend, waitForUnsuspend,
addExtraGas, addExtraGas,
chooseGasPriceOptions,
applyMinGasFeeBump,
setIntervalAndRun, setIntervalAndRun,
watchdog, watchdog,
add0xPrefix, add0xPrefix,

@ -6,7 +6,11 @@ const logger = require('./services/logger')
const { getShutdownFlag } = require('./services/shutdownState') const { getShutdownFlag } = require('./services/shutdownState')
const { getBlockNumber, getRequiredBlockConfirmations, getEvents } = require('./tx/web3') const { getBlockNumber, getRequiredBlockConfirmations, getEvents } = require('./tx/web3')
const { checkHTTPS, watchdog } = require('./utils/utils') const { checkHTTPS, watchdog } = require('./utils/utils')
const { EXIT_CODES, BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT } = require('./utils/constants') const {
EXIT_CODES,
BLOCK_NUMBER_PROGRESS_ITERATIONS_LIMIT,
MAX_HISTORY_BLOCK_TO_REPROCESS
} = require('./utils/constants')
if (process.argv.length < 3) { if (process.argv.length < 3) {
logger.error('Please check the number of arguments, config file was not provided') logger.error('Please check the number of arguments, config file was not provided')
@ -26,9 +30,21 @@ const processAMBInformationRequests = require('./events/processAMBInformationReq
const { getTokensState } = require('./utils/tokenState') const { getTokensState } = require('./utils/tokenState')
const { web3, bridgeContract, eventContract, startBlock, pollingInterval, chain } = config.main const {
web3,
bridgeContract,
eventContract,
startBlock,
pollingInterval,
chain,
reprocessingOptions,
blockPollingLimit
} = config.main
const lastBlockRedisKey = `${config.id}:lastProcessedBlock` const lastBlockRedisKey = `${config.id}:lastProcessedBlock`
const lastReprocessedBlockRedisKey = `${config.id}:lastReprocessedBlock`
const seenEventsRedisKey = `${config.id}:seenEvents`
let lastProcessedBlock = Math.max(startBlock - 1, 0) let lastProcessedBlock = Math.max(startBlock - 1, 0)
let lastReprocessedBlock
let lastSeenBlockNumber = 0 let lastSeenBlockNumber = 0
let sameBlockNumberCounter = 0 let sameBlockNumberCounter = 0
@ -39,6 +55,8 @@ async function initialize() {
web3.currentProvider.urls.forEach(checkHttps(chain)) web3.currentProvider.urls.forEach(checkHttps(chain))
await getLastProcessedBlock() await getLastProcessedBlock()
await getLastReprocessedBlock()
await checkConditions()
connectWatcherToQueue({ connectWatcherToQueue({
queueName: config.queue, queueName: config.queue,
cb: runMain cb: runMain
@ -76,11 +94,34 @@ async function getLastProcessedBlock() {
lastProcessedBlock = result ? parseInt(result, 10) : lastProcessedBlock lastProcessedBlock = result ? parseInt(result, 10) : lastProcessedBlock
} }
async function getLastReprocessedBlock() {
if (reprocessingOptions.enabled) {
const result = await redis.get(lastReprocessedBlockRedisKey)
if (result) {
lastReprocessedBlock = Math.max(parseInt(result, 10), lastProcessedBlock - MAX_HISTORY_BLOCK_TO_REPROCESS)
} else {
lastReprocessedBlock = lastProcessedBlock
}
logger.debug({ block: lastReprocessedBlock }, 'Last reprocessed block obtained')
} else {
// when reprocessing is being enabled not for the first time,
// we do not want to process blocks for which we didn't recorded seen events,
// instead, we want to start from the current block.
// Thus we should delete this reprocessing pointer once it is disabled.
await redis.del(lastReprocessedBlockRedisKey)
}
}
function updateLastProcessedBlock(lastBlockNumber) { function updateLastProcessedBlock(lastBlockNumber) {
lastProcessedBlock = lastBlockNumber lastProcessedBlock = lastBlockNumber
return redis.set(lastBlockRedisKey, lastProcessedBlock) return redis.set(lastBlockRedisKey, lastProcessedBlock)
} }
function updateLastReprocessedBlock(lastBlockNumber) {
lastReprocessedBlock = lastBlockNumber
return redis.set(lastReprocessedBlockRedisKey, lastReprocessedBlock)
}
function processEvents(events) { function processEvents(events) {
switch (config.id) { switch (config.id) {
case 'erc-native-signature-request': case 'erc-native-signature-request':
@ -114,6 +155,71 @@ async function checkConditions() {
} }
} }
const eventKey = e => `${e.transactionHash}-${e.logIndex}`
async function reprocessOldLogs(sendToQueue) {
const fromBlock = lastReprocessedBlock + 1
let toBlock = lastReprocessedBlock + reprocessingOptions.batchSize
const events = await getEvents({
contract: eventContract,
event: config.event,
fromBlock,
toBlock,
filter: config.eventFilter
})
const alreadySeenEvents = await getSeenEvents(fromBlock, toBlock)
const missingEvents = events.filter(e => !alreadySeenEvents[eventKey(e)])
if (missingEvents.length === 0) {
logger.debug('No missed events were found')
} else {
logger.info(`Found ${missingEvents.length} ${config.event} missed events`)
let job
if (config.id === 'amb-information-request') {
// obtain block number and events from the earliest block
const batchBlockNumber = missingEvents[0].blockNumber
const batchEvents = missingEvents.filter(event => event.blockNumber === batchBlockNumber)
// if there are some other events in the later blocks,
// adjust lastReprocessedBlock so that these events will be processed again on the next iteration
if (batchEvents.length < missingEvents.length) {
// pick event outside from the batch
toBlock = missingEvents[batchEvents.length].blockNumber - 1
}
job = await processAMBInformationRequests(batchEvents)
if (job === null) {
return
}
} else {
job = await processEvents(missingEvents)
}
logger.info('Missed events transactions to send:', job.length)
if (job.length) {
await sendToQueue(job)
}
}
await updateLastReprocessedBlock(toBlock)
await deleteSeenEvents(0, toBlock)
}
async function getSeenEvents(fromBlock, toBlock) {
const keys = await redis.zrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
const res = {}
keys.forEach(k => {
res[k] = true
})
return res
}
function deleteSeenEvents(fromBlock, toBlock) {
return redis.zremrangebyscore(seenEventsRedisKey, fromBlock, toBlock)
}
function addSeenEvents(events) {
return redis.zadd(seenEventsRedisKey, ...events.flatMap(e => [e.blockNumber, eventKey(e)]))
}
async function getLastBlockToProcess(web3, bridgeContract) { async function getLastBlockToProcess(web3, bridgeContract) {
const [lastBlockNumber, requiredBlockConfirmations] = await Promise.all([ const [lastBlockNumber, requiredBlockConfirmations] = await Promise.all([
getBlockNumber(web3), getBlockNumber(web3),
@ -158,24 +264,29 @@ async function main({ sendToQueue }) {
const lastBlockToProcess = await getLastBlockToProcess(web3, bridgeContract) const lastBlockToProcess = await getLastBlockToProcess(web3, bridgeContract)
if (reprocessingOptions.enabled) {
if (lastReprocessedBlock + reprocessingOptions.batchSize + reprocessingOptions.blockDelay < lastBlockToProcess) {
await reprocessOldLogs(sendToQueue)
return
}
}
if (lastBlockToProcess <= lastProcessedBlock) { if (lastBlockToProcess <= lastProcessedBlock) {
logger.debug('All blocks already processed') logger.debug('All blocks already processed')
return return
} }
await checkConditions()
const fromBlock = lastProcessedBlock + 1 const fromBlock = lastProcessedBlock + 1
const rangeEndBlock = config.blockPollingLimit ? fromBlock + config.blockPollingLimit : lastBlockToProcess const rangeEndBlock = blockPollingLimit ? fromBlock + blockPollingLimit : lastBlockToProcess
let toBlock = Math.min(lastBlockToProcess, rangeEndBlock) let toBlock = Math.min(lastBlockToProcess, rangeEndBlock)
const events = (await getEvents({ let events = await getEvents({
contract: eventContract, contract: eventContract,
event: config.event, event: config.event,
fromBlock, fromBlock,
toBlock, toBlock,
filter: config.eventFilter filter: config.eventFilter
})).sort((a, b) => a.blockNumber - b.blockNumber) })
logger.info(`Found ${events.length} ${config.event} events`) logger.info(`Found ${events.length} ${config.event} events`)
if (events.length) { if (events.length) {
@ -192,9 +303,10 @@ async function main({ sendToQueue }) {
if (batchEvents.length < events.length) { if (batchEvents.length < events.length) {
// pick event outside from the batch // pick event outside from the batch
toBlock = events[batchEvents.length].blockNumber - 1 toBlock = events[batchEvents.length].blockNumber - 1
events = batchEvents
} }
job = await processAMBInformationRequests(batchEvents) job = await processAMBInformationRequests(events)
if (job === null) { if (job === null) {
return return
} }
@ -206,6 +318,9 @@ async function main({ sendToQueue }) {
if (job.length) { if (job.length) {
await sendToQueue(job) await sendToQueue(job)
} }
if (reprocessingOptions.enabled) {
await addSeenEvents(events)
}
} }
logger.debug({ lastProcessedBlock: toBlock.toString() }, 'Updating last processed block') logger.debug({ lastProcessedBlock: toBlock.toString() }, 'Updating last processed block')

@ -71,10 +71,10 @@ describe('gasPrice', () => {
await gasPrice.start('home') await gasPrice.start('home')
// when // when
await gasPrice.fetchGasPrice('standard', 1, null, null) await gasPrice.fetchGasPrice('standard', 1, null, null, null)
// then // then
expect(gasPrice.getPrice()).to.equal('101000000000') expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '101000000000' })
}) })
it('should fetch gas from supplier', async () => { it('should fetch gas from supplier', async () => {
@ -82,10 +82,10 @@ describe('gasPrice', () => {
await gasPrice.start('home') await gasPrice.start('home')
// when // when
await gasPrice.fetchGasPrice('standard', 1, null, 'url') await gasPrice.fetchGasPrice('standard', 1, null, null, 'url')
// then // then
expect(gasPrice.getPrice().toString()).to.equal('103000000000') expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '103000000000' })
}) })
it('should fetch gas from contract', async () => { it('should fetch gas from contract', async () => {
@ -101,10 +101,10 @@ describe('gasPrice', () => {
} }
// when // when
await gasPrice.fetchGasPrice('standard', 1, bridgeContractMock, null) await gasPrice.fetchGasPrice('standard', 1, null, bridgeContractMock, null)
// then // then
expect(gasPrice.getPrice().toString()).to.equal('102000000000') expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '102000000000' })
}) })
it('should fetch the gas price from the oracle first', async () => { it('should fetch the gas price from the oracle first', async () => {
@ -120,10 +120,10 @@ describe('gasPrice', () => {
} }
// when // when
await gasPrice.fetchGasPrice('standard', 1, bridgeContractMock, 'url') await gasPrice.fetchGasPrice('standard', 1, null, bridgeContractMock, 'url')
// then // then
expect(gasPrice.getPrice().toString()).to.equal('103000000000') expect(gasPrice.gasPriceOptions()).to.eql({ gasPrice: '103000000000' })
}) })
it('log error using the logger', async () => { it('log error using the logger', async () => {
@ -131,7 +131,7 @@ describe('gasPrice', () => {
await gasPrice.start('home') await gasPrice.start('home')
// when // when
await gasPrice.fetchGasPrice('standard', 1, null, null) await gasPrice.fetchGasPrice('standard', 1, null, null, null)
// then // then
expect(fakeLogger.warn.calledOnce).to.equal(true) // one warning expect(fakeLogger.warn.calledOnce).to.equal(true) // one warning

@ -3,7 +3,13 @@ const chai = require('chai')
const chaiAsPromised = require('chai-as-promised') const chaiAsPromised = require('chai-as-promised')
const BigNumber = require('bignumber.js') const BigNumber = require('bignumber.js')
const proxyquire = require('proxyquire') const proxyquire = require('proxyquire')
const { addExtraGas, syncForEach, promiseAny } = require('../src/utils/utils') const {
addExtraGas,
applyMinGasFeeBump,
chooseGasPriceOptions,
syncForEach,
promiseAny
} = require('../src/utils/utils')
chai.use(chaiAsPromised) chai.use(chaiAsPromised)
chai.should() chai.should()
@ -173,4 +179,43 @@ describe('utils', () => {
await promiseAny(array.map(f)).should.be.rejected await promiseAny(array.map(f)).should.be.rejected
}) })
}) })
describe('applyMinGasFeeBump', () => {
it('should bump pre-eip1559 fee', () => {
const job = { gasPriceOptions: { gasPrice: '100000000000' } }
const newJob = applyMinGasFeeBump(job)
expect(newJob.gasPriceOptions.gasPrice).to.be.equal('110000000000')
})
it('should bump eip1559 fee', () => {
const job = { gasPriceOptions: { maxFeePerGas: '100000000000', maxPriorityFeePerGas: '20000000000' } }
const newJob = applyMinGasFeeBump(job)
expect(newJob.gasPriceOptions.maxFeePerGas).to.be.equal('110000000000')
expect(newJob.gasPriceOptions.maxPriorityFeePerGas).to.be.equal('22000000000')
})
})
describe('chooseGasPriceOptions', () => {
it('should choose max pre-eip1559 fee', () => {
const opts1 = { gasPrice: '100000000000' }
const opts2 = { gasPrice: '101000000000' }
expect(chooseGasPriceOptions(opts1, opts2).gasPrice).to.be.equal('101000000000')
expect(chooseGasPriceOptions(opts2, opts1).gasPrice).to.be.equal('101000000000')
expect(chooseGasPriceOptions(opts2, undefined).gasPrice).to.be.equal('101000000000')
expect(chooseGasPriceOptions(undefined, opts2).gasPrice).to.be.equal('101000000000')
})
it('should choose max eip1559 fee', () => {
const opts1 = { maxFeePerGas: '100000000000', maxPriorityFeePerGas: '21000000000' }
const opts2 = { maxFeePerGas: '101000000000', maxPriorityFeePerGas: '20000000000' }
expect(chooseGasPriceOptions(opts1, opts2).maxFeePerGas).to.be.equal('101000000000')
expect(chooseGasPriceOptions(opts1, opts2).maxPriorityFeePerGas).to.be.equal('21000000000')
expect(chooseGasPriceOptions(opts2, opts1).maxFeePerGas).to.be.equal('101000000000')
expect(chooseGasPriceOptions(opts2, opts1).maxPriorityFeePerGas).to.be.equal('21000000000')
expect(chooseGasPriceOptions(opts2, undefined).maxFeePerGas).to.be.equal('101000000000')
expect(chooseGasPriceOptions(opts2, undefined).maxPriorityFeePerGas).to.be.equal('20000000000')
expect(chooseGasPriceOptions(undefined, opts2).maxFeePerGas).to.be.equal('101000000000')
expect(chooseGasPriceOptions(undefined, opts2).maxPriorityFeePerGas).to.be.equal('20000000000')
})
})
}) })

@ -45,5 +45,8 @@
"compile:contracts": "yarn workspace tokenbridge-contracts run compile", "compile:contracts": "yarn workspace tokenbridge-contracts run compile",
"install:deploy": "cd contracts/deploy && npm install --unsafe-perm --silent", "install:deploy": "cd contracts/deploy && npm install --unsafe-perm --silent",
"postinstall": "test -n \"$NOYARNPOSTINSTALL\" || ln -sf $(pwd)/node_modules/openzeppelin-solidity/ contracts/node_modules/openzeppelin-solidity" "postinstall": "test -n \"$NOYARNPOSTINSTALL\" || ln -sf $(pwd)/node_modules/openzeppelin-solidity/ contracts/node_modules/openzeppelin-solidity"
},
"resolutions": {
"**/@mycrypto/eth-scan": "3.5.3"
} }
} }

732
yarn.lock

File diff suppressed because it is too large Load Diff