Skip to content

Commit

Permalink
add fallback for web3 json-rpc.
Browse files Browse the repository at this point in the history
  • Loading branch information
Jagden committed Dec 10, 2024
1 parent f40fec4 commit 5b80709
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 30 deletions.
2 changes: 1 addition & 1 deletion src/api/render-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ test.serial('[integration] getNodeManagement responds according to Ethereum and
t.timeout(5 * 60 * 1000);

const ethereum = new EthereumTestDriver(true);
const ethereumEndpoint = 'http://localhost:7545';
const ethereumEndpoint = ['http://localhost:7545'];
const maticEndpoint = 'mock-endpoint';
const finalityBufferBlocks = 5;

Expand Down
4 changes: 2 additions & 2 deletions src/cli-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ test('parseOptions with no config and no environment variables', (t) => {
});

test('parseOptions: environment variables and no config', (t) => {
const mockEthereumEndpoint = 'https://mainnet.infura.io/v3/1234567890';
const mockEthereumEndpoint = ['https://mainnet.infura.io/v3/1234567890'];
const mockNodeAddress = '0x1234567890';
process.env.ETHEREUM_ENDPOINT = mockEthereumEndpoint;

Check failure on line 67 in src/cli-args.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Type 'string[]' is not assignable to type 'string'.
process.env.NODE_ADDRESS = mockNodeAddress;
Expand All @@ -76,7 +76,7 @@ test('parseOptions: environment variables and no config', (t) => {
});

test('parseOptions: env vars take precedence', (t) => {
const mockEthereumEndpoint = 'https://mainnet.infura.io/v3/1234567890';
const mockEthereumEndpoint = ['https://mainnet.infura.io/v3/1234567890'];
const mockNodeAddress = '0x1234567890';
process.env.ETHEREUM_ENDPOINT = mockEthereumEndpoint;

Check failure on line 81 in src/cli-args.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Type 'string[]' is not assignable to type 'string'.
process.env.NODE_ADDRESS = mockNodeAddress;
Expand Down
2 changes: 1 addition & 1 deletion src/config.example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const exampleConfig: ServiceConfiguration = {
Port: 8080,
EthereumGenesisContract: '0xD859701C81119aB12A1e62AF6270aD2AE05c7AB3',
EthereumFirstBlock: 11191390,
EthereumEndpoint: 'http://ganache:7545',
EthereumEndpoint: ['http://ganache:7545'],
DeploymentDescriptorUrl: 'https://deployment.orbs.network/mainnet.json',
ElectionsAuditOnly: false,
StatusJsonPath: './status/status.json',
Expand Down
6 changes: 3 additions & 3 deletions src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ test('accepts legal config', (t) => {
BootstrapMode: false,
Port: 2,
EthereumGenesisContract: 'foo',
EthereumEndpoint: 'http://localhost:7545',
EthereumEndpoint: ['http://localhost:7545'],
EthereumPollIntervalSeconds: 0.1,
EthereumRequestsPerSecondLimit: 0,
ElectionsStaleUpdateSeconds: 7 * 24 * 60 * 60,
Expand All @@ -34,7 +34,7 @@ test('declines illegal config (1)', (t) => {
BootstrapMode: false,
Port: 2,
EthereumGenesisContract: 'foo',
EthereumEndpoint: 'http://localhost:7545',
EthereumEndpoint: ['http://localhost:7545'],
EthereumPollIntervalSeconds: 0.1,
EthereumRequestsPerSecondLimit: 0,
ElectionsStaleUpdateSeconds: 7 * 24 * 60 * 60,
Expand All @@ -60,7 +60,7 @@ test('declines illegal config (2)', (t) => {
BootstrapMode: false,
Port: 2,
EthereumGenesisContract: 'foo',
EthereumEndpoint: 'foo-bar:123',
EthereumEndpoint: ['foo-bar:123'],
EthereumPollIntervalSeconds: 0.1,
EthereumRequestsPerSecondLimit: 0,
ElectionsStaleUpdateSeconds: 7 * 24 * 60 * 60,
Expand Down
43 changes: 35 additions & 8 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import validate from 'validate.js';
import validate, { isEmpty } from 'validate.js';

Check failure on line 1 in src/config.ts

View workflow job for this annotation

GitHub Actions / build-and-test

'isEmpty' is declared but its value is never read.

Check failure on line 1 in src/config.ts

View workflow job for this annotation

GitHub Actions / build-and-release-to-staging

'isEmpty' is declared but its value is never read.

export interface ServiceConfiguration {
BootstrapMode: boolean;
Port: number;
EthereumGenesisContract: string;
EthereumEndpoint: string;
EthereumEndpoint: string[];
/** @deprecated Use `EthereumEndpoint` instead */
MaticEndpoint?: string;
DeploymentDescriptorUrl: string;
Expand Down Expand Up @@ -47,6 +47,25 @@ export const defaultServiceConfiguration = {
Verbose: false,
};

validate.validators.array = function(value, options, key, attributes) {

Check failure on line 50 in src/config.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Not all code paths return a value.

Check failure on line 50 in src/config.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Parameter 'value' implicitly has an 'any' type.

Check failure on line 50 in src/config.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Parameter 'options' implicitly has an 'any' type.

Check failure on line 50 in src/config.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Parameter 'key' implicitly has an 'any' type.

Check failure on line 50 in src/config.ts

View workflow job for this annotation

GitHub Actions / build-and-test

'attributes' is declared but its value is never read.

Check failure on line 50 in src/config.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Parameter 'attributes' implicitly has an 'any' type.

Check failure on line 50 in src/config.ts

View workflow job for this annotation

GitHub Actions / build-and-release-to-staging

Not all code paths return a value.

Check failure on line 50 in src/config.ts

View workflow job for this annotation

GitHub Actions / build-and-release-to-staging

Parameter 'value' implicitly has an 'any' type.

Check failure on line 50 in src/config.ts

View workflow job for this annotation

GitHub Actions / build-and-release-to-staging

Parameter 'options' implicitly has an 'any' type.

Check failure on line 50 in src/config.ts

View workflow job for this annotation

GitHub Actions / build-and-release-to-staging

Parameter 'key' implicitly has an 'any' type.

Check failure on line 50 in src/config.ts

View workflow job for this annotation

GitHub Actions / build-and-release-to-staging

'attributes' is declared but its value is never read.

Check failure on line 50 in src/config.ts

View workflow job for this annotation

GitHub Actions / build-and-release-to-staging

Parameter 'attributes' implicitly has an 'any' type.
if (!Array.isArray(value)) {
return `${key} must be an array.`;
}

if (options && options.item) {
const errors = value.map((item, index) => {

Check failure on line 56 in src/config.ts

View workflow job for this annotation

GitHub Actions / build-and-release-to-staging

Not all code paths return a value.
const error = validate.single(item, options.item);
if (error) {
return `Item ${index + 1}: ${error.join(", ")}`;
}
}).filter(error => error);

if (errors.length > 0) {
return errors;
}
}
};

export function validateServiceConfiguration(c: Partial<ServiceConfiguration>): string[] | undefined {
const serviceConfigConstraints = {
BootstrapMode: {
Expand Down Expand Up @@ -101,12 +120,20 @@ export function validateServiceConfiguration(c: Partial<ServiceConfiguration>):
numericality: { noStrings: true },
},
EthereumEndpoint: {
presence: { allowEmpty: false },
type: 'string',
url: {
allowLocal: true,
},
},
presence: true, // Ensure the attribute is present
type: "array", // Ensure it's an array
array: {
item: {
presence: true, // Ensure each item is not empty
type: "string", // Ensure each item in the array is a string
format: {
pattern: /^(https?:\/\/[^\s$.?#].[^\s]*)$/i, // URL regex pattern
message: "must be a valid URL"
}
}
}
},

EthereumGenesisContract: {
presence: { allowEmpty: false },
type: 'string',
Expand Down
3 changes: 2 additions & 1 deletion src/env-var-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export function setConfigEnvVars(config: ServiceConfiguration): void {
config.BootstrapMode = process.env.BOOTSTRAP_MODE ? process.env.BOOTSTRAP_MODE === 'true' : config.BootstrapMode;
config.Port = process.env.PORT ? Number(process.env.PORT) : config.Port;
config.EthereumGenesisContract = process.env.ETHEREUM_GENESIS_CONTRACT ?? config.EthereumGenesisContract;
config.EthereumEndpoint = process.env.ETHEREUM_ENDPOINT ?? config.EthereumEndpoint;
// parse ETHEREUM_ENDPOINT, if it has multiple values, split by comma
config.EthereumEndpoint = process.env.ETHEREUM_ENDPOINT ? process.env.ETHEREUM_ENDPOINT.split(',') : config.EthereumEndpoint;
config.DeploymentDescriptorUrl = process.env.DEPLOYMENT_DESCRIPTOR_URL ?? config.DeploymentDescriptorUrl;
config.ElectionsAuditOnly = process.env.ELECTIONS_AUDIT_ONLY
? process.env.ELECTIONS_AUDIT_ONLY === 'true'
Expand Down
2 changes: 1 addition & 1 deletion src/ethereum/block-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ test.serial('[integration] BlockSync reads registry for contract addresses', asy
t.timeout(5 * 60 * 1000);

const ethereum = new EthereumTestDriver(true);
const ethereumEndpoint = 'http://localhost:7545';
const ethereumEndpoint = ['http://localhost:7545'];
const finalityBufferBlocks = 5;

// setup Ethereum state
Expand Down
68 changes: 59 additions & 9 deletions src/ethereum/ethereum-reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ import https from 'https';
const HTTP_TIMEOUT_SEC = 20;

const subDomain = 'eth-api'
const domain = 'orbs.network'
const domain = 'orbs.network'
let timer: NodeJS.Timeout | null = null;

export type EthereumConfiguration = {
EthereumEndpoint: string;
EthereumEndpoint: string[];
EthereumRequestsPerSecondLimit: number;
};

export class EthereumReader {
private web3: Web3;
private currentWeb3Index = 0;
private web3s: Web3[];
private throttled?: pThrottle.ThrottledFunction<[], void>;
private agent: https.Agent;
private blockTimeSinceFail: number;
Expand All @@ -29,21 +31,49 @@ export class EthereumReader {
maxSockets: 5,
});
this.blockTimeSinceFail = 0;
this.web3 = new Web3(
new Web3.providers.HttpProvider(config.EthereumEndpoint, {

this.web3s = config.EthereumEndpoint.map(endpoint => new Web3(
new Web3.providers.HttpProvider(endpoint, {
keepAlive: true,
timeout: HTTP_TIMEOUT_SEC * 1000,
})
);
));

if (config.EthereumRequestsPerSecondLimit > 0) {
this.throttled = pThrottle(() => Promise.resolve(), config.EthereumRequestsPerSecondLimit, 1000);
}
}

getWeb3(): Web3 {
return this.web3s[this.currentWeb3Index];
}

switchWeb3() {
this.currentWeb3Index = (this.currentWeb3Index + 1) % this.web3s.length;

if (this.currentWeb3Index != 0) {
if (timer!=null) {
clearTimeout (timer);
}

timer = setTimeout(() => {
this.currentWeb3Index = 0;
console.log('switchWeb3: switching to web3 to first provider.');
}, 1000 * 60 * 60); // after an hour, return to the first web3
}
}

async getBlockNumber(): Promise<number> {
if (this.throttled) await this.throttled();
this.requestStats.add(1);
return this.web3.eth.getBlockNumber();

try {
return await this.getWeb3().eth.getBlockNumber();
} catch (error) {
console.error("Error fetching block number:", error);
this.switchWeb3();
return await this.getWeb3().eth.getBlockNumber();
}
}

// orbs GET api dediated to serve block time from cache
Expand Down Expand Up @@ -92,7 +122,16 @@ export class EthereumReader {
// fallback to web3
if (this.throttled) await this.throttled();
this.requestStats.add(1);
const block = await this.web3.eth.getBlock(blockNumber);

let block
try {
block = await this.getWeb3().eth.getBlock(blockNumber);
} catch (error) {
console.error("Error fetching block number:", error);
this.switchWeb3();
block = await this.getWeb3().eth.getBlock(blockNumber);
}

if (!block) {
throw new Error(`web3.eth.getBlock for ${blockNumber} return empty block.`);
}
Expand All @@ -102,7 +141,18 @@ export class EthereumReader {
getContractForEvent(eventName: EventName, address: string): Contract {
const contractName = contractByEventName(eventName);
const abi = getAbiForContract(address, contractName);
return new this.web3.eth.Contract(abi, address);

try {
const web3instance = this.getWeb3();
//return new this.getWeb3().eth.Contract(abi, address);
return new web3instance.eth.Contract(abi, address);
} catch (error) {
console.error("Error fetching contract:", error);
this.switchWeb3();
const web3instance = this.getWeb3();
return new web3instance.eth.Contract(abi, address);
//return new this.getWeb3().eth.Contract(abi, address);
}
}

// throws error if fails, caller needs to decrease page size if needed
Expand Down
4 changes: 2 additions & 2 deletions src/ethereum/test-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ export class EthereumTestDriver {
return await d.web3.eth.getBlockNumber();
}

async getCurrentBlockPreDeploy(ethereumEndpoint: string): Promise<number> {
const web3 = new Web3(ethereumEndpoint);
async getCurrentBlockPreDeploy(ethereumEndpoint: string[]): Promise<number> {
const web3 = new Web3(ethereumEndpoint[0]);
return await web3.eth.getBlockNumber();
}

Expand Down
6 changes: 4 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ process.on('uncaughtException', function (err) {
});

function censorConfig(conf: ServiceConfiguration) {
const censoredEthEndpointArray = conf.EthereumEndpoint.map((endpoint) => endpoint.slice(0, 30) + "**********");

const external: {[key: string]: any} = {
...conf.ExternalLaunchConfig,
EthereumEndpoint: conf.EthereumEndpoint.slice(0, -10) + "**********",
EthereumEndpoint: censoredEthEndpointArray,
}
const censored = {
...conf,
EthereumEndpoint: conf.EthereumEndpoint.slice(0, -10) + "**********",
EthereumEndpoint: censoredEthEndpointArray,
ExternalLaunchConfig: external
}

Expand Down

0 comments on commit 5b80709

Please sign in to comment.