Skip to content

Commit

Permalink
Save relevate ibc relay tx
Browse files Browse the repository at this point in the history
  • Loading branch information
grod220 committed Sep 24, 2024
1 parent ff2012c commit 7f3ee3b
Show file tree
Hide file tree
Showing 13 changed files with 230 additions and 29 deletions.
9 changes: 9 additions & 0 deletions packages/bech32m/src/penumbracompat1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,13 @@ export const compatAddressFromBech32 = (penumbracompat1: string): { [innerName]:
[innerName]: fromBech32(penumbracompat1 as `${typeof prefix}1${string}`, prefix),
});

export const isCompatAddress = (check: string): check is `${typeof prefix}1${string}` => {
try {
compatAddressFromBech32(check);
return true;
} catch {
return false;
}
};

export { PENUMBRA_BECH32M_ADDRESS_LENGTH, PENUMBRA_BECH32M_ADDRESS_PREFIX } from './index.js';
6 changes: 5 additions & 1 deletion packages/protobuf/src/registry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IMessageTypeRegistry, createRegistry } from '@bufbuild/protobuf';
import { createRegistry, IMessageTypeRegistry } from '@bufbuild/protobuf';

import * as ibcCore from './services/cosmos-ibc-core.js';
import * as penumbraCnidarium from './services/penumbra-cnidarium.js';
Expand All @@ -8,6 +8,7 @@ import * as penumbraUtil from './services/penumbra-util.js';
import * as penumbraView from './services/penumbra-view.js';

import { MsgRecvPacket } from '../gen/ibc/core/channel/v1/tx_pb.js';
import { MsgUpdateClient } from '../gen/ibc/core/client/v1/tx_pb.js';
import { ClientState, Header } from '../gen/ibc/lightclients/tendermint/v1/tendermint_pb.js';
import { DutchAuction } from '../gen/penumbra/core/component/auction/v1/auction_pb.js';

