Skip to content

Commit

Permalink
Detect other relay actions (#249)
Browse files Browse the repository at this point in the history
* Detect MsgAcknowledgement & MsgTimeout

* adjust which field to check

* review updates + additional test
  • Loading branch information
grod220 authored Dec 5, 2024
1 parent b85b362 commit d5c47ce
Show file tree
Hide file tree
Showing 2 changed files with 217 additions and 33 deletions.
177 changes: 161 additions & 16 deletions packages/query/src/helpers/identify-txs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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<Nullifier>();
const commitmentRecords = new Map<StateCommitment, SpendableNoteRecord | SwapRecord>();

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<Nullifier>();
const commitmentRecords = new Map<StateCommitment, SpendableNoteRecord | SwapRecord>();

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<Nullifier>();
const commitmentRecords = new Map<StateCommitment, SpendableNoteRecord | SwapRecord>();

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<Nullifier>();
const commitmentRecords = new Map<StateCommitment, SpendableNoteRecord | SwapRecord>();

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<Nullifier>();
const commitmentRecords = new Map<StateCommitment, SpendableNoteRecord | SwapRecord>();

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<Nullifier>();
const commitmentRecords = new Map<StateCommitment, SpendableNoteRecord | SwapRecord>();

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(
Expand All @@ -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 }) },
});
};
73 changes: 56 additions & 17 deletions packages/query/src/helpers/identify-txs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand All @@ -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 = <T>(value: T | null | undefined): value is NonNullable<T> =>
value !== null && value !== undefined;
Expand Down

0 comments on commit d5c47ce

Please sign in to comment.