Skip to content

Commit

Permalink
feat: adds estimation for confirmation time
Browse files Browse the repository at this point in the history
  • Loading branch information
douglance committed Oct 8, 2024
1 parent 957fdf0 commit 570c87f
Show file tree
Hide file tree
Showing 6 changed files with 570 additions and 165 deletions.
3 changes: 2 additions & 1 deletion packages/scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"coverage": "vitest run --coverage",
"start": "node dist/index.js",
"add-orbit-chain": "node dist/scripts.cjs.js add-orbit-chain",
"validate-orbit-chains-data": "node dist/scripts.cjs.js validate-orbit-chains-data"
"validate-orbit-chains-data": "node dist/scripts.cjs.js validate-orbit-chains-data",
"test:confirmation-time": "vitest run src/getConfirmationTime/index.test.ts"
},
"author": "",
"license": "ISC",
Expand Down
4 changes: 2 additions & 2 deletions packages/scripts/src/addOrbitChain/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ export const tokenBridgeSchema = z.object({
parentCustomGateway: addressSchema,
parentErc20Gateway: addressSchema,
parentGatewayRouter: addressSchema,
parentMulticall: addressSchema.optional(),
parentMultiCall: addressSchema.optional(),
parentProxyAdmin: addressSchema,
parentWeth: addressSchema,
parentWethGateway: addressSchema,
childCustomGateway: addressSchema,
childErc20Gateway: addressSchema,
childGatewayRouter: addressSchema,
childMulticall: addressSchema.optional(),
childMultiCall: addressSchema.optional(),
childProxyAdmin: addressSchema,
childWeth: addressSchema,
childWethGateway: addressSchema,
Expand Down
44 changes: 44 additions & 0 deletions packages/scripts/src/getConfirmationTime/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, it, expect, vi } from "vitest";
import {
calculateConfirmationTime,
getOrbitChainIds,
updateAllConfirmationTimes,
} from "./index";
import * as transforms from "../addOrbitChain/transforms";

// Mock the updateOrbitChainsFile function
// vi.mock("../addOrbitChain/transforms", () => ({
// updateOrbitChainsFile: vi.fn(),
// }));

describe("calculateConfirmationTime", () => {
const orbitChainIds = getOrbitChainIds();

it.each(orbitChainIds)(
"should calculate the confirmation time for chain %i",
async (chainId) => {
const result = await calculateConfirmationTime(chainId);
expect(typeof result).toBe("number");
expect(result).toBeGreaterThan(0);
// expect(transforms.updateOrbitChainsFile).toHaveBeenCalled();
},
60000 // Increase timeout to 60 seconds
);

it("should throw an error when chain is not found", async () => {
await expect(calculateConfirmationTime(999)).rejects.toThrow(
"Chain with ID 999 not found in orbitChainsData"
);
});
});

describe.skip("updateAllConfirmationTimes", () => {
it("should update confirmation times for all chains", async () => {
await updateAllConfirmationTimes();
// expect(transforms.updateOrbitChainsFile).toHaveBeenCalledTimes(
// getOrbitChainIds().length
// );
}, 100000);
});
1728390038;
1728389788;
260 changes: 260 additions & 0 deletions packages/scripts/src/getConfirmationTime/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import { ethers } from "ethers";
import {
chainSchema,
OrbitChain,
OrbitChainsList,
} from "../addOrbitChain/schemas";
import orbitChainsData from "../../../arb-token-bridge-ui/src/util/orbitChainsData.json";
import {
ParentChainInfo,
ConfirmationTimeSummary,
ROLLUP_ABI,
} from "./schemas";
import { updateOrbitChainsFile } from "../addOrbitChain/transforms";

async function calculateAverageBlockTime(
provider: ethers.providers.JsonRpcProvider
): Promise<number> {
const latestBlock = await provider.getBlock("latest");
const oldBlock = await provider.getBlock(latestBlock.number - 1000);
const timeDifference = latestBlock.timestamp - oldBlock.timestamp;
const blockDifference = latestBlock.number - oldBlock.number;
return timeDifference / blockDifference;
}

async function sampleNodeCreationTimes(
rollupContract: ethers.Contract,
sampleSize = 100
): Promise<number> {
const samples: number[] = [];
const latestNodeCreated = await rollupContract.latestNodeCreated();

// Determine the maximum number of samples we can take
const maxSamples = Math.min(
sampleSize,
Math.floor(latestNodeCreated.toNumber() / 100)
);

for (let i = 0; i < maxSamples; i++) {
const endNodeNum = latestNodeCreated.sub(i * 100);
const startNodeNum = latestNodeCreated.sub((i + 1) * 100);

// Ensure we're not trying to access negative node numbers
if (startNodeNum.lt(0)) {
break;
}

const endNode = await rollupContract.getNode(endNodeNum);
const startNode = await rollupContract.getNode(startNodeNum);
const timeDiff = Number(BigInt(endNode[10]) - BigInt(startNode[10]));
samples.push(timeDiff / 100);
}

// If we couldn't get any samples, throw an error
if (samples.length === 0) {
throw new Error(
"Unable to sample node creation times: not enough historical data"
);
}

// Calculate mean and standard deviation
const mean = samples.reduce((a, b) => a + b) / samples.length;
const variance =
samples.reduce((a, b) => a + Math.pow(b - mean, 2), 0) /
(samples.length - 1);
const stdDev = Math.sqrt(variance);

// Calculate 95% confidence interval
const confidenceInterval = 1.96 * (stdDev / Math.sqrt(samples.length));

console.log(`Mean node creation time: ${mean.toFixed(2)} blocks`);
console.log(
`95% Confidence Interval: ±${confidenceInterval.toFixed(2)} blocks`
);
console.log(`Number of samples: ${samples.length}`);

return mean + confidenceInterval; // Return upper bound of confidence interval
}

export async function calculateConfirmationTime(
chainId: number
): Promise<number> {
const summary: ConfirmationTimeSummary = {
chainId,
chainName: "",
parentChainId: 0,
averageNodeCreationTime: BigInt(0),
estimatedConfirmationTime: 0,
usedFallback: false,
};

try {
const chainData = findChainById(
chainId,
orbitChainsData as OrbitChainsList
);
if (!chainData) {
throw new Error(`Chain with ID ${chainId} not found in orbitChainsData`);
}

const validatedChain = await chainSchema.parseAsync(chainData);
const parentChainInfo = getParentChainInfo(validatedChain.parentChainId);

summary.chainName = validatedChain.name;
summary.parentChainId = validatedChain.parentChainId;

const provider = new ethers.providers.JsonRpcProvider(
parentChainInfo.rpcUrl
);
const rollupContract = new ethers.Contract(
validatedChain.ethBridge.rollup,
ROLLUP_ABI,
provider
);

try {
const averageCreationTime = await sampleNodeCreationTimes(rollupContract);
summary.averageNodeCreationTime = BigInt(Math.round(averageCreationTime));

const estimatedConfirmationTimeBlocks = averageCreationTime * 2;

// Calculate average block time
const averageBlockTime = await calculateAverageBlockTime(provider);
console.log(`Average block time: ${averageBlockTime.toFixed(2)} seconds`);

// Convert blocks to minutes
const estimatedConfirmationTimeMinutes =
(estimatedConfirmationTimeBlocks * averageBlockTime) / 60;

console.log(
`Estimated confirmation time: ${estimatedConfirmationTimeMinutes.toFixed(
2
)} minutes`
);

summary.estimatedConfirmationTime = Math.ceil(
estimatedConfirmationTimeMinutes
);

// Update the orbitChainsData.json file
const updatedChain = {
...validatedChain,
estimatedConfirmationTime: summary.estimatedConfirmationTime,
};
const targetJsonPath =
"../arb-token-bridge-ui/src/util/orbitChainsData.json";
updateOrbitChainsFile(updatedChain, targetJsonPath);

return summary.estimatedConfirmationTime;
} catch (error) {
console.warn(
`Failed to calculate confirmation time using contract data for chain ${chainId}. Falling back to confirmPeriodBlocks.`
);
console.log(error);
summary.usedFallback = true;

// Fallback: use confirmPeriodBlocks and calculated average block time
const averageBlockTime = await calculateAverageBlockTime(provider);
const estimatedConfirmationTimeMinutes =
(validatedChain.confirmPeriodBlocks * averageBlockTime) / 60;

summary.estimatedConfirmationTime = Math.ceil(
estimatedConfirmationTimeMinutes
);

// Update the orbitChainsData.json file with fallback value
const updatedChain = {
...validatedChain,
estimatedConfirmationTime: summary.estimatedConfirmationTime,
};
const targetJsonPath =
"../arb-token-bridge-ui/src/util/orbitChainsData.json";
updateOrbitChainsFile(updatedChain, targetJsonPath);

return summary.estimatedConfirmationTime;
}
} catch (error) {
console.error(
`Error calculating confirmation time for chain ${chainId}:`,
error
);
throw error;
} finally {
console.log(`Chain ${chainId} (${summary.chainName}):`);
console.log(
` Estimated Confirmation Time: ${summary.estimatedConfirmationTime.toFixed(
2
)} minutes`
);
console.log(` Used Fallback: ${summary.usedFallback}`);
}
}

function findChainById(
chainId: number,
chainsList: OrbitChainsList
): OrbitChain | undefined {
const allChains = [...chainsList.mainnet, ...chainsList.testnet];
return allChains.find((chain) => chain.chainId === chainId);
}

export function getOrbitChainIds(): number[] {
const allChains = [...orbitChainsData.mainnet, ...orbitChainsData.testnet];
return allChains.map((chain) => chain.chainId);
}

function getParentChainInfo(parentChainId: number): ParentChainInfo {
switch (parentChainId) {
case 1: // Ethereum Mainnet
return {
rpcUrl: "https://eth.llamarpc.com",
blockExplorer: "https://etherscan.io",
chainId: 1,
name: "Ethereum",
};
case 42161: // Arbitrum One
return {
rpcUrl: "https://arb1.arbitrum.io/rpc",
blockExplorer: "https://arbiscan.io",
chainId: 42161,
name: "Arbitrum One",
};
case 11155111: // Sepolia
return {
rpcUrl: "https://ethereum-sepolia-rpc.publicnode.com",
blockExplorer: "https://sepolia.etherscan.io",
chainId: 11155111,
name: "Sepolia",
};
case 421614: // Arbitrum Sepolia
return {
rpcUrl: "https://sepolia-rollup.arbitrum.io/rpc",
blockExplorer: "https://sepolia.arbiscan.io",
chainId: 421614,
name: "Arbitrum Sepolia",
};
case 17000: // Holesky
return {
rpcUrl: "https://ethereum-holesky-rpc.publicnode.com",
blockExplorer: "https://holesky.etherscan.io/",
chainId: 17000,
name: "Holesky",
};
default:
throw new Error(`Unsupported parent chain ID: ${parentChainId}`);
}
}

export async function updateAllConfirmationTimes(): Promise<void> {
const chainIds = getOrbitChainIds();
for (const chainId of chainIds) {
try {
await calculateConfirmationTime(chainId);
} catch (error) {
console.error(
`Failed to update confirmation time for chain ${chainId}:`,
error
);
}
}
}
60 changes: 60 additions & 0 deletions packages/scripts/src/getConfirmationTime/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
export const ROLLUP_ABI = [
{
inputs: [],
name: "latestNodeCreated",
outputs: [{ internalType: "uint64", name: "", type: "uint64" }],
stateMutability: "view",
type: "function",
},
{
inputs: [{ internalType: "uint64", name: "nodeNum", type: "uint64" }],
name: "getNode",
outputs: [
{
components: [
{ internalType: "uint64", name: "prevNum", type: "uint64" },
{ internalType: "uint64", name: "deadlineBlock", type: "uint64" },
{
internalType: "uint64",
name: "noChildConfirmedBeforeBlock",
type: "uint64",
},
{ internalType: "uint64", name: "stakerCount", type: "uint64" },
{
internalType: "uint64",
name: "childProposedBlocks",
type: "uint64",
},
{ internalType: "uint64", name: "firstChildBlock", type: "uint64" },
{ internalType: "uint64", name: "latestChildNumber", type: "uint64" },
{ internalType: "uint64", name: "createdAtBlock", type: "uint64" },
{ internalType: "bytes32", name: "confirmData", type: "bytes32" },
{ internalType: "bytes32", name: "prevHash", type: "bytes32" },
{ internalType: "bytes32", name: "nodeHash", type: "bytes32" },
{ internalType: "bytes32", name: "inboxMaxCount", type: "bytes32" },
],
internalType: "struct Node",
name: "",
type: "tuple",
},
],
stateMutability: "view",
type: "function",
},
];

export interface ParentChainInfo {
rpcUrl: string;
blockExplorer: string;
chainId: number;
name: string;
}

export interface ConfirmationTimeSummary {
chainId: number;
chainName: string;
parentChainId: number;
averageNodeCreationTime: bigint;
estimatedConfirmationTime: number;
usedFallback: boolean;
}
Loading

0 comments on commit 570c87f

Please sign in to comment.