Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/http list provider #112

Merged
merged 8 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
| MAX_NATIVE_AMOUNT_FAUCET | Maximal amount of faucet value (in ETH) | integer |
| TREE_UPDATE_PARAMS_PATH | Local path to tree update circuit parameters | string |
| TRANSFER_PARAMS_PATH | Local path to transfer circuit parameters | string |
| TX_VK_PATH | Local path to transaction curcuit verification key | string |
| TX_VK_PATH | Local path to transaction circuit verification key | string |
| STATE_DIR_PATH | Path to persistent state files related to tree and transactions storage. Default: `./POOL_STATE` | string |
| GAS_PRICE_FALLBACK | Default fallback gas price | integer |
| GAS_PRICE_ESTIMATION_TYPE | Gas price estimation type | `web3` / `gas-price-oracle` / `eip1559-gas-estimation` / `polygon-gasstation-v2` |
Expand All @@ -27,7 +27,8 @@
| EVENTS_PROCESSING_BATCH_SIZE | Batch size for one `eth_getLogs` request when reprocessing old logs. Defaults to `10000` | integer
| RELAYER_LOG_LEVEL | Log level | Winston log level |
| RELAYER_REDIS_URL | Url to redis instance | URL |
| RPC_URL | Url to RPC node | URL |
| RPC_URL | The HTTPS URL(s) used to communicate to the RPC nodes. 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 |
| RELAYER_TX_REDUNDANCY | If set to `true`, instructs relayer to send `eth_sendRawTransaction` requests through all available RPC urls defined in `RPC_URL` variables instead of using first available one. Defaults to `false` | boolean |
| SENT_TX_DELAY | Delay in milliseconds for sentTxWorker to verify submitted transactions | integer |
| PERMIT_DEADLINE_THRESHOLD_INITIAL | Minimum time threshold in seconds for permit signature deadline to be valid (before initial transaction submition) | integer |
| PERMIT_DEADLINE_THRESHOLD_INITIAL | Minimum time threshold in seconds for permit signature deadline to be valid (before initial transaction submission) | integer |
| PERMIT_DEADLINE_THRESHOLD_RESEND | Minimum time threshold in seconds for permit signature deadline to be valid (for re-send attempts) | integer |
6 changes: 4 additions & 2 deletions docker/Dockerfile.relayer
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ WORKDIR /app

RUN apt-get update && \
apt-get -y install curl && \
curl -sL https://deb.nodesource.com/setup_16.x | bash && \
curl -sL https://deb.nodesource.com/setup_18.x | bash && \
apt-get -y install nodejs libclang-dev clang && \
npm install -g yarn cargo-cp-artifact

Expand All @@ -25,9 +25,11 @@ FROM base as build

COPY zp-relayer ./zp-relayer
RUN yarn build:relayer
# Prune devDependencies
RUN yarn install --frozen-lockfile --production


FROM node:16
FROM node:18

