From d5c47ce5f50aaf5820356cb51667fa15b88b7c8e Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Thu, 5 Dec 2024 13:58:58 +0100 Subject: [PATCH] Detect other relay actions (#249) * Detect MsgAcknowledgement & MsgTimeout * adjust which field to check * review updates + additional test --- .../query/src/helpers/identify-txs.test.ts | 177 ++++++++++++++++-- packages/query/src/helpers/identify-txs.ts | 73 ++++++-- 2 files changed, 217 insertions(+), 33 deletions(-) diff --git a/packages/query/src/helpers/identify-txs.test.ts b/packages/query/src/helpers/identify-txs.test.ts index 9d6e08d1..9e6e9c1e 100644 --- a/packages/query/src/helpers/identify-txs.test.ts +++ b/packages/query/src/helpers/identify-txs.test.ts @@ -33,7 +33,11 @@ import { import { addressFromBech32m } from '@penumbra-zone/bech32m/penumbra'; import { Address } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; import { Packet } from '@penumbra-zone/protobuf/ibc/core/channel/v1/channel_pb'; -import { MsgRecvPacket } from '@penumbra-zone/protobuf/ibc/core/channel/v1/tx_pb'; +import { + MsgAcknowledgement, + MsgRecvPacket, + MsgTimeout, +} from '@penumbra-zone/protobuf/ibc/core/channel/v1/tx_pb'; describe('getCommitmentsFromActions', () => { test('returns empty array when tx.body.actions is undefined', () => { @@ -342,31 +346,146 @@ describe('identifyTransactions', () => { expect(commitmentRecordsBeforeSize).toEqual(commitmentRecords.size); }); - test('identifies ibc relays', async () => { + describe('ibc relays', () => { const knownAddr = 'penumbra1e8k5cyds484dxvapeamwveh5khqv4jsvyvaf5wwxaaccgfghm229qw03pcar3ryy8smptevstycch0qk3uu0rgkvtjpxy3cu3rjd0agawqtlz6erev28a6sg69u7cxy0t02nd4'; const unknownAddr = 'penumbracompat1147mfall0zr6am5r45qkwht7xqqrdsp50czde7empv7yq2nk3z8yyfh9k9520ddgswkmzar22vhz9dwtuem7uxw0qytfpv7lk3q9dp8ccaw2fn5c838rfackazmgf3ahhwqq0da'; - const tx = new Transaction({ - body: { - actions: [createIbcRelay(knownAddr), createIbcRelay(unknownAddr)], - }, + + test('identifies relevant MsgRecvPacket', async () => { + const txA = new Transaction({ + body: { + actions: [createMsgReceive(knownAddr), createMsgReceive(unknownAddr)], + }, + }); + const txB = new Transaction({ + body: { + actions: [createMsgReceive(unknownAddr)], + }, + }); + const blockTx = [txA, txB]; + const spentNullifiers = new Set(); + const commitmentRecords = new Map(); + + const result = await identifyTransactions(spentNullifiers, commitmentRecords, blockTx, addr => + addr.equals(new Address(addressFromBech32m(knownAddr))), + ); + + expect(result.relevantTxs.length).toBe(1); + expect(result.relevantTxs[0]?.data.equals(txA)).toBeTruthy(); + expect(result.recoveredSourceRecords.length).toBe(0); }); - const blockTx = [tx]; - const spentNullifiers = new Set(); - const commitmentRecords = new Map(); - const result = await identifyTransactions(spentNullifiers, commitmentRecords, blockTx, addr => - addr.equals(new Address(addressFromBech32m(knownAddr))), - ); + test('identifies relevant MsgAcknowledgement', async () => { + const txA = new Transaction({ + body: { + actions: [createMsgAcknowledgement(knownAddr), createMsgAcknowledgement(unknownAddr)], + }, + }); + const txB = new Transaction({ + body: { + actions: [createMsgAcknowledgement(unknownAddr)], + }, + }); + const blockTx = [txA, txB]; + const spentNullifiers = new Set(); + const commitmentRecords = new Map(); + + const result = await identifyTransactions(spentNullifiers, commitmentRecords, blockTx, addr => + addr.equals(new Address(addressFromBech32m(knownAddr))), + ); + + expect(result.relevantTxs.length).toBe(1); + expect(result.relevantTxs[0]?.data.equals(txA)).toBeTruthy(); + expect(result.recoveredSourceRecords.length).toBe(0); + }); - expect(result.relevantTxs.length).toBe(1); - expect(result.relevantTxs[0]?.data.equals(tx)).toBeTruthy(); - expect(result.recoveredSourceRecords.length).toBe(0); + test('identifies relevant MsgTimeout', async () => { + const txA = new Transaction({ + body: { + actions: [createMsgTimeout(knownAddr), createMsgTimeout(unknownAddr)], + }, + }); + const txB = new Transaction({ + body: { + actions: [createMsgTimeout(unknownAddr)], + }, + }); + const blockTx = [txA, txB]; + const spentNullifiers = new Set(); + const commitmentRecords = new Map(); + + const result = await identifyTransactions(spentNullifiers, commitmentRecords, blockTx, addr => + addr.equals(new Address(addressFromBech32m(knownAddr))), + ); + + expect(result.relevantTxs.length).toBe(1); + expect(result.relevantTxs[0]?.data.equals(txA)).toBeTruthy(); + expect(result.recoveredSourceRecords.length).toBe(0); + }); + + test('ignores irrelevant ibc relays', async () => { + const tx = new Transaction({ + body: { + actions: [ + createMsgReceive(unknownAddr), + createMsgAcknowledgement(unknownAddr), + createMsgTimeout(unknownAddr), + ], + }, + }); + const blockTx = [tx]; + const spentNullifiers = new Set(); + const commitmentRecords = new Map(); + + const result = await identifyTransactions(spentNullifiers, commitmentRecords, blockTx, addr => + addr.equals(new Address(addressFromBech32m(knownAddr))), + ); + + expect(result.relevantTxs.length).toBe(0); + }); + + test('ignores errors if any unpack fails', async () => { + const noAction = new Action({ + action: { case: 'ibcRelayAction', value: new IbcRelay({}) }, + }); + const noPacket = new Action({ + action: { + case: 'ibcRelayAction', + value: new IbcRelay({ rawAction: Any.pack(new MsgRecvPacket({})) }), + }, + }); + const badDataPacket = new Action({ + action: { + case: 'ibcRelayAction', + value: new IbcRelay({ + rawAction: Any.pack( + new MsgRecvPacket({ + packet: new Packet({ data: new Uint8Array([1, 2, 3, 4, 5, 6, 7]) }), + }), + ), + }), + }, + }); + const tx = new Transaction({ + body: { + actions: [noAction, noPacket, badDataPacket], + }, + }); + const blockTx = [tx]; + const spentNullifiers = new Set(); + const commitmentRecords = new Map(); + + const result = await identifyTransactions(spentNullifiers, commitmentRecords, blockTx, addr => + addr.equals(new Address(addressFromBech32m(knownAddr))), + ); + + expect(result.relevantTxs.length).toBe(0); + }); }); }); -const createIbcRelay = (receiver: string): Action => { +const createMsgReceive = (receiver: string): Action => { const tokenPacketData = new FungibleTokenPacketData({ receiver }); const encoder = new TextEncoder(); const relevantRelay = Any.pack( @@ -378,3 +497,29 @@ const createIbcRelay = (receiver: string): Action => { action: { case: 'ibcRelayAction', value: new IbcRelay({ rawAction: relevantRelay }) }, }); }; + +const createMsgAcknowledgement = (sender: string): Action => { + const tokenPacketData = new FungibleTokenPacketData({ sender }); + const encoder = new TextEncoder(); + const relevantRelay = Any.pack( + new MsgAcknowledgement({ + packet: new Packet({ data: encoder.encode(tokenPacketData.toJsonString()) }), + }), + ); + return new Action({ + action: { case: 'ibcRelayAction', value: new IbcRelay({ rawAction: relevantRelay }) }, + }); +}; + +const createMsgTimeout = (sender: string): Action => { + const tokenPacketData = new FungibleTokenPacketData({ sender }); + const encoder = new TextEncoder(); + const relevantRelay = Any.pack( + new MsgTimeout({ + packet: new Packet({ data: encoder.encode(tokenPacketData.toJsonString()) }), + }), + ); + return new Action({ + action: { case: 'ibcRelayAction', value: new IbcRelay({ rawAction: relevantRelay }) }, + }); +}; diff --git a/packages/query/src/helpers/identify-txs.ts b/packages/query/src/helpers/identify-txs.ts index f74a9531..4caff3b2 100644 --- a/packages/query/src/helpers/identify-txs.ts +++ b/packages/query/src/helpers/identify-txs.ts @@ -7,16 +7,27 @@ import { SpendableNoteRecord, SwapRecord } from '@penumbra-zone/protobuf/penumbr import { Transaction } from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb'; import { TransactionId } from '@penumbra-zone/protobuf/penumbra/core/txhash/v1/txhash_pb'; import { sha256Hash } from '@penumbra-zone/crypto-web/sha256'; -import { MsgRecvPacket } from '@penumbra-zone/protobuf/ibc/core/channel/v1/tx_pb'; +import { + MsgAcknowledgement, + MsgRecvPacket, + MsgTimeout, +} from '@penumbra-zone/protobuf/ibc/core/channel/v1/tx_pb'; import { FungibleTokenPacketData } from '@penumbra-zone/protobuf/penumbra/core/component/ibc/v1/ibc_pb'; import { ViewServerInterface } from '@penumbra-zone/types/servers'; import { parseIntoAddr } from '@penumbra-zone/types/address'; +import { Packet } from '@penumbra-zone/protobuf/ibc/core/channel/v1/channel_pb'; export const BLANK_TX_SOURCE = new CommitmentSource({ source: { case: 'transaction', value: { id: new Uint8Array() } }, }); -// Identifies if a tx with a relay action of which the receiver is the user +/** + * Identifies if a tx has a relay action of which the receiver is the user. + * In terms of minting notes in the shielded pool, three IBC actions are relevant: + * - MsgRecvPacket (containing an ICS20 inbound transfer) + * - MsgAcknowledgement (containing an error acknowledgement, thus triggering a refund on our end) + * - MsgTimeout + */ const hasRelevantIbcRelay = ( tx: Transaction, isControlledAddr: ViewServerInterface['isControlledAddress'], @@ -26,32 +37,60 @@ const hasRelevantIbcRelay = ( return false; } - if (!action.action.value.rawAction?.is(MsgRecvPacket.typeName)) { + const rawAction = action.action.value.rawAction; + if (!rawAction) { return false; } - const recvPacket = new MsgRecvPacket(); - const success = action.action.value.rawAction.unpackTo(recvPacket); - if (!success) { - throw new Error('Error while trying to unpack Any to MsgRecvPacket'); + if (rawAction.is(MsgRecvPacket.typeName)) { + const recvPacket = new MsgRecvPacket(); + rawAction.unpackTo(recvPacket); + if (!recvPacket.packet) { + return false; + } + return isControlledByUser(recvPacket.packet, isControlledAddr, 'receiver'); } - if (!recvPacket.packet?.data) { - throw new Error('No FungibleTokenPacketData MsgRecvPacket'); + if (rawAction.is(MsgAcknowledgement.typeName)) { + const ackPacket = new MsgAcknowledgement(); + rawAction.unpackTo(ackPacket); + if (!ackPacket.packet) { + return false; + } + return isControlledByUser(ackPacket.packet, isControlledAddr, 'sender'); } - try { - const dataString = new TextDecoder().decode(recvPacket.packet.data); - const { receiver } = FungibleTokenPacketData.fromJsonString(dataString); - const receivingAddr = parseIntoAddr(receiver); - return isControlledAddr(receivingAddr); - // eslint-disable-next-line @typescript-eslint/no-unused-vars -- TODO: Fix eslint issue - } catch (e) { - return false; + if (rawAction.is(MsgTimeout.typeName)) { + const timeout = new MsgTimeout(); + rawAction.unpackTo(timeout); + if (!timeout.packet) { + return false; + } + return isControlledByUser(timeout.packet, isControlledAddr, 'sender'); } + + // Not a potentially relevant ibc relay action + return false; }); }; +// Determines if the packet data points to the user as the receiver +const isControlledByUser = ( + packet: Packet, + isControlledAddr: ViewServerInterface['isControlledAddress'], + entityToCheck: 'sender' | 'receiver', +): boolean => { + try { + const dataString = new TextDecoder().decode(packet.data); + const { sender, receiver } = FungibleTokenPacketData.fromJsonString(dataString); + const addrStr = entityToCheck === 'sender' ? sender : receiver; + const addrToCheck = parseIntoAddr(addrStr); + return isControlledAddr(addrToCheck); + } catch { + return false; + } +}; + // Used as a type-check helper as .filter(Boolean) still results with undefined as a possible value const isDefined = (value: T | null | undefined): value is NonNullable => value !== null && value !== undefined;