From 02c89b7f0332990afe01031e7cf1d542f5d93173 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Mon, 26 Jan 2026 18:28:15 +0530 Subject: [PATCH] feat: Impl V2 Delegate --- src/lib.rs | 9 + src/processor/fast/mod.rs | 2 +- src/processor/mod.rs | 2 +- src/v2/instruction.rs | 112 +++++ src/v2/mod.rs | 9 + src/v2/processor/delegate.rs | 20 + .../processor/internal/delegate_internal.rs | 224 +++++++++ src/v2/processor/internal/mod.rs | 3 + src/v2/processor/mod.rs | 4 + src/v2/requires.rs | 439 ++++++++++++++++++ src/v2/state/delegation_state.rs | 84 ++++ src/v2/state/mod.rs | 5 + src/v2/state/types.rs | 38 ++ 13 files changed, 949 insertions(+), 2 deletions(-) create mode 100644 src/v2/instruction.rs create mode 100644 src/v2/mod.rs create mode 100644 src/v2/processor/delegate.rs create mode 100644 src/v2/processor/internal/delegate_internal.rs create mode 100644 src/v2/processor/internal/mod.rs create mode 100644 src/v2/processor/mod.rs create mode 100644 src/v2/requires.rs create mode 100644 src/v2/state/delegation_state.rs create mode 100644 src/v2/state/mod.rs create mode 100644 src/v2/state/types.rs diff --git a/src/lib.rs b/src/lib.rs index 3e94e488..57a1ce99 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,8 @@ pub mod state; mod account_size_class; +mod v2; + pub use account_size_class::*; #[cfg(not(feature = "sdk"))] @@ -79,6 +81,8 @@ pub fn fast_process_instruction( accounts: &[pinocchio::AccountView], data: &[u8], ) -> Option { + use crate::v2::DlpInstruction; + if data.len() < 8 { return Some(Err( pinocchio::error::ProgramError::InvalidInstructionData, @@ -87,6 +91,11 @@ pub fn fast_process_instruction( let (discriminator_bytes, data) = data.split_at(8); + if discriminator_bytes[0] >= DlpInstruction::Delegate as u8 { + use crate::v2::v2_process_instruction; + return Some(v2_process_instruction(accounts, data)); + } + let discriminator = match DlpDiscriminator::try_from(discriminator_bytes[0]) { Ok(discriminator) => discriminator, diff --git a/src/processor/fast/mod.rs b/src/processor/fast/mod.rs index 8413fdcb..0e578d09 100644 --- a/src/processor/fast/mod.rs +++ b/src/processor/fast/mod.rs @@ -8,7 +8,7 @@ mod delegate; mod finalize; mod undelegate; mod undelegate_confined_account; -mod utils; +pub mod utils; pub(crate) mod internal; diff --git a/src/processor/mod.rs b/src/processor/mod.rs index 08be1d70..55e74a89 100644 --- a/src/processor/mod.rs +++ b/src/processor/mod.rs @@ -7,7 +7,7 @@ mod init_protocol_fees_vault; mod init_validator_fees_vault; mod protocol_claim_fees; mod top_up_ephemeral_balance; -mod utils; +pub mod utils; mod validator_claim_fees; mod whitelist_validator_for_program; diff --git a/src/v2/instruction.rs b/src/v2/instruction.rs new file mode 100644 index 00000000..ed83a59b --- /dev/null +++ b/src/v2/instruction.rs @@ -0,0 +1,112 @@ +use num_enum::TryFromPrimitive; +use pinocchio::{error::ProgramError, ProgramResult}; +use strum::IntoStaticStr; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive, IntoStaticStr)] +#[rustfmt::skip] +pub enum DlpInstruction { + /// + /// Delegate group: [101, 110] => 10 slots + /// + Delegate = 101, + DelegateWithAnyValidator = 102, + + /// + /// Commit group: [111, 120] => 10 slots + /// + Commit = 111, + CommitFromBuffer = 112, + CommitFinalize = 113, + CommitFinalizeFromBuffer = 114, + + /// + /// Finalize group: [121, 130] => 10 slots + /// + Finalize = 121, + + /// + /// Undelegate group: [131, 140] => 10 slots + /// + Undelegate = 131, + UndelegateConfinedAccount = 132, + + /// + /// User group: [141, 150] => 10 slots + /// + CallHandler = 141, + + /// + /// Vaults group: [151, 160] => 10 slots + /// + InitProtocolFeesVault = 151, + ProtocolClaimFees = 152, + InitValidatorFeesVault = 153, + ValidatorClaimFees = 154, + CloseValidatorFeesVault = 155, + + /// + /// Misc group: [161, 180] => 20 slots + /// + WhitelistValidatorForProgram = 161, + TopUpEphemeralBalance = 162, + DelegateEphemeralBalance = 163, + CloseEphemeralBalance = 164, +} + +impl DlpInstruction { + pub fn to_vec(self) -> Vec { + let num = self as u64; + num.to_le_bytes().to_vec() + } + + pub fn name(&self) -> &'static str { + self.into() + } +} + +pub fn v2_process_instruction( + accounts: &[pinocchio::AccountView], + data: &[u8], +) -> ProgramResult { + let (ix, data) = data.split_at(8); + + let ix = match DlpInstruction::try_from(ix[0]) { + Ok(discriminator) => discriminator, + Err(_) => { + pinocchio_log::log!("Failed to read and parse discriminator"); + return Err(pinocchio::error::ProgramError::InvalidInstructionData); + } + }; + + use super::processor::*; + + let coming_soon = || { + solana_program::msg!("Instruction {:#?} not yet implemented", ix); + return Err(ProgramError::InvalidInstructionData); + }; + + match ix { + DlpInstruction::Delegate => process_delegate(accounts, data), + DlpInstruction::DelegateWithAnyValidator => { + process_delegate_with_any_validator(accounts, data) + } + DlpInstruction::Commit => coming_soon(), + DlpInstruction::CommitFromBuffer => coming_soon(), + DlpInstruction::CommitFinalize => coming_soon(), + DlpInstruction::CommitFinalizeFromBuffer => coming_soon(), + DlpInstruction::Finalize => coming_soon(), + DlpInstruction::Undelegate => coming_soon(), + DlpInstruction::UndelegateConfinedAccount => coming_soon(), + DlpInstruction::CallHandler => coming_soon(), + DlpInstruction::InitProtocolFeesVault => coming_soon(), + DlpInstruction::ProtocolClaimFees => coming_soon(), + DlpInstruction::InitValidatorFeesVault => coming_soon(), + DlpInstruction::ValidatorClaimFees => coming_soon(), + DlpInstruction::CloseValidatorFeesVault => coming_soon(), + DlpInstruction::WhitelistValidatorForProgram => coming_soon(), + DlpInstruction::TopUpEphemeralBalance => coming_soon(), + DlpInstruction::DelegateEphemeralBalance => coming_soon(), + DlpInstruction::CloseEphemeralBalance => coming_soon(), + } +} diff --git a/src/v2/mod.rs b/src/v2/mod.rs new file mode 100644 index 00000000..b3db3b51 --- /dev/null +++ b/src/v2/mod.rs @@ -0,0 +1,9 @@ +mod instruction; +mod processor; +mod requires; +mod state; + +pub use instruction::*; +pub use processor::*; +pub use requires::*; +pub use state::*; diff --git a/src/v2/processor/delegate.rs b/src/v2/processor/delegate.rs new file mode 100644 index 00000000..ac97366d --- /dev/null +++ b/src/v2/processor/delegate.rs @@ -0,0 +1,20 @@ +use pinocchio::{AccountView, ProgramResult}; + +use crate::v2::processor::internal::process_delegate_internal; + +pub fn process_delegate( + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + process_delegate_internal::(accounts, data) +} + +/// +/// delegates an account while allowing any validator identity +/// +pub fn process_delegate_with_any_validator( + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + process_delegate_internal::(accounts, data) +} diff --git a/src/v2/processor/internal/delegate_internal.rs b/src/v2/processor/internal/delegate_internal.rs new file mode 100644 index 00000000..fd9b91ce --- /dev/null +++ b/src/v2/processor/internal/delegate_internal.rs @@ -0,0 +1,224 @@ +use borsh::BorshDeserialize; +use bytemuck::{Pod, Zeroable}; +use pinocchio::{ + address::address_eq, + cpi::{Seed, Signer}, + error::ProgramError, + sysvars::{clock::Clock, Sysvar}, + AccountView, Address, ProgramResult, +}; +use pinocchio_log::log; +use pinocchio_system::instructions as system; +use solana_address::PDA_MARKER; + +use crate::{ + args::ArgsWithBuffer, + consts::RENT_EXCEPTION_ZERO_BYTES_LAMPORTS, + error::DlpError, + pda, + pod_view::PodView, + processor::{fast::utils::pda::create_pda, utils::curve::is_on_curve_fast}, + v2::{DelegationState, DelegationStateHeader, ValidatedDelegationBindings}, + v2_require, v2_require_eq_keys, v2_require_n_accounts, v2_require_owned_by, + v2_require_pda_fast, v2_require_signer, v2_require_uninitialized_pda, +}; + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct DelegateArgsHeader { + /// The frequency at which the validator should commit the account data + /// if no commit is triggered by the owning program + pub commit_frequency_ms: u32, + /// The seeds used to derive the PDA of the delegated account + /// The validator authority that is added to the delegation record + pub validator: Address, + + delegate_buffer_bump: u8, + delegation_state_bump: u8, + + reserved_padding0: [u8; 2], + //pub seeds: Vec>, +} + +// buffer contains the seeds +type DelegateArgs<'a> = ArgsWithBuffer<'a, DelegateArgsHeader>; + +pub(crate) fn process_delegate_internal< + const ALLOW_SYSTEM_PROGRAM_VALIDATOR: bool, +>( + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + let [ + payer, // force multi-line + delegated_account, + owner_program, + delegate_buffer_account, + delegation_state, + _system_program + ] = v2_require_n_accounts!(accounts, 6); + + v2_require_owned_by!(delegated_account, &crate::fast::ID); + + // Check that payer and delegated_account are signers, this ensures the instruction is being called from CPI + v2_require_signer!(payer); + v2_require_signer!(delegated_account); + + let args = DelegateArgs::from_bytes(data)?; + + // Check that the buffer PDA is initialized and derived correctly from the PDA + v2_require_pda_fast!( + delegate_buffer_account, + &[ + pda::DELEGATE_BUFFER_TAG, + delegated_account.address().as_ref(), + &[args.delegate_buffer_bump], + owner_program.address().as_ref(), + PDA_MARKER, + ], + true + ); + + v2_require_uninitialized_pda!( + delegation_state, + &[ + DelegationStateHeader::SEED, + delegated_account.address().as_ref(), + &[args.delegation_state_bump], + ] + ); + + if !ALLOW_SYSTEM_PROGRAM_VALIDATOR { + if args.validator.to_bytes() == pinocchio_system::ID.to_bytes() { + return Err(DlpError::DelegationToSystemProgramNotAllowed.into()); + } + } + + // Validate seeds if the delegate account is not on curve, i.e. is a PDA + // If the owner is the system program, we check if the account is derived from the delegation program, + // allowing delegation of escrow accounts + if !is_on_curve_fast(delegated_account.address()) { + let program_id = + if address_eq(owner_program.address(), &pinocchio_system::ID) { + &crate::fast::ID + } else { + owner_program.address() + }; + let seeds_to_validate: &[&[u8]] = &[]; + // let seeds_to_validate: &[&[u8]] = match args.seeds.len() { + // 1 => &[&args.seeds[0]], + // 2 => &[&args.seeds[0], &args.seeds[1]], + // 3 => &[&args.seeds[0], &args.seeds[1], &args.seeds[2]], + // 4 => &[ + // &args.seeds[0], + // &args.seeds[1], + // &args.seeds[2], + // &args.seeds[3], + // ], + // 5 => &[ + // &args.seeds[0], + // &args.seeds[1], + // &args.seeds[2], + // &args.seeds[3], + // &args.seeds[4], + // ], + // 6 => &[ + // &args.seeds[0], + // &args.seeds[1], + // &args.seeds[2], + // &args.seeds[3], + // &args.seeds[4], + // &args.seeds[5], + // ], + // 7 => &[ + // &args.seeds[0], + // &args.seeds[1], + // &args.seeds[2], + // &args.seeds[3], + // &args.seeds[4], + // &args.seeds[5], + // &args.seeds[6], + // ], + // 8 => &[ + // &args.seeds[0], + // &args.seeds[1], + // &args.seeds[2], + // &args.seeds[3], + // &args.seeds[4], + // &args.seeds[5], + // &args.seeds[6], + // &args.seeds[7], + // ], + // _ => return Err(DlpError::TooManySeeds.into()), + // }; + let derived_pda = + Address::find_program_address(seeds_to_validate, program_id).0; + + v2_require_eq_keys!( + &derived_pda, + delegated_account.address(), + ProgramError::InvalidSeeds + ); + } + + create_pda( + delegation_state, + &crate::fast::ID, + DelegationStateHeader::SPACE + args.buffer.len(), + &[Signer::from(&[ + Seed::from(DelegationStateHeader::SEED), + Seed::from(delegated_account.address().as_ref()), + Seed::from(&[args.delegation_state_bump]), + ])], + payer, + )?; + + let mut delegation_state_data = delegation_state.try_borrow_mut()?; + //let mut delegation_state_view = + // DelegationState::from_bytes(&mut delegation_state_data)?; + + // Initialize the delegation record + let header = DelegationStateHeader { + discriminator: DelegationStateHeader::DISCRIMINATOR, + original_owner: owner_program.address().to_bytes().into(), + delegation_slot: Clock::get()?.slot, + lamports: delegated_account.lamports(), + commit_frequency_ms: args.commit_frequency_ms as u64, + bindings: ValidatedDelegationBindings { + delegation_account: *delegated_account.address(), + validator_as_authority: args.validator, + // validator_fees_vault: validator_fees_vault + }, + last_commit_id: 0, + rent_payer: payer.address().to_bytes().into(), + is_undelegatable: false.into(), + reserved_padding0: Default::default(), + }; + + header.try_copy_to( + &mut delegation_state_data.as_mut()[..DelegationStateHeader::SPACE], + )?; + + // let delegation_metadata = DelegationMetadata { + // seeds: args.seeds, + // }; + + // Copy the data from the buffer into the original account + if !delegate_buffer_account.is_data_empty() { + let mut delegated_data = delegated_account.try_borrow_mut()?; + let delegate_buffer_data = delegate_buffer_account.try_borrow()?; + (*delegated_data).copy_from_slice(&delegate_buffer_data); + } + + // Make the account rent exempt if it's not + if delegated_account.lamports() == 0 && delegated_account.data_len() == 0 { + system::Transfer { + from: payer, + to: delegated_account, + lamports: RENT_EXCEPTION_ZERO_BYTES_LAMPORTS, + } + .invoke()?; + } + + Ok(()) +} diff --git a/src/v2/processor/internal/mod.rs b/src/v2/processor/internal/mod.rs new file mode 100644 index 00000000..36fe9d04 --- /dev/null +++ b/src/v2/processor/internal/mod.rs @@ -0,0 +1,3 @@ +mod delegate_internal; + +pub(crate) use delegate_internal::*; diff --git a/src/v2/processor/mod.rs b/src/v2/processor/mod.rs new file mode 100644 index 00000000..60af3b17 --- /dev/null +++ b/src/v2/processor/mod.rs @@ -0,0 +1,4 @@ +mod delegate; +mod internal; + +pub use delegate::*; diff --git a/src/v2/requires.rs b/src/v2/requires.rs new file mode 100644 index 00000000..cc487944 --- /dev/null +++ b/src/v2/requires.rs @@ -0,0 +1,439 @@ +use pinocchio::{ + address::{address_eq, Address}, + error::ProgramError, + AccountView, +}; +use pinocchio_log::log; + +use crate::{ + error::DlpError, + pda::{ + self, program_config_from_program_id, + validator_fees_vault_pda_from_validator, + }, +}; + +// require true +#[macro_export] +macro_rules! v2_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! v2_require_signer { + ($info: expr) => {{ + if !$info.is_signer() { + log!("require_signer!({}): ", stringify!($info)); + $info.address().log(); + return Err(ProgramError::MissingRequiredSignature); + } + }}; +} + +// require key1 == key2 +#[macro_export] +macro_rules! v2_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! v2_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! v2_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! v2_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! v2_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! v2_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! v2_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! v2_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! v2_require_owned_by { + ($info: expr, $owner: expr) => {{ + if !address_eq(unsafe { $info.owner() }, $owner) { + pinocchio_log::log!( + "require_owned_pda!({}, {})", + stringify!($info), + stringify!($owner) + ); + $info.address().log(); + $owner.log(); + return Err(ProgramError::InvalidAccountOwner); + } + }}; +} + +#[macro_export] +macro_rules! v2_require_uninitialized_pda { + ($info:expr, $seeds: expr) => {{ + let pda = match pinocchio::Address::create_program_address($seeds, &$crate::fast::ID) { + Ok(pda) => pda, + Err(_) => { + log!( + "require_uninitialized_pda!({}, {}); create_program_address failed", + stringify!($info), + stringify!($seeds) + ); + return Err(ProgramError::InvalidSeeds); + } + }; + if !address_eq($info.address(), &pda) { + log!( + "require_uninitialized_pda!({}, {}); address_eq failed", + stringify!($info), + stringify!($seeds), + ); + $info.address().log(); + return Err(ProgramError::InvalidSeeds); + } + + v2_require_owned_by!($info, &$crate::fast::ID); + + if $info.is_writable() { + log!( + "require_initialized_pda!({}, {}); is_writable expectation failed", + stringify!($info), + stringify!($seeds), + ); + $info.address().log(); + return Err(ProgramError::Immutable); + } + }}; +} + +/// +/// require_initialized_pda( +/// info: &AccountView, +/// seeds: &[&[u8]], +/// program_id: &Address, +/// is_writable: bool, +/// ) -> Result<(), ProgramError> { +/// +#[macro_export] +macro_rules! v2_require_initialized_pda { + ($info:expr, $seeds: expr, $program_id: expr, $is_writable: expr) => {{ + let pda = match pinocchio::Address::create_program_address($seeds, $program_id) { + Ok(pda) => pda, + Err(_) => { + log!( + "require_initialized_pda!({}, {}, {}, {}); create_program_address failed", + stringify!($info), + stringify!($seeds), + stringify!($program_id), + stringify!($is_writable), + ); + return Err(ProgramError::InvalidSeeds); + } + }; + if !address_eq($info.address(), &pda) { + log!( + "require_initialized_pda!({}, {}, {}, {}); address_eq failed", + stringify!($info), + stringify!($seeds), + stringify!($program_id), + stringify!($is_writable) + ); + $info.address().log(); + $program_id.log(); + return Err(ProgramError::InvalidSeeds); + } + + require_owned_by!($info, $program_id); + + if $is_writable && !$info.is_writable() { + log!( + "require_initialized_pda!({}, {}, {}, {}); is_writable expectation failed", + stringify!($info), + stringify!($seeds), + stringify!($program_id), + stringify!($is_writable) + ); + $info.address().log(); + return Err(ProgramError::Immutable); + } + }}; +} + +#[macro_export] +macro_rules! v2_require_initialized_pda_fast { + ($info:expr, $seeds: expr, $is_writable: expr) => {{ + use solana_sha256_hasher::hashv; + let pda = hashv($seeds).to_bytes().into(); + if !address_eq($info.address(), &pda) { + log!( + "require_initialized_pda!({}, {}, {}); address_eq failed", + stringify!($info), + stringify!($seeds), + stringify!($is_writable) + ); + $info.address().log(); + return Err(ProgramError::InvalidSeeds); + } + + require_owned_by!($info, &$crate::fast::ID); + + if $is_writable && !$info.is_writable() { + log!( + "require_initialized_pda!({}, {}, {}); is_writable expectation failed", + stringify!($info), + stringify!($seeds), + stringify!($is_writable) + ); + $info.address().log(); + return Err(ProgramError::Immutable); + } + }}; +} + +#[macro_export] +macro_rules! v2_require_pda { + ($info:expr, $seeds: expr, $program_id: expr, $is_writable: expr) => {{ + let pda = match pinocchio::Address::create_program_address($seeds, $program_id) { + Ok(pda) => pda, + Err(_) => { + log!( + "require_pda!({}, {}, {}, {}); create_program_address failed", + stringify!($info), + stringify!($seeds), + stringify!($program_id), + stringify!($is_writable), + ); + return Err(ProgramError::InvalidSeeds); + } + }; + if !address_eq($info.address(), &pda) { + log!( + "require_pda!({}, {}, {}, {}); address_eq failed", + stringify!($info), + stringify!($seeds), + stringify!($program_id), + stringify!($is_writable) + ); + $info.address().log(); + $program_id.log(); + return Err(ProgramError::InvalidSeeds); + } + + if $is_writable && !$info.is_writable() { + log!( + "require_pda!({}, {}, {}, {}); is_writable expectation failed", + stringify!($info), + stringify!($seeds), + stringify!($program_id), + stringify!($is_writable) + ); + $info.address().log(); + return Err(ProgramError::Immutable); + } + }}; +} + +#[macro_export] +macro_rules! v2_require_pda_fast { + ($info:expr, $seeds: expr, $is_writable: expr) => {{ + use solana_sha256_hasher::hashv; + let pda = hashv($seeds).to_bytes().into(); + if !address_eq($info.address(), &pda) { + log!( + "require_pda!({}, {}, {}); address_eq failed", + stringify!($info), + stringify!($seeds), + stringify!($is_writable) + ); + $info.address().log(); + return Err(ProgramError::InvalidSeeds); + } + + if $is_writable && !$info.is_writable() { + log!( + "require_pda!({}, {}, {}); is_writable expectation failed", + stringify!($info), + stringify!($seeds), + stringify!($is_writable) + ); + $info.address().log(); + return Err(ProgramError::Immutable); + } + }}; +} + +/// pub fn require_program_config( +/// program_config: &AccountView, +/// program: &Address, +/// bump: u8, +/// is_writable: bool, +/// ) -> Result { +#[macro_export] +macro_rules! v2_require_program_config { + ($program_config: expr, $program: expr, $bump: expr, $is_writable: expr) => {{ + $crate::require_pda!( + $program_config, + &[pda::PROGRAM_CONFIG_TAG, $program.as_ref(), &[$bump]], + &$crate::fast::ID, + $is_writable + ); + !address_eq(unsafe { $program_config.owner() }, &pinocchio_system::ID) + }}; +} + +/// pub fn require_program_config( +/// program_config: &AccountView, +/// program: &Address, +/// bump: u8, +/// is_writable: bool, +/// ) -> Result { +#[macro_export] +macro_rules! v2_require_program_config_fast { + ($program_config: expr, $program: expr, $bump: expr, $is_writable: expr) => {{ + $crate::require_pda_fast!( + $program_config, + &[ + pda::PROGRAM_CONFIG_TAG, + $program.as_ref(), + &[$bump], + &$crate::fast::ID.as_ref(), + PDA_MARKER + ], + $is_writable + ); + !address_eq(unsafe { $program_config.owner() }, &pinocchio_system::ID) + }}; +} diff --git a/src/v2/state/delegation_state.rs b/src/v2/state/delegation_state.rs new file mode 100644 index 00000000..20588159 --- /dev/null +++ b/src/v2/state/delegation_state.rs @@ -0,0 +1,84 @@ +use bytemuck::{Pod, Zeroable}; +use pinocchio::Address; + +use crate::{args::Boolean, v2::HeaderWithBuffer}; + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +pub struct DelegationStateHeader { + /// discriminator that identifies this account type + pub discriminator: [u8; 8], + + /// the original owner of the account + pub original_owner: Address, + + /// the slot at which the delegation was created + pub delegation_slot: u64, + + /// the lamports at the time of delegation or from the last state finalization + /// stored as lamports can be received even if the account is delegated + pub lamports: u64, + + /// the state update frequency in milliseconds + pub commit_frequency_ms: u64, + + /// validated, immutable account bindings for this delegation. + pub bindings: ValidatedDelegationBindings, + + /// the last commit-id account had during delegation update + /// Deprecated: The last slot at which the delegation was updated + pub last_commit_id: u64, + + /// The account that paid the rent for the delegation PDAs + pub rent_payer: Address, + + /// Whether the account can be undelegated or not + pub is_undelegatable: Boolean, + + pub reserved_padding0: [u8; 7], + // The seeds of the account, used to reopen it on undelegation + //pub seeds: Vec>, +} + +/// +/// A fixed, authoritative set of accounts bound to a delegation. +/// +/// These bindings are fully validated at delegation time and are immutable +/// for the lifetime of the delegation. After creation, commit and finalize +/// instructions rely on simple key equality checks against these bindings, +/// avoiding repeated expensive PDA derivation and seed/bump validation. +/// +/// Security Model +/// ============== +/// +/// This design relies on the following Solana runtime guarantees: +/// +/// - Account data owned by a program can only be created or modified by that program. +/// - Program-owned account data cannot be forged or mutated by users or other programs. +/// - A `DelegationState` account is considered valid if and only if it is owned by +/// this program and its discriminator matches the expected value. +/// +/// Under these assumptions, the bindings stored here are authoritative and can be +/// safely used for fast-path account validation. +/// +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +pub struct ValidatedDelegationBindings { + pub delegation_account: Address, + //pub validator_fees_vault: Address, + pub validator_as_authority: Address, +} + +pub type DelegationState<'a> = HeaderWithBuffer<'a, DelegationStateHeader>; + +impl DelegationStateHeader { + pub const SEED: &'static [u8] = b"delegation_state"; + + /// + /// v1 means version 1 of this account type. this allows us to evolve without + /// account migration. + /// + pub const DISCRIMINATOR: [u8; 8] = *b"v1.state"; + + pub const DISCRIMINATOR_FAST: u64 = u64::from_le_bytes(Self::DISCRIMINATOR); +} diff --git a/src/v2/state/mod.rs b/src/v2/state/mod.rs new file mode 100644 index 00000000..0fbaa9bf --- /dev/null +++ b/src/v2/state/mod.rs @@ -0,0 +1,5 @@ +mod delegation_state; +mod types; + +pub use delegation_state::*; +pub use types::*; diff --git a/src/v2/state/types.rs b/src/v2/state/types.rs new file mode 100644 index 00000000..edf5cad3 --- /dev/null +++ b/src/v2/state/types.rs @@ -0,0 +1,38 @@ +use std::ops::Deref; + +use pinocchio::error::ProgramError; + +use crate::{pod_view::PodView, v2_require_ge}; + +pub struct HeaderWithBuffer<'a, Header> { + header: &'a Header, + pub buffer: &'a [u8], +} + +impl<'a, Header: PodView> HeaderWithBuffer<'a, Header> { + pub fn from_bytes(input: &'a [u8]) -> Result { + v2_require_ge!( + input.len(), + Header::SPACE, + ProgramError::InvalidInstructionData + ); + + let (header_bytes, buffer) = input.split_at(Header::SPACE); + + Ok(Self { + header: Header::try_view_from(header_bytes)?, + buffer, + }) + } + + pub fn space(&self) -> usize { + Header::SPACE + self.buffer.len() + } +} + +impl
Deref for HeaderWithBuffer<'_, Header> { + type Target = Header; + fn deref(&self) -> &Self::Target { + self.header + } +}