Skip to content
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
79 changes: 76 additions & 3 deletions packages/sdk/src/cross-chain-messenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
} from './interfaces'
import {
toSignerOrProvider,
toBigNumber,
toNumber,
toTransactionHash,
DeepPartial,
getAllOEContracts,
Expand All @@ -46,6 +46,8 @@ import {
makeMerkleTreeProof,
makeStateTrieProof,
encodeCrossChainMessage,
DEPOSIT_CONFIRMATION_BLOCKS,
CHAIN_BLOCK_TIMES,
} from './utils'

export class CrossChainMessenger implements ICrossChainMessenger {
Expand All @@ -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.
Expand All @@ -62,24 +66,40 @@ 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.
*/
constructor(opts: {
l1SignerOrProvider: SignerOrProviderLike
l2SignerOrProvider: SignerOrProviderLike
l1ChainId: NumberLike
depositConfirmationBlocks?: NumberLike
l1BlockTimeSeconds?: NumberLike
contracts?: DeepPartial<OEContractsLike>
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,
})
Expand Down Expand Up @@ -478,7 +498,60 @@ export class CrossChainMessenger implements ICrossChainMessenger {
public async estimateMessageWaitTimeSeconds(
message: MessageLike
): Promise<number> {
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<number> {
Expand Down
10 changes: 10 additions & 0 deletions packages/sdk/src/interfaces/cross-chain-messenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
22 changes: 22 additions & 0 deletions packages/sdk/src/utils/chain-constants.ts
Original file line number Diff line number Diff line change
@@ -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,
}
10 changes: 10 additions & 0 deletions packages/sdk/src/utils/coercion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './message-encoding'
export * from './type-utils'
export * from './misc-utils'
export * from './merkle-utils'
export * from './chain-constants'
164 changes: 138 additions & 26 deletions packages/sdk/test/cross-chain-messenger.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down