Skip to content

Commit

Permalink
improve(ProviderUtils): Use StaticJsonRpcProvider
Browse files Browse the repository at this point in the history
Since ethers v5, all RPC requests have been preceed by an eth_chainId
lookup. This is described by ethers as a safety feature to mitigate
being "RPC rugged" - i.e. where a wallet silently changes RPC without
notifying the provider.

Inspecting the Across logs, eth_chainId is the second-most popular RPC
call, after only eth_getLogs. Over the past 30 days, Infura usage has
been:

  eth_getLogs                264M
  eth_chainId                 30M
  eth_call                    24M
  eth_getTransactionReceipt   11M
  eth_blockNumber              3M

  Total                      336M

Given that the Across bots maintain a 1:1 relationship between provider
instance and back-end RPC provider, there's no obvious way for the
chainId to ever change. The StaticJsonRpcProvider is therefore provided
by ethers for this scenario.

It should be noted that the 30 day figures might be lower than normal
due to downtime for both Arbitrum Nitro and the merge. In any case,
migrating to StaticJsonRpcProvider would reduce total requests by about
9% based on these figures, and would predominantly help reduce latency
in the bots.

See also: ethers-io/ethers.js#901

Ref: ACX-67
  • Loading branch information
pxrl committed Sep 21, 2022
1 parent d21edee commit ab3f840
Showing 1 changed file with 17 additions and 13 deletions.
30 changes: 17 additions & 13 deletions src/utils/ProviderUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,21 @@ interface RateLimitTask {
reject: (err: any) => void;
}

// This provider is a very small addition to the JsonRpcProvider that ensures that no more than `maxConcurrency`
// StaticJsonRpcProvider is used in place of JsonRpcProvider to avoid redundant eth_chainId queries prior to each
// request. This is safe to use when the back-end provider is guaranteed not to change.
// See https://docs.ethers.io/v5/api/providers/jsonrpc-provider/#StaticJsonRpcProvider

// This provider is a very small addition to the StaticJsonRpcProvider that ensures that no more than `maxConcurrency`
// requests are ever in flight. It uses the async/queue library to manage this.
class RateLimitedProvider extends ethers.providers.JsonRpcProvider {
class RateLimitedProvider extends ethers.providers.StaticJsonRpcProvider {
// The queue object that manages the tasks.
private queue: QueueObject;

// Takes the same arguments as the JsonRpcProvider, but it has an additional maxConcurrency value at the beginning
// of the list.
constructor(
maxConcurrency: number,
...jsonRpcConstructorParams: ConstructorParameters<typeof ethers.providers.JsonRpcProvider>
...jsonRpcConstructorParams: ConstructorParameters<typeof ethers.providers.StaticJsonRpcProvider>
) {
super(...jsonRpcConstructorParams);

Expand Down Expand Up @@ -59,7 +63,7 @@ function delay(s: number): Promise<void> {
return new Promise<void>((resolve) => setTimeout(resolve, Math.round(s * 1000)));
}

function formatProviderError(provider: ethers.providers.JsonRpcProvider, rawErrorText: string) {
function formatProviderError(provider: ethers.providers.StaticJsonRpcProvider, rawErrorText: string) {
return `Provider ${provider.connection.url} failed with error: ${rawErrorText}`;
}

Expand All @@ -68,10 +72,10 @@ function createSendErrorWithMessage(message: string, sendError: any) {
return { ...sendError, ...error };
}

class RetryProvider extends ethers.providers.JsonRpcProvider {
readonly providers: ethers.providers.JsonRpcProvider[];
class RetryProvider extends ethers.providers.StaticJsonRpcProvider {
readonly providers: ethers.providers.StaticJsonRpcProvider[];
constructor(
params: ConstructorParameters<typeof ethers.providers.JsonRpcProvider>[],
params: ConstructorParameters<typeof ethers.providers.StaticJsonRpcProvider>[],
chainId: number,
readonly nodeQuorumThreshold: number,
readonly retries: number,
Expand Down Expand Up @@ -99,17 +103,17 @@ class RetryProvider extends ethers.providers.JsonRpcProvider {
const quorumThreshold = this._getQuorum(method, params);
const requiredProviders = this.providers.slice(0, quorumThreshold);
const fallbackProviders = this.providers.slice(quorumThreshold);
const errors: [ethers.providers.JsonRpcProvider, string][] = [];
const errors: [ethers.providers.StaticJsonRpcProvider, string][] = [];

// This function is used to try to send with a provider and if it fails pop an element off the fallback list to try
// with that one. Once the fallback provider list is empty, the method throws. Because the fallback providers are
// removed, we ensure that no provider is used more than once because we care about quorum, making sure all
// considered responses come from unique providers.
const tryWithFallback = (
provider: ethers.providers.JsonRpcProvider
): Promise<[ethers.providers.JsonRpcProvider, any]> => {
provider: ethers.providers.StaticJsonRpcProvider
): Promise<[ethers.providers.StaticJsonRpcProvider, any]> => {
return this._trySend(provider, method, params)
.then((result): [ethers.providers.JsonRpcProvider, any] => [provider, result])
.then((result): [ethers.providers.StaticJsonRpcProvider, any] => [provider, result])
.catch((err) => {
// Append the provider and error to the error array.
errors.push([provider, err?.stack || err?.toString()]);
Expand Down Expand Up @@ -166,7 +170,7 @@ class RetryProvider extends ethers.providers.JsonRpcProvider {
const fallbackResults = await Promise.allSettled(
fallbackProviders.map((provider) =>
this._trySend(provider, method, params)
.then((result): [ethers.providers.JsonRpcProvider, any] => [provider, result])
.then((result): [ethers.providers.StaticJsonRpcProvider, any] => [provider, result])
.catch((err) => {
errors.push([provider, err?.stack || err?.toString()]);
throw new Error("No fallbacks during quorum search");
Expand Down Expand Up @@ -207,7 +211,7 @@ class RetryProvider extends ethers.providers.JsonRpcProvider {
return quorumResult;
}

_trySend(provider: ethers.providers.JsonRpcProvider, method: string, params: Array<any>): Promise<any> {
_trySend(provider: ethers.providers.StaticJsonRpcProvider, method: string, params: Array<any>): Promise<any> {
let promise = provider.send(method, params);
for (let i = 0; i < this.retries; i++) {
promise = promise.catch(() => delay(this.delay).then(() => provider.send(method, params)));
Expand Down

0 comments on commit ab3f840

Please sign in to comment.