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 @@
"@eth-optimism/core-utils": "0.7.5",
"@ethersproject/abstract-provider": "^5.5.1",
"@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 {
throw new Error(`token pair not supported by bridge`)
}
return this.l1Bridge.depositERC20(
return this.l1Bridge.populateTransaction.depositERC20(
toAddress(l1Token),
toAddress(l2Token),
amount,
......
/* eslint-disable @typescript-eslint/no-unused-vars */
import { ethers, Overrides, Signer, BigNumber } from 'ethers'
import {
TransactionRequest,
......@@ -10,7 +9,6 @@ import {
CrossChainMessageRequest,
ICrossChainMessenger,
ICrossChainProvider,
IBridgeAdapter,
MessageLike,
NumberLike,
AddressLike,
......@@ -77,7 +75,9 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<TransactionResponse> {
throw new Error('Not implemented')
return this.l1Signer.sendTransaction(
await this.populateTransaction.finalizeMessage(message, opts)
)
}
public async depositETH(
......@@ -149,9 +149,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
}
): Promise<TransactionRequest> => {
if (message.direction === MessageDirection.L1_TO_L2) {
return this.provider.contracts.l1.L1CrossDomainMessenger.connect(
this.l1Signer
).populateTransaction.sendMessage(
return this.provider.contracts.l1.L1CrossDomainMessenger.populateTransaction.sendMessage(
message.target,
message.message,
opts?.l2GasLimit ||
......@@ -159,9 +157,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
opts?.overrides || {}
)
} else {
return this.provider.contracts.l2.L2CrossDomainMessenger.connect(
this.l2Signer
).populateTransaction.sendMessage(
return this.provider.contracts.l2.L2CrossDomainMessenger.populateTransaction.sendMessage(
message.target,
message.message,
0, // Gas limit goes unused when sending from L2 to L1
......@@ -182,9 +178,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
throw new Error(`cannot resend L2 to L1 message`)
}
return this.provider.contracts.l1.L1CrossDomainMessenger.connect(
this.l1Signer
).populateTransaction.replayMessage(
return this.provider.contracts.l1.L1CrossDomainMessenger.populateTransaction.replayMessage(
resolved.target,
resolved.sender,
resolved.message,
......@@ -201,7 +195,20 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): 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 (
......@@ -297,7 +304,9 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<BigNumber> => {
throw new Error('Not implemented')
return this.provider.l1Provider.estimateGas(
await this.populateTransaction.finalizeMessage(message, opts)
)
},
depositETH: async (
......@@ -332,7 +341,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
overrides?: Overrides
}
): Promise<BigNumber> => {
return this.provider.l2Provider.estimateGas(
return this.provider.l1Provider.estimateGas(
await this.populateTransaction.depositERC20(
l1Token,
l2Token,
......
......@@ -5,7 +5,7 @@ import {
TransactionReceipt,
} from '@ethersproject/abstract-provider'
import { ethers, BigNumber } from 'ethers'
import { sleep } from '@eth-optimism/core-utils'
import { sleep, remove0x } from '@eth-optimism/core-utils'
import {
ICrossChainProvider,
......@@ -19,6 +19,7 @@ import {
ProviderLike,
CrossChainMessage,
CrossChainMessageRequest,
CrossChainMessageProof,
MessageDirection,
MessageStatus,
TokenBridgeMessage,
......@@ -38,6 +39,9 @@ import {
getAllOEContracts,
getBridgeAdapters,
hashCrossChainMessage,
makeMerkleTreeProof,
makeStateTrieProof,
encodeCrossChainMessage,
} from './utils'
export class CrossChainProvider implements ICrossChainProvider {
......@@ -302,7 +306,7 @@ export class CrossChainProvider implements ICrossChainProvider {
} else {
const challengePeriod = await this.getChallengePeriodSeconds()
const targetBlock = await this.l1Provider.getBlock(
stateRoot.blockNumber
stateRoot.batch.blockNumber
)
const latestBlock = await this.l1Provider.getBlock('latest')
if (targetBlock.timestamp + challengePeriod > latestBlock.timestamp) {
......@@ -492,9 +496,9 @@ export class CrossChainProvider implements ICrossChainProvider {
}
return {
blockNumber: stateRootBatch.blockNumber,
header: stateRootBatch.header,
stateRoot: stateRootBatch.stateRoots[indexInBatch],
stateRootIndexInBatch: indexInBatch,
batch: stateRootBatch,
}
}
......@@ -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 {
AddressLike,
NumberLike,
CrossChainMessage,
CrossChainMessageProof,
MessageDirection,
MessageStatus,
TokenBridgeMessage,
......@@ -287,4 +288,12 @@ export interface ICrossChainProvider {
getStateRootBatchByTransactionIndex(
transactionIndex: number
): 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 {
* Information about a state root, including header, block number, and root iself.
*/
export interface StateRoot {
blockNumber: number
header: StateRootBatchHeader
stateRoot: string
stateRootIndexInBatch: number
batch: StateRootBatch
}
/**
......@@ -230,6 +230,20 @@ export interface StateRootBatch {
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.
*/
......
......@@ -3,3 +3,4 @@ export * from './contracts'
export * from './message-encoding'
export * from './type-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:
treeify "^1.1.0"
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:
version "1.1.2"
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:
dependencies:
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:
version "2.4.1"
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