From f03ebdf0148304fb587ba9775385f79954d0588d Mon Sep 17 00:00:00 2001 From: jsy1218 <91580504+jsy1218@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:45:53 -0700 Subject: [PATCH 1/9] feat: implement quicknode geth vs reth output comparison --- lib/rpc/SingleJsonRpcProvider.ts | 18 +++++++++++++- lib/rpc/UniJsonRpcProvider.ts | 40 ++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/lib/rpc/SingleJsonRpcProvider.ts b/lib/rpc/SingleJsonRpcProvider.ts index c208fe5d25..e2787eeeff 100644 --- a/lib/rpc/SingleJsonRpcProvider.ts +++ b/lib/rpc/SingleJsonRpcProvider.ts @@ -180,7 +180,7 @@ export class SingleJsonRpcProvider extends StaticJsonRpcProvider { this.logEvaluateLatency() this.evaluatingLatency = true try { - await (this as any)[`${methodName}_EvaluateLatency`](...args) + return await (this as any)[`${methodName}_EvaluateLatency`](...args) } catch (error: any) { this.log.error({ error }, `Encounter error for shadow evaluate latency call: ${JSON.stringify(error)}`) // Swallow the error. @@ -241,6 +241,22 @@ export class SingleJsonRpcProvider extends StaticJsonRpcProvider { metric.putMetric(`${this.metricPrefix}_send_${method}`, 1, MetricLoggerUnit.Count) } + logRpcResponseMatch(method: string, otherProvider: SingleJsonRpcProvider) { + metric.putMetric( + `${this.metricPrefix}_other_provider_${otherProvider.providerId}_method_${method}_rpc_match`, + 1, + MetricLoggerUnit.Count + ) + } + + logRpcResponseMismatch(method: string, otherProvider: SingleJsonRpcProvider) { + metric.putMetric( + `${this.metricPrefix}_other_provider_${otherProvider.providerId}_method_${method}_rpc_mismatch`, + 1, + MetricLoggerUnit.Count + ) + } + private async wrappedFunctionCall( callType: CallType, fnName: string, diff --git a/lib/rpc/UniJsonRpcProvider.ts b/lib/rpc/UniJsonRpcProvider.ts index 03b36b6804..73cea1dd63 100644 --- a/lib/rpc/UniJsonRpcProvider.ts +++ b/lib/rpc/UniJsonRpcProvider.ts @@ -214,7 +214,8 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { latency: number, selectedProvider: SingleJsonRpcProvider, methodName: string, - args: any[] + args: any[], + providerResponse: any ): Promise { const healthyProviders = this.providers.filter((provider) => provider.isHealthy()) let count = 0 @@ -230,7 +231,16 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { // Within each provider latency shadow evaluation, we should do block I/O, // because NodeJS runs in single thread, so it's important to make sure // we benchmark the latencies correctly based on the single-threaded sequential evaluation. - await provider.evaluateLatency(methodName, args) + const evaluatedProviderResponse = await provider.evaluateLatency(methodName, args) + this.compareRpcResponses( + providerResponse, + evaluatedProviderResponse, + selectedProvider, + provider, + methodName, + args + ) + count++ }) ) @@ -242,6 +252,27 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { this.log.debug(`Evaluated ${count} other healthy providers`) } + compareRpcResponses( + providerResponse: any, + evaluatedProviderResponse: any, + selectedProvider: SingleJsonRpcProvider, + otherProvider: SingleJsonRpcProvider, + methodName: string, + args: any[] + ) { + if (providerResponse !== evaluatedProviderResponse) { + this.log.error( + { methodName, args }, + `Provider response mismatch: ${JSON.stringify(providerResponse)} from ${ + selectedProvider.providerId + } vs ${JSON.stringify(evaluatedProviderResponse)} from ${otherProvider.providerId}` + ) + selectedProvider.logRpcResponseMismatch(methodName, otherProvider) + } else { + selectedProvider.logRpcResponseMatch(methodName, otherProvider) + } + } + logProviderHealthiness() { for (const provider of this.providers.filter((provider) => provider.isHealthy())) { this.log.debug(`Healthy provider: ${provider.url}`) @@ -289,9 +320,10 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { const selectedProvider = this.selectPreferredProvider(sessionId) selectedProvider.logProviderSelection() let latency = 0 + let result try { const start = Date.now() - const result = await (selectedProvider as any)[`${fnName}`](...args) + result = await (selectedProvider as any)[`${fnName}`](...args) latency = Date.now() - start return result } catch (error: any) { @@ -308,7 +340,7 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { sessionId ) { // fire and forget to evaluate latency of other healthy providers - this.checkOtherHealthyProvider(latency, selectedProvider, fnName, args) + this.checkOtherHealthyProvider(latency, selectedProvider, fnName, args, result) } if (Math.random() < this.healthCheckSampleProb && sessionId) { From 8a6f112bbe3b3fad791d651cbb796349b65e8014 Mon Sep 17 00:00:00 2001 From: jsy1218 <91580504+jsy1218@users.noreply.github.com> Date: Mon, 1 Jul 2024 18:21:42 -0700 Subject: [PATCH 2/9] feat: implement quicknode geth vs reth output comparison --- lib/rpc/UniJsonRpcProvider.ts | 59 ++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/lib/rpc/UniJsonRpcProvider.ts b/lib/rpc/UniJsonRpcProvider.ts index 73cea1dd63..eb55402004 100644 --- a/lib/rpc/UniJsonRpcProvider.ts +++ b/lib/rpc/UniJsonRpcProvider.ts @@ -231,7 +231,11 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { // Within each provider latency shadow evaluation, we should do block I/O, // because NodeJS runs in single thread, so it's important to make sure // we benchmark the latencies correctly based on the single-threaded sequential evaluation. - const evaluatedProviderResponse = await provider.evaluateLatency(methodName, args) + const evaluatedProviderResponse = await (provider as any)[`evaluateLatency`](methodName, ...args) + // below invocation does not make the call/send RPC return the correct data + // both call and send will return "0x" for some reason + // I have to change to above invocation to make call/send return geniun RPC response + // const evaluatedProviderResponse = await provider.evaluateLatency(methodName, args) this.compareRpcResponses( providerResponse, evaluatedProviderResponse, @@ -260,12 +264,59 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { methodName: string, args: any[] ) { - if (providerResponse !== evaluatedProviderResponse) { + // we need to serialized the response, because in case of send(), + // we might get an object back, e.g. eth_feeHistory response object + // { + // "oldestBlock":"0x1347665", + // "reward":[ + // [ + // "0x21f43815" + // ], + // [ + // "0x140eca05" + // ], + // [ + // "0x140eca05" + // ], + // [ + // "0x1374bed6" + // ] + // ], + // "baseFeePerGas":[ + // "0x7750ad57", + // "0x75d05b08", + // "0x7ab15525", + // "0x73f8e916", + // "0x747ba29d" + // ], + // "gasUsedRatio":[ + // 0.4496709, + // 0.6656448666666667, + // 0.28090306666666665, + // 0.5176126 + // ], + // "baseFeePerBlobGas":[ + // "0x1", + // "0x1", + // "0x1", + // "0x1", + // "0x1" + // ], + // "blobGasUsedRatio":[ + // 0.16666666666666666, + // 0.5, + // 0, + // 0.5 + // ] + // } + const serializedProviderResponse = JSON.stringify(providerResponse) + const serializedEvaluatedProviderResponse = JSON.stringify(evaluatedProviderResponse) + if (serializedProviderResponse !== serializedEvaluatedProviderResponse) { this.log.error( { methodName, args }, - `Provider response mismatch: ${JSON.stringify(providerResponse)} from ${ + `Provider response mismatch: ${serializedProviderResponse} from ${ selectedProvider.providerId - } vs ${JSON.stringify(evaluatedProviderResponse)} from ${otherProvider.providerId}` + } vs ${serializedEvaluatedProviderResponse} from ${otherProvider.providerId}` ) selectedProvider.logRpcResponseMismatch(methodName, otherProvider) } else { From 31aabe0094a7e28ecb7371ebccb9761c53b90863 Mon Sep 17 00:00:00 2001 From: jsy1218 <91580504+jsy1218@users.noreply.github.com> Date: Mon, 1 Jul 2024 18:24:09 -0700 Subject: [PATCH 3/9] fic prettier --- lib/rpc/UniJsonRpcProvider.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/rpc/UniJsonRpcProvider.ts b/lib/rpc/UniJsonRpcProvider.ts index eb55402004..b21df6723b 100644 --- a/lib/rpc/UniJsonRpcProvider.ts +++ b/lib/rpc/UniJsonRpcProvider.ts @@ -314,9 +314,7 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { if (serializedProviderResponse !== serializedEvaluatedProviderResponse) { this.log.error( { methodName, args }, - `Provider response mismatch: ${serializedProviderResponse} from ${ - selectedProvider.providerId - } vs ${serializedEvaluatedProviderResponse} from ${otherProvider.providerId}` + `Provider response mismatch: ${serializedProviderResponse} from ${selectedProvider.providerId} vs ${serializedEvaluatedProviderResponse} from ${otherProvider.providerId}` ) selectedProvider.logRpcResponseMismatch(methodName, otherProvider) } else { From 1a5dd5e89c83b7091add1512830219f991bf22e1 Mon Sep 17 00:00:00 2001 From: jsy1218 <91580504+jsy1218@users.noreply.github.com> Date: Wed, 3 Jul 2024 21:04:45 +0100 Subject: [PATCH 4/9] feedbacks --- lib/rpc/SingleJsonRpcProvider.ts | 5 +- lib/rpc/UniJsonRpcProvider.ts | 115 ++++++++++++++++--------------- test/utils/eth_feeHistory.ts | 8 +++ 3 files changed, 70 insertions(+), 58 deletions(-) create mode 100644 test/utils/eth_feeHistory.ts diff --git a/lib/rpc/SingleJsonRpcProvider.ts b/lib/rpc/SingleJsonRpcProvider.ts index e2787eeeff..ae086b5f1e 100644 --- a/lib/rpc/SingleJsonRpcProvider.ts +++ b/lib/rpc/SingleJsonRpcProvider.ts @@ -17,7 +17,10 @@ import { Network } from '@ethersproject/networks' import { getProviderId } from './utils' import { ProviderHealthiness } from './ProviderHealthState' -export const MAJOR_METHOD_NAMES: string[] = ['getBlockNumber', 'call', 'send'] +export const GET_BLOCK_NUMBER_METHOD_NAME = 'getBlockNumber' +export const CALL_METHOD_NAME = 'call' +export const SEND_METHOD_NAME = 'send' +export const MAJOR_METHOD_NAMES: string[] = [GET_BLOCK_NUMBER_METHOD_NAME, CALL_METHOD_NAME, SEND_METHOD_NAME] export enum CallType { NORMAL, diff --git a/lib/rpc/UniJsonRpcProvider.ts b/lib/rpc/UniJsonRpcProvider.ts index b21df6723b..8f6ccd9dad 100644 --- a/lib/rpc/UniJsonRpcProvider.ts +++ b/lib/rpc/UniJsonRpcProvider.ts @@ -1,4 +1,10 @@ -import { CallType, MAJOR_METHOD_NAMES, SingleJsonRpcProvider } from './SingleJsonRpcProvider' +import { + CALL_METHOD_NAME, + CallType, + GET_BLOCK_NUMBER_METHOD_NAME, + MAJOR_METHOD_NAMES, SEND_METHOD_NAME, + SingleJsonRpcProvider +} from './SingleJsonRpcProvider' import { StaticJsonRpcProvider, TransactionRequest } from '@ethersproject/providers' import { isEmpty } from 'lodash' import { ChainId } from '@uniswap/sdk-core' @@ -15,6 +21,7 @@ import { BigNumber, BigNumberish } from '@ethersproject/bignumber' import { Deferrable } from '@ethersproject/properties' import Logger from 'bunyan' import { UniJsonRpcProviderConfig } from './config' +import { EthFeeHistory } from '../../test/utils/eth_feeHistory' export class UniJsonRpcProvider extends StaticJsonRpcProvider { readonly chainId: ChainId = ChainId.MAINNET @@ -231,7 +238,7 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { // Within each provider latency shadow evaluation, we should do block I/O, // because NodeJS runs in single thread, so it's important to make sure // we benchmark the latencies correctly based on the single-threaded sequential evaluation. - const evaluatedProviderResponse = await (provider as any)[`evaluateLatency`](methodName, ...args) + const evaluatedProviderResponse = await (provider)[`evaluateLatency`](methodName, ...args) // below invocation does not make the call/send RPC return the correct data // both call and send will return "0x" for some reason // I have to change to above invocation to make call/send return geniun RPC response @@ -264,61 +271,55 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { methodName: string, args: any[] ) { - // we need to serialized the response, because in case of send(), - // we might get an object back, e.g. eth_feeHistory response object - // { - // "oldestBlock":"0x1347665", - // "reward":[ - // [ - // "0x21f43815" - // ], - // [ - // "0x140eca05" - // ], - // [ - // "0x140eca05" - // ], - // [ - // "0x1374bed6" - // ] - // ], - // "baseFeePerGas":[ - // "0x7750ad57", - // "0x75d05b08", - // "0x7ab15525", - // "0x73f8e916", - // "0x747ba29d" - // ], - // "gasUsedRatio":[ - // 0.4496709, - // 0.6656448666666667, - // 0.28090306666666665, - // 0.5176126 - // ], - // "baseFeePerBlobGas":[ - // "0x1", - // "0x1", - // "0x1", - // "0x1", - // "0x1" - // ], - // "blobGasUsedRatio":[ - // 0.16666666666666666, - // 0.5, - // 0, - // 0.5 - // ] - // } - const serializedProviderResponse = JSON.stringify(providerResponse) - const serializedEvaluatedProviderResponse = JSON.stringify(evaluatedProviderResponse) - if (serializedProviderResponse !== serializedEvaluatedProviderResponse) { - this.log.error( - { methodName, args }, - `Provider response mismatch: ${serializedProviderResponse} from ${selectedProvider.providerId} vs ${serializedEvaluatedProviderResponse} from ${otherProvider.providerId}` - ) - selectedProvider.logRpcResponseMismatch(methodName, otherProvider) - } else { - selectedProvider.logRpcResponseMatch(methodName, otherProvider) + if (methodName === GET_BLOCK_NUMBER_METHOD_NAME) { + // if it's get block number, there's no guarantee that two providers will return the same block number + // since the node might be syncing, so we don't need to compare the response + return; + } else if (methodName === CALL_METHOD_NAME) { + // if it's eth_call, then we know the response data type is string, so we can compare directly + if (providerResponse !== evaluatedProviderResponse) { + this.log.error( + { methodName, args }, + `Provider response mismatch: ${providerResponse} from ${selectedProvider.providerId} vs ${evaluatedProviderResponse} from ${otherProvider.providerId}` + ) + selectedProvider.logRpcResponseMismatch(methodName, otherProvider) + } else { + selectedProvider.logRpcResponseMatch(methodName, otherProvider) + } + } else if (methodName === SEND_METHOD_NAME) { + // send is complicated, because it could be eth_call, eth_blockNumber, eth_feeHistory, eth_estimateGas + // so we need to compare the response based on the method name + const underlyingMethodName = args[0] + const stitchedMethodName = `${SEND_METHOD_NAME}_${underlyingMethodName}` + if (underlyingMethodName === 'eth_call' || underlyingMethodName === 'eth_estimateGas') { + if (providerResponse !== evaluatedProviderResponse) { + this.log.error( + { stitchedMethodName, args }, + `Provider response mismatch: ${providerResponse} from ${selectedProvider.providerId} vs ${evaluatedProviderResponse} from ${otherProvider.providerId}` + ) + selectedProvider.logRpcResponseMismatch(stitchedMethodName, otherProvider) + } else { + selectedProvider.logRpcResponseMatch(stitchedMethodName, otherProvider) + } + } else if (underlyingMethodName === 'eth_feeHistory') { + const castedProviderResponse = providerResponse as EthFeeHistory + const castedEvaluatedProviderResponse = evaluatedProviderResponse as EthFeeHistory + const mismatch = castedProviderResponse.oldestBlock !== castedEvaluatedProviderResponse.oldestBlock || + JSON.stringify(castedProviderResponse.reward) !== JSON.stringify(castedEvaluatedProviderResponse.reward) || + JSON.stringify(castedProviderResponse.baseFeePerGas) !== JSON.stringify(castedEvaluatedProviderResponse.baseFeePerGas) || + JSON.stringify(castedProviderResponse.gasUsedRatio) !== JSON.stringify(castedEvaluatedProviderResponse.gasUsedRatio) || + JSON.stringify(castedProviderResponse.baseFeePerBlobGas) !== JSON.stringify(castedEvaluatedProviderResponse.baseFeePerBlobGas) || + JSON.stringify(castedProviderResponse.blobGasUsedRatio) !== JSON.stringify(castedEvaluatedProviderResponse.blobGasUsedRatio) + if (mismatch) { + this.log.error( + { stitchedMethodName, args }, + `Provider response mismatch: ${providerResponse} from ${selectedProvider.providerId} vs ${evaluatedProviderResponse} from ${otherProvider.providerId}` + ) + selectedProvider.logRpcResponseMismatch(stitchedMethodName, otherProvider) + } else { + selectedProvider.logRpcResponseMatch(stitchedMethodName, otherProvider) + } + } } } diff --git a/test/utils/eth_feeHistory.ts b/test/utils/eth_feeHistory.ts new file mode 100644 index 0000000000..fd293cae8e --- /dev/null +++ b/test/utils/eth_feeHistory.ts @@ -0,0 +1,8 @@ +export type EthFeeHistory = { + oldestBlock: number + reward: string[] + baseFeePerGas: string[] + gasUsedRatio: number[] + baseFeePerBlobGas: string[] + blobGasUsedRatio: number[] +} \ No newline at end of file From 81780837e3cbca12639ca45c1ab509952c807528 Mon Sep 17 00:00:00 2001 From: jsy1218 <91580504+jsy1218@users.noreply.github.com> Date: Wed, 3 Jul 2024 21:05:09 +0100 Subject: [PATCH 5/9] fix prettier --- lib/rpc/UniJsonRpcProvider.ts | 24 +++++++++++++++--------- test/utils/eth_feeHistory.ts | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/rpc/UniJsonRpcProvider.ts b/lib/rpc/UniJsonRpcProvider.ts index 8f6ccd9dad..fc6eb0e834 100644 --- a/lib/rpc/UniJsonRpcProvider.ts +++ b/lib/rpc/UniJsonRpcProvider.ts @@ -2,8 +2,9 @@ import { CALL_METHOD_NAME, CallType, GET_BLOCK_NUMBER_METHOD_NAME, - MAJOR_METHOD_NAMES, SEND_METHOD_NAME, - SingleJsonRpcProvider + MAJOR_METHOD_NAMES, + SEND_METHOD_NAME, + SingleJsonRpcProvider, } from './SingleJsonRpcProvider' import { StaticJsonRpcProvider, TransactionRequest } from '@ethersproject/providers' import { isEmpty } from 'lodash' @@ -238,7 +239,7 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { // Within each provider latency shadow evaluation, we should do block I/O, // because NodeJS runs in single thread, so it's important to make sure // we benchmark the latencies correctly based on the single-threaded sequential evaluation. - const evaluatedProviderResponse = await (provider)[`evaluateLatency`](methodName, ...args) + const evaluatedProviderResponse = await provider[`evaluateLatency`](methodName, ...args) // below invocation does not make the call/send RPC return the correct data // both call and send will return "0x" for some reason // I have to change to above invocation to make call/send return geniun RPC response @@ -274,7 +275,7 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { if (methodName === GET_BLOCK_NUMBER_METHOD_NAME) { // if it's get block number, there's no guarantee that two providers will return the same block number // since the node might be syncing, so we don't need to compare the response - return; + return } else if (methodName === CALL_METHOD_NAME) { // if it's eth_call, then we know the response data type is string, so we can compare directly if (providerResponse !== evaluatedProviderResponse) { @@ -304,12 +305,17 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { } else if (underlyingMethodName === 'eth_feeHistory') { const castedProviderResponse = providerResponse as EthFeeHistory const castedEvaluatedProviderResponse = evaluatedProviderResponse as EthFeeHistory - const mismatch = castedProviderResponse.oldestBlock !== castedEvaluatedProviderResponse.oldestBlock || + const mismatch = + castedProviderResponse.oldestBlock !== castedEvaluatedProviderResponse.oldestBlock || JSON.stringify(castedProviderResponse.reward) !== JSON.stringify(castedEvaluatedProviderResponse.reward) || - JSON.stringify(castedProviderResponse.baseFeePerGas) !== JSON.stringify(castedEvaluatedProviderResponse.baseFeePerGas) || - JSON.stringify(castedProviderResponse.gasUsedRatio) !== JSON.stringify(castedEvaluatedProviderResponse.gasUsedRatio) || - JSON.stringify(castedProviderResponse.baseFeePerBlobGas) !== JSON.stringify(castedEvaluatedProviderResponse.baseFeePerBlobGas) || - JSON.stringify(castedProviderResponse.blobGasUsedRatio) !== JSON.stringify(castedEvaluatedProviderResponse.blobGasUsedRatio) + JSON.stringify(castedProviderResponse.baseFeePerGas) !== + JSON.stringify(castedEvaluatedProviderResponse.baseFeePerGas) || + JSON.stringify(castedProviderResponse.gasUsedRatio) !== + JSON.stringify(castedEvaluatedProviderResponse.gasUsedRatio) || + JSON.stringify(castedProviderResponse.baseFeePerBlobGas) !== + JSON.stringify(castedEvaluatedProviderResponse.baseFeePerBlobGas) || + JSON.stringify(castedProviderResponse.blobGasUsedRatio) !== + JSON.stringify(castedEvaluatedProviderResponse.blobGasUsedRatio) if (mismatch) { this.log.error( { stitchedMethodName, args }, diff --git a/test/utils/eth_feeHistory.ts b/test/utils/eth_feeHistory.ts index fd293cae8e..6963d58f81 100644 --- a/test/utils/eth_feeHistory.ts +++ b/test/utils/eth_feeHistory.ts @@ -5,4 +5,4 @@ export type EthFeeHistory = { gasUsedRatio: number[] baseFeePerBlobGas: string[] blobGasUsedRatio: number[] -} \ No newline at end of file +} From ba0393b233e973dde9f6ed690793e314414d622e Mon Sep 17 00:00:00 2001 From: jsy1218 <91580504+jsy1218@users.noreply.github.com> Date: Fri, 5 Jul 2024 06:58:09 +0100 Subject: [PATCH 6/9] fix the send method comparison --- lib/rpc/UniJsonRpcProvider.ts | 109 ++++++++++-------- {test/utils => lib/util}/eth_feeHistory.ts | 0 .../mocha/unit/rpc/UniJsonRpcProvider.test.ts | 11 ++ 3 files changed, 71 insertions(+), 49 deletions(-) rename {test/utils => lib/util}/eth_feeHistory.ts (100%) diff --git a/lib/rpc/UniJsonRpcProvider.ts b/lib/rpc/UniJsonRpcProvider.ts index fc6eb0e834..c9d1148e46 100644 --- a/lib/rpc/UniJsonRpcProvider.ts +++ b/lib/rpc/UniJsonRpcProvider.ts @@ -1,7 +1,6 @@ import { CALL_METHOD_NAME, CallType, - GET_BLOCK_NUMBER_METHOD_NAME, MAJOR_METHOD_NAMES, SEND_METHOD_NAME, SingleJsonRpcProvider, @@ -22,7 +21,7 @@ import { BigNumber, BigNumberish } from '@ethersproject/bignumber' import { Deferrable } from '@ethersproject/properties' import Logger from 'bunyan' import { UniJsonRpcProviderConfig } from './config' -import { EthFeeHistory } from '../../test/utils/eth_feeHistory' +import { EthFeeHistory } from '../util/eth_feeHistory' export class UniJsonRpcProvider extends StaticJsonRpcProvider { readonly chainId: ChainId = ChainId.MAINNET @@ -272,60 +271,72 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { methodName: string, args: any[] ) { - if (methodName === GET_BLOCK_NUMBER_METHOD_NAME) { - // if it's get block number, there's no guarantee that two providers will return the same block number - // since the node might be syncing, so we don't need to compare the response - return - } else if (methodName === CALL_METHOD_NAME) { - // if it's eth_call, then we know the response data type is string, so we can compare directly - if (providerResponse !== evaluatedProviderResponse) { - this.log.error( - { methodName, args }, - `Provider response mismatch: ${providerResponse} from ${selectedProvider.providerId} vs ${evaluatedProviderResponse} from ${otherProvider.providerId}` - ) - selectedProvider.logRpcResponseMismatch(methodName, otherProvider) - } else { - selectedProvider.logRpcResponseMatch(methodName, otherProvider) - } - } else if (methodName === SEND_METHOD_NAME) { - // send is complicated, because it could be eth_call, eth_blockNumber, eth_feeHistory, eth_estimateGas - // so we need to compare the response based on the method name - const underlyingMethodName = args[0] - const stitchedMethodName = `${SEND_METHOD_NAME}_${underlyingMethodName}` - if (underlyingMethodName === 'eth_call' || underlyingMethodName === 'eth_estimateGas') { + switch (methodName) { + case CALL_METHOD_NAME: + // if it's eth_call, then we know the response data type is string, so we can compare directly if (providerResponse !== evaluatedProviderResponse) { this.log.error( - { stitchedMethodName, args }, + { methodName, args }, `Provider response mismatch: ${providerResponse} from ${selectedProvider.providerId} vs ${evaluatedProviderResponse} from ${otherProvider.providerId}` ) - selectedProvider.logRpcResponseMismatch(stitchedMethodName, otherProvider) + selectedProvider.logRpcResponseMismatch(methodName, otherProvider) } else { - selectedProvider.logRpcResponseMatch(stitchedMethodName, otherProvider) + selectedProvider.logRpcResponseMatch(methodName, otherProvider) } - } else if (underlyingMethodName === 'eth_feeHistory') { - const castedProviderResponse = providerResponse as EthFeeHistory - const castedEvaluatedProviderResponse = evaluatedProviderResponse as EthFeeHistory - const mismatch = - castedProviderResponse.oldestBlock !== castedEvaluatedProviderResponse.oldestBlock || - JSON.stringify(castedProviderResponse.reward) !== JSON.stringify(castedEvaluatedProviderResponse.reward) || - JSON.stringify(castedProviderResponse.baseFeePerGas) !== - JSON.stringify(castedEvaluatedProviderResponse.baseFeePerGas) || - JSON.stringify(castedProviderResponse.gasUsedRatio) !== - JSON.stringify(castedEvaluatedProviderResponse.gasUsedRatio) || - JSON.stringify(castedProviderResponse.baseFeePerBlobGas) !== - JSON.stringify(castedEvaluatedProviderResponse.baseFeePerBlobGas) || - JSON.stringify(castedProviderResponse.blobGasUsedRatio) !== - JSON.stringify(castedEvaluatedProviderResponse.blobGasUsedRatio) - if (mismatch) { - this.log.error( - { stitchedMethodName, args }, - `Provider response mismatch: ${providerResponse} from ${selectedProvider.providerId} vs ${evaluatedProviderResponse} from ${otherProvider.providerId}` - ) - selectedProvider.logRpcResponseMismatch(stitchedMethodName, otherProvider) - } else { - selectedProvider.logRpcResponseMatch(stitchedMethodName, otherProvider) + break + case SEND_METHOD_NAME: + // send is complicated, because it could be eth_call, eth_blockNumber, eth_feeHistory, eth_estimateGas + // so we need to compare the response based on the method name + const underlyingMethodName = args[0] + const stitchedMethodName = `${SEND_METHOD_NAME}_${underlyingMethodName}` + switch (underlyingMethodName) { + case 'eth_call': + case 'eth_estimateGas': + if (providerResponse !== evaluatedProviderResponse) { + this.log.error( + { stitchedMethodName, args }, + `Provider response mismatch: ${providerResponse} from ${selectedProvider.providerId} vs ${evaluatedProviderResponse} from ${otherProvider.providerId}` + ) + selectedProvider.logRpcResponseMismatch(stitchedMethodName, otherProvider) + } else { + selectedProvider.logRpcResponseMatch(stitchedMethodName, otherProvider) + } + break + case 'eth_feeHistory': + const castedProviderResponse = providerResponse as EthFeeHistory + const castedEvaluatedProviderResponse = evaluatedProviderResponse as EthFeeHistory + const mismatch = + castedProviderResponse.oldestBlock !== castedEvaluatedProviderResponse.oldestBlock || + JSON.stringify(castedProviderResponse.reward) !== + JSON.stringify(castedEvaluatedProviderResponse.reward) || + JSON.stringify(castedProviderResponse.baseFeePerGas) !== + JSON.stringify(castedEvaluatedProviderResponse.baseFeePerGas) || + JSON.stringify(castedProviderResponse.gasUsedRatio) !== + JSON.stringify(castedEvaluatedProviderResponse.gasUsedRatio) || + JSON.stringify(castedProviderResponse.baseFeePerBlobGas) !== + JSON.stringify(castedEvaluatedProviderResponse.baseFeePerBlobGas) || + JSON.stringify(castedProviderResponse.blobGasUsedRatio) !== + JSON.stringify(castedEvaluatedProviderResponse.blobGasUsedRatio) + if (mismatch) { + this.log.error( + { stitchedMethodName, args }, + `Provider response mismatch: ${providerResponse} from ${selectedProvider.providerId} vs ${evaluatedProviderResponse} from ${otherProvider.providerId}` + ) + selectedProvider.logRpcResponseMismatch(stitchedMethodName, otherProvider) + } else { + selectedProvider.logRpcResponseMatch(stitchedMethodName, otherProvider) + } + break + default: + // if it's get block number, there's no guarantee that two providers will return the same block number + // since the node might be syncing, so we don't need to compare the response + return } - } + break + default: + // if it's get block number, there's no guarantee that two providers will return the same block number + // since the node might be syncing, so we don't need to compare the response + return } } diff --git a/test/utils/eth_feeHistory.ts b/lib/util/eth_feeHistory.ts similarity index 100% rename from test/utils/eth_feeHistory.ts rename to lib/util/eth_feeHistory.ts diff --git a/test/mocha/unit/rpc/UniJsonRpcProvider.test.ts b/test/mocha/unit/rpc/UniJsonRpcProvider.test.ts index 5974cb0791..b641c48020 100644 --- a/test/mocha/unit/rpc/UniJsonRpcProvider.test.ts +++ b/test/mocha/unit/rpc/UniJsonRpcProvider.test.ts @@ -1007,4 +1007,15 @@ describe('UniJsonRpcProvider', () => { expect(spy1.callCount).to.equal(1) expect(spy2.callCount).to.equal(1) }) + + it('Test compare RPC response for eth_blockNumber', async () => { + uniProvider = new UniJsonRpcProvider( + ChainId.MAINNET, + SINGLE_RPC_PROVIDERS[ChainId.MAINNET], + log, + UNI_PROVIDER_TEST_CONFIG, + 1.0, + 1 + ) + }) }) From 1a7607209dc2d10ab3b2b536c02ac17d2874780e Mon Sep 17 00:00:00 2001 From: jsy1218 <91580504+jsy1218@users.noreply.github.com> Date: Mon, 15 Jul 2024 12:35:00 +0200 Subject: [PATCH 7/9] send method ethers library returns the result as is based on the lib inspection, so we are deserializing the result object ourselves --- lib/rpc/UniJsonRpcProvider.ts | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/rpc/UniJsonRpcProvider.ts b/lib/rpc/UniJsonRpcProvider.ts index c9d1148e46..9aa17a71f2 100644 --- a/lib/rpc/UniJsonRpcProvider.ts +++ b/lib/rpc/UniJsonRpcProvider.ts @@ -22,6 +22,7 @@ import { Deferrable } from '@ethersproject/properties' import Logger from 'bunyan' import { UniJsonRpcProviderConfig } from './config' import { EthFeeHistory } from '../util/eth_feeHistory' +import { JsonRpcResponse } from 'hardhat/types' export class UniJsonRpcProvider extends StaticJsonRpcProvider { readonly chainId: ChainId = ChainId.MAINNET @@ -289,10 +290,14 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { // so we need to compare the response based on the method name const underlyingMethodName = args[0] const stitchedMethodName = `${SEND_METHOD_NAME}_${underlyingMethodName}` + const castedProviderResponse = providerResponse as JsonRpcResponse + const castedEvaluatedProviderResponse = evaluatedProviderResponse as JsonRpcResponse switch (underlyingMethodName) { + // eth_call result type is string, so we can compare directly case 'eth_call': + // eth_estimateGas result type is number, so we can compare directly without casting to number type case 'eth_estimateGas': - if (providerResponse !== evaluatedProviderResponse) { + if (castedProviderResponse.result !== castedProviderResponse.result) { this.log.error( { stitchedMethodName, args }, `Provider response mismatch: ${providerResponse} from ${selectedProvider.providerId} vs ${evaluatedProviderResponse} from ${otherProvider.providerId}` @@ -303,20 +308,20 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { } break case 'eth_feeHistory': - const castedProviderResponse = providerResponse as EthFeeHistory - const castedEvaluatedProviderResponse = evaluatedProviderResponse as EthFeeHistory + const ethFeeHistory = castedProviderResponse.result as EthFeeHistory + const evaluatedEthFeeHistory = castedEvaluatedProviderResponse.result as EthFeeHistory const mismatch = - castedProviderResponse.oldestBlock !== castedEvaluatedProviderResponse.oldestBlock || - JSON.stringify(castedProviderResponse.reward) !== - JSON.stringify(castedEvaluatedProviderResponse.reward) || - JSON.stringify(castedProviderResponse.baseFeePerGas) !== - JSON.stringify(castedEvaluatedProviderResponse.baseFeePerGas) || - JSON.stringify(castedProviderResponse.gasUsedRatio) !== - JSON.stringify(castedEvaluatedProviderResponse.gasUsedRatio) || - JSON.stringify(castedProviderResponse.baseFeePerBlobGas) !== - JSON.stringify(castedEvaluatedProviderResponse.baseFeePerBlobGas) || - JSON.stringify(castedProviderResponse.blobGasUsedRatio) !== - JSON.stringify(castedEvaluatedProviderResponse.blobGasUsedRatio) + ethFeeHistory.oldestBlock !== evaluatedEthFeeHistory.oldestBlock || + JSON.stringify(ethFeeHistory.reward) !== + JSON.stringify(evaluatedEthFeeHistory.reward) || + JSON.stringify(ethFeeHistory.baseFeePerGas) !== + JSON.stringify(evaluatedEthFeeHistory.baseFeePerGas) || + JSON.stringify(ethFeeHistory.gasUsedRatio) !== + JSON.stringify(evaluatedEthFeeHistory.gasUsedRatio) || + JSON.stringify(ethFeeHistory.baseFeePerBlobGas) !== + JSON.stringify(evaluatedEthFeeHistory.baseFeePerBlobGas) || + JSON.stringify(ethFeeHistory.blobGasUsedRatio) !== + JSON.stringify(evaluatedEthFeeHistory.blobGasUsedRatio) if (mismatch) { this.log.error( { stitchedMethodName, args }, From e393fffb4b52cc054d17133062b8c1f9b1c03195 Mon Sep 17 00:00:00 2001 From: jsy1218 <91580504+jsy1218@users.noreply.github.com> Date: Mon, 15 Jul 2024 12:48:11 +0200 Subject: [PATCH 8/9] send method ethers library returns the result as is based on the lib inspection, so we are deserializing the result object ourselves --- lib/rpc/UniJsonRpcProvider.ts | 58 +++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/lib/rpc/UniJsonRpcProvider.ts b/lib/rpc/UniJsonRpcProvider.ts index 9aa17a71f2..712abbdccd 100644 --- a/lib/rpc/UniJsonRpcProvider.ts +++ b/lib/rpc/UniJsonRpcProvider.ts @@ -300,7 +300,16 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { if (castedProviderResponse.result !== castedProviderResponse.result) { this.log.error( { stitchedMethodName, args }, - `Provider response mismatch: ${providerResponse} from ${selectedProvider.providerId} vs ${evaluatedProviderResponse} from ${otherProvider.providerId}` + `Provider result mismatch: ${castedProviderResponse.result} from ${selectedProvider.providerId} vs ${castedProviderResponse.result} from ${otherProvider.providerId}` + ) + selectedProvider.logRpcResponseMismatch(stitchedMethodName, otherProvider) + } else if (castedProviderResponse.error?.data !== castedEvaluatedProviderResponse.error?.data) { + // when comparing the error, the most important part is the data field + this.log.error( + { stitchedMethodName, args }, + `Provider error mismatch: ${JSON.stringify(castedProviderResponse.error)} from ${ + selectedProvider.providerId + } vs ${JSON.stringify(castedEvaluatedProviderResponse.error)} from ${otherProvider.providerId}` ) selectedProvider.logRpcResponseMismatch(stitchedMethodName, otherProvider) } else { @@ -308,29 +317,40 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { } break case 'eth_feeHistory': - const ethFeeHistory = castedProviderResponse.result as EthFeeHistory - const evaluatedEthFeeHistory = castedEvaluatedProviderResponse.result as EthFeeHistory - const mismatch = - ethFeeHistory.oldestBlock !== evaluatedEthFeeHistory.oldestBlock || - JSON.stringify(ethFeeHistory.reward) !== - JSON.stringify(evaluatedEthFeeHistory.reward) || - JSON.stringify(ethFeeHistory.baseFeePerGas) !== - JSON.stringify(evaluatedEthFeeHistory.baseFeePerGas) || - JSON.stringify(ethFeeHistory.gasUsedRatio) !== - JSON.stringify(evaluatedEthFeeHistory.gasUsedRatio) || - JSON.stringify(ethFeeHistory.baseFeePerBlobGas) !== - JSON.stringify(evaluatedEthFeeHistory.baseFeePerBlobGas) || - JSON.stringify(ethFeeHistory.blobGasUsedRatio) !== - JSON.stringify(evaluatedEthFeeHistory.blobGasUsedRatio) - if (mismatch) { + if (castedProviderResponse.result && castedEvaluatedProviderResponse.result) { + const ethFeeHistory = castedProviderResponse.result as EthFeeHistory + const evaluatedEthFeeHistory = castedEvaluatedProviderResponse.result as EthFeeHistory + + const mismatch = + ethFeeHistory.oldestBlock !== evaluatedEthFeeHistory.oldestBlock || + JSON.stringify(ethFeeHistory.reward) !== JSON.stringify(evaluatedEthFeeHistory.reward) || + JSON.stringify(ethFeeHistory.baseFeePerGas) !== JSON.stringify(evaluatedEthFeeHistory.baseFeePerGas) || + JSON.stringify(ethFeeHistory.gasUsedRatio) !== JSON.stringify(evaluatedEthFeeHistory.gasUsedRatio) || + JSON.stringify(ethFeeHistory.baseFeePerBlobGas) !== + JSON.stringify(evaluatedEthFeeHistory.baseFeePerBlobGas) || + JSON.stringify(ethFeeHistory.blobGasUsedRatio) !== + JSON.stringify(evaluatedEthFeeHistory.blobGasUsedRatio) + if (mismatch) { + this.log.error( + { stitchedMethodName, args }, + `Provider result mismatch: ${JSON.stringify(ethFeeHistory)} from ${ + selectedProvider.providerId + } vs ${JSON.stringify(evaluatedEthFeeHistory)} from ${otherProvider.providerId}` + ) + selectedProvider.logRpcResponseMismatch(stitchedMethodName, otherProvider) + } else { + selectedProvider.logRpcResponseMatch(stitchedMethodName, otherProvider) + } + } else if (castedProviderResponse.error?.data !== castedEvaluatedProviderResponse.error?.data) { this.log.error( { stitchedMethodName, args }, - `Provider response mismatch: ${providerResponse} from ${selectedProvider.providerId} vs ${evaluatedProviderResponse} from ${otherProvider.providerId}` + `Provider error mismatch: ${JSON.stringify(castedProviderResponse.error)} from ${ + selectedProvider.providerId + } vs ${JSON.stringify(castedEvaluatedProviderResponse.error)} from ${otherProvider.providerId}` ) selectedProvider.logRpcResponseMismatch(stitchedMethodName, otherProvider) - } else { - selectedProvider.logRpcResponseMatch(stitchedMethodName, otherProvider) } + break default: // if it's get block number, there's no guarantee that two providers will return the same block number From 76b625552cfa0c747bbf5307c56bc7375445b760 Mon Sep 17 00:00:00 2001 From: jsy1218 <91580504+jsy1218@users.noreply.github.com> Date: Mon, 15 Jul 2024 13:30:54 +0200 Subject: [PATCH 9/9] complete unit test coverage --- lib/rpc/UniJsonRpcProvider.ts | 5 +- lib/util/eth_feeHistory.ts | 2 +- .../mocha/unit/rpc/UniJsonRpcProvider.test.ts | 161 +++++++++++++++++- 3 files changed, 156 insertions(+), 12 deletions(-) diff --git a/lib/rpc/UniJsonRpcProvider.ts b/lib/rpc/UniJsonRpcProvider.ts index 712abbdccd..4bcc0116db 100644 --- a/lib/rpc/UniJsonRpcProvider.ts +++ b/lib/rpc/UniJsonRpcProvider.ts @@ -297,10 +297,10 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { case 'eth_call': // eth_estimateGas result type is number, so we can compare directly without casting to number type case 'eth_estimateGas': - if (castedProviderResponse.result !== castedProviderResponse.result) { + if (castedProviderResponse.result !== castedEvaluatedProviderResponse.result) { this.log.error( { stitchedMethodName, args }, - `Provider result mismatch: ${castedProviderResponse.result} from ${selectedProvider.providerId} vs ${castedProviderResponse.result} from ${otherProvider.providerId}` + `Provider result mismatch: ${castedProviderResponse.result} from ${selectedProvider.providerId} vs ${castedEvaluatedProviderResponse.result} from ${otherProvider.providerId}` ) selectedProvider.logRpcResponseMismatch(stitchedMethodName, otherProvider) } else if (castedProviderResponse.error?.data !== castedEvaluatedProviderResponse.error?.data) { @@ -342,6 +342,7 @@ export class UniJsonRpcProvider extends StaticJsonRpcProvider { selectedProvider.logRpcResponseMatch(stitchedMethodName, otherProvider) } } else if (castedProviderResponse.error?.data !== castedEvaluatedProviderResponse.error?.data) { + // when comparing the error, the most important part is the data field this.log.error( { stitchedMethodName, args }, `Provider error mismatch: ${JSON.stringify(castedProviderResponse.error)} from ${ diff --git a/lib/util/eth_feeHistory.ts b/lib/util/eth_feeHistory.ts index 6963d58f81..bae458ced8 100644 --- a/lib/util/eth_feeHistory.ts +++ b/lib/util/eth_feeHistory.ts @@ -1,5 +1,5 @@ export type EthFeeHistory = { - oldestBlock: number + oldestBlock: string reward: string[] baseFeePerGas: string[] gasUsedRatio: number[] diff --git a/test/mocha/unit/rpc/UniJsonRpcProvider.test.ts b/test/mocha/unit/rpc/UniJsonRpcProvider.test.ts index b641c48020..4d61316aff 100644 --- a/test/mocha/unit/rpc/UniJsonRpcProvider.test.ts +++ b/test/mocha/unit/rpc/UniJsonRpcProvider.test.ts @@ -11,6 +11,8 @@ import { import { SingleJsonRpcProvider } from '../../../../lib/rpc/SingleJsonRpcProvider' import { default as bunyan } from 'bunyan' import { ProviderHealthiness } from '../../../../lib/rpc/ProviderHealthState' +import { JsonRpcResponse } from 'hardhat/types' +import { EthFeeHistory } from '../../../../lib/util/eth_feeHistory' const UNI_PROVIDER_TEST_CONFIG: UniJsonRpcProviderConfig = { HEALTH_EVALUATION_WAIT_PERIOD_IN_S: 0, @@ -1008,14 +1010,155 @@ describe('UniJsonRpcProvider', () => { expect(spy2.callCount).to.equal(1) }) - it('Test compare RPC response for eth_blockNumber', async () => { - uniProvider = new UniJsonRpcProvider( - ChainId.MAINNET, - SINGLE_RPC_PROVIDERS[ChainId.MAINNET], - log, - UNI_PROVIDER_TEST_CONFIG, - 1.0, - 1 - ) + it('Test compare RPC result for eth_call with same results', async () => { + const rpcProviders = createNewSingleJsonRpcProviders() + const selectedProvider = rpcProviders[0] + const otherProvider = rpcProviders[1] + const spy = sandbox.spy(selectedProvider, 'logRpcResponseMatch') + + uniProvider = new UniJsonRpcProvider(ChainId.MAINNET, rpcProviders, log, UNI_PROVIDER_TEST_CONFIG, 1.0, 1) + + uniProvider.compareRpcResponses('0x123', '0x123', selectedProvider, otherProvider, 'call', []) + + expect(spy.callCount).to.equal(1) + }) + + it('Test compare RPC result for eth_call with different results', async () => { + const rpcProviders = createNewSingleJsonRpcProviders() + const selectedProvider = rpcProviders[0] + const otherProvider = rpcProviders[1] + const spy = sandbox.spy(selectedProvider, 'logRpcResponseMismatch') + + uniProvider = new UniJsonRpcProvider(ChainId.MAINNET, rpcProviders, log, UNI_PROVIDER_TEST_CONFIG, 1.0, 1) + + uniProvider.compareRpcResponses('0x321', '0x123', selectedProvider, otherProvider, 'call', []) + + expect(spy.callCount).to.equal(1) + }) + + it('Test compare RPC result for eth_estimateGas with same results', async () => { + const rpcProviders = createNewSingleJsonRpcProviders() + const selectedProvider = rpcProviders[0] + const otherProvider = rpcProviders[1] + const spy = sandbox.spy(selectedProvider, 'logRpcResponseMatch') + + uniProvider = new UniJsonRpcProvider(ChainId.MAINNET, rpcProviders, log, UNI_PROVIDER_TEST_CONFIG, 1.0, 1) + + const providerResult: JsonRpcResponse = { jsonrpc: '2.0', result: '0x123', id: 76 } + const otherProviderResult: JsonRpcResponse = { jsonrpc: '2.0', result: '0x123', id: 76 } + uniProvider.compareRpcResponses(providerResult, otherProviderResult, selectedProvider, otherProvider, 'send', [ + 'eth_call', + ]) + + expect(spy.callCount).to.equal(1) + }) + + it('Test compare RPC result for eth_estimateGas with different results', async () => { + const rpcProviders = createNewSingleJsonRpcProviders() + const selectedProvider = rpcProviders[0] + const otherProvider = rpcProviders[1] + const spy = sandbox.spy(selectedProvider, 'logRpcResponseMismatch') + + uniProvider = new UniJsonRpcProvider(ChainId.MAINNET, rpcProviders, log, UNI_PROVIDER_TEST_CONFIG, 1.0, 1) + + const providerResult: JsonRpcResponse = { jsonrpc: '2.0', result: '0x123', id: 76 } + const otherProviderResult: JsonRpcResponse = { jsonrpc: '2.0', result: '0x321', id: 76 } + uniProvider.compareRpcResponses(providerResult, otherProviderResult, selectedProvider, otherProvider, 'send', [ + 'eth_call', + ]) + + expect(spy.callCount).to.equal(1) + }) + + it('Test compare RPC error for eth_estimateGas with same errors', async () => { + const rpcProviders = createNewSingleJsonRpcProviders() + const selectedProvider = rpcProviders[0] + const otherProvider = rpcProviders[1] + const spy = sandbox.spy(selectedProvider, 'logRpcResponseMatch') + + uniProvider = new UniJsonRpcProvider(ChainId.MAINNET, rpcProviders, log, UNI_PROVIDER_TEST_CONFIG, 1.0, 1) + + const providerError = { code: '123', data: '0x123', error: 'CALL_EXCEPTION' } + const otherProviderError = { code: '123', data: '0x123', error: 'CALL_EXCEPTION' } + uniProvider.compareRpcResponses(providerError, otherProviderError, selectedProvider, otherProvider, 'send', [ + 'eth_call', + ]) + + expect(spy.callCount).to.equal(1) + }) + + it('Test compare RPC error for eth_estimateGas with different errors', async () => { + const rpcProviders = createNewSingleJsonRpcProviders() + const selectedProvider = rpcProviders[0] + const otherProvider = rpcProviders[1] + const spy = sandbox.spy(selectedProvider, 'logRpcResponseMatch') + + uniProvider = new UniJsonRpcProvider(ChainId.MAINNET, rpcProviders, log, UNI_PROVIDER_TEST_CONFIG, 1.0, 1) + + const providerError = { code: '123', data: '0x321', error: 'CALL_EXCEPTION' } + const otherProviderError = { code: '123', data: '0x123', error: 'CALL_EXCEPTION' } + uniProvider.compareRpcResponses(providerError, otherProviderError, selectedProvider, otherProvider, 'send', [ + 'eth_call', + ]) + + expect(spy.callCount).to.equal(1) + }) + + it('Test compare RPC result for eth_feeHistory with different results, but one is a number and the other is a string', async () => { + const rpcProviders = createNewSingleJsonRpcProviders() + const selectedProvider = rpcProviders[0] + const otherProvider = rpcProviders[1] + const spy = sandbox.spy(selectedProvider, 'logRpcResponseMatch') + + uniProvider = new UniJsonRpcProvider(ChainId.MAINNET, rpcProviders, log, UNI_PROVIDER_TEST_CONFIG, 1.0, 1) + + const ethFeeHistory: EthFeeHistory = { + oldestBlock: '0x1347665', + reward: ['0x21f43815'], + baseFeePerGas: ['0x7750ad57'], + gasUsedRatio: [0.4496709], + baseFeePerBlobGas: ['0x1'], + blobGasUsedRatio: [0.4496709], + } + const providerResult: JsonRpcResponse = { jsonrpc: '2.0', result: ethFeeHistory, id: 76 } + const otherProviderResult: JsonRpcResponse = { jsonrpc: '2.0', result: ethFeeHistory, id: 76 } + uniProvider.compareRpcResponses(providerResult, otherProviderResult, selectedProvider, otherProvider, 'send', [ + 'eth_feeHistory', + ]) + + expect(spy.callCount).to.equal(1) + }) + + it('Test compare RPC result for eth_feeHistory with different results', async () => { + const rpcProviders = createNewSingleJsonRpcProviders() + const selectedProvider = rpcProviders[0] + const otherProvider = rpcProviders[1] + const spy = sandbox.spy(selectedProvider, 'logRpcResponseMismatch') + + uniProvider = new UniJsonRpcProvider(ChainId.MAINNET, rpcProviders, log, UNI_PROVIDER_TEST_CONFIG, 1.0, 1) + + const ethFeeHistory: EthFeeHistory = { + oldestBlock: '0x1347665', + reward: ['0x21f43815'], + baseFeePerGas: ['0x7750ad57'], + gasUsedRatio: [0.4496709], + baseFeePerBlobGas: ['0x1'], + blobGasUsedRatio: [0.4496709], + } + const ethFeeHistory2: EthFeeHistory = { + oldestBlock: '0x1347661', + reward: ['0x21f43815'], + baseFeePerGas: ['0x7750ad57'], + gasUsedRatio: [0.4496709], + baseFeePerBlobGas: ['0x1'], + blobGasUsedRatio: [0.4496709], + } + const providerResult: JsonRpcResponse = { jsonrpc: '2.0', result: ethFeeHistory, id: 76 } + const otherProviderResult: JsonRpcResponse = { jsonrpc: '2.0', result: ethFeeHistory2, id: 76 } + uniProvider.compareRpcResponses(providerResult, otherProviderResult, selectedProvider, otherProvider, 'send', [ + 'eth_feeHistory', + ]) + + expect(spy.callCount).to.equal(1) }) })