Skip to content

Commit

Permalink
zero-alloc memo parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
mina86 committed Oct 25, 2024
1 parent 5e31872 commit 593d597
Showing 1 changed file with 116 additions and 77 deletions.
193 changes: 116 additions & 77 deletions solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::result::Result;
use std::str;
use std::str::{self, FromStr};

use anchor_lang::prelude::*;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -163,85 +163,10 @@ impl ibc::Module for IbcStorage<'_, '_> {
false
};

fn call_bridge_escrow(
accounts: &[AccountInfo],
data: Vec<u8>,
) -> Result<(), ibc::AcknowledgementStatus> {
// Perform hooks
let data =
serde_json::from_slice::<PacketData>(&data).map_err(|_| {
ibc::AcknowledgementStatus::error(
ibc::TokenTransferError::PacketDataDeserialization
.into(),
)
})?;

// The hook would only be called if the transferred token is the one
// we are interested in
if data.token.denom.base_denom.as_str() != HOOK_TOKEN_ADDRESS {
return Ok(());
}

// The memo is a string and the structure is as follow:
// "<number of accounts>,<AccountKey1> ..... <AccountKeyN>,<intent_id>,<memo>"
//
// The relayer would parse the memo and pass the relevant accounts
// The intent_id and memo needs to be stripped
let memo = data.memo.as_ref();
let (accounts_size, rest) = memo.split_once(',').ok_or(
ibc::AcknowledgementStatus::error(
ibc::TokenTransferError::Other("Invalid memo".to_string())
.into(),
),
)?;
// This is the 8 byte discriminant since the program is written in
// anchor. it is hash of "<namespace>:<function_name>" which is
// "global:on_receive_transfer" respectively.
const INSTRUCTION_DISCRIMINANT: [u8; 8] =
[149, 112, 68, 208, 4, 206, 248, 125];
let values = rest.split(',').collect::<Vec<&str>>();
let (_passed_accounts, ix_data) =
values.split_at(accounts_size.parse::<usize>().unwrap());
let intent_id =
ix_data.first().ok_or(ibc::AcknowledgementStatus::error(
ibc::TokenTransferError::Other("Invalid memo".to_string())
.into(),
))?;
let memo = ix_data[1..].join(",");
let instruction_data = [
&INSTRUCTION_DISCRIMINANT[..],
intent_id.as_bytes(),
memo.as_bytes(),
]
.concat();

let account_metas = accounts
.iter()
.map(|account| AccountMeta {
pubkey: *account.key,
is_signer: account.is_signer,
is_writable: account.is_writable,
})
.collect::<Vec<AccountMeta>>();
let instruction = Instruction::new_with_bytes(
BRIDGE_ESCROW_PROGRAM_ID,
&instruction_data,
account_metas,
);

invoke(&instruction, accounts).map_err(|err| {
ibc::AcknowledgementStatus::error(
ibc::TokenTransferError::Other(err.to_string()).into(),
)
})?;
msg!("Hook: Bridge escrow call successful");
Ok(())
}

if success {
let store = self.borrow();
let accounts = &store.accounts.remaining_accounts;
let result = call_bridge_escrow(accounts, maybe_ft_packet.data);
let result = call_bridge_escrow(accounts, &maybe_ft_packet.data);
if let Err(status) = result {
ack = status.into();
}
Expand Down Expand Up @@ -463,3 +388,117 @@ impl From<FtPacketData> for FungibleTokenPacketData {
}
}
}


