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] ...@@ -22,7 +22,7 @@ const command = args[0]
switch (command) { switch (command) {
case 'decodeVersionedNonce': { case 'decodeVersionedNonce': {
const input = BigNumber.from(args[1]) const input = BigNumber.from(args[1])
const [nonce, version] = decodeVersionedNonce(input) const { nonce, version } = decodeVersionedNonce(input)
const output = utils.defaultAbiCoder.encode( const output = utils.defaultAbiCoder.encode(
['uint256', 'uint256'], ['uint256', 'uint256'],
......
...@@ -10,9 +10,6 @@ const nonceMask = BigNumber.from( ...@@ -10,9 +10,6 @@ const nonceMask = BigNumber.from(
'0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' '0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
) )
export const big0 = BigNumber.from(0)
export const big1 = BigNumber.from(1)
/** /**
* Encodes the version into the nonce. * Encodes the version into the nonce.
* *
...@@ -34,8 +31,16 @@ export const encodeVersionedNonce = ( ...@@ -34,8 +31,16 @@ export const encodeVersionedNonce = (
* *
* @param nonce * @param nonce
*/ */
export const decodeVersionedNonce = (nonce: BigNumber): BigNumber[] => { export const decodeVersionedNonce = (
return [nonce.and(nonceMask), nonce.shr(240)] nonce: BigNumber
): {
version: BigNumber
nonce: BigNumber
} => {
return {
version: nonce.shr(240),
nonce: nonce.and(nonceMask),
}
} }
/** /**
...@@ -104,10 +109,10 @@ export const encodeCrossDomainMessage = ( ...@@ -104,10 +109,10 @@ export const encodeCrossDomainMessage = (
gasLimit: BigNumber, gasLimit: BigNumber,
data: string data: string
) => { ) => {
const [, version] = decodeVersionedNonce(nonce) const { version } = decodeVersionedNonce(nonce)
if (version.eq(big0)) { if (version.eq(0)) {
return encodeCrossDomainMessageV0(target, sender, data, nonce) return encodeCrossDomainMessageV0(target, sender, data, nonce)
} else if (version.eq(big1)) { } else if (version.eq(1)) {
return encodeCrossDomainMessageV1( return encodeCrossDomainMessageV1(
nonce, nonce,
sender, sender,
......
...@@ -6,8 +6,6 @@ import { ...@@ -6,8 +6,6 @@ import {
decodeVersionedNonce, decodeVersionedNonce,
encodeCrossDomainMessageV0, encodeCrossDomainMessageV0,
encodeCrossDomainMessageV1, encodeCrossDomainMessageV1,
big0,
big1,
} from './encoding' } from './encoding'
/** /**
...@@ -34,6 +32,7 @@ export interface OutputRootProof { ...@@ -34,6 +32,7 @@ export interface OutputRootProof {
* Bedrock proof data required to finalize an L2 to L1 message. * Bedrock proof data required to finalize an L2 to L1 message.
*/ */
export interface BedrockCrossChainMessageProof { export interface BedrockCrossChainMessageProof {
l2OutputIndex: number
outputRootProof: OutputRootProof outputRootProof: OutputRootProof
withdrawalProof: string[] withdrawalProof: string[]
} }
...@@ -65,10 +64,10 @@ export const hashCrossDomainMessage = ( ...@@ -65,10 +64,10 @@ export const hashCrossDomainMessage = (
gasLimit: BigNumber, gasLimit: BigNumber,
data: string data: string
) => { ) => {
const [, version] = decodeVersionedNonce(nonce) const { version } = decodeVersionedNonce(nonce)
if (version.eq(big0)) { if (version.eq(0)) {
return hashCrossDomainMessagev0(target, sender, data, nonce) return hashCrossDomainMessagev0(target, sender, data, nonce)
} else if (version.eq(big1)) { } else if (version.eq(1)) {
return hashCrossDomainMessagev1( return hashCrossDomainMessagev1(
nonce, nonce,
sender, sender,
......
...@@ -19,18 +19,19 @@ import { ...@@ -19,18 +19,19 @@ import {
remove0x, remove0x,
toHexString, toHexString,
toRpcHexString, toRpcHexString,
hashWithdrawal,
encodeCrossDomainMessageV0,
hashCrossDomainMessage, hashCrossDomainMessage,
encodeCrossDomainMessageV0,
encodeCrossDomainMessageV1,
L2OutputOracleParameters, L2OutputOracleParameters,
BedrockOutputData, BedrockOutputData,
BedrockCrossChainMessageProof, BedrockCrossChainMessageProof,
decodeVersionedNonce,
encodeVersionedNonce,
} from '@eth-optimism/core-utils' } from '@eth-optimism/core-utils'
import { getContractInterface, predeploys } from '@eth-optimism/contracts' import { getContractInterface, predeploys } from '@eth-optimism/contracts'
import * as rlp from 'rlp' import * as rlp from 'rlp'
import { import {
CoreCrossChainMessage,
OEContracts, OEContracts,
OEContractsLike, OEContractsLike,
MessageLike, MessageLike,
...@@ -53,7 +54,7 @@ import { ...@@ -53,7 +54,7 @@ import {
StateRootBatch, StateRootBatch,
IBridgeAdapter, IBridgeAdapter,
ProvenWithdrawal, ProvenWithdrawal,
WithdrawalEntry, LowLevelMessage,
} from './interfaces' } from './interfaces'
import { import {
toSignerOrProvider, toSignerOrProvider,
...@@ -64,6 +65,7 @@ import { ...@@ -64,6 +65,7 @@ import {
getBridgeAdapters, getBridgeAdapters,
makeMerkleTreeProof, makeMerkleTreeProof,
makeStateTrieProof, makeStateTrieProof,
hashLowLevelMessage,
DEPOSIT_CONFIRMATION_BLOCKS, DEPOSIT_CONFIRMATION_BLOCKS,
CHAIN_BLOCK_TIMES, CHAIN_BLOCK_TIMES,
} from './utils' } from './utils'
...@@ -356,6 +358,131 @@ export class CrossChainMessenger { ...@@ -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( // public async getMessagesByAddress(
// address: AddressLike, // address: AddressLike,
// opts?: { // opts?: {
...@@ -563,20 +690,13 @@ export class CrossChainMessenger { ...@@ -563,20 +690,13 @@ export class CrossChainMessenger {
return MessageStatus.STATE_ROOT_NOT_PUBLISHED return MessageStatus.STATE_ROOT_NOT_PUBLISHED
} }
// Fetch the receipt for the resolved CrossChainMessage // Convert the message to the low level message that was proven.
const _receipt = await this.l2Provider.getTransactionReceipt( const withdrawal = await this.toLowLevelMessage(resolved)
resolved.transactionHash
)
// Get the withdrawal hash for the receipt
const [_, withdrawalHash] = this.getWithdrawalFromReceipt(
_receipt,
resolved
)
// Attempt to fetch the proven withdrawal // Attempt to fetch the proven withdrawal.
const provenWithdrawal = await this.getProvenWithdrawal( const provenWithdrawal =
withdrawalHash await this.contracts.l1.OptimismPortal.provenWithdrawals(
hashLowLevelMessage(withdrawal)
) )
// If the withdrawal hash has not been proven on L1, // If the withdrawal hash has not been proven on L1,
...@@ -1248,9 +1368,7 @@ export class CrossChainMessenger { ...@@ -1248,9 +1368,7 @@ export class CrossChainMessenger {
*/ */
public async getBedrockMessageProof( public async getBedrockMessageProof(
message: MessageLike message: MessageLike
): Promise< ): Promise<BedrockCrossChainMessageProof> {
[BedrockCrossChainMessageProof, BedrockOutputData, CoreCrossChainMessage]
> {
const resolved = await this.toCrossChainMessage(message) const resolved = await this.toCrossChainMessage(message)
if (resolved.direction === MessageDirection.L1_TO_L2) { if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`can only generate proofs for L2 to L1 messages`) throw new Error(`can only generate proofs for L2 to L1 messages`)
...@@ -1261,33 +1379,13 @@ export class CrossChainMessenger { ...@@ -1261,33 +1379,13 @@ export class CrossChainMessenger {
throw new Error(`state root for message not yet published`) throw new Error(`state root for message not yet published`)
} }
const receipt = await this.l2Provider.getTransactionReceipt( const withdrawal = await this.toLowLevelMessage(resolved)
resolved.transactionHash const messageSlot = ethers.utils.keccak256(
) ethers.utils.defaultAbiCoder.encode(
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(
['bytes32', 'uint256'], ['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( const stateTrieProof = await makeStateTrieProof(
this.l2Provider as ethers.providers.JsonRpcProvider, this.l2Provider as ethers.providers.JsonRpcProvider,
...@@ -1296,11 +1394,6 @@ export class CrossChainMessenger { ...@@ -1296,11 +1394,6 @@ export class CrossChainMessenger {
messageSlot 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 ( const block = await (
this.l2Provider as ethers.providers.JsonRpcProvider this.l2Provider as ethers.providers.JsonRpcProvider
).send('eth_getBlockByNumber', [ ).send('eth_getBlockByNumber', [
...@@ -1308,82 +1401,18 @@ export class CrossChainMessenger { ...@@ -1308,82 +1401,18 @@ export class CrossChainMessenger {
false, false,
]) ])
return [ return {
{
outputRootProof: { outputRootProof: {
// TODO: Handle multiple versions in the future
version: ethers.constants.HashZero, version: ethers.constants.HashZero,
stateRoot: block.stateRoot, stateRoot: block.stateRoot,
messagePasserStorageRoot: stateTrieProof.storageRoot, messagePasserStorageRoot: stateTrieProof.storageRoot,
latestBlockhash: block.hash, latestBlockhash: block.hash,
}, },
withdrawalProof: stateTrieProof.storageProof, withdrawalProof: stateTrieProof.storageProof,
}, l2OutputIndex: output.l2OutputIndex,
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,
}
}
} }
} }
// 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 * Sends a given cross chain message. Where the message is sent depends on the direction attached
* to the message itself. * to the message itself.
...@@ -1760,20 +1789,18 @@ export class CrossChainMessenger { ...@@ -1760,20 +1789,18 @@ export class CrossChainMessenger {
) )
} }
const [proof, output, withdrawalTx] = await this.getBedrockMessageProof( const withdrawal = await this.toLowLevelMessage(resolved)
message const proof = await this.getBedrockMessageProof(resolved)
)
return this.contracts.l1.OptimismPortal.populateTransaction.proveWithdrawalTransaction( return this.contracts.l1.OptimismPortal.populateTransaction.proveWithdrawalTransaction(
[ [
withdrawalTx.messageNonce, withdrawal.messageNonce,
withdrawalTx.sender, withdrawal.sender,
withdrawalTx.target, withdrawal.target,
withdrawalTx.value, withdrawal.value,
withdrawalTx.minGasLimit, withdrawal.minGasLimit,
withdrawalTx.message, withdrawal.message,
], ],
output.l2OutputIndex, proof.l2OutputIndex,
[ [
proof.outputRootProof.version, proof.outputRootProof.version,
proof.outputRootProof.stateRoot, proof.outputRootProof.stateRoot,
...@@ -1807,16 +1834,15 @@ export class CrossChainMessenger { ...@@ -1807,16 +1834,15 @@ export class CrossChainMessenger {
} }
if (this.bedrock) { if (this.bedrock) {
const [, , withdrawalTx] = await this.getBedrockMessageProof(message) const withdrawal = await this.toLowLevelMessage(resolved)
return this.contracts.l1.OptimismPortal.populateTransaction.finalizeWithdrawalTransaction( return this.contracts.l1.OptimismPortal.populateTransaction.finalizeWithdrawalTransaction(
[ [
withdrawalTx.messageNonce, withdrawal.messageNonce,
withdrawalTx.sender, withdrawal.sender,
withdrawalTx.target, withdrawal.target,
withdrawalTx.value, withdrawal.value,
withdrawalTx.minGasLimit, withdrawal.minGasLimit,
withdrawalTx.message, withdrawal.message,
], ],
opts?.overrides || {} opts?.overrides || {}
) )
......
...@@ -203,6 +203,12 @@ export interface CrossChainMessage extends CoreCrossChainMessage { ...@@ -203,6 +203,12 @@ export interface CrossChainMessage extends CoreCrossChainMessage {
transactionHash: string 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 * Describes a token withdrawal or deposit, along with the underlying raw cross chain message
* behind the deposit or withdrawal. * behind the deposit or withdrawal.
......
...@@ -4,3 +4,4 @@ export * from './type-utils' ...@@ -4,3 +4,4 @@ export * from './type-utils'
export * from './misc-utils' export * from './misc-utils'
export * from './merkle-utils' export * from './merkle-utils'
export * from './chain-constants' 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