Skip to content

Commit

Permalink
feat: gas service contract (#575)
Browse files Browse the repository at this point in the history
* feat: bytemuck helper trait moved to program-utils
* feat: initial gas service layout
* feat: native gas methods
* chore: refactor gas service test utils
* refactor: simplified gateway event stack
* feat: added event parsing to solana gas service
* feat: added event parsing to the gateway event stack lib
* feat: can pay native gas for contract call
* refactor: using 64 bytes for tx hashes (solana signature len)
* refactor: more errors made explicitly as "relayer should continue"
  • Loading branch information
roberts-pumpurs authored Dec 16, 2024
1 parent 93bd4ac commit 5206cb5
Show file tree
Hide file tree
Showing 45 changed files with 2,049 additions and 143 deletions.
18 changes: 18 additions & 0 deletions solana/Cargo.lock

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

1 change: 1 addition & 0 deletions solana/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ async-recursion = "1"

# solana programs
axelar-solana-its = { path = "programs/axelar-solana-its" }
axelar-solana-gas-service = { path = "programs/axelar-solana-gas-service" }
axelar-solana-memo-program = { path = "programs/axelar-solana-memo-program" }
axelar-solana-memo-program-old = { path = "programs/axelar-solana-memo-program-old" }
axelar-solana-multicall = { path = "programs/axelar-solana-multicall" }
Expand Down
10 changes: 6 additions & 4 deletions solana/crates/axelar-executable/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ use core::borrow::Borrow;
use core::str::FromStr;

use axelar_solana_encoding::types::messages::Message;
use axelar_solana_gateway::get_validate_message_signing_pda;
use axelar_solana_gateway::error::GatewayError;
use axelar_solana_gateway::state::incoming_message::{command_id, IncomingMessage};
use axelar_solana_gateway::state::BytemuckedPda;
use axelar_solana_gateway::{get_validate_message_signing_pda, BytemuckedPda};
use num_traits::{FromPrimitive, ToPrimitive};
use rkyv::bytecheck::{self, CheckBytes};
use rkyv::{Deserialize, Infallible};
Expand Down Expand Up @@ -56,7 +56,8 @@ pub fn validate_message(
let accounts_iter = &mut relayer_prepended_accs.iter();
let incoming_message_pda = next_account_info(accounts_iter)?;
let incoming_message_data = incoming_message_pda.try_borrow_data()?;
let incoming_message = IncomingMessage::read(&incoming_message_data)?;
let incoming_message = IncomingMessage::read(&incoming_message_data)
.ok_or(GatewayError::BytemuckDataLenInvalid)?;
incoming_message.signing_pda_bump
};

Expand Down Expand Up @@ -106,7 +107,8 @@ pub fn validate_with_gmp_metadata(
let accounts_iter = &mut accounts.iter();
let incoming_message_pda = next_account_info(accounts_iter)?;
let incoming_message_data = incoming_message_pda.try_borrow_data()?;
let incoming_message = IncomingMessage::read(&incoming_message_data)?;
let incoming_message = IncomingMessage::read(&incoming_message_data)
.ok_or(GatewayError::BytemuckDataLenInvalid)?;
incoming_message.signing_pda_bump
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ libsecp256k1.workspace = true
rand.workspace = true
libsecp-rand.workspace = true
gateway-event-stack.workspace = true
axelar-solana-gas-service.workspace = true

[lints]
workspace = true
8 changes: 8 additions & 0 deletions solana/crates/axelar-solana-gateway-test-fixtures/src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use solana_sdk::instruction::Instruction;
use solana_sdk::signature::Keypair;
use solana_sdk::signer::Signer as _;
use solana_sdk::signers::Signers;
use solana_sdk::system_instruction;
use solana_sdk::transaction::Transaction;

/// Base test fixture wrapper that's agnostic to the Axelar Solana Gateway, it
Expand Down Expand Up @@ -268,6 +269,13 @@ impl TestFixture {
Some(_) => Err(BanksClientError::ClientError("unexpected account owner")),
}
}

/// Funds the account using the `self.payer` as the bank
pub async fn fund_account(&mut self, to: &Pubkey, amount: u64) {
let from = self.payer.pubkey();
let ix = system_instruction::transfer(&from, to, amount);
self.send_tx(&[ix]).await.expect("failed to fund account");
}
}

/// Utility triat to find a specific log within the
Expand Down
114 changes: 114 additions & 0 deletions solana/crates/axelar-solana-gateway-test-fixtures/src/gas_service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//! Utilities for working with the Axelar gas service
use crate::base::TestFixture;
use axelar_solana_gas_service::processor::GasServiceEvent;
use axelar_solana_gateway::BytemuckedPda;
use gateway_event_stack::{MatchContext, ProgramInvocationState};
use solana_program_test::{tokio, BanksTransactionResultWithMetadata};
use solana_sdk::{
account::ReadableAccount, keccak, pubkey::Pubkey, signature::Keypair, signer::Signer,
};

