Commit c975c962 authored by smartcontracts's avatar smartcontracts Committed by GitHub

feat(sdk): support legacy withdraws after Bedrock (#4119)

Adds support for finalizing legacy withdrawals after the Bedrock
migration. Cleans up some SDK code at the same time.
parent 6a1abd26
---
'@eth-optimism/core-utils': minor
'@eth-optimism/sdk': minor
---
Add suppory for finalizing legacy withdrawals after the Bedrock migration
......@@ -22,7 +22,7 @@ const command = args[0]
switch (command) {
case 'decodeVersionedNonce': {
const input = BigNumber.from(args[1])
const [nonce, version] = decodeVersionedNonce(input)
const { nonce, version } = decodeVersionedNonce(input)
const output = utils.defaultAbiCoder.encode(
['uint256', 'uint256'],
......
......@@ -10,9 +10,6 @@ const nonceMask = BigNumber.from(
'0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
)
export const big0 = BigNumber.from(0)
export const big1 = BigNumber.from(1)
/**
* Encodes the version into the nonce.
*
......@@ -34,8 +31,16 @@ export const encodeVersionedNonce = (
*
* @param nonce
*/
export const decodeVersionedNonce = (nonce: BigNumber): BigNumber[] => {
return [nonce.and(nonceMask), nonce.shr(240)]
export const decodeVersionedNonce = (
nonce: BigNumber
): {
version: BigNumber
nonce: BigNumber
} => {
return {
version: nonce.shr(240),
nonce: nonce.and(nonceMask),
}
}
/**
......@@ -104,10 +109,10 @@ export const encodeCrossDomainMessage = (
gasLimit: BigNumber,
data: string
) => {
const [, version] = decodeVersionedNonce(nonce)
if (version.eq(big0)) {
const { version } = decodeVersionedNonce(nonce)
if (version.eq(0)) {
return encodeCrossDomainMessageV0(target, sender, data, nonce)
} else if (version.eq(big1)) {
} else if (version.eq(1)) {
return encodeCrossDomainMessageV1(
nonce,
sender,
......
......@@ -6,8 +6,6 @@ import {
decodeVersionedNonce,
encodeCrossDomainMessageV0,
encodeCrossDomainMessageV1,
big0,
big1,
} from './encoding'
/**
......@@ -34,6 +32,7 @@ export interface OutputRootProof {
* Bedrock proof data required to finalize an L2 to L1 message.
*/
export interface BedrockCrossChainMessageProof {
l2OutputIndex: number
outputRootProof: OutputRootProof
withdrawalProof: string[]
}
......@@ -65,10 +64,10 @@ export const hashCrossDomainMessage = (
gasLimit: BigNumber,
data: string
) => {
const [, version] = decodeVersionedNonce(nonce)
if (version.eq(big0)) {
const { version } = decodeVersionedNonce(nonce)
if (version.eq(0)) {
return hashCrossDomainMessagev0(target, sender, data, nonce)
} else if (version.eq(big1)) {
} else if (version.eq(1)) {
return hashCrossDomainMessagev1(
nonce,
sender,
......
......@@ -19,18 +19,19 @@ import {
remove0x,
toHexString,
toRpcHexString,
hashWithdrawal,
encodeCrossDomainMessageV0,
hashCrossDomainMessage,
encodeCrossDomainMessageV0,
encodeCrossDomainMessageV1,
L2OutputOracleParameters,
BedrockOutputData,
BedrockCrossChainMessageProof,
decodeVersionedNonce,
encodeVersionedNonce,
} from '@eth-optimism/core-utils'
import { getContractInterface, predeploys } from '@eth-optimism/contracts'
import * as rlp from 'rlp'
import {
CoreCrossChainMessage,
OEContracts,
OEContractsLike,
MessageLike,
......@@ -53,7 +54,7 @@ import {
StateRootBatch,
IBridgeAdapter,
ProvenWithdrawal,
WithdrawalEntry,
LowLevelMessage,
} from './interfaces'
import {
toSignerOrProvider,
......@@ -64,6 +65,7 @@ import {
getBridgeAdapters,
makeMerkleTreeProof,
makeStateTrieProof,
hashLowLevelMessage,
DEPOSIT_CONFIRMATION_BLOCKS,
CHAIN_BLOCK_TIMES,
} from './utils'
......@@ -356,6 +358,131 @@ export class CrossChainMessenger {
})
}
/**
* Transforms a legacy message into its corresponding Bedrock representation.
*
* @param message Legacy message to transform.
* @returns Bedrock representation of the message.
*/
public async toBedrockCrossChainMessage(
message: MessageLike
): Promise<CrossChainMessage> {
const resolved = await this.toCrossChainMessage(message)
// Bedrock messages are already in the correct format.
const { version } = decodeVersionedNonce(resolved.messageNonce)
if (version.eq(1)) {
return resolved
}
let value = BigNumber.from(0)
if (
resolved.direction === MessageDirection.L2_TO_L1 &&
resolved.sender === this.contracts.l2.L2StandardBridge.address &&
resolved.target === this.contracts.l1.L1StandardBridge.address
) {
try {
;[, , value] =
this.contracts.l1.L1StandardBridge.interface.decodeFunctionData(
'finalizeETHWithdrawal',
resolved.message
)
} catch (err) {
// No problem, not a message with value.
}
}
return {
...resolved,
value,
minGasLimit: BigNumber.from(0),
messageNonce: encodeVersionedNonce(
BigNumber.from(1),
resolved.messageNonce
),
}
}
/**
* Transforms a CrossChainMessenger message into its low-level representation inside the
* L2ToL1MessagePasser contract on L2.
*
* @param message Message to transform.
* @return Transformed message.
*/
public async toLowLevelMessage(
message: MessageLike
): Promise<LowLevelMessage> {
const resolved = await this.toCrossChainMessage(message)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`can only convert L2 to L1 messages to low level`)
}
// We may have to update the message if it's a legacy message.
const { version } = decodeVersionedNonce(resolved.messageNonce)
let updated: CrossChainMessage
if (version.eq(0)) {
updated = await this.toBedrockCrossChainMessage(resolved)
} else {
updated = resolved
}
// We need to figure out the final withdrawal data that was used to compute the withdrawal hash
// inside the L2ToL1Message passer contract. Exact mechanism here depends on whether or not
// this is a legacy message or a new Bedrock message.
let gasLimit: BigNumber
let messageNonce: BigNumber
if (version.eq(0)) {
gasLimit = BigNumber.from(0)
messageNonce = resolved.messageNonce
} else {
const receipt = await this.l2Provider.getTransactionReceipt(
resolved.transactionHash
)
const withdrawals: any[] = []
for (const log of receipt.logs) {
if (log.address === this.contracts.l2.BedrockMessagePasser.address) {
const decoded =
this.contracts.l2.L2ToL1MessagePasser.interface.parseLog(log)
if (decoded.name === 'MessagePassed') {
withdrawals.push(decoded.args)
}
}
}
// Should not happen.
if (withdrawals.length === 0) {
throw new Error(`no withdrawals found in receipt`)
}
// TODO: Add support for multiple withdrawals.
if (withdrawals.length > 1) {
throw new Error(`multiple withdrawals found in receipt`)
}
const withdrawal = withdrawals[0]
messageNonce = withdrawal.nonce
gasLimit = withdrawal.gasLimit
}
return {
messageNonce,
sender: this.contracts.l2.L2CrossDomainMessenger.address,
target: this.contracts.l1.L1CrossDomainMessenger.address,
value: updated.value,
minGasLimit: gasLimit,
message: encodeCrossDomainMessageV1(
updated.messageNonce,
updated.sender,
updated.target,
updated.value,
updated.minGasLimit,
updated.message
),
}
}
// public async getMessagesByAddress(
// address: AddressLike,
// opts?: {
......@@ -563,20 +690,13 @@ export class CrossChainMessenger {
return MessageStatus.STATE_ROOT_NOT_PUBLISHED
}
// Fetch the receipt for the resolved CrossChainMessage
const _receipt = await this.l2Provider.getTransactionReceipt(
resolved.transactionHash
)
// Get the withdrawal hash for the receipt
const [_, withdrawalHash] = this.getWithdrawalFromReceipt(
_receipt,
resolved
)
// Convert the message to the low level message that was proven.
const withdrawal = await this.toLowLevelMessage(resolved)
// Attempt to fetch the proven withdrawal
const provenWithdrawal = await this.getProvenWithdrawal(
withdrawalHash
// Attempt to fetch the proven withdrawal.
const provenWithdrawal =
await this.contracts.l1.OptimismPortal.provenWithdrawals(
hashLowLevelMessage(withdrawal)
)
// If the withdrawal hash has not been proven on L1,
......@@ -1248,9 +1368,7 @@ export class CrossChainMessenger {
*/
public async getBedrockMessageProof(
message: MessageLike
): Promise<
[BedrockCrossChainMessageProof, BedrockOutputData, CoreCrossChainMessage]
> {
): Promise<BedrockCrossChainMessageProof> {
const resolved = await this.toCrossChainMessage(message)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`can only generate proofs for L2 to L1 messages`)
......@@ -1261,33 +1379,13 @@ export class CrossChainMessenger {
throw new Error(`state root for message not yet published`)
}
const receipt = await this.l2Provider.getTransactionReceipt(
resolved.transactionHash
)
const [withdrawal, withdrawalHash] = this.getWithdrawalFromReceipt(
receipt,
resolved
)
// Sanity check
if (withdrawal.MessagePassed.withdrawalHash !== withdrawalHash) {
throw new Error(`Mismatched withdrawal hashes`)
}
// TODO: turn into util
const preimage = ethers.utils.defaultAbiCoder.encode(
const withdrawal = await this.toLowLevelMessage(resolved)
const messageSlot = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
['bytes32', 'uint256'],
[withdrawalHash, ethers.constants.HashZero]
[hashLowLevelMessage(withdrawal)]
)
)
const isMessageSent =
await this.contracts.l2.BedrockMessagePasser.sentMessages(withdrawalHash)
if (!isMessageSent) {
throw new Error(`Withdrawal not initiated on L2`)
}
const messageSlot = ethers.utils.keccak256(preimage)
const stateTrieProof = await makeStateTrieProof(
this.l2Provider as ethers.providers.JsonRpcProvider,
......@@ -1296,11 +1394,6 @@ export class CrossChainMessenger {
messageSlot
)
// Sanity check that the value is set to 1 in the state
if (!stateTrieProof.storageValue.eq(1)) {
throw new Error(`Withdrawal hash ${withdrawalHash} is not set in state`)
}
const block = await (
this.l2Provider as ethers.providers.JsonRpcProvider
).send('eth_getBlockByNumber', [
......@@ -1308,82 +1401,18 @@ export class CrossChainMessenger {
false,
])
return [
{
return {
outputRootProof: {
// TODO: Handle multiple versions in the future
version: ethers.constants.HashZero,
stateRoot: block.stateRoot,
messagePasserStorageRoot: stateTrieProof.storageRoot,
latestBlockhash: block.hash,
},
withdrawalProof: stateTrieProof.storageProof,
},
output,
// TODO(tynes): use better type, typechain?
{
messageNonce: withdrawal.MessagePassed.nonce,
sender: withdrawal.MessagePassed.sender,
target: withdrawal.MessagePassed.target,
value: withdrawal.MessagePassed.value,
minGasLimit: withdrawal.MessagePassed.gasLimit,
message: withdrawal.MessagePassed.data,
},
]
}
/**
* Helper function that gets a withdrawal and a withdrawal hash from the logs
* of a L2 to L2 CrossChainMessage and its transaction receipt.
*
* TODO: Process multiple withdrawals in a single transaction.
*/
public getWithdrawalFromReceipt(
receipt: TransactionReceipt,
message: CrossChainMessage
): [WithdrawalEntry, string] {
// Handle multiple withdrawals in the same tx
const logs: Partial<{ number: WithdrawalEntry }> = {}
for (const [_, log] of Object.entries(receipt.logs)) {
if (log.address === this.contracts.l2.BedrockMessagePasser.address) {
const decoded =
this.contracts.l2.L2ToL1MessagePasser.interface.parseLog(log)
// Find the withdrawal initiated events
if (decoded.name === 'MessagePassed') {
logs[log.logIndex] = {
MessagePassed: decoded.args,
}
}
l2OutputIndex: output.l2OutputIndex,
}
}
// TODO(tynes): be able to handle transactions that do multiple withdrawals
// in a single transaction. Right now just go for the first one.
const withdrawal = Object.values(logs)[0]
if (!withdrawal) {
throw new Error(
`Cannot find withdrawal logs for ${message.transactionHash}`
)
}
const withdrawalHash = hashWithdrawal(
withdrawal.MessagePassed.nonce,
withdrawal.MessagePassed.sender,
withdrawal.MessagePassed.target,
withdrawal.MessagePassed.value,
withdrawal.MessagePassed.gasLimit,
withdrawal.MessagePassed.data
)
if (withdrawalHash !== withdrawal.MessagePassed.withdrawalHash) {
throw new Error(
'Locally computed withdrawal hash is not equal to the withdrawal hash computed on-chain!'
)
}
return [withdrawal, withdrawalHash]
}
/**
* Sends a given cross chain message. Where the message is sent depends on the direction attached
* to the message itself.
......@@ -1760,20 +1789,18 @@ export class CrossChainMessenger {
)
}
const [proof, output, withdrawalTx] = await this.getBedrockMessageProof(
message
)
const withdrawal = await this.toLowLevelMessage(resolved)
const proof = await this.getBedrockMessageProof(resolved)
return this.contracts.l1.OptimismPortal.populateTransaction.proveWithdrawalTransaction(
[
withdrawalTx.messageNonce,
withdrawalTx.sender,
withdrawalTx.target,
withdrawalTx.value,
withdrawalTx.minGasLimit,
withdrawalTx.message,
withdrawal.messageNonce,
withdrawal.sender,
withdrawal.target,
withdrawal.value,
withdrawal.minGasLimit,
withdrawal.message,
],
output.l2OutputIndex,
proof.l2OutputIndex,
[
proof.outputRootProof.version,
proof.outputRootProof.stateRoot,
......@@ -1807,16 +1834,15 @@ export class CrossChainMessenger {
}
if (this.bedrock) {
const [, , withdrawalTx] = await this.getBedrockMessageProof(message)
const withdrawal = await this.toLowLevelMessage(resolved)
return this.contracts.l1.OptimismPortal.populateTransaction.finalizeWithdrawalTransaction(
[
withdrawalTx.messageNonce,
withdrawalTx.sender,
withdrawalTx.target,
withdrawalTx.value,
withdrawalTx.minGasLimit,
withdrawalTx.message,
withdrawal.messageNonce,
withdrawal.sender,
withdrawal.target,
withdrawal.value,
withdrawal.minGasLimit,
withdrawal.message,
],
opts?.overrides || {}
)
......
......@@ -203,6 +203,12 @@ export interface CrossChainMessage extends CoreCrossChainMessage {
transactionHash: string
}
/**
* Describes messages sent inside the L2ToL1MessagePasser on L2. Happens to be the same structure
* as the CoreCrossChainMessage so we'll reuse the type for now.
*/
export type LowLevelMessage = CoreCrossChainMessage
/**
* Describes a token withdrawal or deposit, along with the underlying raw cross chain message
* behind the deposit or withdrawal.
......
......@@ -4,3 +4,4 @@ export * from './type-utils'
export * from './misc-utils'
export * from './merkle-utils'
export * from './chain-constants'
export * from './message-utils'
import { hashWithdrawal } from '@eth-optimism/core-utils'
import { LowLevelMessage } from '../interfaces'
/**
* Utility for hashing a LowLevelMessage object.
*
* @param message LowLevelMessage object to hash.
* @returns Hash of the given LowLevelMessage.
*/
export const hashLowLevelMessage = (message: LowLevelMessage): string => {
return hashWithdrawal(
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message
)
}
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