diff --git a/Cargo.lock b/Cargo.lock index 00b9d566..1e321961 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3170,6 +3170,7 @@ dependencies = [ "strum 0.28.0", "thiserror 2.0.18", "tokio", + "wheels", ] [[package]] @@ -3204,6 +3205,7 @@ dependencies = [ "static_assertions", "strum 0.28.0", "thiserror 1.0.69", + "wheels", ] [[package]] @@ -9629,6 +9631,26 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "wheels" +version = "0.1.0" +dependencies = [ + "bytemuck", + "pinocchio 0.10.2", + "pinocchio-log", + "solana-address 2.0.0", + "wheels-macros", +] + +[[package]] +name = "wheels-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 38526224..ad707115 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ solana-security-txt = { version = ">=1.1", optional = true } static_assertions = "1.1.0" strum = { version = ">=0.27", features = ["derive"] } thiserror = { version = ">=1" } +wheels = { path = "../magicblock-wheels/rust/wheels" } pinocchio = { version = "0.10.1", optional = true } pinocchio-log = { version = "0.5.1" , optional = true } diff --git a/dlp-api/Cargo.toml b/dlp-api/Cargo.toml index b5dd3315..4e13b42e 100644 --- a/dlp-api/Cargo.toml +++ b/dlp-api/Cargo.toml @@ -49,6 +49,7 @@ static_assertions = "1.1.0" strum = { version = ">=0.27", features = ["derive"] } thiserror = { version = ">=1" } serde = { version = "1.0.228", default-features = false, features = ["derive"] } +wheels = { path = "../../magicblock-wheels/rust/wheels" } solana-pubkey-compat = { package = "solana-pubkey", version = "2.4", features = ["borsh", "bytemuck", "curve25519"] } diff --git a/dlp-api/src/args/delegate_with_actions.rs b/dlp-api/src/args/delegate_with_actions.rs index 25463c6d..97f7f201 100644 --- a/dlp-api/src/args/delegate_with_actions.rs +++ b/dlp-api/src/args/delegate_with_actions.rs @@ -1,11 +1,11 @@ use borsh::{BorshDeserialize, BorshSerialize}; use pinocchio::error::ProgramError; +use wheels::{require, require_le}; use super::DelegateArgs; use crate::{ compact::{self, MAX_PUBKEYS}, compat::borsh, - require, require_le, }; #[derive(Debug, BorshSerialize, BorshDeserialize)] diff --git a/dlp-api/src/args/types.rs b/dlp-api/src/args/types.rs index d1b3a097..6b247ab8 100644 --- a/dlp-api/src/args/types.rs +++ b/dlp-api/src/args/types.rs @@ -3,7 +3,9 @@ use std::ops::Deref; use bytemuck::{Pod, Zeroable}; use pinocchio::error::ProgramError; -use crate::{pod_view::PodView, require_ge}; +use wheels::require_ge; + +use crate::pod_view::PodView; /// /// Boolean diff --git a/dlp-api/src/diff/algorithm.rs b/dlp-api/src/diff/algorithm.rs index d6529236..82700135 100644 --- a/dlp-api/src/diff/algorithm.rs +++ b/dlp-api/src/diff/algorithm.rs @@ -7,7 +7,9 @@ use super::{ DiffSet, OffsetInData, SizeChanged, SIZE_OF_CHANGED_LEN, SIZE_OF_NUM_OFFSET_PAIRS, SIZE_OF_SINGLE_OFFSET_PAIR, }; -use crate::{error::DlpError, require_eq, require_le}; +use wheels::{require_eq, require_le}; + +use crate::error::DlpError; /// /// Compute diff between original and changed. diff --git a/dlp-api/src/diff/types.rs b/dlp-api/src/diff/types.rs index 9f7d3cfd..f8c6093f 100644 --- a/dlp-api/src/diff/types.rs +++ b/dlp-api/src/diff/types.rs @@ -7,7 +7,9 @@ use std::{cmp::Ordering, ops::Range}; use pinocchio::error::ProgramError; use static_assertions::const_assert; -use crate::{error::DlpError, require_eq, require_ge, require_le, require_lt}; +use wheels::{require_eq, require_ge, require_le, require_lt}; + +use crate::error::DlpError; #[derive(Debug, Clone, Copy)] pub enum SizeChanged { diff --git a/dlp-api/src/lib.rs b/dlp-api/src/lib.rs index 01a22b04..3a4318d1 100644 --- a/dlp-api/src/lib.rs +++ b/dlp-api/src/lib.rs @@ -17,6 +17,7 @@ pub mod pda; pub mod pod_view; pub mod requires; pub mod state; +pub mod v2; pub use account_size_class::*; diff --git a/dlp-api/src/requires.rs b/dlp-api/src/requires.rs index a71d2b86..f481d9d7 100644 --- a/dlp-api/src/requires.rs +++ b/dlp-api/src/requires.rs @@ -12,222 +12,7 @@ use crate::{ validator_fees_vault_pda_from_validator, }, }; - -// require true -#[macro_export] -macro_rules! require { - ($cond:expr, $error:expr) => {{ - if !$cond { - let expr = stringify!($cond); - pinocchio_log::log!("require!({}) failed.", expr); - return Err($error.into()); - } - }}; -} - -// require (info.is_signer()) -#[macro_export] -macro_rules! require_signer { - ($info: expr) => {{ - if !$info.is_signer() { - pinocchio_log::log!("require_signer!({}): ", stringify!($info)); - $info.address().log(); - return Err(ProgramError::MissingRequiredSignature); - } - }}; -} - -// require key1 == key2 -#[macro_export] -macro_rules! require_eq_keys { - ( $key1:expr, $key2:expr, $error:expr) => {{ - if !pinocchio::address::address_eq($key1, $key2) { - pinocchio_log::log!( - "require_eq_keys!({}, {}) failed: ", - stringify!($key1), - stringify!($key2) - ); - $key1.log(); - $key2.log(); - return Err($error.into()); - } - }}; -} - -// require a == b -#[macro_export] -macro_rules! require_eq { - ( $val1:expr, $val2:expr, $error:expr) => {{ - if !($val1 == $val2) { - pinocchio_log::log!( - "require_eq!({}, {}) failed: {} == {}", - stringify!($val1), - stringify!($val2), - $val1, - $val2 - ); - return Err($error.into()); - } - }}; -} - -// require a <= b -#[macro_export] -macro_rules! require_le { - ( $val1:expr, $val2:expr, $error:expr) => {{ - if !($val1 <= $val2) { - pinocchio_log::log!( - "require_le!({}, {}) failed: {} <= {}", - stringify!($val1), - stringify!($val2), - $val1, - $val2 - ); - return Err($error.into()); - } - }}; -} - -// require a < b -#[macro_export] -macro_rules! require_lt { - ( $val1:expr, $val2:expr, $error:expr) => {{ - if !($val1 < $val2) { - pinocchio_log::log!( - "require_lt!({}, {}) failed: {} < {}", - stringify!($val1), - stringify!($val2), - $val1, - $val2 - ); - return Err($error.into()); - } - }}; -} - -// require a >= b -#[macro_export] -macro_rules! require_ge { - ( $val1:expr, $val2:expr, $error:expr) => {{ - if !($val1 >= $val2) { - pinocchio_log::log!( - "require_ge!({}, {}) failed: {} >= {}", - stringify!($val1), - stringify!($val2), - $val1, - $val2 - ); - return Err($error.into()); - } - }}; -} - -// require a > b -#[macro_export] -macro_rules! require_gt { - ( $val1:expr, $val2:expr, $error:expr) => {{ - if !($val1 > $val2) { - pinocchio_log::log!( - "require_gt!({}, {}) failed: {} > {}", - stringify!($val1), - stringify!($val2), - $val1, - $val2 - ); - return Err($error.into()); - } - }}; -} - -#[macro_export] -macro_rules! require_n_accounts { - ( $accounts:expr, $n:literal) => {{ - match $accounts.len().cmp(&$n) { - core::cmp::Ordering::Less => { - pinocchio_log::log!( - "Need {} accounts, but got less ({}) accounts", - $n, - $accounts.len() - ); - return Err( - pinocchio::error::ProgramError::NotEnoughAccountKeys, - ); - } - core::cmp::Ordering::Equal => { - TryInto::<&[_; $n]>::try_into($accounts) - .map_err(|_| $crate::error::DlpError::InfallibleError)? - } - core::cmp::Ordering::Greater => { - pinocchio_log::log!( - "Need {} accounts, but got more ({}) accounts", - $n, - $accounts.len() - ); - return Err($crate::error::DlpError::TooManyAccountKeys.into()); - } - } - }}; -} - -#[macro_export] -macro_rules! require_n_accounts_with_optionals { - ( $accounts:expr, $n:literal) => {{ - match $accounts.len().cmp(&$n) { - core::cmp::Ordering::Less => { - pinocchio_log::log!( - "Need {} accounts, but got less ({}) accounts", - $n, - $accounts.len() - ); - return Err( - pinocchio::error::ProgramError::NotEnoughAccountKeys, - ); - } - _ => { - let (exact, optionals) = $accounts.split_at($n); - - ( - TryInto::<&[_; $n]>::try_into(exact).map_err(|_| { - $crate::error::DlpError::InfallibleError - })?, - optionals, - ) - } - } - }}; -} - -#[macro_export] -macro_rules! require_some { - ($option:expr, $error:expr) => {{ - match $option { - Some(val) => val, - None => return Err($error.into()), - } - }}; -} - -/// -/// require_owned_by( -/// info: &AccountView, -/// owner: &Address -/// ) -> Result<(), ProgramError> -/// -#[macro_export] -macro_rules! require_owned_by { - ($info: expr, $owner: expr) => {{ - if !address_eq(unsafe { $info.owner() }, $owner) { - pinocchio_log::log!( - "require_owned_by!({}, {})", - stringify!($info), - stringify!($owner) - ); - $info.address().log(); - $owner.log(); - return Err(ProgramError::InvalidAccountOwner); - } - }}; -} +use wheels::require_eq_keys; /// /// require_initialized_pda( @@ -266,7 +51,7 @@ macro_rules! require_initialized_pda { return Err(ProgramError::InvalidSeeds); } - require_owned_by!($info, $program_id); + wheels::require_owned_by!($info, $program_id); if $is_writable && !$info.is_writable() { pinocchio_log::log!( @@ -297,7 +82,7 @@ macro_rules! require_initialized_pda_fast { return Err(ProgramError::InvalidSeeds); } - require_owned_by!($info, &$crate::fast::ID); + wheels::require_owned_by!($info, &$crate::fast::ID); if $is_writable && !$info.is_writable() { pinocchio_log::log!( @@ -703,6 +488,39 @@ pub trait RequireUninitializedAccountCtx { fn immutable(&self) -> ProgramError; } +#[derive(Clone, Copy, Debug)] +pub struct StandardCtx { + label: &'static str, +} + +impl StandardCtx { + pub const fn new(label: &'static str) -> Self { + Self { label } + } +} + +impl RequireUninitializedAccountCtx for StandardCtx { + fn label(&self) -> &str { + self.label + } + + fn invalid_seeds(&self) -> ProgramError { + ProgramError::InvalidSeeds + } + + fn invalid_account_owner(&self) -> ProgramError { + ProgramError::InvalidAccountOwner + } + + fn account_already_initialized(&self) -> ProgramError { + ProgramError::AccountAlreadyInitialized + } + + fn immutable(&self) -> ProgramError { + ProgramError::Immutable + } +} + macro_rules! define_uninitialized_ctx { ( $name:ident, diff --git a/dlp-api/src/state/delegation_metadata.rs b/dlp-api/src/state/delegation_metadata.rs index 79d2f077..6160d63c 100644 --- a/dlp-api/src/state/delegation_metadata.rs +++ b/dlp-api/src/state/delegation_metadata.rs @@ -2,12 +2,13 @@ use std::ptr; use borsh::{BorshDeserialize, BorshSerialize}; use pinocchio::{account::RefMut, error::ProgramError, AccountView}; +use wheels::require_ge; use super::discriminator::{AccountDiscriminator, AccountWithDiscriminator}; use crate::{ compat::{borsh, Pubkey}, impl_to_bytes_with_discriminator_borsh, - impl_try_from_bytes_with_discriminator_borsh, require_ge, + impl_try_from_bytes_with_discriminator_borsh, }; /// Identifies who requested that a delegated account should move toward diff --git a/dlp-api/src/v2/args/init_protocol_config.rs b/dlp-api/src/v2/args/init_protocol_config.rs new file mode 100644 index 00000000..c2a6f87c --- /dev/null +++ b/dlp-api/src/v2/args/init_protocol_config.rs @@ -0,0 +1,23 @@ +use wheels::variable_offset_layout; + +use crate::compat::Pubkey; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[variable_offset_layout(buffer_offset = 1)] +pub struct InitProtocolConfigArgs { + pub resolver: Pubkey, + + pub min_operator_bond: u64, + pub min_verifier_bond: u64, + pub min_challenger_stake: u64, + + pub challenge_window_slots: u64, + pub operator_response_timeout_slots: u64, + pub challenger_reveal_timeout_slots: u64, + pub payout_timelock_slots: u64, + + pub verifiers_per_commitment: u16, + pub approval_threshold: u16, + pub max_window_extensions: u16, + pub match_penalty_bps: u16, +} diff --git a/dlp-api/src/v2/args/mod.rs b/dlp-api/src/v2/args/mod.rs new file mode 100644 index 00000000..d399fbcc --- /dev/null +++ b/dlp-api/src/v2/args/mod.rs @@ -0,0 +1,6 @@ +// V2 processors decode args from `instruction_data[1..]` after the one-byte +// instruction tag, so v2 instruction args use `buffer_offset = 1`. + +mod init_protocol_config; + +pub use init_protocol_config::*; diff --git a/dlp-api/src/v2/instruction.rs b/dlp-api/src/v2/instruction.rs new file mode 100644 index 00000000..fc1751ac --- /dev/null +++ b/dlp-api/src/v2/instruction.rs @@ -0,0 +1,20 @@ +use num_enum::TryFromPrimitive; +use strum::IntoStaticStr; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive, IntoStaticStr)] +#[rustfmt::skip] +pub enum DlpV2Instruction { + /// Creates the global v2 protocol config and verifier registry accounts. + InitProtocolConfig = 100, +} + +impl DlpV2Instruction { + pub fn to_vec(self) -> Vec { + vec![self as u8] + } + + pub fn name(&self) -> &'static str { + self.into() + } +} diff --git a/dlp-api/src/v2/instruction_builder/init_protocol_config.rs b/dlp-api/src/v2/instruction_builder/init_protocol_config.rs new file mode 100644 index 00000000..4f79aa08 --- /dev/null +++ b/dlp-api/src/v2/instruction_builder/init_protocol_config.rs @@ -0,0 +1,35 @@ +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; +use solana_sdk_ids::system_program; +use wheels::layout::Encodable; + +use crate::{ + compat::Modernize, + v2::{ + pda::{protocol_config_pda, verifier_registry_pda}, + DlpV2Instruction, InitProtocolConfigArgs, + }, +}; + +/// Builds the instruction that creates the global v2 config accounts. +pub fn init_protocol_config( + authority: Pubkey, + args: InitProtocolConfigArgs, +) -> Instruction { + Instruction { + program_id: crate::id().modernize(), + accounts: vec![ + AccountMeta::new(authority, true), + AccountMeta::new(protocol_config_pda().modernize(), false), + AccountMeta::new(verifier_registry_pda().modernize(), false), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: [ + DlpV2Instruction::InitProtocolConfig.to_vec(), + args.encode().unwrap(), + ] + .concat(), + } +} diff --git a/dlp-api/src/v2/instruction_builder/mod.rs b/dlp-api/src/v2/instruction_builder/mod.rs new file mode 100644 index 00000000..13cc48ad --- /dev/null +++ b/dlp-api/src/v2/instruction_builder/mod.rs @@ -0,0 +1,3 @@ +mod init_protocol_config; + +pub use init_protocol_config::*; diff --git a/dlp-api/src/v2/mod.rs b/dlp-api/src/v2/mod.rs new file mode 100644 index 00000000..5a6b3abc --- /dev/null +++ b/dlp-api/src/v2/mod.rs @@ -0,0 +1,12 @@ +pub mod args; +pub mod instruction; +pub mod pda; +pub mod state; + +#[cfg(feature = "instruction")] +pub mod instruction_builder; + +pub use args::*; +pub use instruction::*; +pub use pda::*; +pub use state::*; diff --git a/dlp-api/src/v2/pda.rs b/dlp-api/src/v2/pda.rs new file mode 100644 index 00000000..b1f2ad9f --- /dev/null +++ b/dlp-api/src/v2/pda.rs @@ -0,0 +1,14 @@ +use crate::compat::Pubkey; + +pub const PROTOCOL_CONFIG_SEED: &[u8] = b"protocol-config"; +pub const VERIFIER_REGISTRY_SEED: &[u8] = b"verifier-registry"; + +// TODO (snawaz): Precompute these addresses if PDA derivation becomes const-safe. + +pub fn protocol_config_pda() -> Pubkey { + Pubkey::find_program_address(&[PROTOCOL_CONFIG_SEED], &crate::id()).0 +} + +pub fn verifier_registry_pda() -> Pubkey { + Pubkey::find_program_address(&[VERIFIER_REGISTRY_SEED], &crate::id()).0 +} diff --git a/dlp-api/src/v2/state/mod.rs b/dlp-api/src/v2/state/mod.rs new file mode 100644 index 00000000..31678d9d --- /dev/null +++ b/dlp-api/src/v2/state/mod.rs @@ -0,0 +1,5 @@ +mod protocol_config; +mod verifier_registry; + +pub use protocol_config::*; +pub use verifier_registry::*; diff --git a/dlp-api/src/v2/state/protocol_config.rs b/dlp-api/src/v2/state/protocol_config.rs new file mode 100644 index 00000000..9b740949 --- /dev/null +++ b/dlp-api/src/v2/state/protocol_config.rs @@ -0,0 +1,67 @@ +use wheels::fixed_offset_layout; + +use crate::compat::Pubkey; + +/// PDA: `["protocol-config"]`. +/// Created by `InitProtocolConfig`; not normally closed. +#[derive(Clone, Debug, PartialEq, Eq)] +#[fixed_offset_layout(buffer_offset = 0)] +pub struct ProtocolConfig { + /// Account type marker. + pub discriminator: [u8; 8], + + /// Canonical PDA bump for this account. + pub bump: u8, + + /// Signer allowed to update config and permissioned bootstrap state. + pub authority: Pubkey, + + /// Emergency stop for new commitments and other non-exit activity. + pub paused: bool, + + /// Multisig-controlled signer allowed to resolve disputes. + pub resolver: Pubkey, + + /// Vault receiving protocol fees, penalties, or slashed funds. + pub protocol_fee_vault: Pubkey, + + /// Minimum stake required for an operator to register and stay active. + pub min_operator_bond: u64, + + /// Minimum stake required for a verifier to register and stay active. + pub min_verifier_bond: u64, + + /// Minimum stake locked by a challenge to prevent cheap spam. + pub min_challenger_stake: u64, + + /// Slots available for approval/challenge after commitment post. + pub challenge_window_slots: u64, + + /// Slots the operator gets to open state after a challenge. + pub operator_response_timeout_slots: u64, + + /// Slots the challenger gets to reveal after operator response or timeout. + pub challenger_reveal_timeout_slots: u64, + + /// Delay before a winning challenger can claim payout. + pub payout_timelock_slots: u64, + + /// Maximum number of verifiers selected for one commitment. + pub verifiers_per_commitment: u16, + + /// Approvals required for happy-path finalization. + pub approval_threshold: u16, + + /// Maximum under-approval extensions before the commitment expires. + pub max_window_extensions: u16, + + /// Penalty for a valid reveal that matches the operator state. + pub match_penalty_bps: u16, +} + +impl ProtocolConfig { + pub const DISCRIMINATOR: [u8; 8] = *b"v2cfg000"; + + /// Maximum value for basis-points config fields. + pub const MAX_MATCH_PENALTY_BPS: u16 = 10_000; +} diff --git a/dlp-api/src/v2/state/verifier_registry.rs b/dlp-api/src/v2/state/verifier_registry.rs new file mode 100644 index 00000000..0a60151a --- /dev/null +++ b/dlp-api/src/v2/state/verifier_registry.rs @@ -0,0 +1,40 @@ +use wheels::fixed_offset_layout; + +use crate::compat::Pubkey; + +/// PDA: `["verifier-registry"]`. +/// Created by `InitProtocolConfig`; updated by `UpdateVerifierRegistry`. +/// Not normally closed. +#[derive(Clone, Debug, PartialEq, Eq)] +#[fixed_offset_layout(buffer_offset = 0)] +pub struct VerifierRegistry { + /// Account type marker. + pub discriminator: [u8; 8], + + /// Canonical PDA bump for this account. + pub bump: u8, + + /// Round-robin start cursor used by the next commitment selection. + pub next_selection_index: u64, + + /// All registered verifiers DLP can select from. + #[extendable = 2] + pub entries: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +#[fixed_offset_layout(buffer_offset = 3)] +pub struct VerifierRegistryEntry { + /// Verifier identity selectable by DLP. + pub verifier_identity: Pubkey, + + /// Bond account proving this verifier has active stake. + pub verifier_bond: Pubkey, + + /// Selection weight. Keep as 1 until weighted selection is implemented. + pub weight: u64, +} + +impl VerifierRegistry { + pub const DISCRIMINATOR: [u8; 8] = *b"v2vreg00"; +} diff --git a/src/lib.rs b/src/lib.rs index b7d3e54c..a29441b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,9 @@ extern crate dlp_api; pub use ::solana_program; + +pub type RequireError = dlp_api::error::DlpError; + #[allow(unused_imports)] pub(crate) use dlp_api::diff; #[allow(unused_imports)] @@ -25,13 +28,16 @@ pub(crate) use dlp_api::{ pub use dlp_api::{id, ID}; #[allow(unused_imports)] pub(crate) use dlp_api::{ - require, require_eq, require_eq_keys, require_ge, require_gt, - require_initialized_pda, require_initialized_pda_fast, require_le, - require_lt, require_n_accounts, require_n_accounts_with_optionals, - require_owned_by, require_pda, require_signer, require_some, + require_initialized_pda, require_initialized_pda_fast, require_pda, }; #[cfg(feature = "logging")] use solana_program::msg; +#[allow(unused_imports)] +pub(crate) use wheels::{ + require, require_eq, require_eq_keys, require_ge, require_gt, require_le, + require_lt, require_n_accounts, require_n_accounts_with_optionals, + require_owned_by, require_signer, require_some, +}; #[cfg(feature = "processor")] use { dlp_api::discriminator::DlpDiscriminator, @@ -44,6 +50,9 @@ use { #[cfg(feature = "processor")] mod processor; +#[cfg(feature = "processor")] +mod v2; + #[allow(unused_imports)] pub(crate) use diff::*; @@ -76,16 +85,25 @@ pub fn fast_process_instruction( accounts: &[pinocchio::AccountView], data: &[u8], ) -> Option { + let Some((&tag, v2_data)) = data.split_first() else { + return Some(Err( + pinocchio::error::ProgramError::InvalidInstructionData, + )); + }; + + if let Ok(ix) = dlp_api::v2::DlpV2Instruction::try_from(tag) { + return Some(v2::process_instruction(accounts, v2_data, ix)); + } + if data.len() < 8 { return Some(Err( pinocchio::error::ProgramError::InvalidInstructionData, )); } - let (discriminator_bytes, data) = data.split_at(8); + let (_, data) = data.split_at(8); - let discriminator = match DlpDiscriminator::try_from(discriminator_bytes[0]) - { + let discriminator = match DlpDiscriminator::try_from(tag) { Ok(discriminator) => discriminator, Err(_) => { pinocchio_log::log!("Failed to read and parse discriminator"); @@ -98,6 +116,17 @@ pub fn fast_process_instruction( #[cfg(feature = "logging")] msg!("Processing instruction: {:?}", discriminator); + process_v1_fast_instruction(discriminator, program_id, accounts, data) +} + +#[cfg(feature = "processor")] +#[inline(never)] +fn process_v1_fast_instruction( + discriminator: DlpDiscriminator, + program_id: &pinocchio::Address, + accounts: &[pinocchio::AccountView], + data: &[u8], +) -> Option { match discriminator { DlpDiscriminator::Delegate => Some(processor::fast::process_delegate( program_id, accounts, data, @@ -169,12 +198,21 @@ pub fn slow_process_instruction( accounts: &[AccountInfo], data: &[u8], ) -> ProgramResult { + let Some((&tag, _)) = data.split_first() else { + return Err(ProgramError::InvalidInstructionData); + }; + + if dlp_api::v2::DlpV2Instruction::try_from(tag).is_ok() { + return Err(ProgramError::InvalidInstructionData); + } + if data.len() < 8 { return Err(ProgramError::InvalidInstructionData); } - let (tag, data) = data.split_at(8); - let ix = DlpDiscriminator::try_from(tag[0]) + let (_, data) = data.split_at(8); + + let ix = DlpDiscriminator::try_from(tag) .or(Err(ProgramError::InvalidInstructionData))?; match ix { diff --git a/src/processor/fast/delegate_with_actions.rs b/src/processor/fast/delegate_with_actions.rs index d78bfd0d..cac5a298 100644 --- a/src/processor/fast/delegate_with_actions.rs +++ b/src/processor/fast/delegate_with_actions.rs @@ -1,4 +1,4 @@ -use dlp_api::{compat::borsh, require_eq}; +use dlp_api::compat::borsh; use pinocchio::{ address::address_eq, cpi::{Seed, Signer}, @@ -18,7 +18,7 @@ use crate::{ fast::{to_pinocchio_program_error, utils::pda::create_pda}, utils::curve::is_on_curve_fast, }, - require_n_accounts_with_optionals, + require_eq, require_n_accounts_with_optionals, requires::{ require_owned_pda, require_pda, require_signer, require_uninitialized_pda, DelegationMetadataCtx, DelegationRecordCtx, diff --git a/src/processor/fast/mod.rs b/src/processor/fast/mod.rs index b01c11a5..8d44a38f 100644 --- a/src/processor/fast/mod.rs +++ b/src/processor/fast/mod.rs @@ -11,7 +11,7 @@ mod request_undelegation; mod undelegate; mod undelegate_confined_account; mod undelegate_with_rollback_after_timeout; -mod utils; +pub(crate) mod utils; pub(crate) mod internal; diff --git a/src/processor/fast/undelegate.rs b/src/processor/fast/undelegate.rs index f5dcfdfe..7a67d2d7 100644 --- a/src/processor/fast/undelegate.rs +++ b/src/processor/fast/undelegate.rs @@ -1,4 +1,4 @@ -use dlp_api::{compat::borsh, require_eq_keys}; +use dlp_api::compat::borsh; use pinocchio::{ address::{address_eq, Address}, cpi::{invoke_signed, Signer}, @@ -21,7 +21,7 @@ use crate::{ error::DlpError, pda, processor::fast::utils::pda::{close_pda, close_pda_with_fees, create_pda}, - require_n_accounts_with_optionals, + require_eq_keys, require_n_accounts_with_optionals, requires::{ require_initialized_delegation_metadata, require_initialized_delegation_record, require_initialized_pda, diff --git a/src/processor/mod.rs b/src/processor/mod.rs index 757c25f9..848f38f6 100644 --- a/src/processor/mod.rs +++ b/src/processor/mod.rs @@ -9,7 +9,7 @@ mod init_protocol_fees_vault; mod init_validator_fees_vault; mod protocol_claim_fees; mod top_up_ephemeral_balance; -mod utils; +pub(crate) mod utils; mod validator_claim_fees; mod whitelist_validator_for_program; diff --git a/src/v2/mod.rs b/src/v2/mod.rs new file mode 100644 index 00000000..c0725635 --- /dev/null +++ b/src/v2/mod.rs @@ -0,0 +1,3 @@ +mod processor; + +pub use processor::*; diff --git a/src/v2/processor/bootstrap/init_protocol_config.rs b/src/v2/processor/bootstrap/init_protocol_config.rs new file mode 100644 index 00000000..5a9f0e44 --- /dev/null +++ b/src/v2/processor/bootstrap/init_protocol_config.rs @@ -0,0 +1,182 @@ +use dlp_api::{ + compat::Pubkey, + pda::fees_vault_pda, + v2::{ + pda::{PROTOCOL_CONFIG_SEED, VERIFIER_REGISTRY_SEED}, + InitProtocolConfigArgs, InitProtocolConfigArgsView, ProtocolConfig, + VerifierRegistry, + }, +}; +use pinocchio::{ + cpi::{Seed, Signer}, + error::ProgramError, + AccountView, ProgramResult, +}; + +use crate::{ + processor::fast::utils::pda::create_pda, + requires::{require_uninitialized_pda, StandardCtx}, +}; +use wheels::{ + layout::{Decodable, Encodable}, + require_eq, require_le, require_n_accounts, require_ne, require_ne_keys, + require_signer, +}; + +/// Initialize the global protocol config accounts. +/// +/// Accounts: +/// 0: `[signer, writable]` authority that pays rent and controls v2 config +/// 1: `[writable]` ProtocolConfig PDA +/// 2: `[writable]` VerifierRegistry PDA +/// 3: `[]` system program, required by system CPI +#[inline(never)] +pub fn process_init_protocol_config( + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + let [ + authority, // force multi-line + protocol_config, + verifier_registry, + _system_program, + ] = require_n_accounts!(accounts, 4); + + require_signer!(authority); + + let args = InitProtocolConfigArgs::decode(data)?; + + validate_protocol_config_args(&args)?; + + let protocol_config_bump = require_uninitialized_pda( + protocol_config, + &[PROTOCOL_CONFIG_SEED], + &crate::fast::ID, + true, + StandardCtx::new("protocol config"), + )?; + let verifier_registry_bump = require_uninitialized_pda( + verifier_registry, + &[VERIFIER_REGISTRY_SEED], + &crate::fast::ID, + true, + StandardCtx::new("verifier registry"), + )?; + + create_pda( + protocol_config, + &crate::fast::ID, + ProtocolConfig::DATA_LEN, + &[Signer::from(&[ + Seed::from(PROTOCOL_CONFIG_SEED), + Seed::from(&[protocol_config_bump]), + ])], + authority, + )?; + + create_pda( + verifier_registry, + &crate::fast::ID, + VerifierRegistry::MIN_DATA_LEN, + &[Signer::from(&[ + Seed::from(VERIFIER_REGISTRY_SEED), + Seed::from(&[verifier_registry_bump]), + ])], + authority, + )?; + + ProtocolConfig { + discriminator: ProtocolConfig::DISCRIMINATOR, + bump: protocol_config_bump, + authority: authority.address().to_bytes().into(), + paused: false, + resolver: *args.resolver(), + protocol_fee_vault: fees_vault_pda(), + min_operator_bond: args.min_operator_bond(), + min_verifier_bond: args.min_verifier_bond(), + min_challenger_stake: args.min_challenger_stake(), + challenge_window_slots: args.challenge_window_slots(), + operator_response_timeout_slots: args.operator_response_timeout_slots(), + challenger_reveal_timeout_slots: args.challenger_reveal_timeout_slots(), + payout_timelock_slots: args.payout_timelock_slots(), + verifiers_per_commitment: args.verifiers_per_commitment(), + approval_threshold: args.approval_threshold(), + max_window_extensions: args.max_window_extensions(), + match_penalty_bps: args.match_penalty_bps(), + } + .encode_to(protocol_config.try_borrow_mut()?.as_mut())?; + + VerifierRegistry { + discriminator: VerifierRegistry::DISCRIMINATOR, + bump: verifier_registry_bump, + next_selection_index: 0, + entries: Vec::new(), + } + .encode_to(verifier_registry.try_borrow_mut()?.as_mut())?; + + Ok(()) +} + +pub(super) fn validate_protocol_config_args( + args: &InitProtocolConfigArgsView<'_>, +) -> ProgramResult { + let default_pubkey = Pubkey::default(); + + require_ne_keys!( + args.resolver(), + &default_pubkey, + ProgramError::InvalidInstructionData + ); + require_ne!( + args.min_operator_bond(), + 0, + ProgramError::InvalidInstructionData + ); + require_ne!( + args.min_verifier_bond(), + 0, + ProgramError::InvalidInstructionData + ); + require_ne!( + args.min_challenger_stake(), + 0, + ProgramError::InvalidInstructionData + ); + require_ne!( + args.challenge_window_slots(), + 0, + ProgramError::InvalidInstructionData + ); + require_ne!( + args.operator_response_timeout_slots(), + 0, + ProgramError::InvalidInstructionData + ); + require_ne!( + args.challenger_reveal_timeout_slots(), + 0, + ProgramError::InvalidInstructionData + ); + require_ne!( + args.payout_timelock_slots(), + 0, + ProgramError::InvalidInstructionData + ); + require_eq!( + args.verifiers_per_commitment(), + 1, + ProgramError::InvalidInstructionData + ); + require_eq!( + args.approval_threshold(), + 1, + ProgramError::InvalidInstructionData + ); + require_le!( + args.match_penalty_bps(), + ProtocolConfig::MAX_MATCH_PENALTY_BPS, + ProgramError::InvalidInstructionData + ); + + Ok(()) +} diff --git a/src/v2/processor/bootstrap/mod.rs b/src/v2/processor/bootstrap/mod.rs new file mode 100644 index 00000000..13cc48ad --- /dev/null +++ b/src/v2/processor/bootstrap/mod.rs @@ -0,0 +1,3 @@ +mod init_protocol_config; + +pub use init_protocol_config::*; diff --git a/src/v2/processor/fraud_proofs/mod.rs b/src/v2/processor/fraud_proofs/mod.rs new file mode 100644 index 00000000..c6d8c16b --- /dev/null +++ b/src/v2/processor/fraud_proofs/mod.rs @@ -0,0 +1 @@ +//! Processors for v2 fraud-proof instructions. diff --git a/src/v2/processor/mod.rs b/src/v2/processor/mod.rs new file mode 100644 index 00000000..666462a0 --- /dev/null +++ b/src/v2/processor/mod.rs @@ -0,0 +1,20 @@ +mod bootstrap; +mod fraud_proofs; + +use dlp_api::v2::DlpV2Instruction; + +use pinocchio::{AccountView, ProgramResult}; + +pub use bootstrap::*; + +pub fn process_instruction( + accounts: &[AccountView], + data: &[u8], + ix: DlpV2Instruction, +) -> ProgramResult { + match ix { + DlpV2Instruction::InitProtocolConfig => { + process_init_protocol_config(accounts, data) + } + } +} diff --git a/tests/test_commit_finalize.rs b/tests/test_commit_finalize.rs index 3f78ecf8..afb17e62 100644 --- a/tests/test_commit_finalize.rs +++ b/tests/test_commit_finalize.rs @@ -33,12 +33,12 @@ mod fixtures; #[tokio::test] async fn test_commit_finalize_data_perf() { - run_test_commit_finalize(vec![0; 10240], vec![1; 10240], false, 1400).await; + run_test_commit_finalize(vec![0; 10240], vec![1; 10240], false, 1450).await; } #[tokio::test] async fn test_commit_finalize_diff_perf() { - run_test_commit_finalize(vec![0; 10240], vec![1; 10240], true, 1650).await; + run_test_commit_finalize(vec![0; 10240], vec![1; 10240], true, 1700).await; } async fn run_test_commit_finalize( diff --git a/tests/test_commit_finalize_from_buffer.rs b/tests/test_commit_finalize_from_buffer.rs index 5c861190..329e908f 100644 --- a/tests/test_commit_finalize_from_buffer.rs +++ b/tests/test_commit_finalize_from_buffer.rs @@ -68,7 +68,7 @@ async fn test_commit_finalize_from_buffer_perf() { let metadata = metadata.unwrap(); - assertables::assert_lt!(metadata.compute_units_consumed, 1450); + assertables::assert_lt!(metadata.compute_units_consumed, 1480); assert_eq!( metadata.log_messages.len(), diff --git a/tests/test_v2_init_protocol_config.rs b/tests/test_v2_init_protocol_config.rs new file mode 100644 index 00000000..10787960 --- /dev/null +++ b/tests/test_v2_init_protocol_config.rs @@ -0,0 +1,291 @@ +use dlp::solana_program; +use dlp_api::{ + pda::fees_vault_pda, + v2::{ + instruction_builder::init_protocol_config, + pda::{ + protocol_config_pda, verifier_registry_pda, PROTOCOL_CONFIG_SEED, + VERIFIER_REGISTRY_SEED, + }, + InitProtocolConfigArgs, ProtocolConfig, VerifierRegistry, + }, +}; +use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL}; +use solana_program_test::{BanksClient, ProgramTest}; +use solana_sdk::{ + account::Account, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use solana_sdk_ids::system_program; +use wheels::layout::Decodable; + +mod fixtures; + +#[tokio::test] +async fn test_init_protocol_config() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + + let args = valid_protocol_config_args(); + let ix = init_protocol_config(authority.pubkey(), args.clone()); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &authority], + blockhash, + ); + + banks.process_transaction(tx).await.unwrap(); + + let protocol_config_account = banks + .get_account(protocol_config_pda()) + .await + .unwrap() + .unwrap(); + let protocol_config = + ProtocolConfig::decode(&protocol_config_account.data).unwrap(); + let (_, expected_protocol_config_bump) = + Pubkey::find_program_address(&[PROTOCOL_CONFIG_SEED], &dlp_api::id()); + + assert_eq!( + protocol_config.discriminator(), + ProtocolConfig::DISCRIMINATOR + ); + assert_eq!(protocol_config.bump(), expected_protocol_config_bump); + assert_eq!(*protocol_config.authority(), authority.pubkey()); + assert!(!protocol_config.paused()); + assert_eq!(*protocol_config.resolver(), args.resolver); + assert_eq!(*protocol_config.protocol_fee_vault(), fees_vault_pda()); + assert_eq!(protocol_config.min_operator_bond(), args.min_operator_bond); + assert_eq!(protocol_config.min_verifier_bond(), args.min_verifier_bond); + assert_eq!( + protocol_config.min_challenger_stake(), + args.min_challenger_stake + ); + assert_eq!( + protocol_config.challenge_window_slots(), + args.challenge_window_slots + ); + assert_eq!( + protocol_config.operator_response_timeout_slots(), + args.operator_response_timeout_slots + ); + assert_eq!( + protocol_config.challenger_reveal_timeout_slots(), + args.challenger_reveal_timeout_slots + ); + assert_eq!( + protocol_config.payout_timelock_slots(), + args.payout_timelock_slots + ); + assert_eq!( + protocol_config.verifiers_per_commitment(), + args.verifiers_per_commitment + ); + assert_eq!( + protocol_config.approval_threshold(), + args.approval_threshold + ); + assert_eq!( + protocol_config.max_window_extensions(), + args.max_window_extensions + ); + assert_eq!(protocol_config.match_penalty_bps(), args.match_penalty_bps); + + let verifier_registry_account = banks + .get_account(verifier_registry_pda()) + .await + .unwrap() + .unwrap(); + let verifier_registry = + VerifierRegistry::decode(&verifier_registry_account.data).unwrap(); + let (_, expected_verifier_registry_bump) = + Pubkey::find_program_address(&[VERIFIER_REGISTRY_SEED], &dlp_api::id()); + + assert_eq!( + verifier_registry.discriminator(), + VerifierRegistry::DISCRIMINATOR + ); + assert_eq!(verifier_registry.bump(), expected_verifier_registry_bump); + assert_eq!(verifier_registry.next_selection_index(), 0); + assert!(verifier_registry.entries().is_empty()); +} + +#[tokio::test] +async fn test_init_protocol_config_fails_twice() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + + let ix = + init_protocol_config(authority.pubkey(), valid_protocol_config_args()); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &authority], + blockhash, + ); + banks.process_transaction(tx).await.unwrap(); + + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = + init_protocol_config(authority.pubkey(), valid_protocol_config_args()); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &authority], + blockhash, + ); + + assert!(banks.process_transaction(tx).await.is_err()); +} + +#[tokio::test] +async fn test_init_protocol_config_fails_with_wrong_protocol_config_pda() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + + let mut ix = + init_protocol_config(authority.pubkey(), valid_protocol_config_args()); + + ix.accounts[1].pubkey = Pubkey::new_unique(); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &authority], + blockhash, + ); + + assert!(banks.process_transaction(tx).await.is_err()); +} + +#[tokio::test] +async fn test_init_protocol_config_fails_with_wrong_verifier_registry_pda() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + + let mut ix = + init_protocol_config(authority.pubkey(), valid_protocol_config_args()); + + ix.accounts[2].pubkey = Pubkey::new_unique(); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &authority], + blockhash, + ); + + assert!(banks.process_transaction(tx).await.is_err()); +} + +#[tokio::test] +async fn test_init_protocol_config_fails_without_authority_signature() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + + let mut ix = + init_protocol_config(authority.pubkey(), valid_protocol_config_args()); + + ix.accounts[0].is_signer = false; + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer], + blockhash, + ); + + assert!(banks.process_transaction(tx).await.is_err()); +} + +#[tokio::test] +async fn test_init_protocol_config_fails_with_invalid_args() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + + let mut args = valid_protocol_config_args(); + + args.approval_threshold = args.verifiers_per_commitment + 1; + + let ix = init_protocol_config(authority.pubkey(), args); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &authority], + blockhash, + ); + + assert!(banks.process_transaction(tx).await.is_err()); +} + +#[tokio::test] +async fn test_init_protocol_config_fails_with_zero_verifier_cap() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + + let mut args = valid_protocol_config_args(); + + args.verifiers_per_commitment = 0; + + let ix = init_protocol_config(authority.pubkey(), args); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &authority], + blockhash, + ); + + assert!(banks.process_transaction(tx).await.is_err()); +} + +#[tokio::test] +async fn test_init_protocol_config_fails_with_zero_approval_threshold() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + + let mut args = valid_protocol_config_args(); + + args.approval_threshold = 0; + + let ix = init_protocol_config(authority.pubkey(), args); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &authority], + blockhash, + ); + + assert!(banks.process_transaction(tx).await.is_err()); +} + +fn valid_protocol_config_args() -> InitProtocolConfigArgs { + InitProtocolConfigArgs { + resolver: Pubkey::new_unique(), + min_operator_bond: 1, + min_verifier_bond: 1, + min_challenger_stake: 1, + challenge_window_slots: 10, + operator_response_timeout_slots: 10, + challenger_reveal_timeout_slots: 10, + payout_timelock_slots: 10, + verifiers_per_commitment: 1, + approval_threshold: 1, + max_window_extensions: 1, + match_penalty_bps: 500, + } +} + +async fn setup_program_test_env() -> (BanksClient, Keypair, Keypair, Hash) { + let mut program_test = ProgramTest::new("dlp", dlp_api::ID, None); + program_test.prefer_bpf(true); + + let authority = Keypair::new(); + program_test.add_account( + authority.pubkey(), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (banks, payer, blockhash) = program_test.start().await; + (banks, payer, authority, blockhash) +}