diff --git a/.circleci/config.yml b/.circleci/config.yml index cd4584ebe760c..505cda689eef1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -535,6 +535,55 @@ jobs: name: Upload coverage command: codecov --verbose --clean --flags <> + sdk-next-tests: + docker: + - image: ethereumoptimism/ci-builder:latest + resource_class: large + steps: + - checkout + - attach_workspace: { at: "." } + - check-changed: + patterns: sdk,contracts-bedrock,contracts + - restore_cache: + name: Restore Yarn Package Cache + keys: + - yarn-packages-v2-{{ checksum "yarn.lock" }} + - run: + name: anvil-l1 + background: true + # atm this is goerli but we should use mainnet after bedrock is live + command: anvil --fork-url $ANVIL_L1_FORK_URL --fork-block-number 8847426 + - run: + name: anvil-l2 + background: true + # atm this is goerli but we should use mainnet after bedrock is live + command: anvil --fork-url $ANVIL_L2_FORK_URL --port 9545 --fork-block-number 8172732 + - run: + name: build + command: yarn build + working_directory: packages/atst + - run: + name: lint + command: yarn lint:check + working_directory: packages/atst + - run: + name: make sure anvil l1 is up + command: npx wait-on tcp:8545 && cast block-number --rpc-url http://localhost:8545 + - run: + name: make sure anvil l2 is up + command: npx wait-on tcp:9545 && cast block-number --rpc-url http://localhost:9545 + - run: + name: test:next + command: yarn test:next + no_output_timeout: 5m + working_directory: packages/sdk + environment: + # anvil[0] test private key + VITE_E2E_PRIVATE_KEY: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + VITE_E2E_RPC_URL_L1: http://localhost:8545 + VITE_E2E_RPC_URL_L2: http://localhost:9545 + + bedrock-markdown: machine: image: ubuntu-2204:2022.07.1 @@ -1094,6 +1143,10 @@ workflows: dependencies: "(common-ts|core-utils)" requires: - yarn-monorepo + - sdk-next-tests: + name: sdk-next-tests + requires: + - yarn-monorepo - js-lint-test: name: sdk-tests coverage_flag: sdk-tests diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 83a3fd32bbbc2..1b0830341627c 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -17,6 +17,7 @@ "lint:fix": "yarn lint:check --fix", "pre-commit": "lint-staged", "test": "hardhat test", + "test:next": "vitest test-next/proveMessage.spec.ts", "test:coverage": "nyc hardhat test && nyc merge .nyc_output coverage.json", "autogen:docs": "typedoc --out docs src/index.ts" }, @@ -45,7 +46,9 @@ "hardhat-deploy": "^0.11.4", "nyc": "^15.1.0", "typedoc": "^0.22.13", - "mocha": "^10.0.0" + "mocha": "^10.0.0", + "vitest": "^0.28.3", + "zod": "^3.11.6" }, "dependencies": { "@eth-optimism/contracts": "0.5.40", diff --git a/packages/sdk/src/cross-chain-messenger.ts b/packages/sdk/src/cross-chain-messenger.ts index bbd070e34bf2c..d5fddee1536a6 100644 --- a/packages/sdk/src/cross-chain-messenger.ts +++ b/packages/sdk/src/cross-chain-messenger.ts @@ -68,6 +68,7 @@ import { migratedWithdrawalGasLimit, DEPOSIT_CONFIRMATION_BLOCKS, CHAIN_BLOCK_TIMES, + hashMessageHash, } from './utils' export class CrossChainMessenger { @@ -351,14 +352,12 @@ export class CrossChainMessenger { } } - const minGasLimit = migratedWithdrawalGasLimit(resolved.message) - return { ...resolved, value, - minGasLimit, + minGasLimit: BigNumber.from(0), messageNonce: encodeVersionedNonce( - BigNumber.from(1), + BigNumber.from(0), resolved.messageNonce ), } @@ -388,13 +387,23 @@ export class CrossChainMessenger { updated = resolved } + // Encode the updated message, we need this for legacy messages. + const encoded = encodeCrossDomainMessageV1( + updated.messageNonce, + updated.sender, + updated.target, + updated.value, + updated.minGasLimit, + updated.message + ) + // 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) + gasLimit = migratedWithdrawalGasLimit(encoded) messageNonce = resolved.messageNonce } else { const receipt = await this.l2Provider.getTransactionReceipt( @@ -433,14 +442,7 @@ export class CrossChainMessenger { 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 - ), + message: encoded, } } @@ -572,6 +574,9 @@ export class CrossChainMessenger { public async toCrossChainMessage( message: MessageLike ): Promise { + if (!message) { + throw new Error('message is undefined') + } // TODO: Convert these checks into proper type checks. if ((message as CrossChainMessage).message) { return message as CrossChainMessage @@ -1357,12 +1362,8 @@ export class CrossChainMessenger { } const withdrawal = await this.toLowLevelMessage(resolved) - const messageSlot = ethers.utils.keccak256( - ethers.utils.defaultAbiCoder.encode( - ['bytes32', 'uint256'], - [hashLowLevelMessage(withdrawal), ethers.constants.HashZero] - ) - ) + const hash = hashLowLevelMessage(withdrawal) + const messageSlot = hashMessageHash(hash) const stateTrieProof = await makeStateTrieProof( this.l2Provider as ethers.providers.JsonRpcProvider, @@ -1462,9 +1463,8 @@ export class CrossChainMessenger { overrides?: Overrides } ): Promise { - return (opts?.signer || this.l1Signer).sendTransaction( - await this.populateTransaction.proveMessage(message, opts) - ) + const tx = await this.populateTransaction.proveMessage(message, opts) + return (opts?.signer || this.l1Signer).sendTransaction(tx) } /** @@ -1768,7 +1768,8 @@ export class CrossChainMessenger { const withdrawal = await this.toLowLevelMessage(resolved) const proof = await this.getBedrockMessageProof(resolved) - return this.contracts.l1.OptimismPortal.populateTransaction.proveWithdrawalTransaction( + + const args = [ [ withdrawal.messageNonce, withdrawal.sender, @@ -1785,7 +1786,11 @@ export class CrossChainMessenger { proof.outputRootProof.latestBlockhash, ], proof.withdrawalProof, - opts?.overrides || {} + opts?.overrides || {}, + ] as const + + return this.contracts.l1.OptimismPortal.populateTransaction.proveWithdrawalTransaction( + ...args ) }, diff --git a/packages/sdk/src/utils/message-utils.ts b/packages/sdk/src/utils/message-utils.ts index 63f17635303e7..0c447240388db 100644 --- a/packages/sdk/src/utils/message-utils.ts +++ b/packages/sdk/src/utils/message-utils.ts @@ -1,5 +1,5 @@ import { hashWithdrawal } from '@eth-optimism/core-utils' -import { BigNumber, utils } from 'ethers' +import { BigNumber, utils, ethers } from 'ethers' import { LowLevelMessage } from '../interfaces' @@ -22,6 +22,22 @@ export const hashLowLevelMessage = (message: LowLevelMessage): string => { ) } +/** + * Utility for hashing a message hash. This computes the storage slot + * where the message hash will be stored in state. HashZero is used + * because the first mapping in the contract is used. + * + * @param messageHash Message hash to hash. + * @returns Hash of the given message hash. + */ +export const hashMessageHash = (messageHash: string): string => { + const data = ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'uint256'], + [messageHash, ethers.constants.HashZero] + ) + return ethers.utils.keccak256(data) +} + /** * Compute the min gas limit for a migrated withdrawal. */ diff --git a/packages/sdk/test-next/README.md b/packages/sdk/test-next/README.md new file mode 100644 index 0000000000000..20a2fcaee93ff --- /dev/null +++ b/packages/sdk/test-next/README.md @@ -0,0 +1,4 @@ +# test-next + +- The new tests for the next version of sdk will use vitest +- The vitest tests are kept here seperated from mocha tests for now diff --git a/packages/sdk/test-next/proveMessage.spec.ts b/packages/sdk/test-next/proveMessage.spec.ts new file mode 100644 index 0000000000000..163dbf28a45d2 --- /dev/null +++ b/packages/sdk/test-next/proveMessage.spec.ts @@ -0,0 +1,108 @@ +import ethers from 'ethers' +import { describe, expect, it } from 'vitest' +import { z } from 'zod' + +import { CrossChainMessenger } from '../src' + +/** + * This test repros the bug where legacy withdrawals are not provable + */ +/******* +Cast results from runnning cast tx and cast receipt on the l2 tx hash + +cast tx 0xd66fda632b51a8b25a9d260d70da8be57b9930c4616370861526335c3e8eef81 --rpc-url https://goerli.optimism.io + +blockHash 0x67956cee3de38d49206d34b77f560c4c371d77b36584047ade8bf7b67bf210c0 +blockNumber 2337599 +from 0x1d86C2F5cc7fBEc35FEDbd3293b5004A841EA3F0 +gas 118190 +gasPrice 1 +hash 0xd66fda632b51a8b25a9d260d70da8be57b9930c4616370861526335c3e8eef81 +input 0x32b7006d000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddead000000000000000000000000000000000000000000000000000000005af3107a4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000 +nonce 10 +r 0x7e58c5dbb37f57303d936562d89a75a20be2a45f54c5d44dc73119453adf2e08 +s 0x1bc952bd048dd38668a0c3b4bac202945c5a150465b551dd2a768e54a746e2c4 +to 0x4200000000000000000000000000000000000010 +transactionIndex 0 +v 875 +value 0 +index 2337598 +l1BlockNumber 7850866 +l1Timestamp 1666982083 +queueOrigin sequencer +rawTransaction 0xf901070a018301cdae94420000000000000000000000000000000000001080b8a432b7006d000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddead000000000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000082036ba07e58c5dbb37f57303d936562d89a75a20be2a45f54c5d44dc73119453adf2e08a01bc952bd048dd38668a0c3b4bac202945c5a150465b551dd2a768e54a746e2c4 + +cast tx 0xd66fda632b51a8b25a9d260d70da8be57b9930c4616370861526335c3e8eef81 --rpc-url https://goerli.optimism.io + +blockHash 0x67956cee3de38d49206d34b77f560c4c371d77b36584047ade8bf7b67bf210c0 +blockNumber 2337599 +contractAddress +cumulativeGasUsed 115390 +effectiveGasPrice +gasUsed 115390 +logs [{"address":"0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000001d86c2f5cc7fbec35fedbd3293b5004a841ea3f0","0x0000000000000000000000000000000000000000000000000000000000000000"],"data":"0x00000000000000000000000000000000000000000000000000005af3107a4000","blockHash":"0x67956cee3de38d49206d34b77f560c4c371d77b36584047ade8bf7b67bf210c0","blockNumber":"0x23ab3f","transactionHash":"0xd66fda632b51a8b25a9d260d70da8be57b9930c4616370861526335c3e8eef81","transactionIndex":"0x0","logIndex":"0x0","removed":false},{"address":"0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000","topics":["0xcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5","0x0000000000000000000000001d86c2f5cc7fbec35fedbd3293b5004a841ea3f0"],"data":"0x00000000000000000000000000000000000000000000000000005af3107a4000","blockHash":"0x67956cee3de38d49206d34b77f560c4c371d77b36584047ade8bf7b67bf210c0","blockNumber":"0x23ab3f","transactionHash":"0xd66fda632b51a8b25a9d260d70da8be57b9930c4616370861526335c3e8eef81","transactionIndex":"0x0","logIndex":"0x1","removed":false},{"address":"0x4200000000000000000000000000000000000007","topics":["0xcb0f7ffd78f9aee47a248fae8db181db6eee833039123e026dcbff529522e52a","0x000000000000000000000000636af16bf2f682dd3109e60102b8e1a089fedaa8"],"data":"0x00000000000000000000000042000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000001a048000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a41532ec340000000000000000000000001d86c2f5cc7fbec35fedbd3293b5004a841ea3f00000000000000000000000001d86c2f5cc7fbec35fedbd3293b5004a841ea3f000000000000000000000000000000000000000000000000000005af3107a40000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","blockHash":"0x67956cee3de38d49206d34b77f560c4c371d77b36584047ade8bf7b67bf210c0","blockNumber":"0x23ab3f","transactionHash":"0xd66fda632b51a8b25a9d260d70da8be57b9930c4616370861526335c3e8eef81","transactionIndex":"0x0","logIndex":"0x2","removed":false},{"address":"0x4200000000000000000000000000000000000010","topics":["0x73d170910aba9e6d50b102db522b1dbcd796216f5128b445aa2135272886497e","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddead0000","0x0000000000000000000000001d86c2f5cc7fbec35fedbd3293b5004a841ea3f0"],"data":"0x0000000000000000000000001d86c2f5cc7fbec35fedbd3293b5004a841ea3f000000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000","blockHash":"0x67956cee3de38d49206d34b77f560c4c371d77b36584047ade8bf7b67bf210c0","blockNumber":"0x23ab3f","transactionHash":"0xd66fda632b51a8b25a9d260d70da8be57b9930c4616370861526335c3e8eef81","transactionIndex":"0x0","logIndex":"0x3","removed":false}] +logsBloom 0x00000000000000000010000000000000000000000000001000100000001000000000000000000080000000000000008000000800000000000000000000000240000000002000400040000008000000000000000000000000000000000000000100000000020000000000000000000800080000000040000000000010000000000000000000000000000000000000000000800000000000000020000000200000000000000000000001000000000000000000200000000000000000000000000000000002000000200000000400000000000002100000000000000000000020001000000000000000000000000000000000000000000000000000010000008000 +root +status 1 +transactionHash 0xd66fda632b51a8b25a9d260d70da8be57b9930c4616370861526335c3e8eef81 +transactionIndex 0 +type + */ +const E2E_RPC_URL_L1 = z + .string() + .url() + .describe('L1 ethereum rpc Url') + .parse(import.meta.env.VITE_E2E_RPC_URL_L1) +const E2E_RPC_URL_L2 = z + .string() + .url() + .describe('L1 ethereum rpc Url') + .parse(import.meta.env.VITE_E2E_RPC_URL_L2) +const E2E_PRIVATE_KEY = z + .string() + .describe('Private key') + .parse(import.meta.env.VITE_E2E_PRIVATE_KEY) + +const jsonRpcHeaders = { 'User-Agent': 'eth-optimism/@gateway/backend' } +/** + * Initialize the signer, prover, and cross chain messenger + */ +const l1Provider = new ethers.providers.JsonRpcProvider({ + url: E2E_RPC_URL_L1, + headers: jsonRpcHeaders, +}) +const l2Provider = new ethers.providers.JsonRpcProvider({ + url: E2E_RPC_URL_L2, + headers: jsonRpcHeaders, +}) +const l1Wallet = new ethers.Wallet(E2E_PRIVATE_KEY, l1Provider) +const crossChainMessenger = new CrossChainMessenger({ + l1SignerOrProvider: l1Wallet, + l2SignerOrProvider: l2Provider, + l1ChainId: 5, + l2ChainId: 420, + bedrock: true, +}) + +describe('prove message', () => { + it(`should prove a legacy tx + `, async () => { + /** + * Tx hash of legacy withdrawal + * + * @see https://goerli-optimism.etherscan.io/tx/0xd66fda632b51a8b25a9d260d70da8be57b9930c4616370861526335c3e8eef81 + */ + const txWithdrawalHash = + '0xd66fda632b51a8b25a9d260d70da8be57b9930c4616370861526335c3e8eef81' + + const txReceipt = await l2Provider.getTransactionReceipt(txWithdrawalHash) + + expect(txReceipt).toBeDefined() + + const tx = await crossChainMessenger.proveMessage(txWithdrawalHash) + const receipt = await tx.wait() + + // A 1 means the transaction was successful + expect(receipt.status).toBe(1) + }, 20_000) +}) diff --git a/packages/sdk/test/utils/message-utils.spec.ts b/packages/sdk/test/utils/message-utils.spec.ts index b1c2179499797..0643f288b6b59 100644 --- a/packages/sdk/test/utils/message-utils.spec.ts +++ b/packages/sdk/test/utils/message-utils.spec.ts @@ -1,7 +1,11 @@ import { BigNumber } from 'ethers' import { expect } from '../setup' -import { migratedWithdrawalGasLimit } from '../../src/utils/message-utils' +import { + migratedWithdrawalGasLimit, + hashLowLevelMessage, + hashMessageHash, +} from '../../src/utils/message-utils' describe('Message Utils', () => { describe('migratedWithdrawalGasLimit', () => { @@ -26,4 +30,47 @@ describe('Message Utils', () => { } }) }) + + /** + * Test that storage slot computation is correct. The test vectors are + * from actual migrated withdrawals on goerli. + */ + describe('Withdrawal Hashing', () => { + it('should work', () => { + const tests = [ + { + input: { + messageNonce: BigNumber.from(100000), + sender: '0x4200000000000000000000000000000000000007', + target: '0x5086d1eEF304eb5284A0f6720f79403b4e9bE294', + value: BigNumber.from(0), + minGasLimit: BigNumber.from(207744), + message: + '0xd764ad0b00000000000000000000000000000000000000000000000000000000000186a00000000000000000000000004200000000000000000000000000000000000010000000000000000000000000636af16bf2f682dd3109e60102b8e1a089fedaa80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e4a9f9e67500000000000000000000000007865c6e87b9f70255377e024ace6630c1eaa37f0000000000000000000000003b8e53b3ab8e01fb57d0c9e893bc4d655aa67d84000000000000000000000000b91882244f7f82540f2941a759724523c7b9a166000000000000000000000000b91882244f7f82540f2941a759724523c7b9a166000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + }, + result: + '0x7c83d39edf60c0ab61bc7cfd2e5f741efdf02fd6e2da0f12318f0d1858d3773b', + }, + { + input: { + messageNonce: BigNumber.from(100001), + sender: '0x4200000000000000000000000000000000000007', + target: '0x5086d1eEF304eb5284A0f6720f79403b4e9bE294', + value: BigNumber.from(0), + minGasLimit: BigNumber.from(207744), + message: + '0xd764ad0b00000000000000000000000000000000000000000000000000000000000186a10000000000000000000000004200000000000000000000000000000000000010000000000000000000000000636af16bf2f682dd3109e60102b8e1a089fedaa80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e4a9f9e67500000000000000000000000007865c6e87b9f70255377e024ace6630c1eaa37f0000000000000000000000004e62882864fb8ce54affcaf8d899a286762b011b000000000000000000000000b91882244f7f82540f2941a759724523c7b9a166000000000000000000000000b91882244f7f82540f2941a759724523c7b9a166000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + }, + result: + '0x17c90d87508a23d806962f4c5f366ef505e8d80e5cc2a5c87242560c21d7c588', + }, + ] + + for (const test of tests) { + const hash = hashLowLevelMessage(test.input) + const messageSlot = hashMessageHash(hash) + expect(messageSlot).to.eq(test.result) + } + }) + }) })