diff --git a/contracts/solana/Cargo.lock b/contracts/solana/Cargo.lock index 8ce7dd91..719242c8 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", + "ed25519-dalek", + "xcall-lib", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -776,6 +785,29 @@ dependencies = [ "subtle", ] +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + [[package]] name = "either" version = "1.13.0" @@ -1520,6 +1552,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + [[package]] name = "sized-chunks" version = "0.6.5" diff --git a/contracts/solana/programs/cluster-connection/Cargo.toml b/contracts/solana/programs/cluster-connection/Cargo.toml new file mode 100644 index 00000000..868dda7f --- /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 } +ed25519-dalek = { version = "1.0.1" } diff --git a/contracts/solana/programs/cluster-connection/Xargo.toml b/contracts/solana/programs/cluster-connection/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /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 00000000..fe8aad0a --- /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 00000000..20b7115d --- /dev/null +++ b/contracts/solana/programs/cluster-connection/src/contexts.rs @@ -0,0 +1,289 @@ +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)] +#[instruction(src_network: String, conn_sn: u128)] +pub struct RecvMessage<'info> { + #[account(mut)] + pub admin: Signer<'info>, + + pub system_program: Program<'info, System>, + + /// Config + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + has_one = admin @ ConnectionError::OnlyAdmin, + )] + pub config: Account<'info, Config>, + + #[account( + init, + payer = admin, + 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>, +} + +#[derive(Accounts)] +pub struct RevertMessage<'info> { + #[account(mut)] + pub admin: Signer<'info>, + + pub system_program: Program<'info, System>, + + /// Config + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + has_one = admin @ ConnectionError::OnlyAdmin, + )] + pub config: Account<'info, Config>, + + #[account( + seeds = [Authority::SEED_PREFIX.as_bytes()], + bump = authority.bump + )] + pub authority: Account<'info, Authority>, +} + +#[derive(Accounts)] +pub struct SetAdmin<'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 admin: Signer<'info>, + + /// System Program: Required to create program-derived address + pub system_program: Program<'info, System>, + + /// Fee + #[account( + init_if_needed, + payer = admin, + 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 = admin @ ConnectionError::OnlyAdmin, + )] + 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 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)] +pub struct SetThreshold<'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)] +pub struct GetThreshold<'info> { + /// Config + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump + )] + pub config: Account<'info, Config>, +} + +#[derive(Accounts)] +pub struct AddValidator<'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)] +pub struct RemoveValidator<'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)] +pub struct GetValidators<'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 admin: Signer<'info>, + + pub system_program: Program<'info, System>, + + /// Config + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + has_one = admin @ ConnectionError::OnlyAdmin, + )] + pub config: Account<'info, Config>, + + #[account( + init, + payer = admin, + 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 00000000..eb27da60 --- /dev/null +++ b/contracts/solana/programs/cluster-connection/src/error.rs @@ -0,0 +1,16 @@ +use anchor_lang::prelude::*; + +#[error_code] +pub enum ConnectionError { + #[msg("Only admin")] + OnlyAdmin, + + #[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 00000000..e379a589 --- /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 00000000..3c8774b3 --- /dev/null +++ b/contracts/solana/programs/cluster-connection/src/helper.rs @@ -0,0 +1,197 @@ +use anchor_lang::{ + prelude::*, + solana_program::{ + hash, + instruction::{AccountMeta,Instruction}, + program::{invoke, invoke_signed}, + sysvar::{recent_blockhashes::ID as SYSVAR_ID}, + ed25519_program, + system_instruction, + }, +}; + +use ed25519_dalek::{PublicKey as Ed25519PublicKey, Signature, Verifier}; +use crate::contexts::*; +use crate::state::*; + +use xcall_lib::xcall_type; + +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 verify_message(signature: [u8; 64], message: Vec, pubkey: Pubkey) -> Result<()> { + // Recover the public key from the signature + + // let pubkey = Pubkey::from([ + // 57, 234, 243, 206, 86, 29, 102, 46, 179, 39, 246, 137, 159, 74, 167, 40, + // 69, 191, 199, 163, 68, 114, 221, 40, 45, 129, 56, 73, 87, 58, 119, 149 + // ]); + + let validator_pubkey = Ed25519PublicKey::from_bytes(&pubkey.to_bytes()).unwrap(); + + let signature = Signature::from_bytes(&signature).unwrap(); + + validator_pubkey + .verify(&message, &signature) + .unwrap(); + + + Ok(()) +} + +pub fn call_xcall_handle_message<'info>( + ctx: Context<'_, '_, '_, 'info, RecvMessage<'info>>, + from_nid: String, + message: Vec, + sequence_no: u128, +) -> Result<()> { + let mut data = vec![]; + let args = xcall_type::HandleMessageArgs { + from_nid, + message, + sequence_no, + }; + 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.admin, + &ctx.accounts.system_program, + ctx.remaining_accounts, + ) +} + +pub fn call_xcall_handle_message_with_signatures<'info>( + ctx: Context<'_, '_, '_, 'info, ReceiveMessageWithSignatures<'info>>, + from_nid: String, + message: Vec, + sequence_no: u128, + signatures: Vec<[u8; 64]>, +) -> Result<()> { + let mut data = vec![]; + let args = xcall_type::HandleMessageArgs { + from_nid, + message, + sequence_no, + }; + 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.admin, + &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.admin, + &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_verify_message() { + let message: Vec = b"Test message".to_vec(); + let result = verify_message([ + 73, 220, 38, 229, 202, 199, 238, 241, 69, 237, 194, + 226, 113, 76, 59, 167, 134, 83, 13, 213, 245, 151, + 26, 220, 196, 162, 247, 240, 95, 245, 78, 205, 26, + 233, 140, 177, 151, 207, 193, 23, 71, 96, 126, 115, + 200, 21, 108, 178, 48, 133, 117, 208, 27, 80, 237, + 93, 180, 97, 235, 40, 109, 36, 31, 11 + ], message.clone(), Pubkey::new(&[57, 234, 243, 206, 86, 29, 102, 46, 179, 39, 246, 137, 159, 74, 167, 40, + 69, 191, 199, 163, 68, 114, 221, 40, 45, 129, 56, 73, 87, 58, 119, 149] + )); + + // Step 6: Assert that the signature verification is successful + assert!(result.is_ok(), "Signature verification failed"); +} 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 00000000..912ce532 --- /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 00000000..7f149623 --- /dev/null +++ b/contracts/solana/programs/cluster-connection/src/instructions/query_accounts.rs @@ -0,0 +1,194 @@ +use anchor_lang::{ + prelude::*, + solana_program::{ + instruction::Instruction, + program::{get_return_data, invoke}, + 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( + ctx: Context, + 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![]; + 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_message_ix_data(src_network, msg, 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); + + 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, +) -> Result> { + let mut ix_args_data = vec![]; + let ix_args = xcall_type::HandleMessageArgs { + from_nid, + message, + sequence_no, + }; + 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 00000000..e9d737b7 --- /dev/null +++ b/contracts/solana/programs/cluster-connection/src/lib.rs @@ -0,0 +1,196 @@ +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, admin: Pubkey) -> Result<()> { + ctx.accounts + .config + .set_inner(Config::new(xcall, admin, 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, RecvMessage<'info>>, + src_network: String, + conn_sn: u128, + msg: Vec, + sequence_no: u128, + ) -> Result<()> { + helper::call_xcall_handle_message(ctx, src_network, msg, sequence_no) + } + + #[allow(unused_variables)] + pub fn receive_message_with_signatures<'info>( + ctx: Context<'_, '_, '_, 'info, ReceiveMessageWithSignatures<'info>>, + src_network: String, + conn_sn: u128, + msg: Vec, + sequence_no: u128, + signatures: Vec<[u8; 64]>, + ) -> Result<()> { + helper::call_xcall_handle_message_with_signatures(ctx, src_network, msg, 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(()) + } + + #[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<()> { + ctx.accounts.config.set_threshold(threshold); + Ok(()) + } + + pub fn add_validator(ctx: Context, validator: Pubkey) -> Result<()> { + ctx.accounts.config.add_validator(validator); + Ok(()) + } + + pub fn remove_validator(ctx: Context, validator: Pubkey) -> Result<()> { + ctx.accounts.config.remove_validator(validator); + Ok(()) + } + + pub fn get_validators(ctx: Context) -> Result> { + Ok(ctx.accounts.config.get_validators()) + } + + pub fn get_threshold(ctx: Context) -> Result { + Ok(ctx.accounts.config.get_threshold()?) + } + + #[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.admin.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 00000000..c44bfb90 --- /dev/null +++ b/contracts/solana/programs/cluster-connection/src/state.rs @@ -0,0 +1,147 @@ +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 validators: Vec, + 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 + 16 + 1 + 1; + + /// Creates a new centralized connection `Config` state + pub fn new(xcall: Pubkey, admin: Pubkey, bump: u8) -> Self { + Self { + xcall, + admin, + 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 get_threshold(&self) -> Result { + Ok(self.threshold) + } + + pub fn set_threshold(&mut self, threshold: u8) { + self.threshold = threshold + } + + pub fn add_validator(&mut self, validator: Pubkey) { + self.validators.push(validator); + } + + pub fn remove_validator(&mut self, validator: Pubkey) { + if self.admin == validator { + return; + } + if self.validators.len() < self.threshold as usize { + return; + } + self.validators.retain(|x| *x != validator); + } + + pub fn get_validators(&self) -> Vec { + self.validators.clone() + } +} + +#[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 } + } +}