Commit cb81c4cb authored by Kelvin Fichter's avatar Kelvin Fichter Committed by smartcontracts

feat(sdk): implement message wait time estimation

parent 4ddf58da
......@@ -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<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,
})
......@@ -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> {
......
......@@ -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.
*
......
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,
}
......@@ -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.
*
......
......@@ -4,3 +4,4 @@ export * from './message-encoding'
export * from './type-utils'
export * from './misc-utils'
export * from './merkle-utils'
export * from './chain-constants'
......@@ -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', () => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment