Skip to content

Commit

Permalink
feat(gtw): add CallContractOffchainData instruction (#568)
Browse files Browse the repository at this point in the history
* feat(gtw): add CallContractOffchainData instruction

The intent is for this instruction to be used for large messages (those
too big to go through the normal ContractCall flow). The user should
call it with the payload hash only, without the actual contract call
data. The latter should be uploaded directly to the gateway together
with the signature of the event emitted by this instruction.

Signed-off-by: Guilherme Felipe da Silva <[email protected]>

* test(memo): add test for call contract with offchain data

Signed-off-by: Guilherme Felipe da Silva <[email protected]>

* chore: remove spurious println

---------

Signed-off-by: Guilherme Felipe da Silva <[email protected]>
  • Loading branch information
frenzox authored Dec 12, 2024
1 parent b2161e8 commit e212e33
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 2 deletions.
1 change: 1 addition & 0 deletions solana/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions solana/crates/gateway-event-stack/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ where
let event = axelar_solana_gateway::processor::CallContractEvent::new(logs)?;
GatewayEvent::CallContract(event)
}
CALL_CONTRACT_OFFCHAIN_DATA => {
let event = axelar_solana_gateway::processor::CallContractOffchainDataEvent::new(logs)?;
GatewayEvent::CallContractOffchainData(event)
}
MESSAGE_APPROVED => {
let event = axelar_solana_gateway::processor::MessageEvent::new(logs)?;
GatewayEvent::MessageApproved(event)
Expand Down
42 changes: 42 additions & 0 deletions solana/programs/axelar-solana-gateway/src/instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ pub enum GatewayInstruction {
payload: Vec<u8>,
},

/// Represents the `CallContract` Axelar event. The contract call data is expected to be
/// handled off-chain by uploading the data using the relayer API.
///
/// Accounts expected by this instruction:
/// 0. [SIGNER] Sender (origin) of the message)
/// 1. [] Gateway Root Config PDA account
CallContractOffchainData {
/// The name of the target blockchain.
destination_chain: String,
/// The address of the target contract in the destination blockchain.
destination_contract_address: String,
/// Hash of the contract call data, to be uploaded off-chain through the relayer API.
payload_hash: [u8; 32],
},

/// Initializes the Gateway configuration PDA account.
///
/// Accounts expected by this instruction:
Expand Down Expand Up @@ -226,6 +241,33 @@ pub fn call_contract(
})
}

/// Creates a [`CallContractOffchainData`] instruction.
pub fn call_contract_offchain_data(
gateway_program_id: Pubkey,
gateway_root_pda: Pubkey,
sender: Pubkey,
destination_chain: String,
destination_contract_address: String,
payload_hash: [u8; 32],
) -> Result<Instruction, ProgramError> {
let data = to_vec(&GatewayInstruction::CallContractOffchainData {
destination_chain,
destination_contract_address,
payload_hash,
})?;

let accounts = vec![
AccountMeta::new_readonly(sender, true),
AccountMeta::new_readonly(gateway_root_pda, false),
];

Ok(Instruction {
program_id: gateway_program_id,
accounts,
data,
})
}

