Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support L2 block range lookup for Orbit chains #317

Merged
merged 33 commits into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions src/lib/message/L2ToL1MessageNitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
SignerProviderUtils,
SignerOrProvider,
} from '../dataEntities/signerOrProvider'
import { wait } from '../utils/lib'
import { getBlockRangesForL1Block, isArbitrumChain, wait } from '../utils/lib'
import { getL2Network } from '../dataEntities/networks'
import { NodeCreatedEvent, RollupUserLogic } from '../abi/RollupUserLogic'
import { ArbitrumProvider } from '../utils/arbProvider'
Expand Down Expand Up @@ -204,16 +204,40 @@ export class L2ToL1MessageReaderNitro extends L2ToL1MessageNitro {
nodeNum: BigNumber,
l2Provider: Provider
): Promise<ArbBlock> {
const node = await rollup.getNode(nodeNum)
const { createdAtBlock } = await rollup.getNode(nodeNum)

let createdFromBlock = createdAtBlock
let createdToBlock = createdAtBlock

// If L1 is Arbitrum, then L2 is an Orbit chain.
if (await isArbitrumChain(this.l1Provider)) {
try {
const l2BlockRange = await getBlockRangesForL1Block({
forL1Block: createdAtBlock.toNumber(),
provider: this.l1Provider as JsonRpcProvider,
})
const startBlock = l2BlockRange[0]
const endBlock = l2BlockRange[1]
if (!startBlock || !endBlock) {
throw new Error()
}
createdFromBlock = BigNumber.from(startBlock)
createdToBlock = BigNumber.from(endBlock)
} catch (e) {
// fallback to old method if the new method fails
createdFromBlock = createdAtBlock
createdToBlock = createdAtBlock
}
}

// now get the block hash and sendroot for that node
const eventFetcher = new EventFetcher(rollup.provider)
const logs = await eventFetcher.getEvents(
RollupUserLogic__factory,
t => t.filters.NodeCreated(nodeNum),
{
fromBlock: node.createdAtBlock.toNumber(),
toBlock: node.createdAtBlock.toNumber(),
fromBlock: createdFromBlock.toNumber(),
toBlock: createdToBlock.toNumber(),
address: rollup.address,
}
)
Expand Down
144 changes: 143 additions & 1 deletion src/lib/utils/lib.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Provider } from '@ethersproject/abstract-provider'
import { TransactionReceipt } from '@ethersproject/providers'
import { TransactionReceipt, JsonRpcProvider } from '@ethersproject/providers'
import { ArbSdkError } from '../dataEntities/errors'
import { ArbitrumProvider } from './arbProvider'
import { l2Networks } from '../dataEntities/networks'
import { ArbSys__factory } from '../abi/factories/ArbSys__factory'
import { ARB_SYS_ADDRESS } from '../dataEntities/constants'

