Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Replay transactions that can be finalized #9969

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/young-gorillas-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@eth-optimism/sdk': patch
---

Fixed bug where replayable transactions would fail `finalize` if they previously were marked as errors but replayable.
2 changes: 1 addition & 1 deletion bedrock-devnet/devnet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ def devnet_test(paths):
['npx', 'hardhat', 'deposit-eth', '--network', 'devnetL1',
'--l1-contracts-json-path', paths.addresses_json_path, '--signer-index', '15'],
cwd=paths.sdk_dir, timeout=8*60)
], max_workers=2)
], max_workers=1)


def run_commands(commands: list[CommandPreset], max_workers=2):
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,13 @@
"lint-staged": "15.2.0",
"mocha": "^10.2.0",
"nx": "18.1.2",
"nx-cloud": "latest",
roninjin10 marked this conversation as resolved.
Show resolved Hide resolved
"nyc": "^15.1.0",
"prettier": "^2.8.0",
"rimraf": "^5.0.5",
"ts-mocha": "^10.0.0",
"typescript": "^5.3.3",
"nx-cloud": "latest"
"wait-on": "^7.2.0"
roninjin10 marked this conversation as resolved.
Show resolved Hide resolved
},
"dependencies": {
"@changesets/cli": "^2.27.1"
Expand Down
69 changes: 62 additions & 7 deletions packages/sdk/src/cross-chain-messenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,13 @@ export class CrossChainMessenger {
message: MessageLike,
// consider making this an options object next breaking release
messageIndex = 0,
/**
* @deprecated no longer used since no log filters are used
*/
fromBlockOrBlockHash?: BlockTag,
/**
* @deprecated no longer used since no log filters are used
*/
toBlockOrBlockHash?: BlockTag
): Promise<MessageStatus> {
const resolved = await this.toCrossChainMessage(message, messageIndex)
Expand Down Expand Up @@ -2301,15 +2307,64 @@ export class CrossChainMessenger {
}

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(
[
withdrawal.messageNonce,
withdrawal.sender,
withdrawal.target,
withdrawal.value,
withdrawal.minGasLimit,
withdrawal.message,
withdrawal.value.messageNonce,
roninjin10 marked this conversation as resolved.
Show resolved Hide resolved
withdrawal.value.sender,
withdrawal.value.target,
withdrawal.value.value,
withdrawal.value.minGasLimit,
withdrawal.value.message,
],
opts?.overrides || {}
)
Expand Down
95 changes: 95 additions & 0 deletions packages/sdk/test-next/failedMessages.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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()
})
})
1 change: 1 addition & 0 deletions packages/sdk/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"lib": ["ES2021"],
roninjin10 marked this conversation as resolved.
Show resolved Hide resolved
"rootDir": "./src",
"outDir": "./dist"
},
Expand Down
23 changes: 23 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.