Expand Down Expand Up @@ -41,6 +42,9 @@ export const typeRegistry: IMessageTypeRegistry = createRegistry(
// gen/ibc/core/channel/v1/tx_pb
MsgRecvPacket,

// gen/ibc/core/client/v1/tx_pb
MsgUpdateClient,

// penumbra/core/component/auction/v1/auction_pb
DutchAuction,
);
Expand Down
1 change: 1 addition & 0 deletions packages/protobuf/src/services/cosmos-ibc-core.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { Query as IbcChannelService } from '../../gen/ibc/core/channel/v1/query_connect.js';
export { Query as IbcClientService } from '../../gen/ibc/core/client/v1/query_connect.js';
export { Msg as IbcClientMsgService } from '../../gen/ibc/core/client/v1/tx_connect.js';
export { Msg as IbcChannelMsgService } from '../../gen/ibc/core/channel/v1/tx_connect.js';
export { Query as IbcConnectionService } from '../../gen/ibc/core/connection/v1/query_connect.js';
8 changes: 6 additions & 2 deletions packages/protobuf/src/web.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type {
import {
IbcChannelMsgService,
IbcChannelService,
IbcClientMsgService,
IbcClientService,
IbcConnectionService,
} from './services/cosmos-ibc-core.js';
Expand All @@ -11,11 +13,11 @@ import type {
CommunityPoolService,
CompactBlockService,
DexService,
SimulationService,
FeeService,
GovernanceService,
SctService,
ShieldedPoolService,
SimulationService,
StakeService,
} from './services/penumbra-core.js';
import type { TendermintProxyService } from './services/penumbra-util.js';
Expand All @@ -30,7 +32,9 @@ export type PenumbraService =
| typeof FeeService
| typeof GovernanceService
| typeof IbcChannelService
| typeof IbcChannelMsgService
| typeof IbcClientService
| typeof IbcClientMsgService
| typeof IbcConnectionService
| typeof SctService
| typeof ShieldedPoolService
Expand Down
3 changes: 3 additions & 0 deletions packages/query/src/block-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,11 +305,14 @@ export class BlockProcessor implements BlockProcessorInterface {
// this is a network query
const blockTx = await this.querier.app.txsByHeight(compactBlock.height);

// debugger;

// Filter down to transactions & note records in block relevant to user
const { relevantTxs, recoveredSourceRecords } = await identifyTransactions(
spentNullifiers,
recordsByCommitment,
blockTx,
addr => this.viewServer.isControlledAddress(addr),
);

// this simply stores the new records with 'rehydrated' sources to idb
Expand Down
92 changes: 85 additions & 7 deletions packages/query/src/helpers/identify-txs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getCommitmentsFromActions,
getNullifiersFromActions,
identifyTransactions,
parseIntoAddr,
} from './identify-txs.js';
import {
Output,
Expand All @@ -27,6 +28,15 @@ import {
SwapClaimBody,
} from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb';
import { SpendableNoteRecord, SwapRecord } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';
import { Any } from '@bufbuild/protobuf';
import {
FungibleTokenPacketData,
IbcRelay,
} from '@penumbra-zone/protobuf/penumbra/core/component/ibc/v1/ibc_pb';
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';

const BLANK_TX_SOURCE = new CommitmentSource({
source: { case: 'transaction', value: { id: new Uint8Array() } },
Expand Down Expand Up @@ -213,7 +223,12 @@ describe('identifyTransactions', () => {
const spentNullifiers = new Set<Nullifier>();
const commitmentRecords = new Map<StateCommitment, SpendableNoteRecord | SwapRecord>();

const result = await identifyTransactions(spentNullifiers, commitmentRecords, blockTx);
const result = await identifyTransactions(
spentNullifiers,
commitmentRecords,
blockTx,
() => false,
);

expect(result.relevantTxs).toEqual([]);
expect(result.recoveredSourceRecords).toEqual([]);
Expand Down Expand Up @@ -311,12 +326,17 @@ describe('identifyTransactions', () => {

const spentNullifiersBeforeSize = spentNullifiers.size;
const commitmentRecordsBeforeSize = commitmentRecords.size;
const result = await identifyTransactions(spentNullifiers, commitmentRecords, [
tx1, // relevant
tx2, // relevant
tx3, // not
tx4, // not
]);
const result = await identifyTransactions(
spentNullifiers,
commitmentRecords,
[
tx1, // relevant
tx2, // relevant
tx3, // not
tx4, // not
],
() => false,
);

expect(result.relevantTxs.length).toBe(2);
expect(result.recoveredSourceRecords.length).toBe(1);
Expand All @@ -328,4 +348,62 @@ describe('identifyTransactions', () => {
expect(spentNullifiersBeforeSize).toEqual(spentNullifiers.size);
expect(commitmentRecordsBeforeSize).toEqual(commitmentRecords.size);
});

test('identifies ibc relays', async () => {
const knownAddr =
'penumbra1e8k5cyds484dxvapeamwveh5khqv4jsvyvaf5wwxaaccgfghm229qw03pcar3ryy8smptevstycch0qk3uu0rgkvtjpxy3cu3rjd0agawqtlz6erev28a6sg69u7cxy0t02nd4';
const unknownAddr =
'penumbracompat1147mfall0zr6am5r45qkwht7xqqrdsp50czde7empv7yq2nk3z8yyfh9k9520ddgswkmzar22vhz9dwtuem7uxw0qytfpv7lk3q9dp8ccaw2fn5c838rfackazmgf3ahhwqq0da';
const tx = new Transaction({
body: {
actions: [createIbcRelay(knownAddr), createIbcRelay(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(1);
expect(result.relevantTxs[0]?.data.equals(tx)).toBeTruthy();
expect(result.recoveredSourceRecords.length).toBe(0);
});
});

const createIbcRelay = (receiver: string): Action => {
const tokenPacketData = new FungibleTokenPacketData({ receiver });
const encoder = new TextEncoder();
const relevantRelay = Any.pack(
new MsgRecvPacket({
packet: new Packet({ data: encoder.encode(tokenPacketData.toJsonString()) }),
}),
);
return new Action({
action: { case: 'ibcRelayAction', value: new IbcRelay({ rawAction: relevantRelay }) },
});
};

describe('parseIntoAddr', () => {
test('works with compat', () => {
expect(() =>
parseIntoAddr(
'penumbracompat1147mfall0zr6am5r45qkwht7xqqrdsp50czde7empv7yq2nk3z8yyfh9k9520ddgswkmzar22vhz9dwtuem7uxw0qytfpv7lk3q9dp8ccaw2fn5c838rfackazmgf3ahhwqq0da',
),
).not.toThrow();
});

test('works with normal addresses', () => {
expect(() =>
parseIntoAddr(
'penumbra1e8k5cyds484dxvapeamwveh5khqv4jsvyvaf5wwxaaccgfghm229qw03pcar3ryy8smptevstycch0qk3uu0rgkvtjpxy3cu3rjd0agawqtlz6erev28a6sg69u7cxy0t02nd4',
),
).not.toThrow();
});

test('raises on invalid addresses', () => {
expect(() => parseIntoAddr('not_valid_format')).toThrow();
});
});
57 changes: 56 additions & 1 deletion packages/query/src/helpers/identify-txs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,58 @@ 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 { Address } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb';
import { FungibleTokenPacketData } from '@penumbra-zone/protobuf/penumbra/core/component/ibc/v1/ibc_pb';
import { addressFromBech32m } from '@penumbra-zone/bech32m/penumbra';
import { compatAddressFromBech32, isCompatAddress } from '@penumbra-zone/bech32m/penumbracompat1';
import { ViewServerInterface } from '@penumbra-zone/types/servers';

const BLANK_TX_SOURCE = new CommitmentSource({
source: { case: 'transaction', value: { id: new Uint8Array() } },
});

export const parseIntoAddr = (addrStr: string): Address => {
if (isCompatAddress(addrStr)) {
return new Address(compatAddressFromBech32(addrStr));
}
return new Address(addressFromBech32m(addrStr));
};

export const hasRelevantIbcRelay = (
tx: Transaction,
isControlledAddr: ViewServerInterface['isControlledAddress'],
) => {
return tx.body?.actions.some(action => {
if (action.action.case !== 'ibcRelayAction') {
return false;
}

if (!action.action.value.rawAction?.is(MsgRecvPacket.typeName)) {
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 (!recvPacket.packet?.data) {
throw new Error('No FungibleTokenPacketData MsgRecvPacket');
}

try {
const dataString = new TextDecoder().decode(recvPacket.packet.data);
const { receiver } = FungibleTokenPacketData.fromJsonString(dataString);
const receivingAddr = parseIntoAddr(receiver);
return isControlledAddr(receivingAddr);
} catch (e) {
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 Expand Up @@ -70,6 +117,7 @@ const searchRelevant = async (
tx: Transaction,
spentNullifiers: Set<Nullifier>,
commitmentRecords: Map<StateCommitment, SpendableNoteRecord | SwapRecord>,
isControlledAddr: ViewServerInterface['isControlledAddress'],
): Promise<
{ relevantTx: RelevantTx; recoveredSourceRecords: RecoveredSourceRecords } | undefined
> => {
Expand Down Expand Up @@ -99,6 +147,10 @@ const searchRelevant = async (
}
}

if (hasRelevantIbcRelay(tx, isControlledAddr)) {
txId ??= await generateTxId(tx);
}

if (txId) {
return {
relevantTx: { id: txId, data: tx },
Expand All @@ -115,14 +167,17 @@ export const identifyTransactions = async (
spentNullifiers: Set<Nullifier>,
commitmentRecords: Map<StateCommitment, SpendableNoteRecord | SwapRecord>,
blockTx: Transaction[],
isControlledAddr: ViewServerInterface['isControlledAddress'],
): Promise<{
relevantTxs: RelevantTx[];
recoveredSourceRecords: RecoveredSourceRecords;
}> => {
const relevantTxs: RelevantTx[] = [];
const recoveredSourceRecords: RecoveredSourceRecords = [];

const searchPromises = blockTx.map(tx => searchRelevant(tx, spentNullifiers, commitmentRecords));
const searchPromises = blockTx.map(tx =>
searchRelevant(tx, spentNullifiers, commitmentRecords, isControlledAddr),
);
const results = await Promise.all(searchPromises);

for (const result of results) {
Expand Down
6 changes: 6 additions & 0 deletions packages/types/src/servers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { ScanBlockResult } from './state-commitment-tree.js';
import { CompactBlock } from '@penumbra-zone/protobuf/penumbra/core/component/compact_block/v1/compact_block_pb';
import { MerkleRoot } from '@penumbra-zone/protobuf/penumbra/crypto/tct/v1/tct_pb';
import { Address } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb';

export interface ViewServerInterface {
scanBlock(compactBlock: CompactBlock, skipTrialDecrypt: boolean): Promise<boolean>;

flushUpdates(): ScanBlockResult;

resetTreeToStored(): Promise<void>;

getSctRoot(): MerkleRoot;

isControlledAddress(address: Address): boolean;
}
21 changes: 16 additions & 5 deletions packages/wasm/crate/src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ use penumbra_proof_params::{
SPEND_PROOF_PROVING_KEY, SWAPCLAIM_PROOF_PROVING_KEY, SWAP_PROOF_PROVING_KEY,
};
use penumbra_proto::core::keys::v1 as pb;
use penumbra_proto::core::keys::v1::WalletId;
use penumbra_proto::{DomainType, Message};
use penumbra_proto::DomainType;
use rand_core::OsRng;
use wasm_bindgen::prelude::*;

Expand Down Expand Up @@ -75,9 +74,7 @@ pub fn get_wallet_id(full_viewing_key: &[u8]) -> WasmResult<Vec<u8>> {
utils::set_panic_hook();

let fvk: FullViewingKey = FullViewingKey::decode(full_viewing_key)?;
// Can do `fvk.wallet_id().encode_to_vec()` when Domain impl added to WalletId in core
let wallet_id_proto = WalletId::from(fvk.wallet_id());
Ok(wallet_id_proto.encode_to_vec())
Ok(fvk.wallet_id().encode_to_vec())
}

/// get address by index using FVK
Expand Down Expand Up @@ -125,3 +122,17 @@ pub fn get_index_by_address(full_viewing_key: &[u8], address: &[u8]) -> WasmResu
let result = serde_wasm_bindgen::to_value(&index)?;
Ok(result)
}

/// Checks if address is controlled by full viewing key provided
#[wasm_bindgen]
pub fn is_controlled_address(full_viewing_key: &[u8], address: &[u8]) -> WasmResult<bool> {
utils::set_panic_hook();

let address: Address = Address::decode(address)?;
let fvk: FullViewingKey = FullViewingKey::decode(full_viewing_key)?;
Ok(is_controlled_inner(&fvk, &address))
}

pub fn is_controlled_inner(fvk: &FullViewingKey, address: &Address) -> bool {
fvk.address_index(address).is_some()
}
Loading

0 comments on commit 7f3ee3b

Please sign in to comment.