From 1d60dd50cc11fb69ba3832df3fd53dc7fec9bb73 Mon Sep 17 00:00:00 2001 From: gcranju <134275268+gcranju@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:42:19 +0545 Subject: [PATCH] feat: solana cluster connection (#405) * fix: solana cluster connection init * fix: solana cluster connection * fix: solana cluster spec update * fix: tests * feat: solana changes according to centralized * fix: increased config size * fix: break if thereshold met --------- Co-authored-by: gcranju <075bct064.ranju@pcampus.edu.np> Co-authored-by: ibrizsabin <101165234+ibrizsabin@users.noreply.github.com> --- contracts/solana/Cargo.lock | 9 + .../programs/cluster-connection/Cargo.toml | 23 ++ .../programs/cluster-connection/Xargo.toml | 2 + .../cluster-connection/src/constants.rs | 1 + .../cluster-connection/src/contexts.rs | 198 +++++++++++++++++ .../programs/cluster-connection/src/error.rs | 19 ++ .../programs/cluster-connection/src/event.rs | 10 + .../programs/cluster-connection/src/helper.rs | 202 ++++++++++++++++++ .../src/instructions/mod.rs | 3 + .../src/instructions/query_accounts.rs | 200 +++++++++++++++++ .../programs/cluster-connection/src/lib.rs | 190 ++++++++++++++++ .../programs/cluster-connection/src/state.rs | 130 +++++++++++ 12 files changed, 987 insertions(+) create mode 100644 contracts/solana/programs/cluster-connection/Cargo.toml create mode 100644 contracts/solana/programs/cluster-connection/Xargo.toml create mode 100644 contracts/solana/programs/cluster-connection/src/constants.rs create mode 100644 contracts/solana/programs/cluster-connection/src/contexts.rs create mode 100644 contracts/solana/programs/cluster-connection/src/error.rs create mode 100644 contracts/solana/programs/cluster-connection/src/event.rs create mode 100644 contracts/solana/programs/cluster-connection/src/helper.rs create mode 100644 contracts/solana/programs/cluster-connection/src/instructions/mod.rs create mode 100644 contracts/solana/programs/cluster-connection/src/instructions/query_accounts.rs create mode 100644 contracts/solana/programs/cluster-connection/src/lib.rs create mode 100644 contracts/solana/programs/cluster-connection/src/state.rs diff --git a/contracts/solana/Cargo.lock b/contracts/solana/Cargo.lock index 8ce7dd913..292f1a07f 100644 --- a/contracts/solana/Cargo.lock +++ b/contracts/solana/Cargo.lock @@ -645,6 +645,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cluster-connection" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "rlp", + "xcall-lib", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" diff --git a/contracts/solana/programs/cluster-connection/Cargo.toml b/contracts/solana/programs/cluster-connection/Cargo.toml new file mode 100644 index 000000000..d468b7f8e --- /dev/null +++ b/contracts/solana/programs/cluster-connection/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "cluster-connection" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "cluster_connection" + +[features] +default = [] +cpi = ["no-entrypoint"] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +anchor-lang = { workspace = true, features = ["init-if-needed"] } + +xcall-lib = { workspace = true } +rlp = { workspace = true } diff --git a/contracts/solana/programs/cluster-connection/Xargo.toml b/contracts/solana/programs/cluster-connection/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/contracts/solana/programs/cluster-connection/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/contracts/solana/programs/cluster-connection/src/constants.rs b/contracts/solana/programs/cluster-connection/src/constants.rs new file mode 100644 index 000000000..fe8aad0a4 --- /dev/null +++ b/contracts/solana/programs/cluster-connection/src/constants.rs @@ -0,0 +1 @@ +pub const ACCOUNT_DISCRIMINATOR_SIZE: usize = 8; diff --git a/contracts/solana/programs/cluster-connection/src/contexts.rs b/contracts/solana/programs/cluster-connection/src/contexts.rs new file mode 100644 index 000000000..3b51aa894 --- /dev/null +++ b/contracts/solana/programs/cluster-connection/src/contexts.rs @@ -0,0 +1,198 @@ +use anchor_lang::prelude::*; + +use crate::{error::ConnectionError, state::*}; + +#[derive(Accounts)] +pub struct Initialize<'info> { + /// Rent payer + #[account(mut)] + pub signer: Signer<'info>, + + /// System Program: Required for creating the centralized-connection config + pub system_program: Program<'info, System>, + + /// Config + #[account( + init, + payer = signer, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump, + space = Config::LEN + )] + pub config: Account<'info, Config>, + + #[account( + init, + payer = signer, + space = Authority::LEN, + seeds = [Authority::SEED_PREFIX.as_bytes()], + bump + )] + pub authority: Account<'info, Authority>, +} + +#[derive(Accounts)] +#[instruction(to: String)] +pub struct SendMessage<'info> { + #[account(mut)] + pub signer: Signer<'info>, + + pub system_program: Program<'info, System>, + + #[account( + owner = config.xcall @ ConnectionError::OnlyXcall + )] + pub xcall: Signer<'info>, + + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + )] + pub config: Account<'info, Config>, + + #[account( + seeds = [NetworkFee::SEED_PREFIX.as_bytes(), to.as_bytes()], + bump = network_fee.bump + )] + pub network_fee: Account<'info, NetworkFee>, +} + +#[derive(Accounts)] +pub struct RevertMessage<'info> { + #[account(mut)] + pub relayer: Signer<'info>, + + pub system_program: Program<'info, System>, + + /// Config + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + has_one = relayer @ ConnectionError::OnlyRelayer, + )] + pub config: Account<'info, Config>, + + #[account( + seeds = [Authority::SEED_PREFIX.as_bytes()], + bump = authority.bump + )] + pub authority: Account<'info, Authority>, +} + +#[derive(Accounts)] +pub struct SetConfigItem<'info> { + /// Transaction signer + #[account(mut)] + pub admin: Signer<'info>, + + /// Config + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + has_one = admin @ ConnectionError::OnlyAdmin, + )] + pub config: Account<'info, Config>, +} + +#[derive(Accounts)] +#[instruction(network_id: String)] +pub struct SetFee<'info> { + /// Rent payer + #[account(mut)] + pub relayer: Signer<'info>, + + /// System Program: Required to create program-derived address + pub system_program: Program<'info, System>, + + /// Fee + #[account( + init_if_needed, + payer = relayer, + seeds = [NetworkFee::SEED_PREFIX.as_bytes(), network_id.as_bytes()], + bump, + space = NetworkFee::LEN + )] + pub network_fee: Account<'info, NetworkFee>, + + /// Config + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + has_one = relayer @ ConnectionError::OnlyRelayer, + )] + pub config: Account<'info, Config>, +} + +#[derive(Accounts)] +#[instruction(network_id: String)] +pub struct GetFee<'info> { + /// Fee + #[account( + seeds = [NetworkFee::SEED_PREFIX.as_bytes(), network_id.as_bytes()], + bump = network_fee.bump + )] + pub network_fee: Account<'info, NetworkFee>, +} + +#[derive(Accounts)] +pub struct ClaimFees<'info> { + /// Rent payer + #[account(mut)] + pub relayer: Signer<'info>, + + /// Config + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + has_one = relayer @ ConnectionError::OnlyRelayer, + )] + pub config: Account<'info, Config>, +} + +#[derive(Accounts)] +pub struct GetConfigItem<'info> { + /// Config + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump + )] + pub config: Account<'info, Config>, +} + +#[derive(Accounts)] +#[instruction(src_network: String, conn_sn: u128)] +pub struct ReceiveMessageWithSignatures<'info> { + #[account(mut)] + pub relayer: Signer<'info>, + + pub system_program: Program<'info, System>, + + /// Config + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + has_one = relayer @ ConnectionError::OnlyRelayer, + )] + pub config: Account<'info, Config>, + + #[account( + init, + payer = relayer, + seeds = [Receipt::SEED_PREFIX.as_bytes(), src_network.as_bytes(), &conn_sn.to_be_bytes()], + space = Receipt::LEN, + bump + )] + pub receipt: Account<'info, Receipt>, + + #[account( + seeds = [Authority::SEED_PREFIX.as_bytes()], + bump = authority.bump + )] + pub authority: Account<'info, Authority>, +} \ No newline at end of file diff --git a/contracts/solana/programs/cluster-connection/src/error.rs b/contracts/solana/programs/cluster-connection/src/error.rs new file mode 100644 index 000000000..5a150422f --- /dev/null +++ b/contracts/solana/programs/cluster-connection/src/error.rs @@ -0,0 +1,19 @@ +use anchor_lang::prelude::*; + +#[error_code] +pub enum ConnectionError { + #[msg("Only admin")] + OnlyAdmin, + + #[msg("Only relayer")] + OnlyRelayer, + + #[msg("Only xcall")] + OnlyXcall, + + #[msg("Admin Validator Cnnot Be Removed")] + AdminValidatorCnnotBeRemoved, + + #[msg("Validators Must Be Greater Than Threshold")] + ValidatorsMustBeGreaterThanThreshold, +} diff --git a/contracts/solana/programs/cluster-connection/src/event.rs b/contracts/solana/programs/cluster-connection/src/event.rs new file mode 100644 index 000000000..e379a589c --- /dev/null +++ b/contracts/solana/programs/cluster-connection/src/event.rs @@ -0,0 +1,10 @@ +#![allow(non_snake_case)] + +use anchor_lang::prelude::*; + +#[event] +pub struct SendMessage { + pub targetNetwork: String, + pub connSn: u128, + pub msg: Vec, +} diff --git a/contracts/solana/programs/cluster-connection/src/helper.rs b/contracts/solana/programs/cluster-connection/src/helper.rs new file mode 100644 index 000000000..51d57ce58 --- /dev/null +++ b/contracts/solana/programs/cluster-connection/src/helper.rs @@ -0,0 +1,202 @@ +use anchor_lang::{ + prelude::*, + solana_program::{ + hash, instruction::{AccountMeta,Instruction}, keccak::hashv, program::{get_return_data, invoke, invoke_signed}, secp256k1_recover::secp256k1_recover, system_instruction + }, +}; + +use crate::contexts::*; +use crate::state::*; +use crate::error::*; + +use xcall_lib::{network_address:: NetworkAddress, xcall_type}; + +pub const GET_NETWORK_ADDRESS: &str = "get_network_address"; + +pub fn transfer_lamports<'info>( + from: &AccountInfo<'info>, + to: &AccountInfo<'info>, + system_program: &AccountInfo<'info>, + amount: u64, +) -> Result<()> { + let ix = system_instruction::transfer(&from.key(), &to.key(), amount); + invoke( + &ix, + &[from.to_owned(), to.to_owned(), system_program.to_owned()], + )?; + + Ok(()) +} + +pub fn get_instruction_data(ix_name: &str, data: Vec) -> Vec { + let preimage = format!("{}:{}", "global", ix_name); + + let mut ix_discriminator = [0u8; 8]; + ix_discriminator.copy_from_slice(&hash::hash(preimage.as_bytes()).to_bytes()[..8]); + + let mut ix_data = Vec::new(); + ix_data.extend_from_slice(&ix_discriminator); + ix_data.extend_from_slice(&data); + + ix_data +} + +pub fn get_message_hash(from_nid: &String, connection_sn: &u128, message: &Vec, dst_nid: &String) -> [u8; 32] { + let mut encoded_bytes = Vec::new(); + encoded_bytes.extend(from_nid.as_bytes()); + encoded_bytes.extend(connection_sn.to_string().as_bytes()); + encoded_bytes.extend(message); + encoded_bytes.extend(dst_nid.as_bytes()); + + let hash = hashv(&[&encoded_bytes]); + + hash.to_bytes() +} + +pub fn recover_pubkey(message: [u8; 32], sig: [u8; 65]) -> [u8; 64] { + let recovery_id = sig[64] % 27; + let signature = &sig[0..64]; + let recovered_pubkey = secp256k1_recover(&message, recovery_id, signature).unwrap(); + recovered_pubkey.to_bytes() +} + +pub fn get_nid(xcall_config: &AccountInfo, config: &Config) -> String { + let ix_data = get_instruction_data(GET_NETWORK_ADDRESS, vec![]); + let account_metas = vec![AccountMeta::new(xcall_config.key(), false)]; + let account_infos = vec![xcall_config.to_account_info()]; + + let ix = Instruction { + program_id: config.xcall, + accounts: account_metas, + data: ix_data, + }; + + invoke(&ix, &account_infos).unwrap(); + + let network_address = String::from_utf8(get_return_data().unwrap().1).unwrap(); + network_address.parse::().unwrap().nid() +} + +pub fn call_xcall_handle_message_with_signatures<'info>( + ctx: Context<'_, '_, '_, 'info, ReceiveMessageWithSignatures<'info>>, + from_nid: String, + message: Vec, + conn_sn: u128, + sequence_no: u128, + signatures: Vec<[u8; 65]>, +) -> Result<()> { + let mut data = vec![]; + let dst_nid = get_nid(&ctx.remaining_accounts[1], &ctx.accounts.config); + let message_hash = get_message_hash(&from_nid, &conn_sn, &message, &dst_nid); + let mut unique_validators = Vec::new(); + for sig in signatures { + let pubkey = recover_pubkey(message_hash, sig); + if !unique_validators.contains(&pubkey) && ctx.accounts.config.is_validator(&pubkey) { + unique_validators.push(pubkey); + } + if (unique_validators.len() as u8) >= ctx.accounts.config.threshold { + break; + } + } + + if (unique_validators.len() as u8) < ctx.accounts.config.threshold { + return Err(ConnectionError::ValidatorsMustBeGreaterThanThreshold.into()); + } + + let args = xcall_type::HandleMessageArgs { + from_nid, + message, + sequence_no, + conn_sn, + }; + args.serialize(&mut data)?; + + let ix_data = get_instruction_data("handle_message", data); + + invoke_instruction( + ix_data, + &ctx.accounts.config, + &ctx.accounts.authority, + &ctx.accounts.relayer, + &ctx.accounts.system_program, + ctx.remaining_accounts, + ) +} + +pub fn call_xcall_handle_error<'info>( + ctx: Context<'_, '_, '_, 'info, RevertMessage<'info>>, + sequence_no: u128, +) -> Result<()> { + let mut data = vec![]; + let args = xcall_type::HandleErrorArgs { sequence_no }; + args.serialize(&mut data)?; + + let ix_data = get_instruction_data("handle_error", data); + + invoke_instruction( + ix_data, + &ctx.accounts.config, + &ctx.accounts.authority, + &ctx.accounts.relayer, + &ctx.accounts.system_program, + &ctx.remaining_accounts, + ) +} + +pub fn invoke_instruction<'info>( + ix_data: Vec, + config: &Account<'info, Config>, + authority: &Account<'info, Authority>, + admin: &Signer<'info>, + system_program: &Program<'info, System>, + remaining_accounts: &[AccountInfo<'info>], +) -> Result<()> { + let mut account_metas = vec![ + AccountMeta::new(admin.key(), true), + AccountMeta::new_readonly(authority.key(), true), + AccountMeta::new_readonly(system_program.key(), false), + ]; + let mut account_infos = vec![ + admin.to_account_info(), + authority.to_account_info(), + system_program.to_account_info(), + ]; + for i in remaining_accounts { + if i.is_writable { + account_metas.push(AccountMeta::new(i.key(), i.is_signer)); + } else { + account_metas.push(AccountMeta::new_readonly(i.key(), i.is_signer)) + } + account_infos.push(i.to_account_info()); + } + let ix = Instruction { + program_id: config.xcall, + accounts: account_metas, + data: ix_data.clone(), + }; + + invoke_signed( + &ix, + &account_infos, + &[&[Authority::SEED_PREFIX.as_bytes(), &[authority.bump]]], + )?; + + Ok(()) +} + + + +#[test] +fn test_recover_pubkey() { + let from_nid = "0x2.icon"; + let connection_sn = 128; + let message = b"hello"; + let dst_nid = "archway"; + + let message_hash = get_message_hash(&from_nid.to_string(), &connection_sn, &message.to_vec(), &dst_nid.to_string()); + + let signature = [102,13,84,43,63,109,233,205,8,242,56,253,68,19,62,238,191,234,41,11,33,218,231,50,42,99,181,22,197,123,141,241,44,76,10,52,11,96,237,86,124,141,165,53,120,52,108,33,43,39,183,151,235,66,167,95,180,183,7,108,86,122,111,249,28]; + let pubkey = recover_pubkey(message_hash, signature); + print!("pubkey: {:?}", pubkey); + assert_eq!(pubkey, [222,202,81,45,92,184,118,115,178,58,177,12,62,53,114,227,10,43,93,199,140,213,0,191,132,191,6,98,117,192,187,50,12,182,205,38,106,161,121,180,19,35,181,161,138,180,161,112,36,142,216,155,67,107,85,89,186,179,140,129,108,225,34,9] ); +} diff --git a/contracts/solana/programs/cluster-connection/src/instructions/mod.rs b/contracts/solana/programs/cluster-connection/src/instructions/mod.rs new file mode 100644 index 000000000..912ce5321 --- /dev/null +++ b/contracts/solana/programs/cluster-connection/src/instructions/mod.rs @@ -0,0 +1,3 @@ +pub mod query_accounts; + +pub use query_accounts::*; diff --git a/contracts/solana/programs/cluster-connection/src/instructions/query_accounts.rs b/contracts/solana/programs/cluster-connection/src/instructions/query_accounts.rs new file mode 100644 index 000000000..9f21112fc --- /dev/null +++ b/contracts/solana/programs/cluster-connection/src/instructions/query_accounts.rs @@ -0,0 +1,200 @@ +use anchor_lang::{ + prelude::*, + solana_program::{ + instruction::Instruction, + program::{get_return_data, invoke, invoke_signed}, + system_program, + }, +}; +use xcall_lib::{ + query_account_type::{AccountMetadata, QueryAccountsPaginateResponse, QueryAccountsResponse}, + xcall_connection_type, + xcall_type::{self, QUERY_HANDLE_ERROR_ACCOUNTS_IX, QUERY_HANDLE_MESSAGE_ACCOUNTS_IX}, +}; + +use crate::{helper, id, state::*}; + +pub fn query_send_message_accounts<'info>( + ctx: Context<'_, '_, '_, 'info, QueryAccountsCtx<'info>>, + dst_network: String, +) -> Result { + let config = &ctx.accounts.config; + + let (network_fee, _) = Pubkey::find_program_address( + &[NetworkFee::SEED_PREFIX.as_bytes(), dst_network.as_bytes()], + &id(), + ); + + let account_metas = vec![ + AccountMetadata::new(config.key(), false), + AccountMetadata::new(network_fee, false), + ]; + + Ok(QueryAccountsResponse { + accounts: account_metas, + }) +} + +pub fn query_recv_message_accounts<'info>( + ctx: Context<'_, '_, '_, 'info, QueryAccountsCtx<'info>>, + src_network: String, + conn_sn: u128, + msg: Vec, + sequence_no: u128, + page: u8, + limit: u8, +) -> Result { + let config = &ctx.accounts.config; + let (receipt, _) = Pubkey::find_program_address( + &[ + Receipt::SEED_PREFIX.as_bytes(), + src_network.as_bytes(), + &conn_sn.to_be_bytes(), + ], + &id(), + ); + let (authority, _) = Pubkey::find_program_address( + &[xcall_connection_type::CONNECTION_AUTHORITY_SEED.as_bytes()], + &id(), + ); + + let mut account_metas = vec![ + AccountMetadata::new(system_program::id(), false), + AccountMetadata::new(config.key(), false), + AccountMetadata::new(receipt, false), + AccountMetadata::new(authority, false), + ]; + + let mut xcall_account_metas = vec![AccountMeta::new_readonly(config.key(), true)]; + let mut xcall_account_infos = vec![config.to_account_info()]; + + for (_, account) in ctx.remaining_accounts.iter().enumerate() { + if account.is_writable { + xcall_account_metas.push(AccountMeta::new(account.key(), account.is_signer)); + } else { + xcall_account_metas.push(AccountMeta::new_readonly(account.key(), account.is_signer)); + } + + xcall_account_infos.push(account.to_account_info()) + } + + let ix_data = get_handle_message_ix_data(src_network, msg, sequence_no, conn_sn)?; + + let ix = Instruction { + program_id: config.xcall, + accounts: xcall_account_metas, + data: ix_data, + }; + + invoke_signed( + &ix, + &xcall_account_infos, + &[&[Config::SEED_PREFIX.as_bytes(), &[config.bump]]], + )?; + + let (_, data) = get_return_data().unwrap(); + let mut data_slice: &[u8] = &data; + let res = QueryAccountsResponse::deserialize(&mut data_slice)?; + let mut res_accounts = res.accounts; + + account_metas.append(&mut res_accounts); + + Ok(QueryAccountsPaginateResponse::new( + account_metas, + page, + limit, + )) +} + +pub fn query_revert_message_accounts( + ctx: Context, + sequence_no: u128, + page: u8, + limit: u8, +) -> Result { + let config = &ctx.accounts.config; + let (authority, _) = Pubkey::find_program_address( + &[xcall_connection_type::CONNECTION_AUTHORITY_SEED.as_bytes()], + &id(), + ); + + let mut account_metas = vec![ + AccountMetadata::new(system_program::id(), false), + AccountMetadata::new(config.key(), false), + AccountMetadata::new(authority, false), + ]; + + let mut xcall_account_metas = vec![]; + let mut xcall_account_infos = vec![]; + + for (_, account) in ctx.remaining_accounts.iter().enumerate() { + if account.is_writable { + xcall_account_metas.push(AccountMeta::new(account.key(), account.is_signer)); + } else { + xcall_account_metas.push(AccountMeta::new_readonly(account.key(), account.is_signer)); + } + + xcall_account_infos.push(account.to_account_info()) + } + + let ix_data = get_handle_error_ix_data(sequence_no)?; + + let ix = Instruction { + program_id: config.xcall, + accounts: xcall_account_metas, + data: ix_data, + }; + + invoke(&ix, &xcall_account_infos)?; + + let (_, data) = get_return_data().unwrap(); + let mut data_slice: &[u8] = &data; + let res = QueryAccountsResponse::deserialize(&mut data_slice)?; + let mut res_accounts = res.accounts; + + account_metas.append(&mut res_accounts); + account_metas.push(AccountMetadata::new(config.xcall, false)); + + Ok(QueryAccountsPaginateResponse::new( + account_metas, + page, + limit, + )) +} + +pub fn get_handle_error_ix_data(sequence_no: u128) -> Result> { + let mut ix_args_data = vec![]; + let ix_args = xcall_type::HandleErrorArgs { sequence_no }; + ix_args.serialize(&mut ix_args_data)?; + + let ix_data = helper::get_instruction_data(QUERY_HANDLE_ERROR_ACCOUNTS_IX, ix_args_data); + Ok(ix_data) +} + +pub fn get_handle_message_ix_data( + from_nid: String, + message: Vec, + sequence_no: u128, + conn_sn: u128, +) -> Result> { + let mut ix_args_data = vec![]; + let ix_args = xcall_type::HandleMessageArgs { + from_nid, + message, + sequence_no, + conn_sn, + }; + ix_args.serialize(&mut ix_args_data)?; + + let ix_data = helper::get_instruction_data(QUERY_HANDLE_MESSAGE_ACCOUNTS_IX, ix_args_data); + Ok(ix_data) +} + +#[derive(Accounts)] +pub struct QueryAccountsCtx<'info> { + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + )] + pub config: Account<'info, Config>, +} diff --git a/contracts/solana/programs/cluster-connection/src/lib.rs b/contracts/solana/programs/cluster-connection/src/lib.rs new file mode 100644 index 000000000..43b4aa971 --- /dev/null +++ b/contracts/solana/programs/cluster-connection/src/lib.rs @@ -0,0 +1,190 @@ +use std::ops::DerefMut; + +use anchor_lang::prelude::*; + +pub mod constants; +pub mod contexts; +pub mod error; +pub mod event; +pub mod helper; +pub mod instructions; +pub mod state; + +use contexts::*; +use instructions::*; +use state::*; + +use xcall_lib::query_account_type::{QueryAccountsPaginateResponse, QueryAccountsResponse}; + +declare_id!("8oxnXrSmqWJqkb2spZk2uz1cegzPsLy6nJp9XwFhkMD5"); + +#[program] +pub mod centralized_connection { + use super::*; + + pub fn initialize(ctx: Context, xcall: Pubkey, relayer: Pubkey) -> Result<()> { + ctx.accounts + .config + .set_inner(Config::new(xcall, ctx.accounts.signer.key(), relayer, ctx.bumps.config)); + ctx.accounts + .authority + .set_inner(Authority::new(ctx.bumps.authority)); + + Ok(()) + } + + pub fn send_message( + ctx: Context, + to: String, + sn: i64, + msg: Vec, + ) -> Result<()> { + let next_conn_sn = ctx.accounts.config.get_next_conn_sn()?; + + let mut fee = 0; + if sn >= 0 { + fee = ctx.accounts.network_fee.get(sn > 0)?; + } + + if fee > 0 { + helper::transfer_lamports( + &ctx.accounts.signer, + &ctx.accounts.config.to_account_info(), + &ctx.accounts.system_program, + fee, + )? + } + + emit!(event::SendMessage { + targetNetwork: to, + connSn: next_conn_sn, + msg: msg + }); + + Ok(()) + } + + + #[allow(unused_variables)] + pub fn recv_message<'info>( + ctx: Context<'_, '_, '_, 'info, ReceiveMessageWithSignatures<'info>>, + src_network: String, + conn_sn: u128, + msg: Vec, + sequence_no: u128, + signatures: Vec<[u8; 65]>, + ) -> Result<()> { + helper::call_xcall_handle_message_with_signatures(ctx, src_network, msg, conn_sn, sequence_no, signatures) + } + + pub fn revert_message<'info>( + ctx: Context<'_, '_, '_, 'info, RevertMessage<'info>>, + sequence_no: u128, + ) -> Result<()> { + helper::call_xcall_handle_error(ctx, sequence_no) + } + + pub fn set_admin(ctx: Context, account: Pubkey) -> Result<()> { + let config = ctx.accounts.config.deref_mut(); + config.admin = account; + + Ok(()) + } + + pub fn set_relayer(ctx: Context, address: Pubkey) -> Result<()> { + let config = ctx.accounts.config.deref_mut(); + config.relayer = address; + + Ok(()) + } + + #[allow(unused_variables)] + pub fn set_fee( + ctx: Context, + network_id: String, + message_fee: u64, + response_fee: u64, + ) -> Result<()> { + ctx.accounts.network_fee.set_inner(NetworkFee::new( + message_fee, + response_fee, + ctx.bumps.network_fee, + )); + + Ok(()) + } + + pub fn set_threshold(ctx: Context, threshold: u8) -> Result<()> { + if ctx.accounts.config.validators.len() < threshold as usize { + return Err(error::ConnectionError::ValidatorsMustBeGreaterThanThreshold.into()); + } + ctx.accounts.config.threshold = threshold; + Ok(()) + } + + pub fn update_validators(ctx: Context, validators: Vec<[u8; 65]>, threshold: u8) -> Result<()> { + let mut unique_validators = validators.clone(); + unique_validators.sort(); + unique_validators.dedup(); + if unique_validators.len() < threshold as usize { + return Err(error::ConnectionError::ValidatorsMustBeGreaterThanThreshold.into()); + } + ctx.accounts.config.threshold = threshold; + ctx.accounts.config.validators = unique_validators; + Ok(()) + } + + #[allow(unused_variables)] + pub fn get_fee(ctx: Context, network_id: String, response: bool) -> Result { + ctx.accounts.network_fee.get(response) + } + + pub fn claim_fees(ctx: Context) -> Result<()> { + let config = ctx.accounts.config.to_account_info(); + let fee = ctx.accounts.config.get_claimable_fees(&config)?; + + **config.try_borrow_mut_lamports()? -= fee; + **ctx.accounts.relayer.try_borrow_mut_lamports()? += fee; + + Ok(()) + } + + #[allow(unused_variables)] + pub fn query_send_message_accounts<'info>( + ctx: Context<'_, '_, '_, 'info, QueryAccountsCtx<'info>>, + to: String, + sn: i64, + msg: Vec, + ) -> Result { + instructions::query_send_message_accounts(ctx, to) + } + + pub fn query_recv_message_accounts<'info>( + ctx: Context<'_, '_, '_, 'info, QueryAccountsCtx<'info>>, + src_network: String, + conn_sn: u128, + msg: Vec, + sequence_no: u128, + page: u8, + limit: u8, + ) -> Result { + instructions::query_recv_message_accounts( + ctx, + src_network, + conn_sn, + msg, + sequence_no, + page, + limit, + ) + } + + pub fn query_revert_message_accounts<'info>( + ctx: Context<'_, '_, '_, 'info, QueryAccountsCtx<'info>>, + sequence_no: u128, + page: u8, + limit: u8, + ) -> Result { + instructions::query_revert_message_accounts(ctx, sequence_no, page, limit) + } +} diff --git a/contracts/solana/programs/cluster-connection/src/state.rs b/contracts/solana/programs/cluster-connection/src/state.rs new file mode 100644 index 000000000..75cf71056 --- /dev/null +++ b/contracts/solana/programs/cluster-connection/src/state.rs @@ -0,0 +1,130 @@ +use anchor_lang::prelude::*; +use xcall_lib::xcall_connection_type; + +use crate::{constants, error::*}; + +/// The `Config` state of the centralized connection - the inner data of the +/// program-derived address +#[account] +pub struct Config { + pub admin: Pubkey, + pub xcall: Pubkey, + pub relayer: Pubkey, + pub validators: Vec<[u8; 65]>, + pub threshold: u8, + pub sn: u128, + pub bump: u8, +} + +impl Config { + /// The Config seed phrase to derive it's program-derived address + pub const SEED_PREFIX: &'static str = "config"; + + /// Account discriminator + Xcall public key + Admin public key + connection + /// sequence + bump + pub const LEN: usize = constants::ACCOUNT_DISCRIMINATOR_SIZE + 32 + 32 + 32 + 16 + 1 + 1 + 1 + 4 + 65 * 10; + + /// Creates a new centralized connection `Config` state + pub fn new(xcall: Pubkey, admin: Pubkey, relayer: Pubkey, bump: u8) -> Self { + Self { + xcall, + admin, + relayer, + validators: Vec::new(), + threshold: 0, + sn: 0, + bump, + } + } + + /// It throws error if `signer` is not an admin account + pub fn ensure_admin(&self, signer: Pubkey) -> Result<()> { + if self.admin != signer { + return Err(ConnectionError::OnlyAdmin.into()); + } + Ok(()) + } + + /// It throws error if `address` is not an xcall account + pub fn ensure_xcall(&self, address: Pubkey) -> Result<()> { + if self.xcall != address { + return Err(ConnectionError::OnlyXcall.into()); + } + Ok(()) + } + + pub fn get_next_conn_sn(&mut self) -> Result { + self.sn += 1; + Ok(self.sn) + } + + pub fn get_claimable_fees(&self, account: &AccountInfo) -> Result { + let rent = Rent::default(); + let rent_exempt_balance = rent.minimum_balance(Config::LEN); + + Ok(account.lamports() - rent_exempt_balance) + } + + pub fn is_validator(&self, pub_key: &[u8; 64]) -> bool { + let mut pub_key_65: [u8; 65] = [0u8; 65]; + pub_key_65[0] = 0x04; + pub_key_65[1..].copy_from_slice(pub_key); + self.validators.contains(&pub_key_65) + } +} + +#[account] +pub struct NetworkFee { + pub message_fee: u64, + pub response_fee: u64, + pub bump: u8, +} + +impl NetworkFee { + /// The Fee seed phrase to derive it's program-derived address + pub const SEED_PREFIX: &'static str = "fee"; + + /// Account discriminator + Message fee + Response fee + bump + pub const LEN: usize = constants::ACCOUNT_DISCRIMINATOR_SIZE + 8 + 8 + 1; + + /// Creates a new `Fee` state for a network_id + pub fn new(message_fee: u64, response_fee: u64, bump: u8) -> Self { + Self { + message_fee, + response_fee, + bump, + } + } + + pub fn get(&self, response: bool) -> Result { + let mut fee = self.message_fee; + if response { + fee += self.response_fee + } + + Ok(fee) + } +} + +#[account] +pub struct Receipt {} + +impl Receipt { + pub const SEED_PREFIX: &'static str = "receipt"; + + pub const LEN: usize = constants::ACCOUNT_DISCRIMINATOR_SIZE; +} + +#[account] +pub struct Authority { + pub bump: u8, +} + +impl Authority { + pub const SEED_PREFIX: &'static str = xcall_connection_type::CONNECTION_AUTHORITY_SEED; + pub const LEN: usize = 8 + 1; + + pub fn new(bump: u8) -> Self { + Self { bump } + } +}