export const wait = (ms: number): Promise<void> =>
new Promise(res => setTimeout(res, ms))
Expand Down Expand Up @@ -52,3 +56,141 @@ export const getTransactionReceipt = async (

export const isDefined = <T>(val: T | null | undefined): val is T =>
typeof val !== 'undefined' && val !== null

export const isArbitrumChain = async (provider: Provider): Promise<boolean> => {
try {
await ArbSys__factory.connect(ARB_SYS_ADDRESS, provider).arbOSVersion()
} catch (error) {
return false
}
return true
}
Comment on lines +60 to +67
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might want to do some caching here or in the network object, can do later


type GetFirstBlockForL1BlockProps = {
forL1Block: number
provider: JsonRpcProvider
allowGreater?: boolean
minL2Block?: number
maxL2Block?: number | 'latest'
}

/**
* This function performs a binary search to find the first L2 block that corresponds to a given L1 block number.
* The function returns a Promise that resolves to a number if a block is found, or undefined otherwise.
*
* @param {JsonRpcProvider} provider - The L2 provider to use for the search.
* @param {number} forL1Block - The L1 block number to search for.
* @param {boolean} [allowGreater=false] - Whether to allow the search to go past the specified `forL1Block`.
* @param {number|string} minL2Block - The minimum L2 block number to start the search from. Cannot be below the network's `nitroGenesisBlock`.
* @param {number|string} [maxL2Block='latest'] - The maximum L2 block number to end the search at. Can be a `number` or `'latest'`. `'latest'` is the current block.
* @returns {Promise<number | undefined>} - A Promise that resolves to a number if a block is found, or undefined otherwise.
*/
export async function getFirstBlockForL1Block({
provider,
forL1Block,
allowGreater = false,
minL2Block,
maxL2Block = 'latest',
}: GetFirstBlockForL1BlockProps): Promise<number | undefined> {
if (!(await isArbitrumChain(provider))) {
// Provider is L1.
return forL1Block
}

const arbProvider = new ArbitrumProvider(provider)
const currentArbBlock = await arbProvider.getBlockNumber()
const arbitrumChainId = (await arbProvider.getNetwork()).chainId
const { nitroGenesisBlock } = l2Networks[arbitrumChainId]

async function getL1Block(forL2Block: number) {
const { l1BlockNumber } = await arbProvider.getBlock(forL2Block)
return l1BlockNumber
}

if (!minL2Block) {
minL2Block = nitroGenesisBlock
}

if (maxL2Block === 'latest') {
maxL2Block = currentArbBlock
}

if (minL2Block >= maxL2Block) {
throw new Error(
`'minL2Block' (${minL2Block}) must be lower than 'maxL2Block' (${maxL2Block}).`
)
}

if (minL2Block < nitroGenesisBlock) {
throw new Error(
`'minL2Block' (${minL2Block}) cannot be below 'nitroGenesisBlock', which is ${nitroGenesisBlock} for the current network.`
)
}

let start = minL2Block
let end = maxL2Block

let resultForTargetBlock
let resultForGreaterBlock

while (start <= end) {
// Calculate the midpoint of the current range.
const mid = start + Math.floor((end - start) / 2)

const l1Block = await getL1Block(mid)

// If the midpoint matches the target, we've found a match.
// Adjust the range to search for the first occurrence.
if (l1Block === forL1Block) {
end = mid - 1
} else if (l1Block < forL1Block) {
start = mid + 1
} else {
end = mid - 1
}

// Stores last valid L2 block corresponding to the current, or greater, L1 block.
if (l1Block) {
if (l1Block === forL1Block) {
resultForTargetBlock = mid
}
if (allowGreater && l1Block > forL1Block) {
resultForGreaterBlock = mid
}
}
}

return resultForTargetBlock ?? resultForGreaterBlock
}

export const getBlockRangesForL1Block = async (
props: GetFirstBlockForL1BlockProps
) => {
const arbProvider = new ArbitrumProvider(props.provider)
const currentL2Block = await arbProvider.getBlockNumber()

if (!props.maxL2Block || props.maxL2Block === 'latest') {
props.maxL2Block = currentL2Block
}

const result = await Promise.all([
getFirstBlockForL1Block({ ...props, allowGreater: false }),
getFirstBlockForL1Block({
...props,
forL1Block: props.forL1Block + 1,
allowGreater: true,
}),
])

if (!result[0]) {
// If there's no start of the range, there won't be the end either.
return [undefined, undefined]
}

if (result[0] && result[1]) {
// If both results are defined, we can assume that the previous L2 block for the end of the range will be for 'forL1Block'.
return [result[0], result[1] - 1]
}

return [result[0], props.maxL2Block]
}
136 changes: 136 additions & 0 deletions tests/unit/l2BlocksForL1Block.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { BigNumber } from 'ethers'
import { expect } from 'chai'
import { JsonRpcProvider } from '@ethersproject/providers'
import {
getBlockRangesForL1Block,
getFirstBlockForL1Block,
} from '../../src/lib/utils/lib'
import { ArbitrumProvider } from '../../src/lib/utils/arbProvider'
import { ArbBlock } from '../../src/lib/dataEntities/rpc'

describe('L2 blocks lookup for an L1 block', () => {
const provider = new JsonRpcProvider('https://arb1.arbitrum.io/rpc')
const arbProvider = new ArbitrumProvider(provider)

async function validateL2Blocks({
l2Blocks,
l2BlocksCount,
type = 'number',
}: {
l2Blocks: (number | undefined)[]
l2BlocksCount: number
type?: 'number' | 'undefined'
}) {
if (l2Blocks.length !== l2BlocksCount) {
throw new Error(
`Expected L2 block range to have the array length of ${l2BlocksCount}, got ${l2Blocks.length}.`
)
}

if (l2Blocks.some(block => typeof block !== type)) {
throw new Error(`Expected all blocks to be ${type}.`)
}

if (type === 'undefined') {
return
}

const promises: Promise<ArbBlock>[] = []

l2Blocks.forEach((l2Block, index) => {
if (!l2Block) {
throw new Error('L2 block is undefined.')
}
const isStartBlock = index === 0
promises.push(arbProvider.getBlock(l2Block))
// Search for previous or next block.
promises.push(arbProvider.getBlock(l2Block + (isStartBlock ? -1 : 1)))
})

const [startBlock, blockBeforeStartBlock, endBlock, blockAfterEndBlock] =
await Promise.all(promises).then(result =>
result.map(block => BigNumber.from(block.l1BlockNumber))
)

if (startBlock && blockBeforeStartBlock) {
const startBlockCondition = startBlock.gt(blockBeforeStartBlock)

// Check if Arbitrum start block is the first block for this L1 block.
expect(
startBlockCondition,
`L2 block is not the first block in range for L1 block.`
).to.be.true
}

if (endBlock && blockAfterEndBlock) {
const endBlockCondition = endBlock.lt(blockAfterEndBlock)

// Check if Arbitrum end block is the last block for this L1 block.
expect(
endBlockCondition,
`L2 block is not the last block in range for L1 block.`
).to.be.true
}
}

it('successfully searches for an L2 block range', async function () {
const l2Blocks = await getBlockRangesForL1Block({
provider: arbProvider,
forL1Block: 17926532,
// Expected result: 121907680. Narrows down the range to speed up the search.
minL2Block: 121800000,
maxL2Block: 122000000,
})
await validateL2Blocks({ l2Blocks, l2BlocksCount: 2 })
})

it('fails to search for an L2 block range', async function () {
const l2Blocks = await getBlockRangesForL1Block({
provider: arbProvider,
forL1Block: 17926533,
minL2Block: 121800000,
maxL2Block: 122000000,
})
await validateL2Blocks({ l2Blocks, l2BlocksCount: 2, type: 'undefined' })
})

it('successfully searches for the first L2 block', async function () {
const l2Blocks = [
await getFirstBlockForL1Block({
provider: arbProvider,
forL1Block: 17926532,
// Expected result: 121907680. Narrows down the range to speed up the search.
minL2Block: 121800000,
maxL2Block: 122000000,
}),
]
await validateL2Blocks({ l2Blocks, l2BlocksCount: 1 })
})

it('fails to search for the first L2 block, while not using `allowGreater` flag', async function () {
const l2Blocks = [
await getFirstBlockForL1Block({
provider: arbProvider,
forL1Block: 17926533,
allowGreater: false,
minL2Block: 121800000,
maxL2Block: 122000000,
}),
]
await validateL2Blocks({ l2Blocks, l2BlocksCount: 1, type: 'undefined' })
})

it('successfully searches for the first L2 block, while using `allowGreater` flag', async function () {
const l2Blocks = [
await getFirstBlockForL1Block({
provider: arbProvider,
forL1Block: 17926533,
allowGreater: true,
// Expected result: 121907740. Narrows down the range to speed up the search.
minL2Block: 121800000,
maxL2Block: 122000000,
}),
]
await validateL2Blocks({ l2Blocks, l2BlocksCount: 1 })
})
})