diff --git a/.changeset/real-bikes-shave.md b/.changeset/real-bikes-shave.md new file mode 100644 index 0000000000000..e523f87d5917d --- /dev/null +++ b/.changeset/real-bikes-shave.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/sdk': minor +--- + +Adds Bedrock support to the SDK diff --git a/.changeset/wet-rivers-perform.md b/.changeset/wet-rivers-perform.md new file mode 100644 index 0000000000000..36d6c4416a48d --- /dev/null +++ b/.changeset/wet-rivers-perform.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/contracts-bedrock': patch +--- + +Emit SentMessageV2 event with more information diff --git a/packages/contracts-bedrock/.gitignore b/packages/contracts-bedrock/.gitignore index 6ddf84217a0f3..f137310b8b7af 100644 --- a/packages/contracts-bedrock/.gitignore +++ b/packages/contracts-bedrock/.gitignore @@ -7,3 +7,4 @@ coverage.out deployments broadcast genesis.json +src/contract-artifacts.ts diff --git a/packages/contracts-bedrock/contracts/L1/L2OutputOracle.sol b/packages/contracts-bedrock/contracts/L1/L2OutputOracle.sol index c438a34696eee..dc67c5c229043 100644 --- a/packages/contracts-bedrock/contracts/L1/L2OutputOracle.sol +++ b/packages/contracts-bedrock/contracts/L1/L2OutputOracle.sol @@ -280,7 +280,7 @@ contract L2OutputOracle is OwnableUpgradeable { ); return - STARTING_TIMESTAMP + ((_l2BlockNumber - STARTING_BLOCK_NUMBER) * SUBMISSION_INTERVAL); + STARTING_TIMESTAMP + ((_l2BlockNumber - STARTING_BLOCK_NUMBER) * L2_BLOCK_TIME); } /** diff --git a/packages/contracts-bedrock/contracts/test/CommonTest.t.sol b/packages/contracts-bedrock/contracts/test/CommonTest.t.sol index 26d083dc6e3dc..e03079a89ba5b 100644 --- a/packages/contracts-bedrock/contracts/test/CommonTest.t.sol +++ b/packages/contracts-bedrock/contracts/test/CommonTest.t.sol @@ -134,6 +134,11 @@ contract Messenger_Initializer is L2OutputOracle_Initializer { uint256 gasLimit ); + event SentMessageExtraData( + address indexed sender, + uint256 value + ); + event WithdrawalInitiated( uint256 indexed nonce, address indexed sender, diff --git a/packages/contracts-bedrock/contracts/test/L1CrossDomainMessenger.t.sol b/packages/contracts-bedrock/contracts/test/L1CrossDomainMessenger.t.sol index 13b922af24b59..763756bebc4eb 100644 --- a/packages/contracts-bedrock/contracts/test/L1CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/contracts/test/L1CrossDomainMessenger.t.sol @@ -76,7 +76,7 @@ contract L1CrossDomainMessenger_Test is Messenger_Initializer { OptimismPortal.depositTransaction.selector, Lib_PredeployAddresses.L2_CROSS_DOMAIN_MESSENGER, 0, - 100 + L1Messenger.baseGas(hex"ff"), + L1Messenger.baseGas(hex"ff", 100), false, CrossDomainHashing.getVersionedEncoding( L1Messenger.messageNonce(), @@ -96,7 +96,7 @@ contract L1CrossDomainMessenger_Test is Messenger_Initializer { Lib_PredeployAddresses.L2_CROSS_DOMAIN_MESSENGER, 0, 0, - 100 + L1Messenger.baseGas(hex"ff"), + L1Messenger.baseGas(hex"ff", 100), false, CrossDomainHashing.getVersionedEncoding( L1Messenger.messageNonce(), @@ -112,6 +112,13 @@ contract L1CrossDomainMessenger_Test is Messenger_Initializer { vm.expectEmit(true, true, true, true); emit SentMessage(recipient, alice, hex"ff", L1Messenger.messageNonce(), 100); + // SentMessageExtraData event + vm.expectEmit(true, true, true, true); + emit SentMessageExtraData( + alice, + 0 + ); + vm.prank(alice); L1Messenger.sendMessage(recipient, hex"ff", uint32(100)); } diff --git a/packages/contracts-bedrock/contracts/test/L2CrossDomainMessenger.t.sol b/packages/contracts-bedrock/contracts/test/L2CrossDomainMessenger.t.sol index 8d64a62471b67..f0a6a5cdbefa0 100644 --- a/packages/contracts-bedrock/contracts/test/L2CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/contracts/test/L2CrossDomainMessenger.t.sol @@ -44,7 +44,7 @@ contract L2CrossDomainMessenger_Test is Messenger_Initializer { abi.encodeWithSelector( L2ToL1MessagePasser.initiateWithdrawal.selector, address(L1Messenger), - 100 + L2Messenger.baseGas(hex"ff"), + L2Messenger.baseGas(hex"ff", 100), CrossDomainHashing.getVersionedEncoding( L2Messenger.messageNonce(), alice, @@ -63,7 +63,7 @@ contract L2CrossDomainMessenger_Test is Messenger_Initializer { address(L2Messenger), address(L1Messenger), 0, - 100 + L2Messenger.baseGas(hex"ff"), + L2Messenger.baseGas(hex"ff", 100), CrossDomainHashing.getVersionedEncoding( L2Messenger.messageNonce(), alice, diff --git a/packages/contracts-bedrock/contracts/universal/CrossDomainMessenger.sol b/packages/contracts-bedrock/contracts/universal/CrossDomainMessenger.sol index 007c1a61f26cb..4eef1ab036b99 100644 --- a/packages/contracts-bedrock/contracts/universal/CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/contracts/universal/CrossDomainMessenger.sol @@ -42,6 +42,8 @@ abstract contract CrossDomainMessenger is uint256 gasLimit ); + event SentMessageExtraData(address indexed sender, uint256 value); + event RelayedMessage(bytes32 indexed msgHash); event FailedRelayedMessage(bytes32 indexed msgHash); @@ -52,9 +54,13 @@ abstract contract CrossDomainMessenger is uint16 public constant MESSAGE_VERSION = 1; - uint32 public constant MIN_GAS_DYNAMIC_OVERHEAD = 1; + uint32 public constant MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR = 1016; + + uint32 public constant MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR = 1000; + + uint32 public constant MIN_GAS_CALLDATA_OVERHEAD = 16; - uint32 public constant MIN_GAS_CONSTANT_OVERHEAD = 100_000; + uint32 public constant MIN_GAS_CONSTANT_OVERHEAD = 200_000; /// @notice Minimum amount of gas required prior to relaying a message. uint256 internal constant RELAY_GAS_REQUIRED = 45_000; @@ -143,11 +149,15 @@ abstract contract CrossDomainMessenger is * @param _message Message to compute base gas for. * @return Base gas required for message. */ - function baseGas(bytes memory _message) public pure returns (uint32) { + function baseGas(bytes memory _message, uint32 _minGasLimit) public pure returns (uint32) { // TODO: Values here are meant to be good enough to get a devnet running. We need to do // some simple experimentation with the smallest and largest possible message sizes to find // the correct constant and dynamic overhead values. - return (uint32(_message.length) * MIN_GAS_DYNAMIC_OVERHEAD) + MIN_GAS_CONSTANT_OVERHEAD; + return + ((_minGasLimit * MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR) / + MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR) + + (uint32(_message.length) * MIN_GAS_CALLDATA_OVERHEAD) + + MIN_GAS_CONSTANT_OVERHEAD; } /** @@ -166,7 +176,7 @@ abstract contract CrossDomainMessenger is // the minimum gas limit specified by the user. _sendMessage( otherMessenger, - _minGasLimit + baseGas(_message), + baseGas(_message, _minGasLimit), msg.value, abi.encodeWithSelector( this.relayMessage.selector, @@ -180,6 +190,7 @@ abstract contract CrossDomainMessenger is ); emit SentMessage(_target, msg.sender, _message, messageNonce(), _minGasLimit); + emit SentMessageExtraData(msg.sender, msg.value); unchecked { ++msgNonce; diff --git a/packages/contracts-bedrock/deploy/002-L1CrossDomainMessenger.ts b/packages/contracts-bedrock/deploy/002-L1CrossDomainMessenger.ts index 723141a1b37b5..6b6915ba88211 100644 --- a/packages/contracts-bedrock/deploy/002-L1CrossDomainMessenger.ts +++ b/packages/contracts-bedrock/deploy/002-L1CrossDomainMessenger.ts @@ -27,7 +27,7 @@ const deployFn: DeployFunction = async (hre) => { const proxy = await hre.deployments.get('L1CrossDomainMessengerProxy') const Proxy = await hre.ethers.getContractAt('Proxy', proxy.address) const messenger = await hre.deployments.get('L1CrossDomainMessenger') - const portal = await hre.deployments.get('OptimismPortal') + const portal = await hre.deployments.get('OptimismPortalProxy') const L1CrossDomainMessenger = await hre.ethers.getContractAt( 'L1CrossDomainMessenger', diff --git a/packages/contracts-bedrock/package.json b/packages/contracts-bedrock/package.json index 021974aa3e35d..63d559c2c7938 100644 --- a/packages/contracts-bedrock/package.json +++ b/packages/contracts-bedrock/package.json @@ -9,15 +9,16 @@ "dist/**/*.js", "dist/**/*.d.ts", "dist/types/*.ts", - "artifacts/src/**/*.json", + "artifacts/contracts/**/*.json", "deployments/**/*.json", "contracts/**/*.sol" ], "scripts": { "build:forge": "forge build", "prebuild": "yarn ts-node scripts/verifyFoundryInstall.ts", - "build": "hardhat compile && tsc && hardhat typechain", + "build": "hardhat compile && tsc && hardhat typechain && yarn autogen:artifacts", "build:ts": "tsc", + "autogen:artifacts": "ts-node scripts/generate-artifacts.ts", "deploy": "hardhat deploy", "test": "forge test", "gas-snapshot": "forge snapshot", @@ -59,6 +60,7 @@ "dotenv": "^16.0.0", "ethereum-waffle": "^3.0.0", "ethers": "^5.6.8", + "glob": "^7.1.6", "hardhat-deploy": "^0.11.4", "solhint": "^3.3.6", "solhint-plugin-prettier": "^0.0.5", diff --git a/packages/contracts-bedrock/scripts/generate-artifacts.ts b/packages/contracts-bedrock/scripts/generate-artifacts.ts new file mode 100644 index 0000000000000..05d4fd86ab9d0 --- /dev/null +++ b/packages/contracts-bedrock/scripts/generate-artifacts.ts @@ -0,0 +1,67 @@ +import path from 'path' +import fs from 'fs' + +import glob from 'glob' + +/** + * Script for automatically generating a file which has a series of `require` statements for + * importing JSON contract artifacts. We do this to preserve browser compatibility. + */ +const main = async () => { + const contractArtifactsFolder = path.resolve( + __dirname, + `../artifacts/contracts` + ) + + const artifactPaths = glob + .sync(`${contractArtifactsFolder}/**/*.json`) + .filter((match) => { + // Filter out the debug outputs. + return !match.endsWith('.dbg.json') + }) + + const content = ` + /* eslint-disable @typescript-eslint/no-var-requires, no-empty */ + /* + THIS FILE IS AUTOMATICALLY GENERATED. + DO NOT EDIT. + */ + + ${artifactPaths + .map((artifactPath) => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const artifact = require(artifactPath) + // handles the case - '\u' (\utils folder) is considered as an unicode encoded char + const pattern = /\\/g + const relPath = path + .relative(__dirname, artifactPath) + .replace(pattern, '/') + return ` + let ${artifact.contractName} + try { + ${artifact.contractName} = require('${relPath}') + } catch {} + ` + }) + .join('\n')} + + export const getContractArtifact = (name: string): any => { + return { + ${artifactPaths + .map((artifactPath) => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const artifact = require(artifactPath) + return `${artifact.contractName}` + }) + .join(',\n')} + }[name] + } + ` + + fs.writeFileSync( + path.resolve(__dirname, `../src/contract-artifacts.ts`), + content + ) +} + +main() diff --git a/packages/contracts-bedrock/src/contract-defs.ts b/packages/contracts-bedrock/src/contract-defs.ts new file mode 100644 index 0000000000000..2bd0a6f17eb20 --- /dev/null +++ b/packages/contracts-bedrock/src/contract-defs.ts @@ -0,0 +1,52 @@ +import { ethers } from 'ethers' + +/** + * Gets the hardhat artifact for the given contract name. + * Will throw an error if the contract artifact is not found. + * + * @param name Contract name. + * @returns The artifact for the given contract name. + */ +export const getContractDefinition = (name: string): any => { + // We import this using `require` because hardhat tries to build this file when compiling + // the contracts, but we need the contracts to be compiled before the contract-artifacts.ts + // file can be generated. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { getContractArtifact } = require('./contract-artifacts') + const artifact = getContractArtifact(name) + if (artifact === undefined) { + throw new Error(`Unable to find artifact for contract: ${name}`) + } + return artifact +} + +/** + * Gets an ethers Interface instance for the given contract name. + * + * @param name Contract name. + * @returns The interface for the given contract name. + */ +export const getContractInterface = (name: string): ethers.utils.Interface => { + const definition = getContractDefinition(name) + return new ethers.utils.Interface(definition.abi) +} + +/** + * Gets an ethers ContractFactory instance for the given contract name. + * + * @param name Contract name. + * @param signer The signer for the ContractFactory to use. + * @returns The contract factory for the given contract name. + */ +export const getContractFactory = ( + name: string, + signer?: ethers.Signer +): ethers.ContractFactory => { + const definition = getContractDefinition(name) + const contractInterface = getContractInterface(name) + return new ethers.ContractFactory( + contractInterface, + definition.bytecode, + signer + ) +} diff --git a/packages/contracts-bedrock/src/index.ts b/packages/contracts-bedrock/src/index.ts index 4ea5798fa7724..32054cbdf7cc4 100644 --- a/packages/contracts-bedrock/src/index.ts +++ b/packages/contracts-bedrock/src/index.ts @@ -1,3 +1,4 @@ export * from './utils' export * from './generateProofs' export * from './constants' +export * from './contract-defs' diff --git a/packages/contracts-bedrock/tasks/genesis-l1.ts b/packages/contracts-bedrock/tasks/genesis-l1.ts index dd22245423d6e..4eb26d4970b4e 100644 --- a/packages/contracts-bedrock/tasks/genesis-l1.ts +++ b/packages/contracts-bedrock/tasks/genesis-l1.ts @@ -82,7 +82,7 @@ task('genesis-l1', 'create a genesis config') berlinBlock: 0, londonBlock: 0, clique: { - period: 15, + period: 5, epoch: 30000, }, }, diff --git a/packages/contracts-bedrock/tasks/genesis-l2.ts b/packages/contracts-bedrock/tasks/genesis-l2.ts index d189248c5edb8..3a027ea7d9d48 100644 --- a/packages/contracts-bedrock/tasks/genesis-l2.ts +++ b/packages/contracts-bedrock/tasks/genesis-l2.ts @@ -168,6 +168,25 @@ task('genesis-l2', 'create a genesis config') } if (predeployAddrs.has(ethers.utils.getAddress(addr))) { + const predeploy = Object.entries(predeploys).find(([name, address]) => { + return ethers.utils.getAddress(address) === addr + }) + + // Really shouldn't happen, since predeployAddrs is a set generated from predeploys. + if (predeploy === undefined) { + throw new Error('could not find address') + } + + const name = predeploy[0] + if (variables[name]) { + const storageLayout = await getStorageLayout(hre, name) + const slots = computeStorageSlots(storageLayout, variables[name]) + + for (const slot of slots) { + alloc[addr].storage[slot.key] = slot.val + } + } + alloc[addr].storage[implementationSlot] = toCodeAddr(addr) } } @@ -245,13 +264,6 @@ task('genesis-l2', 'create a genesis config') code: artifact.deployedBytecode, storage: {}, } - - const storageLayout = await getStorageLayout(hre, name) - const slots = computeStorageSlots(storageLayout, variables[name]) - - for (const slot of slots) { - alloc[allocAddr].storage[slot.key] = slot.val - } } const genesis: OptimismGenesis = { diff --git a/packages/sdk/hardhat.config.ts b/packages/sdk/hardhat.config.ts index aeea1c1de447b..d31949386f0ea 100644 --- a/packages/sdk/hardhat.config.ts +++ b/packages/sdk/hardhat.config.ts @@ -5,7 +5,7 @@ import '@nomiclabs/hardhat-waffle' const config: HardhatUserConfig = { solidity: { - version: '0.8.9', + version: '0.8.10', }, paths: { sources: './test/contracts', diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 6eaa9d152e986..3d966b1fdc79a 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -47,6 +47,7 @@ }, "dependencies": { "@eth-optimism/contracts": "0.5.29", + "@eth-optimism/contracts-bedrock": "0.4.1", "@eth-optimism/core-utils": "0.9.0", "lodash": "^4.17.21", "merkletreejs": "^0.2.27", diff --git a/packages/sdk/src/adapters/eth-bridge.ts b/packages/sdk/src/adapters/eth-bridge.ts index 028535bfcfe35..266d4d4b49dd5 100644 --- a/packages/sdk/src/adapters/eth-bridge.ts +++ b/packages/sdk/src/adapters/eth-bridge.ts @@ -139,7 +139,7 @@ export class ETHBridgeAdapter extends StandardBridgeAdapter { if (opts?.recipient === undefined) { return this.l1Bridge.populateTransaction.depositETH( - opts?.l2GasLimit || 200_000, // Default to 200k gas limit. + opts?.l2GasLimit || 1_000_000, // Default to 200k gas limit. '0x', // No data. { ...omit(opts?.overrides || {}, 'value'), @@ -149,7 +149,7 @@ export class ETHBridgeAdapter extends StandardBridgeAdapter { } else { return this.l1Bridge.populateTransaction.depositETHTo( toAddress(opts.recipient), - opts?.l2GasLimit || 200_000, // Default to 200k gas limit. + opts?.l2GasLimit || 1_000_000, // Default to 200k gas limit. '0x', // No data. { ...omit(opts?.overrides || {}, 'value'), @@ -178,7 +178,10 @@ export class ETHBridgeAdapter extends StandardBridgeAdapter { amount, 0, // L1 gas not required. '0x', // No data. - opts?.overrides || {} + { + ...omit(opts?.overrides || {}, 'value'), + value: this.messenger.bedrock ? amount : 0, + } ) } else { return this.l2Bridge.populateTransaction.withdrawTo( @@ -187,7 +190,10 @@ export class ETHBridgeAdapter extends StandardBridgeAdapter { amount, 0, // L1 gas not required. '0x', // No data. - opts?.overrides || {} + { + ...omit(opts?.overrides || {}, 'value'), + value: this.messenger.bedrock ? amount : 0, + } ) } }, diff --git a/packages/sdk/src/adapters/standard-bridge.ts b/packages/sdk/src/adapters/standard-bridge.ts index 63db67de35717..f1303348dfe50 100644 --- a/packages/sdk/src/adapters/standard-bridge.ts +++ b/packages/sdk/src/adapters/standard-bridge.ts @@ -13,6 +13,7 @@ import { BlockTag, } from '@ethersproject/abstract-provider' import { getContractInterface, predeploys } from '@eth-optimism/contracts' +import { getContractInterface as getContractInterfaceBedrock } from '@eth-optimism/contracts-bedrock' import { hexStringEquals } from '@eth-optimism/core-utils' import { @@ -49,12 +50,12 @@ export class StandardBridgeAdapter implements IBridgeAdapter { this.messenger = opts.messenger this.l1Bridge = new Contract( toAddress(opts.l1Bridge), - getContractInterface('L1StandardBridge'), + getContractInterfaceBedrock('L1StandardBridge'), this.messenger.l1Provider ) this.l2Bridge = new Contract( toAddress(opts.l2Bridge), - getContractInterface('IL2ERC20Bridge'), + getContractInterfaceBedrock('L2StandardBridge'), this.messenger.l2Provider ) } diff --git a/packages/sdk/src/cross-chain-messenger.ts b/packages/sdk/src/cross-chain-messenger.ts index 65ee4080d4819..999871bdd567c 100644 --- a/packages/sdk/src/cross-chain-messenger.ts +++ b/packages/sdk/src/cross-chain-messenger.ts @@ -8,8 +8,14 @@ import { } from '@ethersproject/abstract-provider' import { Signer } from '@ethersproject/abstract-signer' import { ethers, BigNumber, Overrides, CallOverrides } from 'ethers' -import { sleep, remove0x } from '@eth-optimism/core-utils' -import { predeploys } from '@eth-optimism/contracts' +import { + sleep, + remove0x, + toHexString, + toRpcHexString, +} from '@eth-optimism/core-utils' +import { predeploys, getContractInterface } from '@eth-optimism/contracts' +import * as rlp from 'rlp' import { ICrossChainMessenger, @@ -34,6 +40,9 @@ import { StateRoot, StateRootBatch, IBridgeAdapter, + BedrockCrossChainMessageProof, + BedrockOutputData, + L2OutputOracleParameters, } from './interfaces' import { toSignerOrProvider, @@ -48,6 +57,7 @@ import { encodeCrossChainMessage, DEPOSIT_CONFIRMATION_BLOCKS, CHAIN_BLOCK_TIMES, + hashWithdrawal, } from './utils' export class CrossChainMessenger implements ICrossChainMessenger { @@ -59,6 +69,8 @@ export class CrossChainMessenger implements ICrossChainMessenger { public bridges: BridgeAdapters public depositConfirmationBlocks: number public l1BlockTimeSeconds: number + public bedrock: boolean + private _l2OutputOracleParameters: L2OutputOracleParameters /** * Creates a new CrossChainProvider instance. @@ -72,6 +84,7 @@ export class CrossChainMessenger implements ICrossChainMessenger { * @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. + * @param opts.bedrock Optionally enable Bedrock mode. */ constructor(opts: { l1SignerOrProvider: SignerOrProviderLike @@ -82,11 +95,13 @@ export class CrossChainMessenger implements ICrossChainMessenger { l1BlockTimeSeconds?: NumberLike contracts?: DeepPartial bridges?: BridgeAdapterData + bedrock?: boolean }) { this.l1SignerOrProvider = toSignerOrProvider(opts.l1SignerOrProvider) this.l2SignerOrProvider = toSignerOrProvider(opts.l2SignerOrProvider) this.l1ChainId = toNumber(opts.l1ChainId) this.l2ChainId = toNumber(opts.l2ChainId) + this.bedrock = opts.bedrock ?? false this.depositConfirmationBlocks = opts?.depositConfirmationBlocks !== undefined @@ -141,6 +156,26 @@ export class CrossChainMessenger implements ICrossChainMessenger { } } + public async getL2OutputOracleParameters(): Promise { + if (this._l2OutputOracleParameters) { + return this._l2OutputOracleParameters + } + + this._l2OutputOracleParameters = { + submissionInterval: ( + await this.contracts.l1.L2OutputOracle.SUBMISSION_INTERVAL() + ).toNumber(), + startingBlockNumber: ( + await this.contracts.l1.L2OutputOracle.STARTING_BLOCK_NUMBER() + ).toNumber(), + l2BlockTime: ( + await this.contracts.l1.L2OutputOracle.L2_BLOCK_TIME() + ).toNumber(), + } + + return this._l2OutputOracleParameters + } + public async getMessagesByTransaction( transaction: TransactionLike, opts: { @@ -193,6 +228,19 @@ export class CrossChainMessenger implements ICrossChainMessenger { return parsed.name === 'SentMessage' }) .map((log) => { + // Try to pull out the value field, but only if the very next log is a SentMessageExtraData + // event which was introduced in the Bedrock upgrade. + let value = ethers.BigNumber.from(0) + if (receipt.logs.length > log.logIndex + 1) { + const next = receipt.logs[log.logIndex + 1] + if (next.address === messenger.address) { + const nextParsed = messenger.interface.parseLog(next) + if (nextParsed.name === 'SentMessageExtraData') { + value = nextParsed.args.value + } + } + } + // Convert each SentMessage log into a message object const parsed = messenger.interface.parseLog(log) return { @@ -201,7 +249,8 @@ export class CrossChainMessenger implements ICrossChainMessenger { sender: parsed.args.sender, message: parsed.args.message, messageNonce: parsed.args.messageNonce, - gasLimit: parsed.args.gasLimit, + value, + minGasLimit: parsed.args.gasLimit, logIndex: log.logIndex, blockNumber: log.blockNumber, transactionHash: log.transactionHash, @@ -363,20 +412,31 @@ export class CrossChainMessenger implements ICrossChainMessenger { } } else { if (receipt === null) { - const stateRoot = await this.getMessageStateRoot(resolved) - if (stateRoot === null) { - return MessageStatus.STATE_ROOT_NOT_PUBLISHED + let timestamp: number + if (this.bedrock) { + const output = await this.getMessageBedrockOutput(resolved) + if (output === null) { + return MessageStatus.STATE_ROOT_NOT_PUBLISHED + } + + timestamp = output.l1Timestamp } else { - const challengePeriod = await this.getChallengePeriodSeconds() - const targetBlock = await this.l1Provider.getBlock( - stateRoot.batch.blockNumber - ) - const latestBlock = await this.l1Provider.getBlock('latest') - if (targetBlock.timestamp + challengePeriod > latestBlock.timestamp) { - return MessageStatus.IN_CHALLENGE_PERIOD - } else { - return MessageStatus.READY_FOR_RELAY + const stateRoot = await this.getMessageStateRoot(resolved) + if (stateRoot === null) { + return MessageStatus.STATE_ROOT_NOT_PUBLISHED } + + const bn = stateRoot.batch.blockNumber + const block = await this.l1Provider.getBlock(bn) + timestamp = block.timestamp + } + + const challengePeriod = await this.getChallengePeriodSeconds() + const latestBlock = await this.l1Provider.getBlock('latest') + if (timestamp + challengePeriod > latestBlock.timestamp) { + return MessageStatus.IN_CHALLENGE_PERIOD + } else { + return MessageStatus.READY_FOR_RELAY } } else { if (receipt.receiptStatus === MessageReceiptStatus.RELAYED_SUCCEEDED) { @@ -632,11 +692,53 @@ export class CrossChainMessenger implements ICrossChainMessenger { } public async getChallengePeriodSeconds(): Promise { - const challengePeriod = - await this.contracts.l1.StateCommitmentChain.FRAUD_PROOF_WINDOW() + const challengePeriod = this.bedrock + ? await this.contracts.l1.OptimismPortal.FINALIZATION_PERIOD_SECONDS() + : await this.contracts.l1.StateCommitmentChain.FRAUD_PROOF_WINDOW() return challengePeriod.toNumber() } + public async getMessageBedrockOutput( + message: MessageLike + ): Promise { + const resolved = await this.toCrossChainMessage(message) + + // Outputs are only a thing for L2 to L1 messages. + if (resolved.direction === MessageDirection.L1_TO_L2) { + throw new Error(`cannot get a state root for an L1 to L2 message`) + } + + const l2OutputOracleParameters = await this.getL2OutputOracleParameters() + + // TODO: Handle old messages from before Bedrock upgrade. + const events = await this.contracts.l1.L2OutputOracle.queryFilter( + this.contracts.l1.L2OutputOracle.filters.L2OutputAppended( + undefined, + undefined, + Math.ceil( + (resolved.blockNumber - + l2OutputOracleParameters.startingBlockNumber) / + l2OutputOracleParameters.submissionInterval + ) * l2OutputOracleParameters.submissionInterval + ) + ) + + if (events.length === 0) { + return null + } + + // Should not happen + if (events.length > 1) { + throw new Error(`multiple output roots found for message`) + } + + return { + outputRoot: events[0].args._l2Output, + l1Timestamp: events[0].args._l1Timestamp.toNumber(), + l2BlockNumber: events[0].args._l2BlockNumber.toNumber(), + } + } + public async getMessageStateRoot( message: MessageLike ): Promise { @@ -838,8 +940,51 @@ export class CrossChainMessenger implements ICrossChainMessenger { stateRoot.stateRootIndexInBatch ), }, - stateTrieWitness: stateTrieProof.accountProof, - storageTrieWitness: stateTrieProof.storageProof, + stateTrieWitness: toHexString(rlp.encode(stateTrieProof.accountProof)), + storageTrieWitness: toHexString(rlp.encode(stateTrieProof.storageProof)), + } + } + + public async getBedrockMessageProof( + message: MessageLike + ): Promise { + 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 output = await this.getMessageBedrockOutput(resolved) + if (output === null) { + throw new Error(`state root for message not yet published`) + } + + const messageSlot = ethers.utils.keccak256( + ethers.utils.hexConcat([ + hashWithdrawal(resolved), + ethers.constants.HashZero, + ]) + ) + + const stateTrieProof = await makeStateTrieProof( + this.l2Provider as ethers.providers.JsonRpcProvider, + resolved.blockNumber, + this.contracts.l2.OVM_L2ToL1MessagePasser.address, + messageSlot + ) + + const block = await ( + this.l2Provider as ethers.providers.JsonRpcProvider + ).send('eth_getBlockByNumber', [toRpcHexString(resolved.blockNumber)]) + + return { + outputRootProof: { + // TODO: Get correct version here + version: ethers.constants.HashZero, + stateRoot: block.stateRoot, + withdrawerStorageRoot: stateTrieProof.storageProof[0], + latestBlockhash: block.hash, + }, + withdrawalProof: toHexString(rlp.encode(stateTrieProof.storageProof)), } } @@ -1023,15 +1168,25 @@ export class CrossChainMessenger implements ICrossChainMessenger { throw new Error(`cannot resend L2 to L1 message`) } - return this.contracts.l1.L1CrossDomainMessenger.populateTransaction.replayMessage( - resolved.target, - resolved.sender, - resolved.message, - resolved.messageNonce, - resolved.gasLimit, - messageGasLimit, - opts?.overrides || {} - ) + if (this.bedrock) { + return this.populateTransaction.finalizeMessage(resolved, { + ...(opts || {}), + overrides: { + ...opts?.overrides, + gasLimit: messageGasLimit, + }, + }) + } else { + return this.contracts.l1.L1CrossDomainMessenger.populateTransaction.replayMessage( + resolved.target, + resolved.sender, + resolved.message, + resolved.messageNonce, + resolved.minGasLimit, + messageGasLimit, + opts?.overrides || {} + ) + } }, finalizeMessage: async ( @@ -1045,15 +1200,38 @@ export class CrossChainMessenger implements ICrossChainMessenger { throw new Error(`cannot finalize L1 to L2 message`) } - const proof = await this.getMessageProof(resolved) - return this.contracts.l1.L1CrossDomainMessenger.populateTransaction.relayMessage( - resolved.target, - resolved.sender, - resolved.message, - resolved.messageNonce, - proof, - opts?.overrides || {} - ) + if (this.bedrock) { + const proof = await this.getBedrockMessageProof(resolved) + return this.contracts.l1.OptimismPortal.populateTransaction.finalizeWithdrawalTransaction( + resolved.messageNonce, + resolved.sender, + resolved.target, + resolved.value, + resolved.minGasLimit, + resolved.message, + resolved.blockNumber, + proof.outputRootProof, + proof.withdrawalProof + ) + } else { + // L1CrossDomainMessenger relayMessage is the only method that isn't fully backwards + // compatible, so we need to use the legacy interface. When we fully upgrade to Bedrock we + // should be able to remove this code. + const proof = await this.getMessageProof(resolved) + const legacyL1XDM = new ethers.Contract( + this.contracts.l1.L1CrossDomainMessenger.address, + getContractInterface('L1CrossDomainMessenger'), + this.l1SignerOrProvider + ) + return legacyL1XDM.populateTransaction.relayMessage( + resolved.target, + resolved.sender, + resolved.message, + resolved.messageNonce, + proof, + opts?.overrides || {} + ) + } }, depositETH: async ( diff --git a/packages/sdk/src/interfaces/cross-chain-messenger.ts b/packages/sdk/src/interfaces/cross-chain-messenger.ts index 6cc1a9942b195..a52f7e9bc91db 100644 --- a/packages/sdk/src/interfaces/cross-chain-messenger.ts +++ b/packages/sdk/src/interfaces/cross-chain-messenger.ts @@ -24,6 +24,8 @@ import { StateRoot, StateRootBatch, BridgeAdapters, + BedrockCrossChainMessageProof, + BedrockOutputData, } from './types' import { IBridgeAdapter } from './bridge-adapter' @@ -91,6 +93,11 @@ export interface ICrossChainMessenger { */ l1BlockTimeSeconds: number + /** + * Whether or not the messenger is using Bedrock mode. + */ + bedrock: boolean + /** * Retrieves all cross chain messages sent within a given transaction. * @@ -291,6 +298,16 @@ export interface ICrossChainMessenger { */ getChallengePeriodSeconds(): Promise + /** + * Returns the Bedrock output root that corresponds to the given message. + * + * @param message Message to get the Bedrock output root for. + * @returns Bedrock output root. + */ + getMessageBedrockOutput( + message: MessageLike + ): Promise + /** * Returns the state root that corresponds to a given message. This is the state root for the * block in which the transaction was included, as published to the StateCommitmentChain. If the @@ -342,6 +359,16 @@ export interface ICrossChainMessenger { */ getMessageProof(message: MessageLike): Promise + /** + * Generates the bedrock 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. + */ + getBedrockMessageProof( + message: MessageLike + ): Promise + /** * Sends a given cross chain message. Where the message is sent depends on the direction attached * to the message itself. diff --git a/packages/sdk/src/interfaces/types.ts b/packages/sdk/src/interfaces/types.ts index 93b45a41ce246..c95fad6dc5648 100644 --- a/packages/sdk/src/interfaces/types.ts +++ b/packages/sdk/src/interfaces/types.ts @@ -17,6 +17,7 @@ export enum L1ChainID { GOERLI = 5, KOVAN = 42, HARDHAT_LOCAL = 31337, + BEDROCK_LOCAL_DEVNET = 900, } /** @@ -28,6 +29,7 @@ export enum L2ChainID { OPTIMISM_KOVAN = 69, OPTIMISM_HARDHAT_LOCAL = 31337, OPTIMISM_HARDHAT_DEVNET = 17, + OPTIMISM_BEDROCK_DEVNET = 901, } /** @@ -40,6 +42,8 @@ export interface OEL1Contracts { StateCommitmentChain: Contract CanonicalTransactionChain: Contract BondManager: Contract + OptimismPortal: Contract + L2OutputOracle: Contract } /** @@ -174,7 +178,9 @@ export interface CoreCrossChainMessage { sender: string target: string message: string - messageNonce: number + messageNonce: BigNumber + value: BigNumber + minGasLimit: BigNumber } /** @@ -183,7 +189,6 @@ export interface CoreCrossChainMessage { */ export interface CrossChainMessage extends CoreCrossChainMessage { direction: MessageDirection - gasLimit: number logIndex: number blockNumber: number transactionHash: string @@ -233,6 +238,15 @@ export interface StateRootBatchHeader { extraData: string } +/** + * Bedrock output oracle data. + */ +export interface BedrockOutputData { + outputRoot: string + l1Timestamp: number + l2BlockNumber: number +} + /** * Information about a state root, including header, block number, and root iself. */ @@ -265,6 +279,19 @@ export interface CrossChainMessageProof { storageTrieWitness: string } +/** + * Bedrock proof data required to finalize an L2 to L1 message. + */ +export interface BedrockCrossChainMessageProof { + outputRootProof: { + version: string + stateRoot: string + withdrawerStorageRoot: string + latestBlockhash: string + } + withdrawalProof: string +} + /** * Stuff that can be coerced into a transaction. */ @@ -311,3 +338,12 @@ export type AddressLike = string | Contract * Stuff that can be coerced into a number. */ export type NumberLike = string | number | BigNumber + +/** + * Parameters that govern the L2OutputOracle. + */ +export type L2OutputOracleParameters = { + submissionInterval: number + startingBlockNumber: number + l2BlockTime: number +} diff --git a/packages/sdk/src/utils/chain-constants.ts b/packages/sdk/src/utils/chain-constants.ts index d7d290aa92a87..bb677b5efc50f 100644 --- a/packages/sdk/src/utils/chain-constants.ts +++ b/packages/sdk/src/utils/chain-constants.ts @@ -8,6 +8,7 @@ export const DEPOSIT_CONFIRMATION_BLOCKS: { [L2ChainID.OPTIMISM_KOVAN]: 12 as const, [L2ChainID.OPTIMISM_HARDHAT_LOCAL]: 2 as const, [L2ChainID.OPTIMISM_HARDHAT_DEVNET]: 2 as const, + [L2ChainID.OPTIMISM_BEDROCK_DEVNET]: 2 as const, } export const CHAIN_BLOCK_TIMES: { @@ -17,4 +18,5 @@ export const CHAIN_BLOCK_TIMES: { [L1ChainID.GOERLI]: 15 as const, [L1ChainID.KOVAN]: 4 as const, [L1ChainID.HARDHAT_LOCAL]: 1 as const, + [L1ChainID.BEDROCK_LOCAL_DEVNET]: 1 as const, } diff --git a/packages/sdk/src/utils/contracts.ts b/packages/sdk/src/utils/contracts.ts index 31d753ecd5e8b..53752add6f8ea 100644 --- a/packages/sdk/src/utils/contracts.ts +++ b/packages/sdk/src/utils/contracts.ts @@ -1,5 +1,6 @@ import { getContractInterface, predeploys } from '@eth-optimism/contracts' import { ethers, Contract } from 'ethers' +import { getContractInterface as getContractInterfaceBedrock } from '@eth-optimism/contracts-bedrock' import { toAddress } from './coercion' import { DeepPartial } from './type-utils' @@ -65,6 +66,8 @@ export const CONTRACT_ADDRESSES: { CanonicalTransactionChain: '0x5E4e65926BA27467555EB562121fac00D24E9dD2' as const, BondManager: '0xcd626E1328b41fCF24737F137BcD4CE0c32bc8d1' as const, + OptimismPortal: '0x0000000000000000000000000000000000000000' as const, + L2OutputOracle: '0x0000000000000000000000000000000000000000' as const, }, l2: DEFAULT_L2_CONTRACT_ADDRESSES, }, @@ -79,6 +82,8 @@ export const CONTRACT_ADDRESSES: { CanonicalTransactionChain: '0xf7B88A133202d41Fe5E2Ab22e6309a1A4D50AF74' as const, BondManager: '0xc5a603d273E28185c18Ba4d26A0024B2d2F42740' as const, + OptimismPortal: '0x0000000000000000000000000000000000000000' as const, + L2OutputOracle: '0x0000000000000000000000000000000000000000' as const, }, l2: DEFAULT_L2_CONTRACT_ADDRESSES, }, @@ -93,6 +98,8 @@ export const CONTRACT_ADDRESSES: { CanonicalTransactionChain: '0x2ebA8c4EfDB39A8Cd8f9eD65c50ec079f7CEBD81' as const, BondManager: '0xE5AE60bD6F8DEe4D0c2BC9268e23B92F1cacC58F' as const, + OptimismPortal: '0x0000000000000000000000000000000000000000' as const, + L2OutputOracle: '0x0000000000000000000000000000000000000000' as const, }, l2: DEFAULT_L2_CONTRACT_ADDRESSES, }, @@ -107,6 +114,8 @@ export const CONTRACT_ADDRESSES: { CanonicalTransactionChain: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9' as const, BondManager: '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707' as const, + OptimismPortal: '0x0000000000000000000000000000000000000000' as const, + L2OutputOracle: '0x0000000000000000000000000000000000000000' as const, }, l2: DEFAULT_L2_CONTRACT_ADDRESSES, }, @@ -121,6 +130,24 @@ export const CONTRACT_ADDRESSES: { CanonicalTransactionChain: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9' as const, BondManager: '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707' as const, + OptimismPortal: '0x0000000000000000000000000000000000000000' as const, + L2OutputOracle: '0x0000000000000000000000000000000000000000' as const, + }, + l2: DEFAULT_L2_CONTRACT_ADDRESSES, + }, + [L2ChainID.OPTIMISM_BEDROCK_DEVNET]: { + l1: { + AddressManager: '0x5FbDB2315678afecb367f032d93F642f64180aa3' as const, + L1CrossDomainMessenger: + '0x0165878A594ca255338adfa4d48449f69242Eb8F' as const, + L1StandardBridge: '0x8A791620dd6260079BF849Dc5567aDC3F2FdC318' as const, + StateCommitmentChain: + '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9' as const, + CanonicalTransactionChain: + '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9' as const, + BondManager: '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707' as const, + OptimismPortal: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9' as const, + L2OutputOracle: '0x5FbDB2315678afecb367f032d93F642f64180aa3' as const, }, l2: DEFAULT_L2_CONTRACT_ADDRESSES, }, @@ -191,11 +218,21 @@ export const getOEContract = ( ) } + // Bedrock interfaces are backwards compatible. We can prefer Bedrock interfaces over legacy + // interfaces if they exist. + const name = NAME_REMAPPING[contractName] || contractName + let iface: ethers.utils.Interface + try { + iface = getContractInterfaceBedrock(name) + } catch (err) { + iface = getContractInterface(name) + } + return new Contract( toAddress( opts.address || addresses.l1[contractName] || addresses.l2[contractName] ), - getContractInterface(NAME_REMAPPING[contractName] || contractName), + iface, opts.signerOrProvider ) } @@ -230,6 +267,8 @@ export const getAllOEContracts = ( StateCommitmentChain: undefined, CanonicalTransactionChain: undefined, BondManager: undefined, + OptimismPortal: undefined, + OutputOracle: undefined, }, l2: DEFAULT_L2_CONTRACT_ADDRESSES, } diff --git a/packages/sdk/src/utils/merkle-utils.ts b/packages/sdk/src/utils/merkle-utils.ts index 5e47edfcdc0b0..b78907d4ab40e 100644 --- a/packages/sdk/src/utils/merkle-utils.ts +++ b/packages/sdk/src/utils/merkle-utils.ts @@ -6,7 +6,6 @@ import { toRpcHexString, } from '@eth-optimism/core-utils' import { MerkleTree } from 'merkletreejs' -import * as rlp from 'rlp' /** * Generates a Merkle proof (using the particular scheme we use within Lib_MerkleTree). @@ -60,8 +59,8 @@ export const makeStateTrieProof = async ( address: string, slot: string ): Promise<{ - accountProof: string - storageProof: string + accountProof: string[] + storageProof: string[] }> => { const proof = await provider.send('eth_getProof', [ address, @@ -70,7 +69,7 @@ export const makeStateTrieProof = async ( ]) return { - accountProof: toHexString(rlp.encode(proof.accountProof)), - storageProof: toHexString(rlp.encode(proof.storageProof[0].proof)), + accountProof: proof.accountProof, + storageProof: proof.storageProof[0].proof, } } diff --git a/packages/sdk/src/utils/message-encoding.ts b/packages/sdk/src/utils/message-encoding.ts index 4915946928517..a82b95992cb7a 100644 --- a/packages/sdk/src/utils/message-encoding.ts +++ b/packages/sdk/src/utils/message-encoding.ts @@ -1,8 +1,52 @@ import { getContractInterface } from '@eth-optimism/contracts' -import { ethers } from 'ethers' +import { getContractInterface as getContractInterfaceBedrock } from '@eth-optimism/contracts-bedrock' +import { BigNumber, ethers } from 'ethers' import { CoreCrossChainMessage } from '../interfaces' +/** + * Returns the v0 message encoding. + * + * @param message Message to encode. + * @returns v0 message encoding. + */ +export const encodeV0 = (message: CoreCrossChainMessage): string => { + return getContractInterface('L2CrossDomainMessenger').encodeFunctionData( + 'relayMessage', + [message.target, message.sender, message.message, message.messageNonce] + ) +} + +/** + * Returns the v1 message encoding. + * + * @param message Message to encode. + * @returns v1 message encoding. + */ +export const encodeV1 = (message: CoreCrossChainMessage): string => { + return getContractInterfaceBedrock('CrossDomainMessenger').encodeFunctionData( + 'relayMessage', + [ + message.messageNonce, + message.sender, + message.target, + message.value, + message.minGasLimit, + message.message, + ] + ) +} + +/** + * Pulls version byte from nonce. + * + * @param nonce Nonce to pull version byte from. + * @returns Version byte. + */ +export const getVersionFromNonce = (nonce: BigNumber): number => { + return nonce.shr(240).toNumber() +} + /** * Returns the canonical encoding of a cross chain message. This encoding is used in various * locations within the Optimism smart contracts. @@ -13,10 +57,15 @@ import { CoreCrossChainMessage } from '../interfaces' export const encodeCrossChainMessage = ( message: CoreCrossChainMessage ): string => { - return getContractInterface('L2CrossDomainMessenger').encodeFunctionData( - 'relayMessage', - [message.target, message.sender, message.message, message.messageNonce] - ) + const version = getVersionFromNonce(message.messageNonce) + switch (version) { + case 0: + return encodeV0(message) + case 1: + return encodeV1(message) + default: + throw new Error(`unsupported message version: ${version}`) + } } /** @@ -35,3 +84,28 @@ export const hashCrossChainMessage = ( [encodeCrossChainMessage(message)] ) } + +/** + * Computes the withdrawal hash for a given message. + * + * @param message Message to compute the withdrawal hash for. + * @returns Computed withdrawal hash. + */ +export const hashWithdrawal = (message: CoreCrossChainMessage): string => { + return ethers.utils.solidityKeccak256( + ['bytes'], + [ + ethers.utils.defaultAbiCoder.encode( + ['uint256', 'address', 'address', 'uint256', 'uint256', 'bytes'], + [ + message.messageNonce, + message.sender, + message.target, + message.value, + message.minGasLimit, + message.message, + ] + ), + ] + ) +} diff --git a/packages/sdk/test/contracts/MessageEncodingHelper.sol b/packages/sdk/test/contracts/MessageEncodingHelper.sol index b411b3013490e..0c960e7e9e8fa 100644 --- a/packages/sdk/test/contracts/MessageEncodingHelper.sol +++ b/packages/sdk/test/contracts/MessageEncodingHelper.sol @@ -1,42 +1,60 @@ pragma solidity ^0.8.9; +import { CrossDomainHashing } from "@eth-optimism/contracts-bedrock/contracts/libraries/Lib_CrossDomainHashing.sol"; +import { WithdrawalVerifier } from "@eth-optimism/contracts-bedrock/contracts/libraries/Lib_WithdrawalVerifier.sol"; + contract MessageEncodingHelper { - // This function is copy/pasted from the Lib_CrossDomainUtils library. We have to do this - // because the Lib_CrossDomainUtils library does not provide a function for hashing. Instead, - // I'm duplicating the functionality of the library here and exposing an additional method that - // does the required hashing. This is fragile and will break if we ever update the way that our - // contracts hash the encoded data, but at least it works for now. - // TODO: Next time we're planning to upgrade the contracts, make sure that the library also - // contains a function for hashing. - function encodeXDomainCalldata( - address _target, + function getVersionedEncoding( + uint256 _nonce, address _sender, - bytes memory _message, - uint256 _messageNonce - ) public pure returns (bytes memory) { - return - abi.encodeWithSignature( - "relayMessage(address,address,bytes,uint256)", - _target, - _sender, - _message, - _messageNonce - ); + address _target, + uint256 _value, + uint256 _gasLimit, + bytes memory _data + ) external pure returns (bytes memory) { + return CrossDomainHashing.getVersionedEncoding( + _nonce, + _sender, + _target, + _value, + _gasLimit, + _data + ); } - function hashXDomainCalldata( + function getVersionedHash( + uint256 _nonce, + address _sender, address _target, + uint256 _value, + uint256 _gasLimit, + bytes memory _data + ) external pure returns (bytes32) { + return CrossDomainHashing.getVersionedHash( + _nonce, + _sender, + _target, + _value, + _gasLimit, + _data + ); + } + + function withdrawalHash( + uint256 _nonce, address _sender, - bytes memory _message, - uint256 _messageNonce - ) public pure returns (bytes32) { - return keccak256( - encodeXDomainCalldata( - _target, - _sender, - _message, - _messageNonce - ) + address _target, + uint256 _value, + uint256 _gasLimit, + bytes memory _data + ) external pure returns (bytes32) { + return WithdrawalVerifier.withdrawalHash( + _nonce, + _sender, + _target, + _value, + _gasLimit, + _data ); } } diff --git a/packages/sdk/test/contracts/MockBridge.sol b/packages/sdk/test/contracts/MockBridge.sol index fd46d11537cac..7f3b5155cfb12 100644 --- a/packages/sdk/test/contracts/MockBridge.sol +++ b/packages/sdk/test/contracts/MockBridge.sol @@ -80,7 +80,8 @@ contract MockBridge { address(0), hex"1234", 1234, - 12345678 + 12345678, + 0 ) ); } @@ -101,7 +102,8 @@ contract MockBridge { address(0), hex"1234", 1234, - 12345678 + 12345678, + 0 ) ); } diff --git a/packages/sdk/test/contracts/MockMessenger.sol b/packages/sdk/test/contracts/MockMessenger.sol index c23360f427b76..724d90ed6778c 100644 --- a/packages/sdk/test/contracts/MockMessenger.sol +++ b/packages/sdk/test/contracts/MockMessenger.sol @@ -48,7 +48,8 @@ contract MockMessenger is ICrossDomainMessenger { address sender; bytes message; uint256 messageNonce; - uint256 gasLimit; + uint256 minGasLimit; + uint256 value; } function doNothing() public { @@ -63,7 +64,7 @@ contract MockMessenger is ICrossDomainMessenger { _params.sender, _params.message, _params.messageNonce, - _params.gasLimit + _params.minGasLimit ); } diff --git a/packages/sdk/test/cross-chain-messenger.spec.ts b/packages/sdk/test/cross-chain-messenger.spec.ts index 35587af981d75..ed86485b715a9 100644 --- a/packages/sdk/test/cross-chain-messenger.spec.ts +++ b/packages/sdk/test/cross-chain-messenger.spec.ts @@ -18,7 +18,7 @@ import { L1ChainID, L2ChainID, } from '../src' -import { DUMMY_MESSAGE } from './helpers' +import { DUMMY_MESSAGE, DUMMY_EXTENDED_MESSAGE } from './helpers' describe('CrossChainMessenger', () => { let l1Signer: any @@ -176,6 +176,8 @@ describe('CrossChainMessenger', () => { StateCommitmentChain: '0x' + '14'.repeat(20), CanonicalTransactionChain: '0x' + '15'.repeat(20), BondManager: '0x' + '16'.repeat(20), + OptimismPortal: '0x' + '17'.repeat(20), + OutputOracle: '0x' + '18'.repeat(20), }, l2: { L2CrossDomainMessenger: '0x' + '22'.repeat(20), @@ -292,7 +294,8 @@ describe('CrossChainMessenger', () => { target: message.target, message: message.message, messageNonce: ethers.BigNumber.from(message.messageNonce), - gasLimit: ethers.BigNumber.from(message.gasLimit), + minGasLimit: ethers.BigNumber.from(message.minGasLimit), + value: ethers.BigNumber.from(message.value), logIndex: i, blockNumber: tx.blockNumber, transactionHash: tx.hash, @@ -344,7 +347,8 @@ describe('CrossChainMessenger', () => { target: message.target, message: message.message, messageNonce: ethers.BigNumber.from(message.messageNonce), - gasLimit: ethers.BigNumber.from(message.gasLimit), + minGasLimit: ethers.BigNumber.from(message.minGasLimit), + value: ethers.BigNumber.from(message.value), logIndex: i, blockNumber: tx.blockNumber, transactionHash: tx.hash, @@ -471,15 +475,8 @@ describe('CrossChainMessenger', () => { describe('when the input is a CrossChainMessage', () => { it('should return the input', async () => { const message = { + ...DUMMY_EXTENDED_MESSAGE, direction: MessageDirection.L1_TO_L2, - target: '0x' + '11'.repeat(20), - sender: '0x' + '22'.repeat(20), - message: '0x' + '33'.repeat(64), - messageNonce: 1234, - gasLimit: 0, - logIndex: 0, - blockNumber: 1234, - transactionHash: '0x' + '44'.repeat(32), } expect(await messenger.toCrossChainMessage(message)).to.deep.equal( @@ -795,15 +792,8 @@ describe('CrossChainMessenger', () => { describe('when the relay was successful', () => { it('should return the receipt of the transaction that relayed the message', async () => { const message = { + ...DUMMY_EXTENDED_MESSAGE, direction: MessageDirection.L1_TO_L2, - target: '0x' + '11'.repeat(20), - sender: '0x' + '22'.repeat(20), - message: '0x' + '33'.repeat(64), - messageNonce: 1234, - gasLimit: 0, - logIndex: 0, - blockNumber: 1234, - transactionHash: '0x' + '44'.repeat(32), } const tx = await l2Messenger.triggerRelayedMessageEvents([ @@ -826,15 +816,8 @@ describe('CrossChainMessenger', () => { describe('when the relay failed', () => { it('should return the receipt of the transaction that attempted to relay the message', async () => { const message = { + ...DUMMY_EXTENDED_MESSAGE, direction: MessageDirection.L1_TO_L2, - target: '0x' + '11'.repeat(20), - sender: '0x' + '22'.repeat(20), - message: '0x' + '33'.repeat(64), - messageNonce: 1234, - gasLimit: 0, - logIndex: 0, - blockNumber: 1234, - transactionHash: '0x' + '44'.repeat(32), } const tx = await l2Messenger.triggerFailedRelayedMessageEvents([ @@ -857,15 +840,8 @@ describe('CrossChainMessenger', () => { describe('when the relay failed more than once', () => { it('should return the receipt of the last transaction that attempted to relay the message', async () => { const message = { + ...DUMMY_EXTENDED_MESSAGE, direction: MessageDirection.L1_TO_L2, - target: '0x' + '11'.repeat(20), - sender: '0x' + '22'.repeat(20), - message: '0x' + '33'.repeat(64), - messageNonce: 1234, - gasLimit: 0, - logIndex: 0, - blockNumber: 1234, - transactionHash: '0x' + '44'.repeat(32), } await l2Messenger.triggerFailedRelayedMessageEvents([ @@ -893,15 +869,8 @@ describe('CrossChainMessenger', () => { describe('when the message has not been relayed', () => { it('should return null', async () => { const message = { + ...DUMMY_EXTENDED_MESSAGE, direction: MessageDirection.L1_TO_L2, - target: '0x' + '11'.repeat(20), - sender: '0x' + '22'.repeat(20), - message: '0x' + '33'.repeat(64), - messageNonce: 1234, - gasLimit: 0, - logIndex: 0, - blockNumber: 1234, - transactionHash: '0x' + '44'.repeat(32), } await l2Messenger.doNothing() @@ -939,15 +908,8 @@ describe('CrossChainMessenger', () => { describe('when the message receipt already exists', () => { it('should immediately return the receipt', async () => { const message = { + ...DUMMY_EXTENDED_MESSAGE, direction: MessageDirection.L1_TO_L2, - target: '0x' + '11'.repeat(20), - sender: '0x' + '22'.repeat(20), - message: '0x' + '33'.repeat(64), - messageNonce: 1234, - gasLimit: 0, - logIndex: 0, - blockNumber: 1234, - transactionHash: '0x' + '44'.repeat(32), } const tx = await l2Messenger.triggerRelayedMessageEvents([ @@ -971,15 +933,8 @@ describe('CrossChainMessenger', () => { describe('when no extra options are provided', () => { it('should wait for the receipt to be published', async () => { const message = { + ...DUMMY_EXTENDED_MESSAGE, direction: MessageDirection.L1_TO_L2, - target: '0x' + '11'.repeat(20), - sender: '0x' + '22'.repeat(20), - message: '0x' + '33'.repeat(64), - messageNonce: 1234, - gasLimit: 0, - logIndex: 0, - blockNumber: 1234, - transactionHash: '0x' + '44'.repeat(32), } setTimeout(async () => { @@ -1004,15 +959,8 @@ describe('CrossChainMessenger', () => { describe('when a timeout is provided', () => { it('should throw an error if the timeout is reached', async () => { const message = { + ...DUMMY_EXTENDED_MESSAGE, direction: MessageDirection.L1_TO_L2, - target: '0x' + '11'.repeat(20), - sender: '0x' + '22'.repeat(20), - message: '0x' + '33'.repeat(64), - messageNonce: 1234, - gasLimit: 0, - logIndex: 0, - blockNumber: 1234, - transactionHash: '0x' + '44'.repeat(32), } await expect( @@ -1039,14 +987,8 @@ describe('CrossChainMessenger', () => { describe('when the message is an L1 to L2 message', () => { it('should return an accurate gas estimate plus a ~20% buffer', async () => { const message = { + ...DUMMY_EXTENDED_MESSAGE, direction: MessageDirection.L1_TO_L2, - target: '0x' + '11'.repeat(20), - sender: '0x' + '22'.repeat(20), - message: '0x' + '33'.repeat(64), - messageNonce: 1234, - logIndex: 0, - blockNumber: 1234, - transactionHash: '0x' + '44'.repeat(32), } const estimate = await ethers.provider.estimateGas({ @@ -1068,14 +1010,8 @@ describe('CrossChainMessenger', () => { it('should return an accurate gas estimate when a custom buffer is provided', async () => { const message = { + ...DUMMY_EXTENDED_MESSAGE, direction: MessageDirection.L1_TO_L2, - target: '0x' + '11'.repeat(20), - sender: '0x' + '22'.repeat(20), - message: '0x' + '33'.repeat(64), - messageNonce: 1234, - logIndex: 0, - blockNumber: 1234, - transactionHash: '0x' + '44'.repeat(32), } const estimate = await ethers.provider.estimateGas({ diff --git a/packages/sdk/test/helpers/constants.ts b/packages/sdk/test/helpers/constants.ts index eccdf7a848cd0..8a13a005c8086 100644 --- a/packages/sdk/test/helpers/constants.ts +++ b/packages/sdk/test/helpers/constants.ts @@ -1,7 +1,17 @@ +import { ethers } from 'ethers' + export const DUMMY_MESSAGE = { target: '0x' + '11'.repeat(20), sender: '0x' + '22'.repeat(20), message: '0x' + '33'.repeat(64), - messageNonce: 1234, - gasLimit: 100000, + messageNonce: ethers.BigNumber.from(1234), + value: ethers.BigNumber.from(0), + minGasLimit: ethers.BigNumber.from(5678), +} + +export const DUMMY_EXTENDED_MESSAGE = { + ...DUMMY_MESSAGE, + logIndex: 0, + blockNumber: 1234, + transactionHash: '0x' + '44'.repeat(32), } diff --git a/packages/sdk/test/utils/message-encoding.spec.ts b/packages/sdk/test/utils/message-encoding.spec.ts index 232cefb22aafd..086b97ac74337 100644 --- a/packages/sdk/test/utils/message-encoding.spec.ts +++ b/packages/sdk/test/utils/message-encoding.spec.ts @@ -1,71 +1,145 @@ -import { Contract, Signer } from 'ethers' -import { ethers } from 'hardhat' -import { getContractFactory } from '@eth-optimism/contracts' +import { ethers, Contract } from 'ethers' +import hre from 'hardhat' import { expect } from '../setup' import { CoreCrossChainMessage, encodeCrossChainMessage, hashCrossChainMessage, + encodeV0, + encodeV1, + hashWithdrawal, } from '../../src' +import { DUMMY_MESSAGE } from '../helpers' + +const addVersionToNonce = ( + nonce: ethers.BigNumber, + version: number +): ethers.BigNumber => { + return ethers.BigNumber.from(version).shl(240).or(nonce) +} describe('message encoding utils', () => { - let signers: Signer[] + let MessageEncodingHelper: Contract before(async () => { - signers = (await ethers.getSigners()) as any + MessageEncodingHelper = (await ( + await hre.ethers.getContractFactory('MessageEncodingHelper') + ).deploy()) as any }) - describe('encodeCrossChainMessage', () => { - let Lib_CrossDomainUtils: Contract - before(async () => { - Lib_CrossDomainUtils = (await getContractFactory( - 'TestLib_CrossDomainUtils', - signers[0] - ).deploy()) as any + describe('encodeV0', () => { + it('should properly encode a v0 message', async () => { + const message: CoreCrossChainMessage = DUMMY_MESSAGE + + const actual = encodeV0(message) + const expected = await MessageEncodingHelper.getVersionedEncoding( + message.messageNonce, + message.sender, + message.target, + message.value, + message.minGasLimit, + message.message + ) + + expect(actual).to.equal(expected) }) + }) - it('should properly encode a message', async () => { + describe('encodeV1', () => { + it('should properly encode a v1 message', async () => { const message: CoreCrossChainMessage = { - target: '0x' + '11'.repeat(20), - sender: '0x' + '22'.repeat(20), - message: '0x' + '1234'.repeat(32), - messageNonce: 1234, + ...DUMMY_MESSAGE, + messageNonce: addVersionToNonce(DUMMY_MESSAGE.messageNonce, 1), } - const actual = encodeCrossChainMessage(message) - const expected = await Lib_CrossDomainUtils.encodeXDomainCalldata( - message.target, + const actual = encodeV1(message) + const expected = await MessageEncodingHelper.getVersionedEncoding( + message.messageNonce, message.sender, - message.message, - message.messageNonce + message.target, + message.value, + message.minGasLimit, + message.message ) + + expect(actual).to.equal(expected) + }) + }) + + describe('encodeCrossChainMessage', () => { + it('should return the v0 encoding for a v0 message', async () => { + const message: CoreCrossChainMessage = DUMMY_MESSAGE + + const actual = encodeCrossChainMessage(message) + const expected = encodeV0(message) + + expect(actual).to.equal(expected) + }) + + it('should return the v1 encoding for a v1 message', async () => { + const message: CoreCrossChainMessage = { + ...DUMMY_MESSAGE, + messageNonce: addVersionToNonce(DUMMY_MESSAGE.messageNonce, 1), + } + + const actual = encodeCrossChainMessage(message) + const expected = encodeV1(message) + expect(actual).to.equal(expected) }) }) describe('hashCrossChainMessage', () => { - let MessageEncodingHelper: Contract - before(async () => { - MessageEncodingHelper = (await ( - await ethers.getContractFactory('MessageEncodingHelper') - ).deploy()) as any + it('should properly hash a v0 message', async () => { + const message: CoreCrossChainMessage = DUMMY_MESSAGE + + const actual = hashCrossChainMessage(message) + const expected = await MessageEncodingHelper.getVersionedHash( + message.messageNonce, + message.sender, + message.target, + message.value, + message.minGasLimit, + message.message + ) + + expect(actual).to.equal(expected) }) - it('should properly hash a message', async () => { + it('should properly hash a v1 message', async () => { const message: CoreCrossChainMessage = { - target: '0x' + '11'.repeat(20), - sender: '0x' + '22'.repeat(20), - message: '0x' + '1234'.repeat(32), - messageNonce: 1234, + ...DUMMY_MESSAGE, + messageNonce: addVersionToNonce(DUMMY_MESSAGE.messageNonce, 1), } const actual = hashCrossChainMessage(message) - const expected = await MessageEncodingHelper.hashXDomainCalldata( + const expected = await MessageEncodingHelper.getVersionedHash( + message.messageNonce, + message.sender, message.target, + message.value, + message.minGasLimit, + message.message + ) + + expect(actual).to.equal(expected) + }) + }) + + describe('hashWithdrawal', () => { + it('should properly hash a withdrawal message', async () => { + const message: CoreCrossChainMessage = DUMMY_MESSAGE + + const actual = hashWithdrawal(message) + const expected = await MessageEncodingHelper.withdrawalHash( + message.messageNonce, message.sender, - message.message, - message.messageNonce + message.target, + message.value, + message.minGasLimit, + message.message ) + expect(actual).to.equal(expected) }) })