/// Creates a [`GatewayInstruction::InitializeConfig`] instruction.
pub fn initialize_config(
payer: Pubkey,
Expand Down
2 changes: 2 additions & 0 deletions solana/programs/axelar-solana-gateway/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ pub mod event_prefixes {
pub const MESSAGE_APPROVED: &Disc = b"message approved";
/// Event prefix for an event when an outgoing GMP call gets emitted
pub const CALL_CONTRACT: &Disc = b"call contract___";
/// Event prefix for an event when an outgoing GMP call gets emitted with offchain data
pub const CALL_CONTRACT_OFFCHAIN_DATA: &Disc = b"offchain data___";
/// Event prefix for an event when signers rotate
pub const SIGNERS_ROTATED: &Disc = b"signers rotated_";
/// Event prefix for an event when operatorship was transferred
Expand Down
22 changes: 22 additions & 0 deletions solana/programs/axelar-solana-gateway/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::instructions::GatewayInstruction;

mod approve_message;
mod call_contract;
mod call_contract_offchain_data;
mod initialize_config;
mod initialize_payload_verification_session;
mod rotate_signers;
Expand All @@ -20,6 +21,7 @@ mod validate_message;
mod verify_signature;

pub use call_contract::CallContractEvent;
pub use call_contract_offchain_data::CallContractOffchainDataEvent;
pub use rotate_signers::VerifierSetRotated;
pub use transfer_operatorship::OperatorshipTransferredEvent;
pub use validate_message::MessageEvent;
Expand Down Expand Up @@ -70,6 +72,20 @@ impl Processor {
payload,
)
}
GatewayInstruction::CallContractOffchainData {
destination_chain,
destination_contract_address,
payload_hash,
} => {
msg!("Instruction: Call Contract Offchain Data");
Self::process_call_contract_offchain_data(
program_id,
accounts,
destination_chain,
destination_contract_address,
payload_hash,
)
}
GatewayInstruction::InitializeConfig(init_config) => {
msg!("Instruction: Initialize Config");
Self::process_initialize_config(program_id, accounts, init_config)
Expand Down Expand Up @@ -120,6 +136,12 @@ pub enum GatewayEvent {
/// This event is emitted when a contract call is initiated to an external chain.
CallContract(CallContractEvent),

/// Represents a `CallContractOffchainData` event.
///
/// This event is emitted when a contract call is initiated to an external chain with call data
/// being passed offchain.
CallContractOffchainData(CallContractOffchainDataEvent),

/// Represents a `VerifierSetRotated` event.
VerifierSetRotated(VerifierSetRotated),

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use program_utils::ValidPDA;
use solana_program::account_info::{next_account_info, AccountInfo};
use solana_program::entrypoint::ProgramResult;
use solana_program::log::sol_log_data;
use solana_program::pubkey::Pubkey;

use super::event_utils::{read_array, read_string, EventParseError};
use super::Processor;
use crate::state::{BytemuckedPda, GatewayConfig};
use crate::{assert_valid_gateway_root_pda, event_prefixes};

impl Processor {
/// This function is used to initialize the program.
pub fn process_call_contract_offchain_data(
program_id: &Pubkey,
accounts: &[AccountInfo<'_>],
destination_chain: String,
destination_contract_address: String,
payload_hash: [u8; 32],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let sender = next_account_info(accounts_iter)?;
let gateway_root_pda = next_account_info(accounts_iter)?;

// Check: Gateway Root PDA is initialized.
gateway_root_pda.check_initialized_pda_without_deserialization(program_id)?;
let data = gateway_root_pda.try_borrow_data()?;
let gateway_config = GatewayConfig::read(&data)?;
assert_valid_gateway_root_pda(gateway_config.bump, gateway_root_pda.key)?;

// Check: sender is signer
assert!(sender.is_signer, "Sender must be a signer");

// Emit an event
sol_log_data(&[
event_prefixes::CALL_CONTRACT_OFFCHAIN_DATA,
&sender.key.to_bytes(),
&payload_hash,
destination_chain.as_bytes(),
destination_contract_address.as_bytes(),
]);
Ok(())
}
}

/// Represents a `CallContractEvent`.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct CallContractOffchainDataEvent {
/// Sender's public key.
pub sender_key: Pubkey,

/// Payload hash, 32 bytes.
pub payload_hash: [u8; 32],

/// Destination chain as a `String`.
pub destination_chain: String,

/// Destination contract address as a `String`.
pub destination_contract_address: String,
}

impl CallContractOffchainDataEvent {
/// Constructs a new `CallContractEvent` by parsing the provided data iterator.
pub fn new<I>(mut data: I) -> Result<Self, EventParseError>
where
I: Iterator<Item = Vec<u8>>,
{
let sender_key_data = data
.next()
.ok_or(EventParseError::MissingData("sender_key"))?;
let sender_key = read_array::<32>("sender_key", &sender_key_data)?;
let sender_key = Pubkey::new_from_array(sender_key);

let payload_hash_data = data
.next()
.ok_or(EventParseError::MissingData("payload_hash"))?;
let payload_hash = read_array::<32>("payload_hash", &payload_hash_data)?;

let destination_chain_data = data
.next()
.ok_or(EventParseError::MissingData("destination_chain"))?;
let destination_chain = read_string("destination_chain", destination_chain_data)?;

let destination_contract_address_data = data
.next()
.ok_or(EventParseError::MissingData("destination_contract_address"))?;
let destination_contract_address = read_string(
"destination_contract_address",
destination_contract_address_data,
)?;

Ok(Self {
sender_key,
payload_hash,
destination_chain,
destination_contract_address,
})
}
}
1 change: 1 addition & 0 deletions solana/programs/axelar-solana-memo-program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ solana-sdk.workspace = true
axelar-solana-gateway-test-fixtures.workspace = true
pretty_assertions.workspace = true
axelar-solana-encoding.workspace = true
serde_json.workspace = true
46 changes: 46 additions & 0 deletions solana/programs/axelar-solana-memo-program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ pub enum AxelarMemoInstruction {
/// Destination contract address on the destination chain
destination_address: String,
},

/// Send a memo to a contract deployed on a different chain, but pass the memo offchain. The
/// relayer API must be used to send the memo after calling this instruction.
///
/// Accounts expected by this instruction:
///
/// 0. [signer] The address of payer / sender
/// 1. [] gateway root pda
/// 2. [] gateway program id
SendToGatewayOffchainMemo {
/// Hash of the memo which is going to be sent directly to the relayer.
memo_hash: [u8; 32],
/// Destination chain we want to communicate with
destination_chain: String,
/// Destination contract address on the destination chain
destination_address: String,
},
}

/// Creates a [`AxelarMemoInstruction::Initialize`] instruction.
Expand Down Expand Up @@ -104,6 +121,35 @@ pub fn call_gateway_with_memo(
})
}