/// Utility structure for keeping gas service related state
pub struct GasServiceUtils {
/// upgrade authority of the program
pub upgrade_authority: Keypair,
/// the config authorty
pub config_authority: Keypair,
/// PDA of the gas service config
pub config_pda: Pubkey,
/// salt to derive the config pda
pub salt: [u8; 32],
}

impl TestFixture {
/// Deploy the gas service program and construct a pre-emptive
pub async fn deploy_gas_service(&mut self) -> GasServiceUtils {
// deploy gas service
let gas_service_bytecode =
tokio::fs::read("../../target/deploy/axelar_solana_gas_service.so")
.await
.unwrap();

// Generate a new keypair for the upgrade authority
let upgrade_authority = Keypair::new();

self.register_upgradeable_program(
&gas_service_bytecode,
&upgrade_authority.pubkey(),
&axelar_solana_gas_service::id(),
)
.await;

let config_authority = Keypair::new();
let salt = keccak::hash(b"my gas service").0;
let (config_pda, ..) = axelar_solana_gas_service::get_config_pda(
&axelar_solana_gas_service::ID,
&salt,
&config_authority.pubkey(),
);

GasServiceUtils {
upgrade_authority,
config_authority,
config_pda,
salt,
}
}

/// init the gas service
pub async fn init_gas_config(
&mut self,
utils: &GasServiceUtils,
) -> Result<BanksTransactionResultWithMetadata, BanksTransactionResultWithMetadata> {
self.init_gas_config_with_params(
utils.config_authority.pubkey(),
utils.config_pda,
utils.salt,
)
.await
}

/// init the gas service with raw params
pub async fn init_gas_config_with_params(
&mut self,
config_authority: Pubkey,
config_pda: Pubkey,
salt: [u8; 32],
) -> Result<BanksTransactionResultWithMetadata, BanksTransactionResultWithMetadata> {
let ix = axelar_solana_gas_service::instructions::init_config(
&axelar_solana_gas_service::ID,
&self.payer.pubkey(),
&config_authority,
&config_pda,
salt,
)
.unwrap();
self.send_tx(&[ix]).await
}

/// get the gas service config pda state
pub async fn gas_service_config_state(
&mut self,
config_pda: Pubkey,
) -> axelar_solana_gas_service::state::Config {
let acc = self
.get_account(&config_pda, &axelar_solana_gas_service::ID)
.await;
let config = axelar_solana_gas_service::state::Config::read(acc.data()).unwrap();
*config
}
}

