Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/args/call_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use borsh::{BorshDeserialize, BorshSerialize};

#[derive(BorshSerialize, BorshDeserialize, Clone, Copy)]
pub enum Context {
Commit,
Undelegate,
Standalone,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct CallHandlerArgs {
pub escrow_index: u8,
pub data: Vec<u8>,
pub context: Context,
}
10 changes: 6 additions & 4 deletions src/args/commit_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ use borsh::{BorshDeserialize, BorshSerialize};

#[derive(Default, Debug, BorshSerialize, BorshDeserialize)]
pub struct CommitStateArgs {
/// The ephemeral slot at which the account data is committed
pub slot: u64,
/// "Nonce" of an account. Updates are submitted historically and nonce incremented by 1
/// Deprecated: The ephemeral slot at which the account data is committed
Comment thread
taco-paco marked this conversation as resolved.
pub nonce: u64,
/// The lamports that the account holds in the ephemeral validator
pub lamports: u64,
/// Whether the account can be undelegated after the commit completes
Expand All @@ -14,8 +15,9 @@ pub struct CommitStateArgs {

#[derive(Default, Debug, BorshSerialize, BorshDeserialize)]
pub struct CommitStateFromBufferArgs {
/// The ephemeral slot at which the account data is committed
pub slot: u64,
/// "Nonce" of an account. Updates are submitted historically and nonce incremented by 1
/// Deprecated: The ephemeral slot at which the account data is committed
pub nonce: u64,
/// The lamports that the account holds in the ephemeral validator
pub lamports: u64,
/// Whether the account can be undelegated after the commit completes
Expand Down
2 changes: 2 additions & 0 deletions src/args/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
mod call_handler;
mod commit_state;
mod delegate;
mod delegate_ephemeral_balance;
mod top_up_ephemeral_balance;
mod validator_claim_fees;
mod whitelist_validator_for_program;

pub use call_handler::*;
pub use commit_state::*;
pub use delegate::*;
pub use delegate_ephemeral_balance::*;
Expand Down
4 changes: 4 additions & 0 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@ pub const PROTOCOL_FEES_PERCENTAGE: u8 = 10;
/// The discriminator for the external undelegate instruction.
pub const EXTERNAL_UNDELEGATE_DISCRIMINATOR: [u8; 8] = [196, 28, 41, 206, 48, 37, 51, 167];

/// The discriminator for the external hook after finalization is complete
/// For anchor: corresponds to function/instruction name delegation_program_call_handler
pub const EXTERNAL_CALL_HANDLER_DISCRIMINATOR: [u8; 8] = [157, 197, 228, 30, 0, 80, 121, 135];
Comment thread
GabrielePicco marked this conversation as resolved.

/// The program ID of the delegation program.
pub const DELEGATION_PROGRAM_ID: Pubkey = crate::id();
3 changes: 3 additions & 0 deletions src/discriminator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub enum DlpDiscriminator {
CommitStateFromBuffer = 13,
/// See [crate::processor::process_close_validator_fees_vault] for docs.
CloseValidatorFeesVault = 14,
/// See [crate::processor::process_call_handler] for docs.
CallHandler = 15,
}

impl DlpDiscriminator {
Expand Down Expand Up @@ -60,6 +62,7 @@ impl TryFrom<[u8; 8]> for DlpDiscriminator {
0xc => Ok(DlpDiscriminator::ProtocolClaimFees),
0xd => Ok(DlpDiscriminator::CommitStateFromBuffer),
0xe => Ok(DlpDiscriminator::CloseValidatorFeesVault),
0xf => Ok(DlpDiscriminator::CallHandler),
_ => Err(ProgramError::InvalidInstructionData),
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub enum DlpError {
InvalidWhitelistProgramConfig = 10,
#[error("Account already undelegated")]
AlreadyUndelegated = 11,
#[error("Committed state slot is outdated")]
#[error("Commit is out of order")]
OutdatedSlot = 12,
#[error("Computation overflow detected")]
Overflow = 13,
Expand Down
40 changes: 40 additions & 0 deletions src/instruction_builder/call_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use crate::args::CallHandlerArgs;
use crate::discriminator::DlpDiscriminator;
use crate::pda::{ephemeral_balance_pda_from_payer, validator_fees_vault_pda_from_validator};
use borsh::to_vec;
use solana_program::instruction::Instruction;
use solana_program::{instruction::AccountMeta, pubkey::Pubkey};

/// Builds a call handler instruction.
/// See [crate::processor::call_handler] for docs.
pub fn call_handler(
validator: Pubkey,
destination_program: Pubkey,
escrow_authority: Pubkey,
other_accounts: Vec<AccountMeta>,
args: CallHandlerArgs,
) -> Instruction {
let validator_fees_vault_pda = validator_fees_vault_pda_from_validator(&validator);

// handler accounts
let escrow_account = ephemeral_balance_pda_from_payer(&escrow_authority, args.escrow_index);
let mut accounts = vec![
AccountMeta::new(validator, true),
AccountMeta::new(validator_fees_vault_pda, false),
AccountMeta::new_readonly(destination_program, false),
AccountMeta::new(escrow_authority, false),
AccountMeta::new(escrow_account, false),
];
// append other accounts at the end
accounts.extend(other_accounts);

Instruction {
program_id: crate::id(),
accounts,
data: [
DlpDiscriminator::CallHandler.to_vec(),
to_vec(&args).unwrap(),
]
.concat(),
}
}
5 changes: 3 additions & 2 deletions src/instruction_builder/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
mod call_handler;
mod close_ephemeral_balance;
mod commit_state;

mod close_validator_fees_vault;
mod commit_state;
mod commit_state_from_buffer;
mod delegate;
mod delegate_ephemeral_balance;
Expand All @@ -14,6 +14,7 @@ mod undelegate;
mod validator_claim_fees;
mod whitelist_validator_for_program;

pub use call_handler::*;
pub use close_ephemeral_balance::*;
pub use close_validator_fees_vault::*;
pub use commit_state::*;
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![allow(unexpected_cfgs)] // silence clippy for target_os solana and other solana program custom features

use crate::processor::process_call_handler;
use solana_program::{
account_info::AccountInfo, declare_id, entrypoint::ProgramResult, msg,
program_error::ProgramError, pubkey::Pubkey,
Expand Down Expand Up @@ -93,6 +94,9 @@ pub fn process_instruction(
discriminator::DlpDiscriminator::CloseValidatorFeesVault => {
processor::process_close_validator_fees_vault(program_id, accounts, data)?
}
discriminator::DlpDiscriminator::CallHandler => {
process_call_handler(program_id, accounts, data)?
}
}
Ok(())
}
126 changes: 126 additions & 0 deletions src/processor/call_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use crate::args::CallHandlerArgs;
use crate::consts::EXTERNAL_CALL_HANDLER_DISCRIMINATOR;
use crate::ephemeral_balance_seeds_from_payer;
use crate::processor::utils::loaders::{
load_initialized_validator_fees_vault, load_owned_pda, load_pda, load_signer,
};

use borsh::BorshDeserialize;
use solana_program::account_info::AccountInfo;
use solana_program::entrypoint::ProgramResult;
use solana_program::instruction::{AccountMeta, Instruction};
use solana_program::program::invoke_signed;
use solana_program::program_error::ProgramError;
use solana_program::pubkey::Pubkey;
use solana_program::{msg, system_program};

pub const INVALID_ESCROW_PDA: &str = "invalid escrow pda in CallHandler";
pub const INVALID_ESCROW_OWNER: &str = "escrow can not be delegated in CallHandler";

/// Calls a handler on user specified program
///
/// Accounts:
/// 0: `[signer]` validator
/// 1: `[]` validator fee vault to verify its registration
/// 2: `[]` destination program of an action
/// 3: `[]` escrow authority account which created escrow account
/// 4: `[writable]` non delegated escrow pda created from 3
/// 5: `[readonly/writable]` other accounts needed for action
/// 6: `[readonly/writable]` other accounts needed for action
/// 7: ...
///
/// Requirements:
///
/// - escrow account initialized
/// - escrow account not delegated
/// - validator as a caller
///
/// Steps:
/// 1. Verify that signer is a valid registered validator
/// 2. Verify escrow pda exists and not delegated
/// 3. Invoke signed on behalf of escrow pda user specified action
///
/// Usage:
///
/// This instruction is meant to be called via CPI with the owning program signing for the
/// delegated account.
pub fn process_call_handler(
_program_id: &Pubkey,
accounts: &[AccountInfo],
data: &[u8],
) -> ProgramResult {
const OTHER_ACCOUNTS_OFFSET: usize = 5;

if accounts.len() < OTHER_ACCOUNTS_OFFSET {
return Err(ProgramError::NotEnoughAccountKeys);
}

let (
[validator, validator_fees_vault, destination_program, escrow_authority_account, escrow_account],
other_accounts,
) = accounts.split_at(OTHER_ACCOUNTS_OFFSET)
else {
return Err(ProgramError::NotEnoughAccountKeys);
};

let args = CallHandlerArgs::try_from_slice(data)?;

// verify account is a signer
load_signer(validator, "validator")?;
// verify signer is a registered validator
load_initialized_validator_fees_vault(validator, validator_fees_vault, true)?;
// Check if destination program is executable
if !destination_program.executable {
msg!(
"{} program is not executable: destination program",
destination_program.key
);
return Err(ProgramError::InvalidAccountData);
}

// verify passed escrow_account derived from escrow authority
let escrow_seeds: &[&[u8]] =
ephemeral_balance_seeds_from_payer!(escrow_authority_account.key, args.escrow_index);
let escrow_bump = load_pda(
escrow_account,
escrow_seeds,
&crate::id(),
true,
INVALID_ESCROW_PDA,
)?;
load_owned_pda(escrow_account, &system_program::id(), INVALID_ESCROW_OWNER)?;

// deduce necessary accounts for CPI
let (accounts_meta, handler_accounts): (Vec<AccountMeta>, Vec<AccountInfo>) =
[escrow_authority_account, escrow_account]
.into_iter()
.chain(other_accounts)
.filter(|account| account.key != validator.key)
.map(|account| {
(
// We enable only escrow to be a signer
AccountMeta {
pubkey: *account.key,
is_writable: account.is_writable,
is_signer: account.key == escrow_account.key,
},
account.clone(),
)
})
.collect();

let data = [EXTERNAL_CALL_HANDLER_DISCRIMINATOR.to_vec(), data.to_vec()].concat();
let handler_instruction = Instruction {
program_id: *destination_program.key,
data,
accounts: accounts_meta,
};
let bump_slice = &[escrow_bump];
let escrow_signer_seeds = [escrow_seeds, &[bump_slice]].concat();

invoke_signed(
&handler_instruction,
&handler_accounts,
&[&escrow_signer_seeds],
)
}
23 changes: 10 additions & 13 deletions src/processor/commit_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pub fn process_commit_state(

let commit_state_bytes: &[u8] = args.data.as_ref();
let commit_record_lamports = args.lamports;
let commit_record_slot = args.slot;
let commit_record_nonce = args.nonce;
let allow_undelegation = args.allow_undelegation;

let [validator, delegated_account, commit_state_account, commit_record_account, delegation_record_account, delegation_metadata_account, validator_fees_vault, program_config_account, system_program] =
Expand All @@ -68,7 +68,7 @@ pub fn process_commit_state(
let commit_args = CommitStateInternalArgs {
commit_state_bytes,
commit_record_lamports,
commit_record_slot,
commit_record_nonce,
allow_undelegation,
validator,
delegated_account,
Expand All @@ -88,7 +88,7 @@ pub fn process_commit_state(
pub(crate) struct CommitStateInternalArgs<'a, 'info> {
pub(crate) commit_state_bytes: &'a [u8],
pub(crate) commit_record_lamports: u64,
pub(crate) commit_record_slot: u64,
pub(crate) commit_record_nonce: u64,
pub(crate) allow_undelegation: bool,
pub(crate) validator: &'a AccountInfo<'info>,
pub(crate) delegated_account: &'a AccountInfo<'info>,
Expand Down Expand Up @@ -126,17 +126,14 @@ pub(crate) fn process_commit_state_internal(
let mut delegation_metadata =
DelegationMetadata::try_from_bytes_with_discriminator(&delegation_metadata_data)?;

// If the commit slot is greater or equal than the last update slot, we can proceed.
// If the slot is less, we simply do not commit.
// Since commit instructions are typically bundled, we return without error
// so that correct commits are executed.
if args.commit_record_slot < delegation_metadata.last_update_external_slot {
// To preserve correct history of account updates we require sequential commits
if args.commit_record_nonce != delegation_metadata.last_update_nonce + 1 {
msg!(
"Slot {} is outdated, previous slot is {}. Skipping commit",
args.commit_record_slot,
delegation_metadata.last_update_external_slot
"Slot {} is outdated, previous slot is {}. Rejecting commit",
args.commit_record_nonce,
delegation_metadata.last_update_nonce
);
return Ok(());
return Err(DlpError::OutdatedSlot.into());
Comment thread
GabrielePicco marked this conversation as resolved.
}

// Once the account is marked as undelegatable, any subsequent commit should fail
Expand Down Expand Up @@ -250,7 +247,7 @@ pub(crate) fn process_commit_state_internal(
let commit_record = CommitRecord {
identity: *args.validator.key,
account: *args.delegated_account.key,
slot: args.commit_record_slot,
nonce: args.commit_record_nonce,
lamports: args.commit_record_lamports,
};
let mut commit_record_data = args.commit_record_account.try_borrow_mut_data()?;
Expand Down
4 changes: 2 additions & 2 deletions src/processor/commit_state_from_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub fn process_commit_state_from_buffer(
let args = CommitStateFromBufferArgs::try_from_slice(data)?;

let commit_record_lamports = args.lamports;
let commit_record_slot = args.slot;
let commit_record_nonce = args.nonce;
let allow_undelegation = args.allow_undelegation;

let [validator, delegated_account, commit_state_account, commit_record_account, delegation_record_account, delegation_metadata_account, state_buffer_account, validator_fees_vault, program_config_account, system_program] =
Expand All @@ -60,7 +60,7 @@ pub fn process_commit_state_from_buffer(
let commit_args = CommitStateInternalArgs {
commit_state_bytes,
commit_record_lamports,
commit_record_slot,
commit_record_nonce,
allow_undelegation,
validator,
delegated_account,
Expand Down
2 changes: 1 addition & 1 deletion src/processor/delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ pub fn process_delegate(
let mut delegation_metadata_bytes = vec![];
let delegation_metadata = DelegationMetadata {
seeds: args.seeds,
last_update_external_slot: 0,
last_update_nonce: 0,
is_undelegatable: false,
rent_payer: *payer.key,
};
Expand Down
2 changes: 1 addition & 1 deletion src/processor/finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ pub fn process_finalize(
)?;

// Update the delegation metadata
delegation_metadata.last_update_external_slot = commit_record.slot;
delegation_metadata.last_update_nonce = commit_record.nonce;
delegation_metadata.to_bytes_with_discriminator(&mut delegation_metadata_data.as_mut())?;

// Update the delegation record
Expand Down
Loading