-
Notifications
You must be signed in to change notification settings - Fork 202
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adds estimation for confirmation time
- Loading branch information
Showing
6 changed files
with
570 additions
and
165 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.