/// Get events emitted by the `GasService`
#[must_use]
pub fn get_gas_service_events(
tx: &solana_program_test::BanksTransactionResultWithMetadata,
) -> Vec<ProgramInvocationState<GasServiceEvent>> {
let match_context = MatchContext::new(&axelar_solana_gas_service::ID.to_string());
gateway_event_stack::build_program_event_stack(
&match_context,
tx.metadata.as_ref().unwrap().log_messages.as_slice(),
gateway_event_stack::parse_gas_service_log,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ use axelar_solana_gateway::processor::GatewayEvent;
use axelar_solana_gateway::state::incoming_message::{command_id, IncomingMessage};
use axelar_solana_gateway::state::signature_verification_pda::SignatureVerificationSessionData;
use axelar_solana_gateway::state::verifier_set_tracker::VerifierSetTracker;
use axelar_solana_gateway::state::{BytemuckedPda, GatewayConfig};
use axelar_solana_gateway::state::GatewayConfig;
use axelar_solana_gateway::{
get_gateway_root_config_pda, get_incoming_message_pda, get_verifier_set_tracker_pda,
BytemuckedPda,
};
pub use gateway_event_stack::{MatchContext, ProgramInvocationState};
use rand::Rng as _;
Expand Down
2 changes: 2 additions & 0 deletions solana/crates/axelar-solana-gateway-test-fixtures/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
#![allow(clippy::expect_used)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::multiple_inherent_impl)]

pub mod base;
pub mod gas_service;
pub mod gateway;
pub mod test_signer;

Expand Down
1 change: 1 addition & 0 deletions solana/crates/gateway-event-stack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ edition.workspace = true

[dependencies]
axelar-solana-gateway.workspace = true
axelar-solana-gas-service.workspace = true
tracing.workspace = true
solana-sdk.workspace = true
base64.workspace = true
Expand Down
88 changes: 68 additions & 20 deletions solana/crates/gateway-event-stack/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Parse Solana events from transaction data
use axelar_solana_gateway::processor::{EventParseError, GatewayEvent};
use axelar_solana_gas_service::processor::GasServiceEvent;
use axelar_solana_gateway::processor::GatewayEvent;
use base64::{engine::general_purpose, Engine};

/// Represents the state of a program invocation along with associated events.
Expand Down Expand Up @@ -66,7 +67,7 @@ pub fn build_program_event_stack<T, K, Err, F>(
) -> Vec<ProgramInvocationState<K>>
where
T: AsRef<str>,
F: Fn(&mut [ProgramInvocationState<K>], &T, usize) -> Result<(), Err>,
F: Fn(&T) -> Result<K, Err>,
{
let logs = logs.iter().enumerate();
let mut program_stack: Vec<ProgramInvocationState<K>> = Vec::new();
Expand All @@ -82,8 +83,16 @@ where
handle_failure_log(&mut program_stack);
} else {
// Process logs if inside a program invocation
let Some(&mut ProgramInvocationState::InProgress(ref mut events)) =
program_stack.last_mut()
else {
continue;
};
#[allow(clippy::let_underscore_must_use, clippy::let_underscore_untyped)]
let _ = transformer(&mut program_stack, log, idx);
let Ok(event) = transformer(log) else {
continue;
};
events.push((idx, event));
}
}
program_stack
Expand All @@ -100,31 +109,20 @@ pub fn decode_base64(input: &str) -> Option<Vec<u8>> {
///
/// # Arguments
///
/// * `program_stack` - A mutable slice of program invocation states to update.
/// * `log` - The log entry to parse.
/// * `idx` - The index of the log entry in the logs slice.
///
/// # Returns
///
/// `Ok(())` if parsing is successful, or an `EventParseError` if parsing fails.
///
/// # Errors
///
/// - if the discrimintant for the event is not present
/// - if the event was detected via the discriminant but the data does not match
/// - if the event was detected via the discriminant but the data does not match the discriminant type
pub fn parse_gateway_logs<T>(
program_stack: &mut [ProgramInvocationState<GatewayEvent>],
log: &T,
idx: usize,
) -> Result<(), EventParseError>
) -> Result<GatewayEvent, axelar_solana_gateway::processor::EventParseError>
where
T: AsRef<str>,
{
use axelar_solana_gateway::event_prefixes::*;
let Some(&mut ProgramInvocationState::InProgress(ref mut events)) = program_stack.last_mut()
else {
return Ok(());
};
use axelar_solana_gateway::processor::EventParseError;

let mut logs = log
.as_ref()
Expand Down Expand Up @@ -166,11 +164,61 @@ where
let event = axelar_solana_gateway::processor::VerifierSetRotated::new(logs)?;
GatewayEvent::VerifierSetRotated(event)
}
_ => return Ok(()),
_ => return Err(EventParseError::Other("unsupported discrimintant")),
};

Ok(gateway_event)
}

/// Parses gas service logs and extracts events.
///
/// # Arguments
///
/// * `log` - The log entry to parse.
///
/// # Errors
///
/// - if the discrimintant for the event is not present
/// - if the event was detected via the discriminant but the data does not match the discriminant type
pub fn parse_gas_service_log<T>(
log: &T,
) -> Result<GasServiceEvent, axelar_solana_gas_service::event_utils::EventParseError>
where
T: AsRef<str>,
{
use axelar_solana_gas_service::event_prefixes::*;
use axelar_solana_gas_service::event_utils::EventParseError;
use axelar_solana_gas_service::processor::{
NativeGasAddedEvent, NativeGasPaidForContractCallEvent, NativeGasRefundedEvent,
};

let mut logs = log
.as_ref()
.trim()
.trim_start_matches("Program data:")
.split_whitespace()
.filter_map(decode_base64);
let disc = logs
.next()
.ok_or(EventParseError::MissingData("discriminant"))?;
let disc = disc.as_slice();
let gas_service_event = match disc {
NATIVE_GAS_PAID_FOR_CONTRACT_CALL => {
let event = NativeGasPaidForContractCallEvent::new(logs)?;
GasServiceEvent::NativeGasPaidForncontractCall(event)
}
NATIVE_GAS_ADDED => {
let event = NativeGasAddedEvent::new(logs)?;
GasServiceEvent::NativeGasAdded(event)
}
NATIVE_GAS_REFUNDED => {
let event = NativeGasRefundedEvent::new(logs)?;
GasServiceEvent::NativeGasRefunded(event)
}
_ => return Err(EventParseError::Other("unsupported discrimintant")),
};

events.push((idx, gateway_event));
Ok(())
Ok(gas_service_event)
}

/// Handles a failure log by marking the current program invocation as failed.
Expand Down
Loading

0 comments on commit 5206cb5

Please sign in to comment.