diff --git a/packages/sdk/src/cross-chain-messenger.ts b/packages/sdk/src/cross-chain-messenger.ts index a0027ff488f28..0c19c514e5bfd 100644 --- a/packages/sdk/src/cross-chain-messenger.ts +++ b/packages/sdk/src/cross-chain-messenger.ts @@ -37,7 +37,7 @@ import { } from './interfaces' import { toSignerOrProvider, - toBigNumber, + toNumber, toTransactionHash, DeepPartial, getAllOEContracts, @@ -46,6 +46,8 @@ import { makeMerkleTreeProof, makeStateTrieProof, encodeCrossChainMessage, + DEPOSIT_CONFIRMATION_BLOCKS, + CHAIN_BLOCK_TIMES, } from './utils' export class CrossChainMessenger implements ICrossChainMessenger { @@ -54,6 +56,8 @@ export class CrossChainMessenger implements ICrossChainMessenger { public l1ChainId: number public contracts: OEContracts public bridges: BridgeAdapters + public depositConfirmationBlocks: number + public l1BlockTimeSeconds: number /** * Creates a new CrossChainProvider instance. @@ -62,6 +66,8 @@ export class CrossChainMessenger implements ICrossChainMessenger { * @param opts.l1SignerOrProvider Signer or Provider for the L1 chain, or a JSON-RPC url. * @param opts.l1SignerOrProvider Signer or Provider for the L2 chain, or a JSON-RPC url. * @param opts.l1ChainId Chain ID for the L1 chain. + * @param opts.depositConfirmationBlocks Optional number of blocks before a deposit is confirmed. + * @param opts.l1BlockTimeSeconds Optional estimated block time in seconds for the L1 chain. * @param opts.contracts Optional contract address overrides. * @param opts.bridges Optional bridge address list. */ @@ -69,17 +75,31 @@ export class CrossChainMessenger implements ICrossChainMessenger { l1SignerOrProvider: SignerOrProviderLike l2SignerOrProvider: SignerOrProviderLike l1ChainId: NumberLike + depositConfirmationBlocks?: NumberLike + l1BlockTimeSeconds?: NumberLike contracts?: DeepPartial bridges?: BridgeAdapterData }) { this.l1SignerOrProvider = toSignerOrProvider(opts.l1SignerOrProvider) this.l2SignerOrProvider = toSignerOrProvider(opts.l2SignerOrProvider) - this.l1ChainId = toBigNumber(opts.l1ChainId).toNumber() + this.l1ChainId = toNumber(opts.l1ChainId) + + this.depositConfirmationBlocks = + opts?.depositConfirmationBlocks !== undefined + ? toNumber(opts.depositConfirmationBlocks) + : DEPOSIT_CONFIRMATION_BLOCKS[this.l1ChainId] || 0 + + this.l1BlockTimeSeconds = + opts?.l1BlockTimeSeconds !== undefined + ? toNumber(opts.l1BlockTimeSeconds) + : CHAIN_BLOCK_TIMES[this.l1ChainId] || 1 + this.contracts = getAllOEContracts(this.l1ChainId, { l1SignerOrProvider: this.l1SignerOrProvider, l2SignerOrProvider: this.l2SignerOrProvider, overrides: opts.contracts, }) + this.bridges = getBridgeAdapters(this.l1ChainId, this, { overrides: opts.bridges, }) @@ -478,7 +498,60 @@ export class CrossChainMessenger implements ICrossChainMessenger { public async estimateMessageWaitTimeSeconds( message: MessageLike ): Promise { - throw new Error('Not implemented') + const resolved = await this.toCrossChainMessage(message) + const status = await this.getMessageStatus(resolved) + if (resolved.direction === MessageDirection.L1_TO_L2) { + if ( + status === MessageStatus.RELAYED || + status === MessageStatus.FAILED_L1_TO_L2_MESSAGE + ) { + // Transactions that are relayed or failed are considered completed, so the wait time is 0. + return 0 + } else { + // Otherwise we need to estimate the number of blocks left until the transaction will be + // considered confirmed by the Layer 2 system. Then we multiply this by the estimated + // average L1 block time. + const receipt = await this.l1Provider.getTransactionReceipt( + resolved.transactionHash + ) + const blocksLeft = Math.max( + this.depositConfirmationBlocks - receipt.confirmations, + 0 + ) + return blocksLeft * this.l1BlockTimeSeconds + } + } else { + if ( + status === MessageStatus.RELAYED || + status === MessageStatus.READY_FOR_RELAY + ) { + // Transactions that are relayed or ready for relay are considered complete. + return 0 + } else if (status === MessageStatus.STATE_ROOT_NOT_PUBLISHED) { + // If the state root hasn't been published yet, just assume it'll be published relatively + // quickly and return the challenge period for now. In the future we could use more + // advanced techniques to figure out average time between transaction execution and + // state root publication. + return this.getChallengePeriodSeconds() + } else if (status === MessageStatus.IN_CHALLENGE_PERIOD) { + // If the message is still within the challenge period, then we need to estimate exactly + // the amount of time left until the challenge period expires. The challenge period starts + // when the state root is published. + const stateRoot = await this.getMessageStateRoot(resolved) + const challengePeriod = await this.getChallengePeriodSeconds() + const targetBlock = await this.l1Provider.getBlock( + stateRoot.batch.blockNumber + ) + const latestBlock = await this.l1Provider.getBlock('latest') + return Math.max( + challengePeriod - (latestBlock.timestamp - targetBlock.timestamp), + 0 + ) + } else { + // Should not happen + throw new Error(`unexpected message status`) + } + } } public async getChallengePeriodSeconds(): Promise { diff --git a/packages/sdk/src/interfaces/cross-chain-messenger.ts b/packages/sdk/src/interfaces/cross-chain-messenger.ts index 353d70ef6352d..af73739d6501e 100644 --- a/packages/sdk/src/interfaces/cross-chain-messenger.ts +++ b/packages/sdk/src/interfaces/cross-chain-messenger.ts @@ -76,6 +76,16 @@ export interface ICrossChainMessenger { */ l2Signer: Signer + /** + * Number of blocks before a deposit is considered confirmed. + */ + depositConfirmationBlocks: number + + /** + * Estimated average L1 block time in seconds. + */ + l1BlockTimeSeconds: number + /** * Retrieves all cross chain messages sent within a given transaction. * diff --git a/packages/sdk/src/utils/chain-constants.ts b/packages/sdk/src/utils/chain-constants.ts new file mode 100644 index 0000000000000..36600bad3d13e --- /dev/null +++ b/packages/sdk/src/utils/chain-constants.ts @@ -0,0 +1,22 @@ +export const DEPOSIT_CONFIRMATION_BLOCKS = { + // Mainnet + 1: 50, + // Goerli + 5: 12, + // Kovan + 42: 12, + // Hardhat Local + // 2 just for testing purposes + 31337: 2, +} + +export const CHAIN_BLOCK_TIMES = { + // Mainnet + 1: 13, + // Goerli + 5: 15, + // Kovan + 42: 4, + // Hardhat Local + 31337: 1, +} diff --git a/packages/sdk/src/utils/coercion.ts b/packages/sdk/src/utils/coercion.ts index 3cd39364e8aeb..38d90dc0e75c0 100644 --- a/packages/sdk/src/utils/coercion.ts +++ b/packages/sdk/src/utils/coercion.ts @@ -69,6 +69,16 @@ export const toBigNumber = (num: NumberLike): BigNumber => { return ethers.BigNumber.from(num) } +/** + * Converts a number-like into a number. + * + * @param num Number-like to convert into a number. + * @returns Number-like as a number. + */ +export const toNumber = (num: NumberLike): number => { + return toBigNumber(num).toNumber() +} + /** * Converts an address-like into a 0x-prefixed address string. * diff --git a/packages/sdk/src/utils/index.ts b/packages/sdk/src/utils/index.ts index 86861b18abd77..41dcf3a1375ee 100644 --- a/packages/sdk/src/utils/index.ts +++ b/packages/sdk/src/utils/index.ts @@ -4,3 +4,4 @@ export * from './message-encoding' export * from './type-utils' export * from './misc-utils' export * from './merkle-utils' +export * from './chain-constants' diff --git a/packages/sdk/test/cross-chain-messenger.spec.ts b/packages/sdk/test/cross-chain-messenger.spec.ts index b20b248521c0f..9ee630d6f7d2c 100644 --- a/packages/sdk/test/cross-chain-messenger.spec.ts +++ b/packages/sdk/test/cross-chain-messenger.spec.ts @@ -1281,48 +1281,160 @@ describe('CrossChainMessenger', () => { }) }) - describe('estimateMessageWaitTimeBlocks', () => { - describe('when the message exists', () => { - describe('when the message is an L1 => L2 message', () => { - describe('when the message has not been executed on L2 yet', () => { - it( - 'should return the estimated blocks until the message will be confirmed on L2' + describe('estimateMessageWaitTimeSeconds', () => { + let scc: Contract + let l1Messenger: Contract + let l2Messenger: Contract + let messenger: CrossChainMessenger + beforeEach(async () => { + // TODO: Get rid of the nested awaits here. Could be a good first issue for someone. + scc = (await (await ethers.getContractFactory('MockSCC')).deploy()) as any + l1Messenger = (await ( + await ethers.getContractFactory('MockMessenger') + ).deploy()) as any + l2Messenger = (await ( + await ethers.getContractFactory('MockMessenger') + ).deploy()) as any + + messenger = new CrossChainMessenger({ + l1SignerOrProvider: ethers.provider, + l2SignerOrProvider: ethers.provider, + l1ChainId: 31337, + contracts: { + l1: { + L1CrossDomainMessenger: l1Messenger.address, + StateCommitmentChain: scc.address, + }, + l2: { + L2CrossDomainMessenger: l2Messenger.address, + }, + }, + }) + }) + + const sendAndGetDummyMessage = async (direction: MessageDirection) => { + const mockMessenger = + direction === MessageDirection.L1_TO_L2 ? l1Messenger : l2Messenger + const tx = await mockMessenger.triggerSentMessageEvents([DUMMY_MESSAGE]) + return ( + await messenger.getMessagesByTransaction(tx, { + direction, + }) + )[0] + } + + const submitStateRootBatchForMessage = async ( + message: CrossChainMessage + ) => { + await scc.setSBAParams({ + batchIndex: 0, + batchRoot: ethers.constants.HashZero, + batchSize: 1, + prevTotalElements: message.blockNumber, + extraData: '0x', + }) + await scc.appendStateBatch([ethers.constants.HashZero], 0) + } + + describe('when the message is an L1 => L2 message', () => { + describe('when the message has not been executed on L2 yet', () => { + it('should return the estimated seconds until the message will be confirmed on L2', async () => { + const message = await sendAndGetDummyMessage( + MessageDirection.L1_TO_L2 ) + + await l1Messenger.triggerSentMessageEvents([message]) + + expect( + await messenger.estimateMessageWaitTimeSeconds(message) + ).to.equal(1) }) + }) + + describe('when the message has been executed on L2', () => { + it('should return 0', async () => { + const message = await sendAndGetDummyMessage( + MessageDirection.L1_TO_L2 + ) + + await l1Messenger.triggerSentMessageEvents([message]) + await l2Messenger.triggerRelayedMessageEvents([ + hashCrossChainMessage(message), + ]) - describe('when the message has been executed on L2', () => { - it('should return 0') + expect( + await messenger.estimateMessageWaitTimeSeconds(message) + ).to.equal(0) }) }) + }) - describe('when the message is an L2 => L1 message', () => { - describe('when the state root has not been published', () => { - it( - 'should return the estimated blocks until the state root will be published and pass the challenge period' + describe('when the message is an L2 => L1 message', () => { + describe('when the state root has not been published', () => { + it('should return the estimated seconds until the state root will be published and pass the challenge period', async () => { + const message = await sendAndGetDummyMessage( + MessageDirection.L2_TO_L1 ) + + expect( + await messenger.estimateMessageWaitTimeSeconds(message) + ).to.equal(await messenger.getChallengePeriodSeconds()) }) + }) - describe('when the state root is within the challenge period', () => { - it( - 'should return the estimated blocks until the state root passes the challenge period' + describe('when the state root is within the challenge period', () => { + it('should return the estimated seconds until the state root passes the challenge period', async () => { + const message = await sendAndGetDummyMessage( + MessageDirection.L2_TO_L1 ) + + await submitStateRootBatchForMessage(message) + + const challengePeriod = await messenger.getChallengePeriodSeconds() + ethers.provider.send('evm_increaseTime', [challengePeriod / 2]) + ethers.provider.send('evm_mine', []) + + expect( + await messenger.estimateMessageWaitTimeSeconds(message) + ).to.equal(challengePeriod / 2) }) + }) + + describe('when the state root passes the challenge period', () => { + it('should return 0', async () => { + const message = await sendAndGetDummyMessage( + MessageDirection.L2_TO_L1 + ) + + await submitStateRootBatchForMessage(message) + + const challengePeriod = await messenger.getChallengePeriodSeconds() + ethers.provider.send('evm_increaseTime', [challengePeriod + 1]) + ethers.provider.send('evm_mine', []) - describe('when the state root passes the challenge period', () => { - it('should return 0') + expect( + await messenger.estimateMessageWaitTimeSeconds(message) + ).to.equal(0) }) }) - }) - describe('when the message does not exist', () => { - it('should throw an error') - }) - }) + describe('when the message has been executed', () => { + it('should return 0', async () => { + const message = await sendAndGetDummyMessage( + MessageDirection.L2_TO_L1 + ) - describe('estimateMessageWaitTimeSeconds', () => { - it( - 'should be the result of estimateMessageWaitTimeBlocks multiplied by the L1 block time' - ) + await l2Messenger.triggerSentMessageEvents([message]) + await l1Messenger.triggerRelayedMessageEvents([ + hashCrossChainMessage(message), + ]) + + expect( + await messenger.estimateMessageWaitTimeSeconds(message) + ).to.equal(0) + }) + }) + }) }) describe('sendMessage', () => {