diff --git a/.changeset/fuzzy-lobsters-wait.md b/.changeset/fuzzy-lobsters-wait.md new file mode 100644 index 000000000000..66b462841ba9 --- /dev/null +++ b/.changeset/fuzzy-lobsters-wait.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/sdk': minor +--- + +Fixes issue with legacy withdrawal message status detection diff --git a/.circleci/config.yml b/.circleci/config.yml index 95a976486972..56c6433f32d8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -626,12 +626,12 @@ jobs: 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 9023108 + command: anvil --fork-url $ANVIL_L1_FORK_URL --fork-block-number 9190101 - 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 9504811 + command: anvil --fork-url $ANVIL_L2_FORK_URL --port 9545 --fork-block-number 10756611 - run: name: build command: pnpm build diff --git a/packages/core-utils/src/optimism/hashing.ts b/packages/core-utils/src/optimism/hashing.ts index 9f3b65af308b..ccc20b6c0557 100644 --- a/packages/core-utils/src/optimism/hashing.ts +++ b/packages/core-utils/src/optimism/hashing.ts @@ -62,11 +62,11 @@ export const hashCrossDomainMessage = ( target: string, value: BigNumber, gasLimit: BigNumber, - data: string + message: string ) => { const { version } = decodeVersionedNonce(nonce) if (version.eq(0)) { - return hashCrossDomainMessagev0(target, sender, data, nonce) + return hashCrossDomainMessagev0(target, sender, message, nonce) } else if (version.eq(1)) { return hashCrossDomainMessagev1( nonce, @@ -74,7 +74,7 @@ export const hashCrossDomainMessage = ( target, value, gasLimit, - data + message ) } throw new Error(`unknown version ${version.toString()}`) @@ -85,16 +85,16 @@ export const hashCrossDomainMessage = ( * * @param target The target of the cross domain message * @param sender The sender of the cross domain message - * @param data The data passed along with the cross domain message + * @param message The message passed along with the cross domain message * @param nonce The cross domain message nonce */ export const hashCrossDomainMessagev0 = ( target: string, sender: string, - data: string, + message: string, nonce: BigNumber ) => { - return keccak256(encodeCrossDomainMessageV0(target, sender, data, nonce)) + return keccak256(encodeCrossDomainMessageV0(target, sender, message, nonce)) } /** @@ -105,7 +105,7 @@ export const hashCrossDomainMessagev0 = ( * @param target The target of the cross domain message * @param value The value being sent with the cross domain message * @param gasLimit The gas limit of the cross domain execution - * @param data The data passed along with the cross domain message + * @param message The message passed along with the cross domain message */ export const hashCrossDomainMessagev1 = ( nonce: BigNumber, @@ -113,10 +113,10 @@ export const hashCrossDomainMessagev1 = ( target: string, value: BigNumberish, gasLimit: BigNumberish, - data: string + message: string ) => { return keccak256( - encodeCrossDomainMessageV1(nonce, sender, target, value, gasLimit, data) + encodeCrossDomainMessageV1(nonce, sender, target, value, gasLimit, message) ) } @@ -128,7 +128,7 @@ export const hashCrossDomainMessagev1 = ( * @param target The target of the cross domain message * @param value The value being sent with the cross domain message * @param gasLimit The gas limit of the cross domain execution - * @param data The data passed along with the cross domain message + * @param message The message passed along with the cross domain message */ export const hashWithdrawal = ( nonce: BigNumber, @@ -136,7 +136,7 @@ export const hashWithdrawal = ( target: string, value: BigNumber, gasLimit: BigNumber, - data: string + message: string ): string => { const types = ['uint256', 'address', 'address', 'uint256', 'uint256', 'bytes'] const encoded = defaultAbiCoder.encode(types, [ @@ -145,7 +145,7 @@ export const hashWithdrawal = ( target, value, gasLimit, - data, + message, ]) return keccak256(encoded) } diff --git a/packages/sdk/src/cross-chain-messenger.ts b/packages/sdk/src/cross-chain-messenger.ts index b8573c9a3ae9..764d9b6337c4 100644 --- a/packages/sdk/src/cross-chain-messenger.ts +++ b/packages/sdk/src/cross-chain-messenger.ts @@ -27,6 +27,8 @@ import { decodeVersionedNonce, encodeVersionedNonce, getChainId, + hashCrossDomainMessagev0, + hashCrossDomainMessagev1, } from '@eth-optimism/core-utils' import { getContractInterface, predeploys } from '@eth-optimism/contracts' import * as rlp from 'rlp' @@ -716,7 +718,18 @@ export class CrossChainMessenger { message: MessageLike ): Promise { const resolved = await this.toCrossChainMessage(message) - const messageHash = hashCrossDomainMessage( + // legacy withdrawals relayed prebedrock are v1 + const messageHashV0 = hashCrossDomainMessagev0( + resolved.target, + resolved.sender, + resolved.message, + resolved.messageNonce + ) + // bedrock withdrawals are v1 + // legacy withdrawals relayed postbedrock are v1 + // there is no good way to differentiate between the two types of legacy + // so what we will check for both + const messageHashV1 = hashCrossDomainMessagev1( resolved.messageNonce, resolved.sender, resolved.target, @@ -731,9 +744,15 @@ export class CrossChainMessenger { ? this.contracts.l2.L2CrossDomainMessenger : this.contracts.l1.L1CrossDomainMessenger - const relayedMessageEvents = await messenger.queryFilter( - messenger.filters.RelayedMessage(messageHash) - ) + // this is safe because we can guarantee only one of these filters max will return something + const relayedMessageEvents = [ + ...(await messenger.queryFilter( + messenger.filters.RelayedMessage(messageHashV0) + )), + ...(await messenger.queryFilter( + messenger.filters.RelayedMessage(messageHashV1) + )), + ] // Great, we found the message. Convert it into a transaction receipt. if (relayedMessageEvents.length === 1) { @@ -749,9 +768,14 @@ export class CrossChainMessenger { // We didn't find a transaction that relayed the message. We now attempt to find // FailedRelayedMessage events instead. - const failedRelayedMessageEvents = await messenger.queryFilter( - messenger.filters.FailedRelayedMessage(messageHash) - ) + const failedRelayedMessageEvents = [ + ...(await messenger.queryFilter( + messenger.filters.FailedRelayedMessage(messageHashV0) + )), + ...(await messenger.queryFilter( + messenger.filters.FailedRelayedMessage(messageHashV1) + )), + ] // A transaction can fail to be relayed multiple times. We'll always return the last // transaction that attempted to relay the message. diff --git a/packages/sdk/test-next/messageStatus.ts b/packages/sdk/test-next/messageStatus.ts new file mode 100644 index 000000000000..99809d4cec0d --- /dev/null +++ b/packages/sdk/test-next/messageStatus.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest' + +import { CrossChainMessenger, MessageStatus } from '../src' +import { l1Provider, l2Provider } from './testUtils/ethersProviders' + +const crossChainMessenger = new CrossChainMessenger({ + l1SignerOrProvider: l1Provider, + l2SignerOrProvider: l2Provider, + l1ChainId: 5, + l2ChainId: 420, + bedrock: true, +}) + +describe('prove message', () => { + it(`should be able to correctly find a finalized withdrawal`, async () => { + /** + * Tx hash of legacy withdrawal that was claimed + * + * @see https://goerli-optimism.etherscan.io/tx/0xda9e9c8dfc7718bc1499e1e64d8df6cddbabc46e819475a6c755db286a41b9fa + */ + const txWithdrawalHash = + '0xda9e9c8dfc7718bc1499e1e64d8df6cddbabc46e819475a6c755db286a41b9fa' + + const txReceipt = await l2Provider.getTransactionReceipt(txWithdrawalHash) + + expect(txReceipt).toBeDefined() + + expect(await crossChainMessenger.getMessageStatus(txWithdrawalHash)).toBe( + MessageStatus.RELAYED + ) + }, 20_000) +})