WORKDIR /app

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
"version": "0.1.0",
"private": true,
"devDependencies": {
"@types/node": "^18.11.17",
"prettier": "^2.7.1",
"ts-loader": "^9.2.4",
"tsc-alias": "^1.7.0",
"tsconfig-paths": "^4.1.0",
"typescript": "^4.3.5"
"typescript": "4.9.4"
},
"workspaces": [
"zp-memo-parser",
Expand Down
5 changes: 2 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
{
"compilerOptions": {
"target": "es2016",
"target": "es2022",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"declaration": true,
"lib": ["es2019.array", "dom"]
"declaration": true
},
"ts-node": {
"require": ["tsconfig-paths/register"]
Expand Down
34 changes: 26 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,24 @@
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"

"@jridgewell/resolve-uri@^3.0.3":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==

"@jridgewell/sourcemap-codec@^1.4.10":
version "1.4.14"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==

"@jridgewell/trace-mapping@0.3.9":
version "0.3.9"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
dependencies:
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"

"@metamask/eth-sig-util@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz#3ad61f6ea9ad73ba5b19db780d40d9aae5157088"
Expand Down Expand Up @@ -510,10 +528,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.27.tgz#4141fcad57c332a120591de883e26fe4bb14aaea"
integrity sha512-qZdePUDSLAZRXXV234bLBEUM0nAQjoxbcSwp1rqSMUe1rZ47mwU6OjciR/JvF1Oo8mc0ys6GE0ks0HGgqAZoGg==

"@types/node@^16.4.11":
version "16.11.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42"
integrity sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==
"@types/node@^18.11.17":
version "18.11.17"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.17.tgz#5c009e1d9c38f4a2a9d45c0b0c493fe6cdb4bcb5"
integrity sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==

"@types/parse-json@^4.0.0":
version "4.0.0"
Expand Down Expand Up @@ -5674,10 +5692,10 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"

typescript@^4.3.5:
version "4.4.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324"
integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==
typescript@4.9.4:
version "4.9.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78"
integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==

ultron@~1.1.0:
version "1.1.1"
Expand Down
7 changes: 6 additions & 1 deletion zp-relayer/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,14 @@ const config = {
eventsProcessingBatchSize: parseInt(process.env.EVENTS_PROCESSING_BATCH_SIZE || '10000'),
logLevel: process.env.RELAYER_LOG_LEVEL || 'debug',
redisUrl: process.env.RELAYER_REDIS_URL as string,
rpcUrl: process.env.RPC_URL as string,
rpcUrls: (process.env.RPC_URL as string).split(' ').filter(url => url.length > 0),
relayerTxRedundancy: process.env.RELAYER_TX_REDUNDANCY === 'true',
sentTxDelay: parseInt(process.env.SENT_TX_DELAY || '30000'),
rpcRequestTimeout: parseInt(process.env.RPC_REQUEST_TIMEOUT || '1000'),
permitDeadlineThresholdInitial: parseInt(process.env.PERMIT_DEADLINE_THRESHOLD_INITIAL || '300'),
relayerJsonRpcErrorCodes: (process.env.RELAYER_JSONRPC_ERROR_CODES || '-32603,-32002,-32005')
.split(',')
.map(s => parseInt(s, 10)),
}

export default config
1 change: 0 additions & 1 deletion zp-relayer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
"@types/express": "^4.17.13",
"@types/ioredis": "^4.27.6",
"@types/mocha": "^9.0.0",
"@types/node": "^16.4.11",
"@types/node-fetch": "^2.5.12",
"@types/promise-retry": "^1.1.3",
"@types/uuid": "^8.3.4",
Expand Down
64 changes: 64 additions & 0 deletions zp-relayer/services/providers/BaseHttpProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import fetch from 'node-fetch'
import { HttpProvider } from 'web3-core'
import type { OperationOptions } from 'retry'
import config from '@/config'

const JSONRPC_ERROR_CODES = config.relayerJsonRpcErrorCodes

export interface ProviderOptions {
name: string
requestTimeout: number
retry: OperationOptions
}

const defaultOptions: ProviderOptions = {
name: 'main',
requestTimeout: 0,
retry: {
retries: 0,
},
}

export default abstract class BaseHttpProvider implements HttpProvider {
options: ProviderOptions
connected = false

constructor(public host: string, options: Partial<ProviderOptions> = {}) {
this.options = { ...defaultOptions, ...options }
}

abstract send(payload: any, callback: any): void | Promise<void>

async _send(url: string, payload: any, options: ProviderOptions) {
const rawResponse = await fetch(url, {
headers: {
'Content-type': 'application/json',
},
method: 'POST',
body: JSON.stringify(payload),
timeout: options.requestTimeout,
})

if (!rawResponse.ok) {
throw new Error(rawResponse.statusText)
}

const response = await rawResponse.json()

if (
response.error &&
(JSONRPC_ERROR_CODES.includes(response.error.code) || response.error.message?.includes('ancient block'))
) {
throw new Error(response?.error.message)
}
return response
}

disconnect(): boolean {
return true
}

supportsSubscriptions(): boolean {
return false
}
}
Original file line number Diff line number Diff line change
@@ -1,59 +1,45 @@
import fetch, { RequestInfo } from 'node-fetch'
// Reference implementation:
// https://github.com/omni/tokenbridge/blob/master/oracle/src/services/HttpListProvider.js
import promiseRetry from 'promise-retry'
import { FALLBACK_RPC_URL_SWITCH_TIMEOUT } from '../utils/constants'
import { FALLBACK_RPC_URL_SWITCH_TIMEOUT } from '@/utils/constants'
import { logger } from '../appLogger'
import BaseHttpProvider, { ProviderOptions } from './BaseHttpProvider'

// From EIP-1474 and Infura documentation
const JSONRPC_ERROR_CODES = [-32603, -32002, -32005]

interface ProviderOptions {
name: string
requestTimeout: number
retry: {
retries: number
}
}

const defaultOptions: ProviderOptions = {
name: 'main',
requestTimeout: 0,
retry: {
retries: 0,
},
}

class HttpListProviderError extends Error {
export class HttpListProviderError extends Error {
errors: Error[]
constructor(message: string, errors: Error[]) {
super(message)
this.errors = errors
}
}

export default class HttpListProvider {
export default class HttpListProvider extends BaseHttpProvider {
urls: string[]
options: ProviderOptions
currentIndex: number
lastTimeUsedPrimary: number

constructor(urls: string[], options = {}) {
constructor(urls: string[], options: Partial<ProviderOptions> = {}) {
if (!urls || !urls.length) {
throw new TypeError(`Invalid URLs: '${urls}'`)
}

this.urls = urls
this.options = { ...defaultOptions, ...options }
super(urls[0], options)
this.currentIndex = 0
this.lastTimeUsedPrimary = 0

this.urls = urls
}

private updateUrlIndex(index: number) {
this.currentIndex = index
this.host = this.urls[this.currentIndex]
k1rill-fedoseev marked this conversation as resolved.
Show resolved Hide resolved
}

async send(payload: any, callback: any) {
// if fallback URL is being used for too long, switch back to the primary URL
if (this.currentIndex > 0 && Date.now() - this.lastTimeUsedPrimary > FALLBACK_RPC_URL_SWITCH_TIMEOUT) {
console.log(
{ oldURL: this.urls[this.currentIndex], newURL: this.urls[0] },
'Switching back to the primary JSON-RPC URL'
)
this.currentIndex = 0
logger.info('Switching back to the primary JSON-RPC URL: %s -> %s', this.urls[this.currentIndex], this.urls[0])
this.updateUrlIndex(0)
}

// save the currentIndex to avoid race condition
Expand All @@ -67,19 +53,21 @@ export default class HttpListProvider {

// if some of URLs failed to respond, current URL index is updated to the first URL that responded
if (currentIndex !== index) {
console.log(
{ index, oldURL: this.urls[currentIndex], newURL: this.urls[index] },
'Switching to fallback JSON-RPC URL'
logger.info(
'Switching to fallback JSON-RPC URL: %s -> %s; Index: %d',
this.urls[this.currentIndex],
this.urls[index],
index
)
this.currentIndex = index
this.updateUrlIndex(index)
}
callback(null, result)
} catch (e) {
callback(e)
}
}

async trySend(payload: any, initialIndex: number) {
private async trySend(payload: any, initialIndex: number) {
const errors: any = []

for (let count = 0; count < this.urls.length; count++) {
Expand All @@ -101,29 +89,4 @@ export default class HttpListProvider {

throw new HttpListProviderError('Request failed for all urls', errors)
}

async _send(url: RequestInfo, payload: any, options: ProviderOptions) {
const rawResponse = await fetch(url, {
headers: {
'Content-type': 'application/json',
},
method: 'POST',
body: JSON.stringify(payload),
timeout: options.requestTimeout,
})

if (!rawResponse.ok) {
throw new Error(rawResponse.statusText)
}

const response = await rawResponse.json()

if (
response.error &&
(JSONRPC_ERROR_CODES.includes(response.error.code) || response.error.message.includes('ancient block'))
) {
throw new Error(response.error.message)
}
return response
}
}
36 changes: 36 additions & 0 deletions zp-relayer/services/providers/RedundantHttpListProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Reference implementation:
// https://github.com/omni/tokenbridge/blob/master/oracle/src/services/RedundantHttpListProvider.js
import promiseRetry from 'promise-retry'
import { HttpListProviderError } from './HttpListProvider'
import BaseHttpProvider, { ProviderOptions } from './BaseHttpProvider'

export default class RedundantHttpListProvider extends BaseHttpProvider {
urls: string[]

constructor(urls: string[], options: Partial<ProviderOptions> = {}) {
if (!urls || !urls.length) {
throw new TypeError(`Invalid URLs: '${urls}'`)
}

super(urls[0], options)
this.urls = urls
}

async send(payload: any, callback: any) {
try {
const result = await promiseRetry(retry => this.trySend(payload).catch(retry), this.options.retry)
callback(null, result)
} catch (e) {
callback(e)
}
}

async trySend(payload: any) {
try {
return await Promise.any(this.urls.map(url => this._send(url, payload, this.options)))
} catch (e) {
const errors = (e as AggregateError).errors
throw new HttpListProviderError('Request failed for all urls', errors)
}
}
}
Loading