-
Notifications
You must be signed in to change notification settings - Fork 252
chore: find reverted gmp transactions in live mode #12311
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
base: master
Are you sure you want to change the base?
Conversation
f04b63d to
28b2ce9
Compare
|
Logs from earlier e2e testing specifically if tx is made by the Wallet Owner: Logs when tx is made non-owner of the Wallet: I’ve made many updates since the last round of testing, so I will need to do E2E testing again. |
5bfdeab to
51dd6d0
Compare
74778d6 to
5d58219
Compare
b9fde86 to
b8b1099
Compare
|
E2E testing results Success Case
Revert Case(Authorized User)
|
…12327) closes: - https://github.com/Agoric/agoric-private/issues/699 ## Description Publishes the expected `sourceAddress` (LCA) for pending GMP transactions into vstorage and updates snapshots accordingly. This enables the GMP watcher to reliably classify reverted Wallet executions by comparing the decoded `sourceAddress` from `calldata` against the expected value, avoiding revert simulation and state-drift issues(see #12311). ### Testing Considerations The vstorage snapshot tests are enough to verify this change. ### Upgrade Considerations - Will require a `ymax0` and `ymax1` update on mainnet.
|
Base branch is changed to master. Please re-run the integration tests by adding 'force:integration' label. |
Revert Case(Non-Authorized)
|
b8b1099 to
183e078
Compare
| const provider = ctx.evmProviders[caipId] as WebSocketProvider; | ||
|
|
||
| // Extract the address portion from sourceAddress (format: 'cosmos:agoric-3:agoric1...') | ||
| const lcaAddress = parseAccountId(sourceAddress).accountAddress; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to assert for lcaAddress presence/correctness? What if accountAddress is not the required format? Or does parseAccountId handle that from inside already (as hard failure)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is required and validated in engine.ts via mustMatch
| if (result.settled) { | ||
| const reason = `${logPrefix} Live mode completed`; | ||
| log(reason); | ||
| abortController.abort(reason); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about handling tx not settled here?
| return { settled: false }; | ||
| } | ||
|
|
||
| deleteTxBlockLowerBound(kvStore, txId); | ||
| return { found: true, txHash: matchingEvent.transactionHash }; | ||
| return { settled: true, txHash: matchingEvent.transactionHash }; | ||
| } catch (error) { | ||
| log(`Error:`, error); | ||
| return { found: false }; | ||
| return { settled: false }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need some extra identifier to differentiate b/w not found and error? Because both says settled: false
| let timeoutId: NodeJS.Timeout | undefined; | ||
| let subId: string | null = null; | ||
|
|
||
| const ws = provider.websocket as WebSocket; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this provider.websocket natively provided by alchemy/ethers, right? We don't need to manually connect to it I see?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, provider.websocket is natively provided by ethers.js when using a WebSocketProvider
| reject(WATCH_GMP_ABORTED); | ||
| return; | ||
| } | ||
| if (signal?.aborted) return resolve({ settled: false }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a comment why resolving instead of rejection now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the moment, it's the same pattern in all watchers. We don't make use of reject. I think ideally we can refactor later in all watchers to reject with rejectionReason.
| `Watching for MulticallStatus and MulticallExecuted events for txId: ${txId} at contract: ${contractAddress}`, | ||
| ); | ||
| // Named so we can remove it | ||
| const onAbort = () => finish({ settled: false }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we wanna explicitly log when we aborted signal? Might help in debugging.
| ws.off('message', messageHandler); | ||
| ws.off('error', onWsError); | ||
| ws.off('close', onWsClose); | ||
| signal?.removeEventListener('abort', onAbort); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a log say: "Cleaning up ws and listeners"..?
| const subscribe = async () => { | ||
| await provider.getNetwork(); | ||
|
|
||
| subId = await provider.send('eth_subscribe', [ | ||
| 'alchemy_minedTransactions', | ||
| { | ||
| addresses: [{ to: contractAddress }], | ||
| includeRemoved: false, | ||
| hashesOnly: false, | ||
| }, | ||
| ]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we handle the failure to subscribe here? Or does the caller handle that in catch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's happening via .catch(fail) on line 327
| if (!done) { | ||
| log( | ||
| `✗ No MulticallStatus or MulticallExecuted found for txId ${txId} within ${timeoutMs / 60000} minutes`, | ||
| ); | ||
| const { status, errorMessage } = await findTxStatusFromAxelarscan( | ||
| txId, | ||
| contractAddress, | ||
| { | ||
| axelarApiUrl, | ||
| fetch, | ||
| }, | ||
| `✗ No transaction status found for txId ${txId} within ${ | ||
| timeoutMs / 60000 | ||
| } minutes`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see we make done = true on failure as well. So done essentially means that tx is processed? Either success/failed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, done means the watcher is finished (stopped watching), regardless of whether the transaction succeeded, failed, or was never observed.
|
|
||
| // Custom error code for aborted GMP watch | ||
| export const WATCH_GMP_ABORTED = 'WATCH_GMP_ABORTED'; | ||
| // Alchemy alchemy_minedTransactions subscription message types |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please link to documentation if possible, and feel free to use //#region///#endregion.
| fetch: typeof fetch; | ||
| axelarApiUrl: string; | ||
| }; | ||
| // AxelarExecutable entrypoint (standard) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please link to documentation if possible.
| const WALLET_EXECUTE_ABI = [ | ||
| 'function execute(bytes32 commandId, string sourceChain, string sourceAddress, bytes payload) external', | ||
| ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GMP_ABI uses the same _ABI name suffix but the JSON ABI representation, and I think particularly the inputs of a function... can we commit to and document a particular convention? I wouldn't mind if it's verbose, e.g. GMP_INPUTS_ABI_JSON and WALLET_EXECUTE_CONTRACT_ABI_TEXT.
| // execute(bytes32 commandId, string sourceChain, string sourceAddress, bytes payload) | ||
| const sourceAddress: string = parsed.args?.[2]; | ||
| const payload: string = parsed.args?.[3]; | ||
| if (!sourceAddress || !payload) return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // execute(bytes32 commandId, string sourceChain, string sourceAddress, bytes payload) | |
| const sourceAddress: string = parsed.args?.[2]; | |
| const payload: string = parsed.args?.[3]; | |
| if (!sourceAddress || !payload) return null; | |
| const [_commandId, _sourceChain, sourceAddress, payload] = parsed.args; | |
| if (!sourceAddress || !payload) return null; |
| // CallMessage { string id; ContractCalls[] calls; } | ||
| const decoded = abiCoder.decode( | ||
| ['tuple(string id, tuple(address target, bytes data)[] calls)'], | ||
| payload, | ||
| ); | ||
|
|
||
| const txId = decoded?.[0]?.id; | ||
| if (typeof txId !== 'string') return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // CallMessage { string id; ContractCalls[] calls; } | |
| const decoded = abiCoder.decode( | |
| ['tuple(string id, tuple(address target, bytes data)[] calls)'], | |
| payload, | |
| ); | |
| const txId = decoded?.[0]?.id; | |
| if (typeof txId !== 'string') return null; | |
| // CallMessage { string id; ContractCalls[] calls; } | |
| const [decoded] = abiCoder.decode( | |
| ['tuple(string id, tuple(address target, bytes data)[] calls)'], | |
| payload, | |
| ); | |
| const txId = decoded?.id; | |
| if (typeof txId !== 'string') return null; |
| if (receipt.status === 1 && matchingLog) { | ||
| log( | ||
| `✅ SUCCESS: txId=${txId} txHash=${txHash} block=${receipt.blockNumber}`, | ||
| ); | ||
| return finish({ settled: true, txHash }); | ||
| } | ||
|
|
||
| if (receipt.status === 0) { | ||
| /** | ||
| * Transaction reverted - since we've already validated that the sourceAddress | ||
| * matches our expected LCA address, this is a legitimate execution attempt | ||
| * from our own wallet that failed. We treat this as a transaction failure. | ||
| * | ||
| * Note: Spurious executions from unauthorized parties are already filtered | ||
| * out by the sourceAddress check above, so any revert we see here represents | ||
| * a genuine failure of the user's operation. | ||
| */ | ||
| log( | ||
| `❌ REVERTED: txId=${txId} txHash=${txHash} block=${receipt.blockNumber} - transaction failed`, | ||
| ); | ||
| return finish({ settled: true, txHash }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find it troubling that these two cases collapse into indistinguishable output.
| subId = await provider.send('eth_subscribe', [ | ||
| 'alchemy_minedTransactions', | ||
| { | ||
| addresses: [{ to: contractAddress }], | ||
| includeRemoved: false, | ||
| hashesOnly: false, | ||
| }, | ||
| ]); | ||
|
|
||
| ws.on('message', messageHandler); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any chance of a race here? Our general pattern is to register listeners before creating circumstances that could cause them to be invoked.
| // Attach listeners (all removable) | ||
| ws.on('error', onWsError); | ||
| ws.on('close', onWsClose); | ||
| signal?.addEventListener('abort', onAbort); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question here.
| export const EVENTS = { | ||
| MULTICALL_EXECUTED: 'executed', | ||
| MULTICALL_STATUS: 'status', | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like the EVENTS container is now superfluous.
|
|
||
| // Mock websocket for gmp-watcher - store event handlers | ||
| const wsEventHandlers = new Map<string, Function[]>(); | ||
| const mockWebSocket = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be hand-rolling a partial implementation of EventEmitter; why not just use what's already provided?
closes:
Description
Removes the dependency on AxelarScan for GMP transaction failure status detection. Instead, it now uses Alchemy WebSocket connections to detect failed GMP transactions on EVM. Alchemy WebSockets are used for both failure and success detection.
This functionality is currently implemented in the live mode of the GMP watcher. Support for lookback mode will be added in a subsequent PR, as detecting reverted transactions in historical logs requires a different mechanism.
Testing Considerations
Existing tests have been adapted to the new changes. Mocks were updated as well. And couple of new tests were added for testing revert behavior.
Upgrade Considerations