Commit 372bca22 authored by Will Cory's avatar Will Cory Committed by GitHub

fix: Replay transactions that can be finalized (#9969)

* fix: Replay transactions that can be finalized

comments

Update packages/sdk/src/cross-chain-messenger.ts

feat: Add test

remove stale changeset

fix: remove the stale goerli tests

fix: run pnpm nx build instead of pnpm build

dpeend on pnpm monorepo instead of nx building

wrong ports

http not https

fix: sepolia chain ids

debugging why it's broke

linter: rip

rekick the tests with env variables set to sepolia op rather than mainnet op

fix: Update to a withdrawal that should actually work

fix:test

* speed up ci by installing wait-on

* clean up

* fix: Test running only 1 at a time

---------
Co-authored-by: default avatarWill Cory <willcory@Wills-MacBook-Pro.local>
parent 7fd5a19c
---
'@eth-optimism/sdk': patch
---
Fixed bug where replayable transactions would fail `finalize` if they previously were marked as errors but replayable.
...@@ -282,7 +282,7 @@ def devnet_test(paths): ...@@ -282,7 +282,7 @@ def devnet_test(paths):
['npx', 'hardhat', 'deposit-eth', '--network', 'devnetL1', ['npx', 'hardhat', 'deposit-eth', '--network', 'devnetL1',
'--l1-contracts-json-path', paths.addresses_json_path, '--signer-index', '15'], '--l1-contracts-json-path', paths.addresses_json_path, '--signer-index', '15'],
cwd=paths.sdk_dir, timeout=8*60) cwd=paths.sdk_dir, timeout=8*60)
], max_workers=2) ], max_workers=1)
def run_commands(commands: list[CommandPreset], max_workers=2): def run_commands(commands: list[CommandPreset], max_workers=2):
......
...@@ -70,12 +70,13 @@ ...@@ -70,12 +70,13 @@
"lint-staged": "15.2.0", "lint-staged": "15.2.0",
"mocha": "^10.2.0", "mocha": "^10.2.0",
"nx": "18.1.2", "nx": "18.1.2",
"nx-cloud": "latest",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"prettier": "^2.8.0", "prettier": "^2.8.0",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"ts-mocha": "^10.0.0", "ts-mocha": "^10.0.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"nx-cloud": "latest" "wait-on": "^7.2.0"
}, },
"dependencies": { "dependencies": {
"@changesets/cli": "^2.27.1" "@changesets/cli": "^2.27.1"
......
...@@ -694,7 +694,13 @@ export class CrossChainMessenger { ...@@ -694,7 +694,13 @@ export class CrossChainMessenger {
message: MessageLike, message: MessageLike,
// consider making this an options object next breaking release // consider making this an options object next breaking release
messageIndex = 0, messageIndex = 0,
/**
* @deprecated no longer used since no log filters are used
*/
fromBlockOrBlockHash?: BlockTag, fromBlockOrBlockHash?: BlockTag,
/**
* @deprecated no longer used since no log filters are used
*/
toBlockOrBlockHash?: BlockTag toBlockOrBlockHash?: BlockTag
): Promise<MessageStatus> { ): Promise<MessageStatus> {
const resolved = await this.toCrossChainMessage(message, messageIndex) const resolved = await this.toCrossChainMessage(message, messageIndex)
...@@ -2301,15 +2307,64 @@ export class CrossChainMessenger { ...@@ -2301,15 +2307,64 @@ export class CrossChainMessenger {
} }
if (this.bedrock) { if (this.bedrock) {
const withdrawal = await this.toLowLevelMessage(resolved, messageIndex) // get everything we need to finalize
const messageHashV1 = hashCrossDomainMessagev1(
resolved.messageNonce,
resolved.sender,
resolved.target,
resolved.value,
resolved.minGasLimit,
resolved.message
)
// fetch the following
// 1. Whether it needs to be replayed because it failed
// 2. The withdrawal as a low level message
const [isFailed, withdrawal] = await Promise.allSettled([
this.contracts.l1.L1CrossDomainMessenger.failedMessages(
messageHashV1
),
this.toLowLevelMessage(resolved, messageIndex),
])
// handle errors
if (
isFailed.status === 'rejected' ||
withdrawal.status === 'rejected'
) {
const rejections = [isFailed, withdrawal]
.filter((p) => p.status === 'rejected')
.map((p: PromiseRejectedResult) => p.reason)
throw rejections.length > 1
? new AggregateError(rejections)
: rejections[0]
}
if (isFailed.value === true) {
const xdmWithdrawal =
this.contracts.l1.L1CrossDomainMessenger.interface.decodeFunctionData(
'relayMessage',
withdrawal.value.message
)
return this.contracts.l1.L1CrossDomainMessenger.populateTransaction.relayMessage(
xdmWithdrawal._nonce,
xdmWithdrawal._sender,
xdmWithdrawal._target,
xdmWithdrawal._value,
xdmWithdrawal._minGasLimit,
xdmWithdrawal._message,
opts?.overrides || {}
)
}
return this.contracts.l1.OptimismPortal.populateTransaction.finalizeWithdrawalTransaction( return this.contracts.l1.OptimismPortal.populateTransaction.finalizeWithdrawalTransaction(
[ [
withdrawal.messageNonce, withdrawal.value.messageNonce,
withdrawal.sender, withdrawal.value.sender,
withdrawal.target, withdrawal.value.target,
withdrawal.value, withdrawal.value.value,
withdrawal.minGasLimit, withdrawal.value.minGasLimit,
withdrawal.message, withdrawal.value.message,
], ],
opts?.overrides || {} opts?.overrides || {}
) )
......
import { describe, it, expect } from 'vitest'
import { Address, Hex, encodePacked, keccak256, toHex } from 'viem'
import { ethers } from 'ethers'
import { z } from 'zod'
import { hashCrossDomainMessagev1 } from '@eth-optimism/core-utils'
import { optimismSepolia } from 'viem/chains'
import { CONTRACT_ADDRESSES, CrossChainMessenger } from '../src'
import { sepoliaPublicClient, sepoliaTestClient } from './testUtils/viemClients'
import { sepoliaProvider, opSepoliaProvider } from './testUtils/ethersProviders'
/**
* Generated on Mar 28 2024 using
* `forge inspect L1CrossDomainMessenger storage-layout`
**/
const failedMessagesStorageLayout = {
astId: 7989,
contract: 'src/L1/L1CrossDomainMessenger.sol:L1CrossDomainMessenger',
label: 'failedMessages',
offset: 0,
slot: 206n,
type: 't_mapping(t_bytes32,t_bool)',
}
const sepoliaCrossDomainMessengerAddress = CONTRACT_ADDRESSES[
optimismSepolia.id
].l1.L1CrossDomainMessenger as Address
const setMessageAsFailed = async (tx: Hex) => {
const message = await crossChainMessenger.toCrossChainMessage(tx)
const messageHash = hashCrossDomainMessagev1(
message.messageNonce,
message.sender,
message.target,
message.value,
message.minGasLimit,
message.message
) as Hex
const keySlotHash = keccak256(
encodePacked(
['bytes32', 'uint256'],
[messageHash, failedMessagesStorageLayout.slot]
)
)
return sepoliaTestClient.setStorageAt({
address: sepoliaCrossDomainMessengerAddress,
index: keySlotHash,
value: toHex(true, { size: 32 }),
})
}
const E2E_PRIVATE_KEY = z
.string()
.describe('Private key')
// Mnemonic: test test test test test test test test test test test junk
.default('0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6')
.parse(import.meta.env.VITE_E2E_PRIVATE_KEY)
const sepoliaWallet = new ethers.Wallet(E2E_PRIVATE_KEY, sepoliaProvider)
const crossChainMessenger = new CrossChainMessenger({
l1SignerOrProvider: sepoliaWallet,
l2SignerOrProvider: opSepoliaProvider,
l1ChainId: 11155111,
l2ChainId: 11155420,
bedrock: true,
})
describe('replaying failed messages', () => {
it('should be able to replay failed messages', async () => {
// Grab an existing tx but mark it as failed
// @see https://sepolia-optimism.etherscan.io/tx/0x28249a36f764afab583a4633d59ff6c2a0e934293062bffa7cedb662e5da9abd
const tx =
'0x28249a36f764afab583a4633d59ff6c2a0e934293062bffa7cedb662e5da9abd'
await setMessageAsFailed(tx)
// debugging ethers.js is brutal because of error message so let's instead
// send the tx with viem. If it succeeds we will then test with ethers
const txData =
await crossChainMessenger.populateTransaction.finalizeMessage(tx)
await sepoliaPublicClient.call({
data: txData.data as Hex,
to: txData.to as Address,
})
// finalize the message
const finalizeTx = await crossChainMessenger.finalizeMessage(tx)
const receipt = await finalizeTx.wait()
expect(receipt.transactionHash).toBeDefined()
})
})
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"lib": ["ES2021"],
"rootDir": "./src", "rootDir": "./src",
"outDir": "./dist" "outDir": "./dist"
}, },
......
...@@ -108,6 +108,9 @@ importers: ...@@ -108,6 +108,9 @@ importers:
typescript: typescript:
specifier: ^5.3.3 specifier: ^5.3.3
version: 5.3.3 version: 5.3.3
wait-on:
specifier: ^7.2.0
version: 7.2.0
endpoint-monitor: {} endpoint-monitor: {}
...@@ -15323,6 +15326,12 @@ packages: ...@@ -15323,6 +15326,12 @@ packages:
resolution: {integrity: sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==} resolution: {integrity: sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==}
dev: true dev: true
/rxjs@7.8.1:
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
dependencies:
tslib: 2.6.2
dev: true
/safe-array-concat@1.0.0: /safe-array-concat@1.0.0:
resolution: {integrity: sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==} resolution: {integrity: sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==}
engines: {node: '>=0.4'} engines: {node: '>=0.4'}
...@@ -17338,6 +17347,20 @@ packages: ...@@ -17338,6 +17347,20 @@ packages:
- zod - zod
dev: true dev: true
/wait-on@7.2.0:
resolution: {integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==}
engines: {node: '>=12.0.0'}
hasBin: true
dependencies:
axios: 1.6.7
joi: 17.11.0
lodash: 4.17.21
minimist: 1.2.8
rxjs: 7.8.1
transitivePeerDependencies:
- debug
dev: true
/walker@1.0.8: /walker@1.0.8:
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
dependencies: dependencies:
......
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