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

Merge pull request #2113 from ethereum-optimism/sc/sdk-finalize-message

feat(sdk): implement finalize message
parents f5691302 400175c9
...@@ -63,6 +63,8 @@ ...@@ -63,6 +63,8 @@
"@eth-optimism/core-utils": "0.7.5", "@eth-optimism/core-utils": "0.7.5",
"@ethersproject/abstract-provider": "^5.5.1", "@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/abstract-signer": "^5.5.0", "@ethersproject/abstract-signer": "^5.5.0",
"ethers": "^5.5.2" "ethers": "^5.5.2",
"merkletreejs": "^0.2.27",
"rlp": "^2.2.7"
} }
} }
...@@ -245,7 +245,7 @@ export class StandardBridgeAdapter implements IBridgeAdapter { ...@@ -245,7 +245,7 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
throw new Error(`token pair not supported by bridge`) throw new Error(`token pair not supported by bridge`)
} }
return this.l1Bridge.depositERC20( return this.l1Bridge.populateTransaction.depositERC20(
toAddress(l1Token), toAddress(l1Token),
toAddress(l2Token), toAddress(l2Token),
amount, amount,
......
/* eslint-disable @typescript-eslint/no-unused-vars */
import { ethers, Overrides, Signer, BigNumber } from 'ethers' import { ethers, Overrides, Signer, BigNumber } from 'ethers'
import { import {
TransactionRequest, TransactionRequest,
...@@ -10,7 +9,6 @@ import { ...@@ -10,7 +9,6 @@ import {
CrossChainMessageRequest, CrossChainMessageRequest,
ICrossChainMessenger, ICrossChainMessenger,
ICrossChainProvider, ICrossChainProvider,
IBridgeAdapter,
MessageLike, MessageLike,
NumberLike, NumberLike,
AddressLike, AddressLike,
...@@ -77,7 +75,9 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -77,7 +75,9 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides overrides?: Overrides
} }
): Promise<TransactionResponse> { ): Promise<TransactionResponse> {
throw new Error('Not implemented') return this.l1Signer.sendTransaction(
await this.populateTransaction.finalizeMessage(message, opts)
)
} }
public async depositETH( public async depositETH(
...@@ -149,9 +149,7 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -149,9 +149,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
} }
): Promise<TransactionRequest> => { ): Promise<TransactionRequest> => {
if (message.direction === MessageDirection.L1_TO_L2) { if (message.direction === MessageDirection.L1_TO_L2) {
return this.provider.contracts.l1.L1CrossDomainMessenger.connect( return this.provider.contracts.l1.L1CrossDomainMessenger.populateTransaction.sendMessage(
this.l1Signer
).populateTransaction.sendMessage(
message.target, message.target,
message.message, message.message,
opts?.l2GasLimit || opts?.l2GasLimit ||
...@@ -159,9 +157,7 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -159,9 +157,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
opts?.overrides || {} opts?.overrides || {}
) )
} else { } else {
return this.provider.contracts.l2.L2CrossDomainMessenger.connect( return this.provider.contracts.l2.L2CrossDomainMessenger.populateTransaction.sendMessage(
this.l2Signer
).populateTransaction.sendMessage(
message.target, message.target,
message.message, message.message,
0, // Gas limit goes unused when sending from L2 to L1 0, // Gas limit goes unused when sending from L2 to L1
...@@ -182,9 +178,7 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -182,9 +178,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
throw new Error(`cannot resend L2 to L1 message`) throw new Error(`cannot resend L2 to L1 message`)
} }
return this.provider.contracts.l1.L1CrossDomainMessenger.connect( return this.provider.contracts.l1.L1CrossDomainMessenger.populateTransaction.replayMessage(
this.l1Signer
).populateTransaction.replayMessage(
resolved.target, resolved.target,
resolved.sender, resolved.sender,
resolved.message, resolved.message,
...@@ -201,7 +195,20 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -201,7 +195,20 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides overrides?: Overrides
} }
): Promise<TransactionRequest> => { ): Promise<TransactionRequest> => {
throw new Error('Not implemented') const resolved = await this.provider.toCrossChainMessage(message)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`cannot finalize L1 to L2 message`)
}
const proof = await this.provider.getMessageProof(resolved)
return this.provider.contracts.l1.L1CrossDomainMessenger.populateTransaction.relayMessage(
resolved.target,
resolved.sender,
resolved.message,
resolved.messageNonce,
proof,
opts?.overrides || {}
)
}, },
depositETH: async ( depositETH: async (
...@@ -297,7 +304,9 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -297,7 +304,9 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides overrides?: Overrides
} }
): Promise<BigNumber> => { ): Promise<BigNumber> => {
throw new Error('Not implemented') return this.provider.l1Provider.estimateGas(
await this.populateTransaction.finalizeMessage(message, opts)
)
}, },
depositETH: async ( depositETH: async (
...@@ -332,7 +341,7 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -332,7 +341,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides overrides?: Overrides
} }
): Promise<BigNumber> => { ): Promise<BigNumber> => {
return this.provider.l2Provider.estimateGas( return this.provider.l1Provider.estimateGas(
await this.populateTransaction.depositERC20( await this.populateTransaction.depositERC20(
l1Token, l1Token,
l2Token, l2Token,
......
...@@ -5,7 +5,7 @@ import { ...@@ -5,7 +5,7 @@ import {
TransactionReceipt, TransactionReceipt,
} from '@ethersproject/abstract-provider' } from '@ethersproject/abstract-provider'
import { ethers, BigNumber } from 'ethers' import { ethers, BigNumber } from 'ethers'
import { sleep } from '@eth-optimism/core-utils' import { sleep, remove0x } from '@eth-optimism/core-utils'
import { import {
ICrossChainProvider, ICrossChainProvider,
...@@ -19,6 +19,7 @@ import { ...@@ -19,6 +19,7 @@ import {
ProviderLike, ProviderLike,
CrossChainMessage, CrossChainMessage,
CrossChainMessageRequest, CrossChainMessageRequest,
CrossChainMessageProof,
MessageDirection, MessageDirection,
MessageStatus, MessageStatus,
TokenBridgeMessage, TokenBridgeMessage,
...@@ -38,6 +39,9 @@ import { ...@@ -38,6 +39,9 @@ import {
getAllOEContracts, getAllOEContracts,
getBridgeAdapters, getBridgeAdapters,
hashCrossChainMessage, hashCrossChainMessage,
makeMerkleTreeProof,
makeStateTrieProof,
encodeCrossChainMessage,
} from './utils' } from './utils'
export class CrossChainProvider implements ICrossChainProvider { export class CrossChainProvider implements ICrossChainProvider {
...@@ -302,7 +306,7 @@ export class CrossChainProvider implements ICrossChainProvider { ...@@ -302,7 +306,7 @@ export class CrossChainProvider implements ICrossChainProvider {
} else { } else {
const challengePeriod = await this.getChallengePeriodSeconds() const challengePeriod = await this.getChallengePeriodSeconds()
const targetBlock = await this.l1Provider.getBlock( const targetBlock = await this.l1Provider.getBlock(
stateRoot.blockNumber stateRoot.batch.blockNumber
) )
const latestBlock = await this.l1Provider.getBlock('latest') const latestBlock = await this.l1Provider.getBlock('latest')
if (targetBlock.timestamp + challengePeriod > latestBlock.timestamp) { if (targetBlock.timestamp + challengePeriod > latestBlock.timestamp) {
...@@ -492,9 +496,9 @@ export class CrossChainProvider implements ICrossChainProvider { ...@@ -492,9 +496,9 @@ export class CrossChainProvider implements ICrossChainProvider {
} }
return { return {
blockNumber: stateRootBatch.blockNumber,
header: stateRootBatch.header,
stateRoot: stateRootBatch.stateRoots[indexInBatch], stateRoot: stateRootBatch.stateRoots[indexInBatch],
stateRootIndexInBatch: indexInBatch,
batch: stateRootBatch,
} }
} }
...@@ -599,4 +603,52 @@ export class CrossChainProvider implements ICrossChainProvider { ...@@ -599,4 +603,52 @@ export class CrossChainProvider implements ICrossChainProvider {
}, },
} }
} }
public async getMessageProof(
message: MessageLike
): Promise<CrossChainMessageProof> {
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`)
}
const stateRoot = await this.getMessageStateRoot(resolved)
if (stateRoot === null) {
throw new Error(`state root for message not yet published`)
}
// We need to calculate the specific storage slot that demonstrates that this message was
// actually included in the L2 chain. The following calculation is based on the fact that
// messages are stored in the following mapping on L2:
// https://github.com/ethereum-optimism/optimism/blob/c84d3450225306abbb39b4e7d6d82424341df2be/packages/contracts/contracts/L2/predeploys/OVM_L2ToL1MessagePasser.sol#L23
// You can read more about how Solidity storage slots are computed for mappings here:
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
const messageSlot = ethers.utils.keccak256(
ethers.utils.keccak256(
encodeCrossChainMessage(resolved) +
remove0x(this.contracts.l2.L2CrossDomainMessenger.address)
) + '00'.repeat(32)
)
const stateTrieProof = await makeStateTrieProof(
this.l2Provider as any,
resolved.blockNumber,
this.contracts.l2.OVM_L2ToL1MessagePasser.address,
messageSlot
)
return {
stateRoot: stateRoot.stateRoot,
stateRootBatchHeader: stateRoot.batch.header,
stateRootProof: {
index: stateRoot.stateRootIndexInBatch,
siblings: makeMerkleTreeProof(
stateRoot.batch.stateRoots,
stateRoot.stateRootIndexInBatch
),
},
stateTrieWitness: stateTrieProof.accountProof,
storageTrieWitness: stateTrieProof.storageProof,
}
}
} }
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
AddressLike, AddressLike,
NumberLike, NumberLike,
CrossChainMessage, CrossChainMessage,
CrossChainMessageProof,
MessageDirection, MessageDirection,
MessageStatus, MessageStatus,
TokenBridgeMessage, TokenBridgeMessage,
...@@ -287,4 +288,12 @@ export interface ICrossChainProvider { ...@@ -287,4 +288,12 @@ export interface ICrossChainProvider {
getStateRootBatchByTransactionIndex( getStateRootBatchByTransactionIndex(
transactionIndex: number transactionIndex: number
): Promise<StateRootBatch | null> ): Promise<StateRootBatch | null>
/**
* Generates the proof required to finalize an L2 to L1 message.
*
* @param message Message to generate a proof for.
* @returns Proof that can be used to finalize the message.
*/
getMessageProof(message: MessageLike): Promise<CrossChainMessageProof>
} }
...@@ -216,9 +216,9 @@ export interface StateRootBatchHeader { ...@@ -216,9 +216,9 @@ export interface StateRootBatchHeader {
* Information about a state root, including header, block number, and root iself. * Information about a state root, including header, block number, and root iself.
*/ */
export interface StateRoot { export interface StateRoot {
blockNumber: number
header: StateRootBatchHeader
stateRoot: string stateRoot: string
stateRootIndexInBatch: number
batch: StateRootBatch
} }
/** /**
...@@ -230,6 +230,20 @@ export interface StateRootBatch { ...@@ -230,6 +230,20 @@ export interface StateRootBatch {
stateRoots: string[] stateRoots: string[]
} }
/**
* Proof data required to finalize an L2 to L1 message.
*/
export interface CrossChainMessageProof {
stateRoot: string
stateRootBatchHeader: StateRootBatchHeader
stateRootProof: {
index: number
siblings: string[]
}
stateTrieWitness: string
storageTrieWitness: string
}
/** /**
* Stuff that can be coerced into a transaction. * Stuff that can be coerced into a transaction.
*/ */
......
...@@ -3,3 +3,4 @@ export * from './contracts' ...@@ -3,3 +3,4 @@ export * from './contracts'
export * from './message-encoding' export * from './message-encoding'
export * from './type-utils' export * from './type-utils'
export * from './misc-utils' export * from './misc-utils'
export * from './merkle-utils'
/* Imports: External */
import { ethers } from 'ethers'
import {
fromHexString,
toHexString,
toRpcHexString,
} from '@eth-optimism/core-utils'
import { MerkleTree } from 'merkletreejs'
import rlp from 'rlp'
/**
* Generates a Merkle proof (using the particular scheme we use within Lib_MerkleTree).
*
* @param leaves Leaves of the merkle tree.
* @param index Index to generate a proof for.
* @returns Merkle proof sibling leaves, as hex strings.
*/
export const makeMerkleTreeProof = (
leaves: string[],
index: number
): string[] => {
// Our specific Merkle tree implementation requires that the number of leaves is a power of 2.
// If the number of given leaves is less than a power of 2, we need to round up to the next
// available power of 2. We fill the remaining space with the hash of bytes32(0).
const correctedTreeSize = Math.pow(2, Math.ceil(Math.log2(leaves.length)))
const parsedLeaves = []
for (let i = 0; i < correctedTreeSize; i++) {
if (i < leaves.length) {
parsedLeaves.push(leaves[i])
} else {
parsedLeaves.push(ethers.utils.keccak256('0x' + '00'.repeat(32)))
}
}
// merkletreejs prefers things to be Buffers.
const bufLeaves = parsedLeaves.map(fromHexString)
const tree = new MerkleTree(bufLeaves, (el: Buffer | string): Buffer => {
return fromHexString(ethers.utils.keccak256(el))
})
const proof = tree.getProof(bufLeaves[index], index).map((element: any) => {
return toHexString(element.data)
})
return proof
}
/**
* Generates a Merkle-Patricia trie proof for a given account and storage slot.
*
* @param provider RPC provider attached to an EVM-compatible chain.
* @param blockNumber Block number to generate the proof at.
* @param address Address to generate the proof for.
* @param slot Storage slot to generate the proof for.
* @returns Account proof and storage proof.
*/
export const makeStateTrieProof = async (
provider: ethers.providers.JsonRpcProvider,
blockNumber: number,
address: string,
slot: string
): Promise<{
accountProof: string
storageProof: string
}> => {
const proof = await provider.send('eth_getProof', [
address,
[slot],
toRpcHexString(blockNumber),
])
return {
accountProof: toHexString(rlp.encode(proof.accountProof)),
storageProof: toHexString(rlp.encode(proof.storageProof[0].proof)),
}
}
...@@ -10920,6 +10920,17 @@ merkletreejs@^0.2.18: ...@@ -10920,6 +10920,17 @@ merkletreejs@^0.2.18:
treeify "^1.1.0" treeify "^1.1.0"
web3-utils "^1.3.4" web3-utils "^1.3.4"
merkletreejs@^0.2.27:
version "0.2.27"
resolved "https://registry.yarnpkg.com/merkletreejs/-/merkletreejs-0.2.27.tgz#0691df1e1c80ebea7e35439dca5d9abd843b21d3"
integrity sha512-6fPGBdfbDyTiprK5JBBAxg+0u33xI3UM8EOeIz7Zy+5czuXH8vOhLMK1hMZFWPdCNgETWkpj+GOMKKhKZPOvaQ==
dependencies:
bignumber.js "^9.0.1"
buffer-reverse "^1.0.1"
crypto-js "^3.1.9-1"
treeify "^1.1.0"
web3-utils "^1.3.4"
methods@^1.1.2, methods@~1.1.2: methods@^1.1.2, methods@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
...@@ -13643,6 +13654,13 @@ rlp@^2.0.0, rlp@^2.2.1, rlp@^2.2.2, rlp@^2.2.3, rlp@^2.2.4, rlp@^2.2.6: ...@@ -13643,6 +13654,13 @@ rlp@^2.0.0, rlp@^2.2.1, rlp@^2.2.2, rlp@^2.2.3, rlp@^2.2.4, rlp@^2.2.6:
dependencies: dependencies:
bn.js "^4.11.1" bn.js "^4.11.1"
rlp@^2.2.7:
version "2.2.7"
resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.7.tgz#33f31c4afac81124ac4b283e2bd4d9720b30beaf"
integrity sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==
dependencies:
bn.js "^5.2.0"
run-async@^2.2.0, run-async@^2.4.0: run-async@^2.2.0, run-async@^2.4.0:
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
......
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