/// Calls bridge escrow after receiving packet if necessary.
///
/// If the packet is for a [`HOOK_TOKEN_ADDRESS`] token, parses the transfer
/// memo and invokes bridge escrow contract with instruction encoded in it.
/// (see [`parse_bridge_memo`] for format of the memo).
fn call_bridge_escrow(
accounts: &[AccountInfo],
data: &[u8],
) -> Result<(), ibc::AcknowledgementStatus> {
// Perform hooks
let data = serde_json::from_slice::<PacketData>(data).map_err(|_| {
ibc::AcknowledgementStatus::error(
ibc::TokenTransferError::PacketDataDeserialization.into(),
)
})?;

// The hook would only be called if the transferred token is the one we are
// interested in
if data.token.denom.base_denom.as_str() != HOOK_TOKEN_ADDRESS {
return Ok(());
}

// The memo is a string and the structure is as follow:
// "<accounts count>,<AccountKey1> ..... <AccountKeyN>,<intent_id>,<memo>"
//
// The relayer would parse the memo and pass the relevant accounts The
// intent_id and memo needs to be stripped
let (intent_id, memo) =
parse_bridge_memo(&data.memo.as_ref()).ok_or_else(|| {

Check failure on line 421 in solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

this expression creates a reference which is immediately dereferenced by the compiler

error: this expression creates a reference which is immediately dereferenced by the compiler --> solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs:421:27 | 421 | parse_bridge_memo(&data.memo.as_ref()).ok_or_else(|| { | ^^^^^^^^^^^^^^^^^^^ help: change this to: `data.memo.as_ref()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow = note: `-D clippy::needless-borrow` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::needless_borrow)]`
let err = ibc::TokenTransferError::Other("Invalid memo".into());
ibc::AcknowledgementStatus::error(err.into())
})?;

// This is the 8 byte discriminant since the program is written in
// anchor. it is hash of "<namespace>:<function_name>" which is
// "global:on_receive_transfer" respectively.
const INSTRUCTION_DISCRIMINANT: [u8; 8] =
[149, 112, 68, 208, 4, 206, 248, 125];

let instruction_data =
[&INSTRUCTION_DISCRIMINANT[..], intent_id.as_bytes(), memo.as_bytes()]
.concat();

let account_metas = accounts
.iter()
.map(|account| AccountMeta {
pubkey: *account.key,
is_signer: account.is_signer,
is_writable: account.is_writable,
})
.collect();
let instruction = Instruction::new_with_bytes(
BRIDGE_ESCROW_PROGRAM_ID,
&instruction_data,
account_metas,
);

invoke(&instruction, accounts).map_err(|err| {
ibc::AcknowledgementStatus::error(
ibc::TokenTransferError::Other(err.to_string()).into(),
)
})?;
msg!("Hook: Bridge escrow call successful");
Ok(())
}


/// Parses memo of a transaction directed at the bridge escrow.
///
/// Memo is comma separated list of the form
/// `N,account-0,account-1,...,account-N-1,intent-id,embedded-memo`. Embedded
/// memo can contain commas. Returns `intent-id` and `embedded-memo` or `None`
/// if the memo does not conform to this format. Note that no validation on
/// accounts is performed.
fn parse_bridge_memo(memo: &str) -> Option<(&str, &str)> {
let (count, mut memo) = memo.split_once(',')?;
// Skip accounts
for _ in 0..usize::from_str(count).ok()? {
let (_, rest) = memo.split_once(',')?;
memo = rest
}
memo.split_once(',')
}

#[test]
fn test_parse_bridge_memo() {
for (intent, memo, data) in [
("intent", "memo", "0,intent,memo"),
("intent", "memo,with,comma", "0,intent,memo,with,comma"),
("intent", "memo", "1,account0,intent,memo"),
("intent", "memo", "3,account0,account1,account2,intent,memo"),
("intent", "memo,comma", "1,account0,intent,memo,comma"),
("intent", "", "1,account0,intent,"),
("", "memo", "1,account0,,memo"),
("", "", "1,account0,,"),
] {
assert_eq!(
Some((intent, memo)),
parse_bridge_memo(data),
"memo: {data}"
);
}

for data in [
"-1,intent,memo",
"foo,intent,memo",
",intent,memo",
"1,account0,intent",
] {
assert!(parse_bridge_memo(data).is_none(), "memo: {data}");
}
}

0 comments on commit 593d597

Please sign in to comment.