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('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 blocks until the message will be confirmed on L2'
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')
it('should return 0', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L1_TO_L2
)
await l1Messenger.triggerSentMessageEvents([message])
await l2Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
])
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'
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'
it('should return the estimated seconds until the state root passes the challenge period', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
})
describe('when the state root passes the challenge period', () => {
it('should return 0')
})
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 message does not exist', () => {
it('should throw an error')
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', [])
expect(
await messenger.estimateMessageWaitTimeSeconds(message)
).to.equal(0)
})
})
describe('estimateMessageWaitTimeSeconds', () => {
it(
'should be the result of estimateMessageWaitTimeBlocks multiplied by the L1 block time'
describe('when the message has been executed', () => {
it('should return 0', async () => {
const message = await sendAndGetDummyMessage(
MessageDirection.L2_TO_L1
)
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