/// Create a [`AxelarMemoInstruction::SendToGatewayOffchainMemo`] instruction which will be
/// used to send a memo to the Solana gateway (create a message that's supposed
/// to land on an external chain)
pub fn call_gateway_with_offchain_memo(
gateway_root_pda: &Pubkey,
memo_counter_pda: &Pubkey,
memo: String,
destination_chain: String,
destination_address: String,
gateway_program_id: &Pubkey,
) -> Result<Instruction, ProgramError> {
let memo_hash = solana_program::keccak::hash(memo.as_bytes()).to_bytes();
let data = to_vec(&AxelarMemoInstruction::SendToGatewayOffchainMemo {
memo_hash,
destination_chain,
destination_address,
})?;
let accounts = vec![
AccountMeta::new_readonly(*memo_counter_pda, false),
AccountMeta::new_readonly(*gateway_root_pda, false),
AccountMeta::new_readonly(*gateway_program_id, false),
];
Ok(Instruction {
program_id: crate::ID,
accounts,
data,
})
}

/// Helper function to build a memo payload instruction
pub mod from_axelar_to_solana {
use axelar_executable::EncodingScheme;
Expand Down
24 changes: 24 additions & 0 deletions solana/programs/axelar-solana-memo-program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,30 @@ pub fn process_native_ix(
&[&[gateway_root_pda.key.as_ref(), &[counter_pda_account.bump]]],
)?;
}
AxelarMemoInstruction::SendToGatewayOffchainMemo {
memo_hash,
destination_chain,
destination_address,
} => {
msg!("Instruction: SendToGatewayOffchainMemo");
let counter_pda = next_account_info(account_info_iter)?;
let gateway_root_pda = next_account_info(account_info_iter)?;
let gateway_program = next_account_info(account_info_iter)?;
let counter_pda_account = counter_pda.check_initialized_pda::<Counter>(program_id)?;
assert_counter_pda_seeds(&counter_pda_account, counter_pda.key, gateway_root_pda.key);
invoke_signed(
&axelar_solana_gateway::instructions::call_contract_offchain_data(
*gateway_program.key,
*gateway_root_pda.key,
*counter_pda.key,
destination_chain,
destination_address,
memo_hash,
)?,
&[counter_pda.clone(), gateway_root_pda.clone()],
&[&[gateway_root_pda.key.as_ref(), &[counter_pda_account.bump]]],
)?;
}
AxelarMemoInstruction::Initialize { counter_pda_bump } => {
process_initialize_memo_program_counter(program_id, accounts, counter_pda_bump)?;
}
Expand Down
Loading

0 comments on commit e212e33

Please sign in to comment.