diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f0edc54b..939bef51 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,45 +1,8 @@ + -> ⚠️ NOTE: Use notes like this to emphasize something important about the PR. -> -> This could include other PRs this PR is built on top of; API breaking changes; reasons for why the PR is on hold; or anything else you would like to draw attention to. + -| Status | Type | ⚠️ Core Change | Issue | -| :---: | :---: | :---: | :--: | -| Ready/Hold | Feature/Bug/Tooling/Refactor/Hotfix | Yes/No | [Link]() | - -## Problem - -_What problem are you trying to solve?_ - - -## Solution - -_How did you solve the problem?_ - - -## Before & After Screenshots - -_Insert screenshots of example code output_ - -**BEFORE**: -[insert screenshot here] - -**AFTER**: -[insert screenshot here] - - -## Other changes (e.g. bug fixes, small refactors) - - -## Deploy Notes - -_Notes regarding deployment of the contained body of work. These should note any -new dependencies, new scripts, etc._ - -**New scripts**: - -- `script` : script details - -**New dependencies**: - -- `dependency` : dependency details \ No newline at end of file +Closes # diff --git a/interface/src/args/mod.rs b/interface/src/args/mod.rs index 66d1b7b3..9eb03e75 100644 --- a/interface/src/args/mod.rs +++ b/interface/src/args/mod.rs @@ -11,6 +11,7 @@ mod raise_challenge; mod register_operator; mod register_verifier; mod resolve_dispute; +mod undelegate; mod update_delegation_cluster; mod update_verifier_registry; mod write_state_buffer; @@ -25,6 +26,7 @@ pub use raise_challenge::*; pub use register_operator::*; pub use register_verifier::*; pub use resolve_dispute::*; +pub use undelegate::*; pub use update_delegation_cluster::*; pub use update_verifier_registry::*; pub use write_state_buffer::*; diff --git a/interface/src/args/propose_commitment.rs b/interface/src/args/propose_commitment.rs index 7c7b5f40..fde180b6 100644 --- a/interface/src/args/propose_commitment.rs +++ b/interface/src/args/propose_commitment.rs @@ -11,6 +11,9 @@ pub struct ProposeCommitmentArgs { pub owner: Pubkey, + /// Whether finalization should make the account undelegatable. + pub allow_undelegation: bool, + /// Hash of replay/data-availability pointer bytes. pub da_pointer_hash: [u8; 32], diff --git a/interface/src/args/undelegate.rs b/interface/src/args/undelegate.rs new file mode 100644 index 00000000..28aa2772 --- /dev/null +++ b/interface/src/args/undelegate.rs @@ -0,0 +1,9 @@ +use wheels::variable_offset_layout; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[variable_offset_layout(buffer_offset = 1)] +pub struct UndelegateArgs { + /// Bump for the temporary DLP-owned undelegation buffer PDA. + /// Set by the instruction builder. + pub undelegate_buffer_bump: u8, +} diff --git a/interface/src/consts.rs b/interface/src/consts.rs new file mode 100644 index 00000000..f5406514 --- /dev/null +++ b/interface/src/consts.rs @@ -0,0 +1,3 @@ +/// Discriminator understood by owner programs during undelegation callbacks. +pub const EXTERNAL_UNDELEGATE_DISCRIMINATOR: [u8; 8] = + [196, 28, 41, 206, 48, 37, 51, 167]; diff --git a/interface/src/error.rs b/interface/src/error.rs index fa5c6987..a18ed025 100644 --- a/interface/src/error.rs +++ b/interface/src/error.rs @@ -191,6 +191,9 @@ pub enum DlpError { #[error("Owner program requested undelegation")] OwnerRequestedUndelegation = 55, + #[error("Account data is too large for one-step undelegation")] + AccountDataTooLargeForUndelegation = 56, + #[error("An infallible error is encountered possibly due to logic error")] InfallibleError = 100, } diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs index 5a518a7b..9859bac6 100644 --- a/interface/src/instruction.rs +++ b/interface/src/instruction.rs @@ -41,6 +41,8 @@ pub enum DlpV2Instruction { CommitFinalizeFromBuffer = 114, /// Grows a delegated account by one step before a large direct finalization. PrepareCommitFinalize = 115, + /// Returns a delegated account to its original owner. + Undelegate = 116, } impl DlpV2Instruction { diff --git a/interface/src/instruction_builder/mod.rs b/interface/src/instruction_builder/mod.rs index 8210273a..69c21618 100644 --- a/interface/src/instruction_builder/mod.rs +++ b/interface/src/instruction_builder/mod.rs @@ -10,6 +10,7 @@ mod raise_challenge; mod register_operator; mod register_verifier; mod resolve_dispute; +mod undelegate; mod update_delegation_cluster; mod update_verifier_registry; mod write_state_buffer; @@ -26,6 +27,7 @@ pub use raise_challenge::*; pub use register_operator::*; pub use register_verifier::*; pub use resolve_dispute::*; +pub use undelegate::*; pub use update_delegation_cluster::*; pub use update_verifier_registry::*; pub use write_state_buffer::*; diff --git a/interface/src/instruction_builder/undelegate.rs b/interface/src/instruction_builder/undelegate.rs new file mode 100644 index 00000000..51b621d1 --- /dev/null +++ b/interface/src/instruction_builder/undelegate.rs @@ -0,0 +1,50 @@ +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; +use solana_sdk_ids::system_program; +use wheels::{layout::Encodable, DataLayoutError}; + +use crate::{ + compat::{Compatize, Modernize}, + pda::{delegation_state_pda, undelegate_buffer_pda}, + DlpV2Instruction, UndelegateArgs, +}; + +/// Builds the shared undelegation instruction. +pub fn undelegate( + current_validator: Pubkey, + delegated_account: Pubkey, + original_owner: Pubkey, + delegation_cluster: Pubkey, + delegation_rent_payer: Pubkey, +) -> Result { + let (undelegate_buffer, undelegate_buffer_bump) = + undelegate_buffer_pda(&delegated_account.compatize()); + + Ok(Instruction { + program_id: crate::id().modernize(), + accounts: vec![ + AccountMeta::new(current_validator, true), + AccountMeta::new(delegated_account, false), + AccountMeta::new_readonly(original_owner, false), + AccountMeta::new(undelegate_buffer.modernize(), false), + AccountMeta::new( + delegation_state_pda(&delegated_account.compatize()) + .modernize(), + false, + ), + AccountMeta::new_readonly(delegation_cluster, false), + AccountMeta::new(delegation_rent_payer, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: [ + DlpV2Instruction::Undelegate.to_vec(), + UndelegateArgs { + undelegate_buffer_bump, + } + .encode()?, + ] + .concat(), + }) +} diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 1c1711e4..4c2640b0 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -3,6 +3,7 @@ pub use ::solana_program; pub mod args; pub mod compact; pub mod compat; +pub mod consts; #[cfg(feature = "diff")] pub mod diff; @@ -18,6 +19,7 @@ pub mod state; pub mod instruction_builder; pub use args::*; +pub use consts::*; pub use instruction::*; pub use pda::*; pub use state::*; diff --git a/interface/src/pda.rs b/interface/src/pda.rs index 0bce2400..919959c7 100644 --- a/interface/src/pda.rs +++ b/interface/src/pda.rs @@ -8,6 +8,7 @@ pub const VERIFIER_REGISTRY_SEED: &[u8] = b"verifier-registry"; pub const STATE_BUFFER_SEED: &[u8] = b"state-buffer"; pub const PENDING_COMMITMENT_SEED: &[u8] = b"pending-commitment"; pub const CHALLENGE_SEED: &[u8] = b"challenge"; +pub const UNDELEGATE_BUFFER_SEED: &[u8] = b"undelegate-buffer"; // TODO (snawaz): Precompute these addresses if PDA derivation becomes const-safe. @@ -103,3 +104,10 @@ pub fn challenge_pda( ) .0 } + +pub fn undelegate_buffer_pda(delegated_account: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[UNDELEGATE_BUFFER_SEED, delegated_account.as_ref()], + &crate::id(), + ) +} diff --git a/interface/src/state/pending_commitment.rs b/interface/src/state/pending_commitment.rs index 3a82b250..77d70e3c 100644 --- a/interface/src/state/pending_commitment.rs +++ b/interface/src/state/pending_commitment.rs @@ -62,6 +62,12 @@ pub struct PendingCommitment { /// Owner committed by the operator. pub owner: Pubkey, + /// Whether finalization should make the account undelegatable. + pub allow_undelegation: bool, + + /// Keeps the following fixed-width fields eight-byte aligned. + pub _pad_after_allow_undelegation: [u8; 7], + /// Hash binding operator, account, commit sequence number, delegation, DA, /// and state. pub state_commitment_hash: [u8; 32], diff --git a/program/src/processor/fraud_proofs/finalize_commitment.rs b/program/src/processor/fraud_proofs/finalize_commitment.rs index ca5c4d19..989f8a25 100644 --- a/program/src/processor/fraud_proofs/finalize_commitment.rs +++ b/program/src/processor/fraud_proofs/finalize_commitment.rs @@ -257,6 +257,10 @@ pub fn process_finalize_commitment( let mut metadata = DelegationMetadataFast::from_account(delegation_metadata)?; metadata.set_last_commit_id(u64::from(pending.commit_seq())); + if pending.allow_undelegation() { + metadata + .set_undelegation_requester(UndelegationRequester::Validator); + } } drop(delegation_cluster_data); diff --git a/program/src/processor/fraud_proofs/propose_commitment.rs b/program/src/processor/fraud_proofs/propose_commitment.rs index ebea3cb8..fa11bd92 100644 --- a/program/src/processor/fraud_proofs/propose_commitment.rs +++ b/program/src/processor/fraud_proofs/propose_commitment.rs @@ -242,6 +242,7 @@ pub fn process_propose_commitment( delegation_record: delegation_record.address(), da_pointer_hash: args.da_pointer_hash(), account_state_hash: &account_state_hash, + allow_undelegation: args.allow_undelegation(), verifier_registry: verifier_registry.address(), challenge_window_id, }); @@ -260,6 +261,8 @@ pub fn process_propose_commitment( data_hash, lamports: args.lamports(), owner: *args.owner(), + allow_undelegation: args.allow_undelegation(), + _pad_after_allow_undelegation: [0; 7], state_commitment_hash, verifier_registry: verifier_registry.address().to_bytes().into(), challenge_window_id, @@ -472,6 +475,7 @@ struct StateCommitmentHashInput<'a> { delegation_record: &'a Address, da_pointer_hash: &'a [u8; 32], account_state_hash: &'a [u8; 32], + allow_undelegation: bool, verifier_registry: &'a Address, challenge_window_id: u64, } @@ -486,6 +490,7 @@ fn state_commitment_hash(input: StateCommitmentHashInput<'_>) -> [u8; 32] { input.delegation_record.as_ref(), input.da_pointer_hash, input.account_state_hash, + &[input.allow_undelegation as u8], input.verifier_registry.as_ref(), &input.challenge_window_id.to_le_bytes(), ]) diff --git a/program/src/processor/internal/commit_finalize.rs b/program/src/processor/internal/commit_finalize.rs index 6f94a9c2..aaaab7d9 100644 --- a/program/src/processor/internal/commit_finalize.rs +++ b/program/src/processor/internal/commit_finalize.rs @@ -177,7 +177,7 @@ pub(crate) fn validate_commit_state( delegation_state: &AccountView, delegation_cluster: &AccountView, state: &DelegationStateView<'_>, - commit_seq: u64, + commit_seq: u32, is_writable: bool, ) -> ProgramResult { let state_bump = [state.bump()]; diff --git a/program/src/processor/internal/mod.rs b/program/src/processor/internal/mod.rs index fe84489c..d7ac9805 100644 --- a/program/src/processor/internal/mod.rs +++ b/program/src/processor/internal/mod.rs @@ -1,3 +1,5 @@ mod commit_finalize; +mod undelegate; pub(crate) use commit_finalize::*; +pub(crate) use undelegate::*; diff --git a/program/src/processor/internal/undelegate.rs b/program/src/processor/internal/undelegate.rs new file mode 100644 index 00000000..793da67a --- /dev/null +++ b/program/src/processor/internal/undelegate.rs @@ -0,0 +1,131 @@ +use delegation_program_interface as dpi; +use dpi::{consts::EXTERNAL_UNDELEGATE_DISCRIMINATOR, UNDELEGATE_BUFFER_SEED}; +use pinocchio::{ + cpi::{invoke_signed, Seed, Signer}, + error::ProgramError, + instruction::{InstructionAccount, InstructionView}, + AccountView, ProgramResult, +}; + +use crate::{ + legacy::processor::fast::utils::pda::{ + close_pda, create_pda, AccountSpace, + }, + require_eq_keys, +}; + +#[allow(clippy::too_many_arguments)] +pub(crate) fn return_account_to_owner( + payer: &AccountView, + delegated_account: &AccountView, + owner_program: &AccountView, + undelegate_buffer: &AccountView, + system_program: &AccountView, + undelegate_buffer_bump: u8, + callback_data: &[u8], +) -> ProgramResult { + let account_lamports = delegated_account.lamports(); + + if delegated_account.is_data_empty() { + unsafe { + delegated_account.assign(owner_program.address()); + } + return Ok(()); + } + + let bump = [undelegate_buffer_bump]; + let signer_seeds = [ + Seed::from(UNDELEGATE_BUFFER_SEED), + Seed::from(delegated_account.address().as_ref()), + Seed::from(&bump), + ]; + let signers = [Signer::from(&signer_seeds)]; + + create_pda( + undelegate_buffer, + &crate::fast::ID, + AccountSpace::CurrentRent(delegated_account.data_len()), + &signers, + payer, + )?; + + undelegate_buffer + .try_borrow_mut()? + .copy_from_slice(delegated_account.try_borrow()?.as_ref()); + delegated_account.try_borrow_mut()?.fill(0); + unsafe { + delegated_account.assign(owner_program.address()); + } + + invoke_signed( + &InstructionView { + program_id: owner_program.address(), + data: callback_data, + accounts: &[ + InstructionAccount::new( + delegated_account.address(), + true, + false, + ), + InstructionAccount::new( + undelegate_buffer.address(), + true, + true, + ), + InstructionAccount::new(payer.address(), true, true), + InstructionAccount::new(system_program.address(), false, false), + ], + }, + &[delegated_account, undelegate_buffer, payer, system_program], + &signers, + )?; + + require_eq_keys!( + unsafe { delegated_account.owner() }, + owner_program.address(), + ProgramError::InvalidAccountOwner + ); + if delegated_account.lamports() != account_lamports { + return Err( + dpi::error::DlpError::InvalidValidatorBalanceAfterCPI.into() + ); + } + if delegated_account.try_borrow()?.as_ref() + != undelegate_buffer.try_borrow()?.as_ref() + { + return Err(dpi::error::DlpError::InvalidAccountDataAfterCPI.into()); + } + + close_pda(undelegate_buffer, payer) +} + +pub(crate) fn owner_callback_data( + state: &dpi::DelegationStateView<'_>, +) -> Result, ProgramError> { + let seeds = state + .seeds() + .map_err(|_| ProgramError::InvalidAccountData)?; + let capacity = EXTERNAL_UNDELEGATE_DISCRIMINATOR + .len() + .checked_add(4) + .and_then(|len| len.checked_add(state.encoded_seeds().len())) + .and_then(|len| len.checked_add(seeds.len().saturating_mul(3))) + .ok_or(dpi::error::DlpError::Overflow)?; + let mut data = Vec::with_capacity(capacity); + data.extend_from_slice(&EXTERNAL_UNDELEGATE_DISCRIMINATOR); + data.extend_from_slice( + &u32::try_from(seeds.len()) + .map_err(|_| dpi::error::DlpError::Overflow)? + .to_le_bytes(), + ); + for seed in seeds.iter() { + data.extend_from_slice( + &u32::try_from(seed.as_slice().len()) + .map_err(|_| dpi::error::DlpError::Overflow)? + .to_le_bytes(), + ); + data.extend_from_slice(seed.as_slice()); + } + + Ok(data) +} diff --git a/program/src/processor/mod.rs b/program/src/processor/mod.rs index 6b76be2a..1af415c5 100644 --- a/program/src/processor/mod.rs +++ b/program/src/processor/mod.rs @@ -4,6 +4,7 @@ mod delegate; mod direct; mod fraud_proofs; pub(crate) mod internal; +mod undelegate; pub use bootstrap::*; pub use delegate::*; @@ -11,6 +12,7 @@ pub use direct::*; use dpi::DlpV2Instruction; pub use fraud_proofs::*; use pinocchio::{AccountView, ProgramResult}; +pub use undelegate::*; #[inline(never)] pub fn process_instruction( @@ -65,5 +67,6 @@ pub fn process_instruction( DlpV2Instruction::PrepareCommitFinalize => { process_prepare_commit_finalize(accounts, data) } + DlpV2Instruction::Undelegate => process_undelegate(accounts, data), } } diff --git a/program/src/processor/undelegate.rs b/program/src/processor/undelegate.rs new file mode 100644 index 00000000..84d2e4dc --- /dev/null +++ b/program/src/processor/undelegate.rs @@ -0,0 +1,171 @@ +use delegation_program_interface as dpi; +use dpi::{ + error::DlpError, + pda::{ + DELEGATION_CLUSTER_SEED, DELEGATION_STATE_SEED, UNDELEGATE_BUFFER_SEED, + }, + DelegationCluster, DelegationState, UndelegateArgs, UndelegationRequester, +}; +use pinocchio::{ + address::address_eq, error::ProgramError, AccountView, ProgramResult, +}; +use wheels::{ + layout::Decodable, require, require_eq, require_eq_keys, require_ge, + require_n_accounts, require_owned_by, require_signer, +}; + +use crate::{ + legacy::processor::fast::utils::pda::close_pda, + processor::internal::{owner_callback_data, return_account_to_owner}, + requires::{require_uninitialized_account, StandardCtx}, +}; + +/// Return a delegated account to its original owner. +/// +/// Accounts: +/// 0: `[signer, writable]` current cluster validator and buffer rent payer +/// 1: `[writable]` delegated account +/// 2: `[]` original owner program +/// 3: `[writable]` temporary undelegation buffer PDA +/// 4: `[writable]` DelegationState PDA +/// 5: `[]` DelegationCluster PDA +/// 6: `[writable]` DelegationState rent payer +/// 7: `[]` system program, forwarded to the owner callback +#[inline(never)] +pub fn process_undelegate( + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + let [current_validator, delegated_account, owner_program, undelegate_buffer, delegation_state, delegation_cluster, delegation_rent_payer, system_program] = + require_n_accounts!(accounts, 8); + + require_signer!(current_validator); + require!(current_validator.is_writable(), ProgramError::Immutable); + require_owned_by!(delegated_account, &crate::fast::ID); + require!(delegated_account.is_writable(), ProgramError::Immutable); + require!(delegation_rent_payer.is_writable(), ProgramError::Immutable); + require_eq_keys!( + system_program.address(), + &pinocchio_system::ID, + ProgramError::IncorrectProgramId + ); + + let args = UndelegateArgs::decode(data)?; + require!( + delegated_account.data_len() + <= pinocchio::account::MAX_PERMITTED_DATA_INCREASE, + DlpError::AccountDataTooLargeForUndelegation + ); + + let state_data = delegation_state.try_borrow()?; + let state = DelegationState::decode(state_data.as_ref())?; + let state_bump = [state.bump()]; + dpi::require_initialized_pda!( + delegation_state, + &[ + DELEGATION_STATE_SEED, + delegated_account.address().as_ref(), + &state_bump, + ], + &crate::fast::ID, + true + ); + require_eq!( + &state.discriminator(), + &DelegationState::DISCRIMINATOR, + ProgramError::InvalidAccountData + ); + require_eq_keys!( + state.delegated_account(), + delegated_account.address(), + DlpError::InvalidDelegatedAccount + ); + require_eq_keys!( + state.delegation_cluster(), + delegation_cluster.address(), + ProgramError::InvalidAccountData + ); + require_eq_keys!( + state.original_owner(), + owner_program.address(), + ProgramError::InvalidAccountOwner + ); + require_eq_keys!( + state.rent_payer(), + delegation_rent_payer.address(), + DlpError::InvalidReimbursementAddressForDelegationRent + ); + require_ge!( + delegated_account.lamports(), + state.lamports(), + DlpError::InvalidDelegatedState + ); + match state.undelegation_requester() { + value if value == UndelegationRequester::Validator.value() => {} + value if value == UndelegationRequester::None.value() => { + return Err(DlpError::NotUndelegatable.into()); + } + value if value == UndelegationRequester::OwnerProgram.value() => { + return Err(DlpError::OwnerRequestedUndelegation.into()); + } + _ => return Err(ProgramError::InvalidAccountData), + } + let callback_data = owner_callback_data(&state)?; + drop(state_data); + + let cluster_data = delegation_cluster.try_borrow()?; + let cluster = DelegationCluster::decode(cluster_data.as_ref())?; + let cluster_bump = [cluster.bump()]; + dpi::require_initialized_pda!( + delegation_cluster, + &[ + DELEGATION_CLUSTER_SEED, + cluster.cluster_id().as_ref(), + &cluster_bump, + ], + &crate::fast::ID, + false + ); + require_eq!( + &cluster.discriminator(), + &DelegationCluster::DISCRIMINATOR, + ProgramError::InvalidAccountData + ); + require_eq_keys!( + cluster.current_validator(), + current_validator.address(), + DlpError::InvalidAuthority + ); + drop(cluster_data); + + let undelegate_buffer_bump = [args.undelegate_buffer_bump()]; + dpi::require_pda!( + undelegate_buffer, + &[ + UNDELEGATE_BUFFER_SEED, + delegated_account.address().as_ref(), + &undelegate_buffer_bump, + ], + &crate::fast::ID, + true + ); + require_uninitialized_account( + undelegate_buffer, + true, + StandardCtx::new("undelegation buffer"), + )?; + if !delegated_account.is_data_empty() { + require!(owner_program.executable(), ProgramError::InvalidAccountData); + } + + return_account_to_owner( + current_validator, + delegated_account, + owner_program, + undelegate_buffer, + system_program, + args.undelegate_buffer_bump(), + &callback_data, + )?; + close_pda(delegation_state, delegation_rent_payer) +} diff --git a/program/tests/test_approve_commitment.rs b/program/tests/test_approve_commitment.rs index c340eb79..2bb58f63 100644 --- a/program/tests/test_approve_commitment.rs +++ b/program/tests/test_approve_commitment.rs @@ -459,6 +459,7 @@ fn propose_commitment_args() -> ProposeCommitmentArgs { commit_seq: 1, lamports: 1_000, owner: Pubkey::new_from_array([7; 32]), + allow_undelegation: false, da_pointer_hash: [9; 32], er_slot: Some(42), } diff --git a/program/tests/test_challenger_reveal.rs b/program/tests/test_challenger_reveal.rs index 63b7be46..83ba7feb 100644 --- a/program/tests/test_challenger_reveal.rs +++ b/program/tests/test_challenger_reveal.rs @@ -499,6 +499,7 @@ async fn propose( commit_seq: env.commit_seq, lamports: env.committed_lamports, owner: env.committed_owner, + allow_undelegation: false, da_pointer_hash: [9; 32], er_slot: Some(42), }, diff --git a/program/tests/test_finalize_commitment.rs b/program/tests/test_finalize_commitment.rs index 28d51c07..2db3b2f5 100644 --- a/program/tests/test_finalize_commitment.rs +++ b/program/tests/test_finalize_commitment.rs @@ -10,7 +10,7 @@ use dpi::{ delegation_metadata_pda_from_delegated_account, delegation_record_pda_from_delegated_account, }, - state::{DelegationMetadata, DelegationRecord}, + state::{DelegationMetadata, DelegationRecord, UndelegationRequester}, }, pda::pending_commitment_pda, PendingCommitment, ProposeCommitmentArgs, RegisterOperatorArgs, @@ -76,6 +76,25 @@ async fn test_finalize_commitment() { ); } +#[tokio::test] +async fn test_finalize_commitment_records_validator_undelegation_request() { + let mut env = + setup_finalize_commitment_env(LAMPORTS_PER_SOL, LAMPORTS_PER_SOL).await; + env.allow_undelegation = true; + + propose(&mut env).await.unwrap(); + approve(&mut env).await.unwrap(); + warp_past_challenge_window(&mut env).await; + finalize(&mut env).await.unwrap(); + + assert_eq!( + read_delegation_metadata(&mut env) + .await + .undelegation_requester, + UndelegationRequester::Validator + ); +} + #[tokio::test] async fn test_finalize_commitment_fails_before_window_closes() { let mut env = @@ -230,6 +249,7 @@ struct FinalizeCommitmentEnv { committed_owner: Pubkey, committed_lamports: u64, commit_seq: u32, + allow_undelegation: bool, final_state_data: Vec, } @@ -351,6 +371,7 @@ async fn setup_finalize_commitment_env( committed_owner, committed_lamports, commit_seq, + allow_undelegation: false, final_state_data, } } @@ -366,6 +387,7 @@ async fn propose( commit_seq: env.commit_seq, lamports: env.committed_lamports, owner: env.committed_owner, + allow_undelegation: env.allow_undelegation, da_pointer_hash: [9; 32], er_slot: Some(42), }, diff --git a/program/tests/test_propose_commitment.rs b/program/tests/test_propose_commitment.rs index d453f4f8..8a4ebf00 100644 --- a/program/tests/test_propose_commitment.rs +++ b/program/tests/test_propose_commitment.rs @@ -548,6 +548,7 @@ fn propose_commitment_args() -> ProposeCommitmentArgs { commit_seq: 1, lamports: 1_000, owner: Pubkey::new_from_array([7; 32]), + allow_undelegation: false, da_pointer_hash: [9; 32], er_slot: Some(42), } diff --git a/program/tests/test_raise_challenge.rs b/program/tests/test_raise_challenge.rs index 4c832e1f..24528a0e 100644 --- a/program/tests/test_raise_challenge.rs +++ b/program/tests/test_raise_challenge.rs @@ -375,6 +375,7 @@ async fn propose(env: &mut RaiseChallengeEnv) -> Result<(), BanksClientError> { commit_seq: env.commit_seq, lamports: env.committed_lamports, owner: env.committed_owner, + allow_undelegation: false, da_pointer_hash: [9; 32], er_slot: Some(42), }, diff --git a/program/tests/test_resolve_dispute.rs b/program/tests/test_resolve_dispute.rs index b865d8a1..0cb16394 100644 --- a/program/tests/test_resolve_dispute.rs +++ b/program/tests/test_resolve_dispute.rs @@ -552,6 +552,7 @@ async fn propose(env: &mut ResolveDisputeEnv) -> Result<(), BanksClientError> { commit_seq: env.commit_seq, lamports: env.committed_lamports, owner: env.committed_owner, + allow_undelegation: false, da_pointer_hash: [9; 32], er_slot: Some(42), }, diff --git a/program/tests/test_undelegate_account.rs b/program/tests/test_undelegate_account.rs new file mode 100644 index 00000000..48820054 --- /dev/null +++ b/program/tests/test_undelegate_account.rs @@ -0,0 +1,416 @@ +use delegation_program::solana_program; +use delegation_program_interface as dpi; +use dpi::{ + consts::EXTERNAL_UNDELEGATE_DISCRIMINATOR, + instruction_builder::undelegate, + pda::{ + delegation_cluster_pda, delegation_state_pda, undelegate_buffer_pda, + }, + DelegationCluster, DelegationSeed, DelegationState, UndelegationRequester, +}; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + native_token::LAMPORTS_PER_SOL, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, +}; +use solana_program_test::{ + processor, BanksClient, BanksClientError, ProgramTest, +}; +use solana_sdk::{ + account::Account, + instruction::InstructionError, + signature::{Keypair, Signer}, + transaction::{Transaction, TransactionError}, +}; +use solana_sdk_ids::system_program; +use wheels::layout::Encodable; + +const OWNER_PROGRAM_ID: Pubkey = + solana_sdk::pubkey!("8qbHbw2BbbTHBW1sbeqakYXV7xPC7rULpRvbi3L6jUjL"); +const ACCOUNT_DATA: &[u8] = b"finalized account state"; +const ACCOUNT_SEED: &[u8] = b"undelegate-account-test"; +const ACCOUNT_LAMPORTS: u64 = LAMPORTS_PER_SOL; + +struct UndelegateTestContext { + banks: BanksClient, + payer: Keypair, + current_validator: Keypair, + delegated_account: Pubkey, + delegation_state: Pubkey, + delegation_cluster: Pubkey, + delegation_rent_payer: Pubkey, + original_owner: Pubkey, + state_rent: u64, +} + +#[tokio::test] +async fn test_undelegate_assigns_account_without_recreating_it() { + let context = setup_undelegate_test( + ACCOUNT_DATA.to_vec(), + OWNER_PROGRAM_ID, + UndelegationRequester::Validator, + true, + ) + .await; + let validator_lamports_before = context + .banks + .get_account(context.current_validator.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + let rent_payer_lamports_before = context + .banks + .get_account(context.delegation_rent_payer) + .await + .unwrap() + .unwrap() + .lamports; + + submit_undelegate(&context, &context.current_validator) + .await + .unwrap(); + + let delegated_account = context + .banks + .get_account(context.delegated_account) + .await + .unwrap() + .unwrap(); + assert_eq!(delegated_account.owner, OWNER_PROGRAM_ID); + assert_eq!(delegated_account.data, ACCOUNT_DATA); + assert_eq!(delegated_account.lamports, ACCOUNT_LAMPORTS); + assert!(context + .banks + .get_account(context.delegation_state) + .await + .unwrap() + .is_none()); + assert!(context + .banks + .get_account(undelegate_buffer_pda(&context.delegated_account).0) + .await + .unwrap() + .is_none()); + assert_eq!( + context + .banks + .get_account(context.current_validator.pubkey()) + .await + .unwrap() + .unwrap() + .lamports, + validator_lamports_before + ); + assert_eq!( + context + .banks + .get_account(context.delegation_rent_payer) + .await + .unwrap() + .unwrap() + .lamports, + rent_payer_lamports_before + context.state_rent + ); +} + +#[tokio::test] +async fn test_undelegate_empty_system_account_without_callback() { + let context = setup_undelegate_test( + vec![], + system_program::id(), + UndelegationRequester::Validator, + false, + ) + .await; + + submit_undelegate(&context, &context.current_validator) + .await + .unwrap(); + + let delegated_account = context + .banks + .get_account(context.delegated_account) + .await + .unwrap() + .unwrap(); + assert_eq!(delegated_account.owner, system_program::id()); + assert!(delegated_account.data.is_empty()); + assert_eq!(delegated_account.lamports, ACCOUNT_LAMPORTS); +} + +#[tokio::test] +async fn test_undelegate_rejects_unrequested_and_owner_requested_accounts() { + for requester in [ + UndelegationRequester::None, + UndelegationRequester::OwnerProgram, + ] { + let context = setup_undelegate_test( + ACCOUNT_DATA.to_vec(), + OWNER_PROGRAM_ID, + requester, + false, + ) + .await; + let error = submit_undelegate(&context, &context.current_validator) + .await + .unwrap_err(); + assert_custom_error( + error, + match requester { + UndelegationRequester::None => { + dpi::error::DlpError::NotUndelegatable + } + UndelegationRequester::OwnerProgram => { + dpi::error::DlpError::OwnerRequestedUndelegation + } + UndelegationRequester::Validator => unreachable!(), + }, + ); + } +} + +#[tokio::test] +async fn test_undelegate_rejects_data_above_one_growth_step() { + let context = setup_undelegate_test( + vec![1; pinocchio::account::MAX_PERMITTED_DATA_INCREASE + 1], + OWNER_PROGRAM_ID, + UndelegationRequester::Validator, + false, + ) + .await; + + assert_custom_error( + submit_undelegate(&context, &context.current_validator) + .await + .unwrap_err(), + dpi::error::DlpError::AccountDataTooLargeForUndelegation, + ); +} + +async fn submit_undelegate( + context: &UndelegateTestContext, + validator: &Keypair, +) -> Result<(), BanksClientError> { + let instruction = undelegate( + validator.pubkey(), + context.delegated_account, + context.original_owner, + context.delegation_cluster, + context.delegation_rent_payer, + ) + .unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&context.payer.pubkey()), + &[&context.payer, validator], + context.banks.get_latest_blockhash().await.unwrap(), + ); + + context.banks.process_transaction(transaction).await +} + +async fn setup_undelegate_test( + account_data: Vec, + original_owner: Pubkey, + requester: UndelegationRequester, + paused: bool, +) -> UndelegateTestContext { + let mut program_test = ProgramTest::default(); + program_test.prefer_bpf(true); + program_test.add_program("delegation_program", dpi::ID, None); + program_test.prefer_bpf(false); + program_test.add_program( + "undelegate-account-owner", + OWNER_PROGRAM_ID, + processor!(owner_program_processor), + ); + + let current_validator = Keypair::new(); + let delegation_rent_payer = Pubkey::new_unique(); + let (delegated_account, delegated_account_bump, encoded_seeds) = + if original_owner == system_program::id() { + (Pubkey::new_unique(), 0, vec![]) + } else { + let (delegated_account, bump) = + Pubkey::find_program_address(&[ACCOUNT_SEED], &original_owner); + ( + delegated_account, + bump, + DelegationSeed { + value: ACCOUNT_SEED.to_vec(), + } + .encode() + .unwrap(), + ) + }; + let cluster_id = Pubkey::new_unique(); + let delegation_cluster = delegation_cluster_pda(&cluster_id); + let (_, cluster_bump) = Pubkey::find_program_address( + &[dpi::pda::DELEGATION_CLUSTER_SEED, cluster_id.as_ref()], + &dpi::id(), + ); + let delegation_state = delegation_state_pda(&delegated_account); + let (_, state_bump) = Pubkey::find_program_address( + &[dpi::pda::DELEGATION_STATE_SEED, delegated_account.as_ref()], + &dpi::id(), + ); + + program_test.add_account( + current_validator.pubkey(), + Account { + lamports: 10 * LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + program_test.add_account( + delegation_rent_payer, + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + program_test.add_account( + delegated_account, + Account { + lamports: ACCOUNT_LAMPORTS, + data: account_data, + owner: dpi::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let cluster_data = DelegationCluster { + discriminator: DelegationCluster::DISCRIMINATOR, + bump: cluster_bump, + cluster_id, + config_authority: Pubkey::new_unique(), + validator_rotation_authority: Pubkey::new_unique(), + current_validator: current_validator.pubkey(), + paused, + protocol_fee_vault: Pubkey::new_unique(), + challenge_config: None, + } + .encode() + .unwrap(); + program_test.add_account( + delegation_cluster, + Account { + lamports: Rent::default().minimum_balance(cluster_data.len()), + data: cluster_data, + owner: dpi::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let state_data = DelegationState { + discriminator: DelegationState::DISCRIMINATOR, + bump: state_bump, + delegated_account, + delegation_cluster, + original_owner, + rent_payer: delegation_rent_payer, + delegation_slot: 0, + lamports: ACCOUNT_LAMPORTS, + last_commit_seq: 1, + undelegation_requester: requester.value(), + delegated_account_bump, + seed_count: u8::from(!encoded_seeds.is_empty()), + encoded_seeds, + } + .encode() + .unwrap(); + let state_rent = Rent::default().minimum_balance(state_data.len()); + program_test.add_account( + delegation_state, + Account { + lamports: state_rent, + data: state_data, + owner: dpi::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (banks, payer, _) = program_test.start().await; + UndelegateTestContext { + banks, + payer, + current_validator, + delegated_account, + delegation_state, + delegation_cluster, + delegation_rent_payer, + original_owner, + state_rent, + } +} + +fn owner_program_processor( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let expected_callback_data = [ + EXTERNAL_UNDELEGATE_DISCRIMINATOR.as_slice(), + 1_u32.to_le_bytes().as_slice(), + (ACCOUNT_SEED.len() as u32).to_le_bytes().as_slice(), + ACCOUNT_SEED, + ] + .concat(); + if program_id != &OWNER_PROGRAM_ID || data != expected_callback_data { + return Err(ProgramError::InvalidInstructionData); + } + + let mut accounts = accounts.iter(); + let delegated_account = next_account_info(&mut accounts)?; + let undelegate_buffer = next_account_info(&mut accounts)?; + let payer = next_account_info(&mut accounts)?; + let system_program_account = next_account_info(&mut accounts)?; + if delegated_account.owner != program_id + || !delegated_account.is_writable + || undelegate_buffer.owner != &dpi::id() + || !undelegate_buffer.is_signer + || !payer.is_signer + || system_program_account.key != &system_program::id() + { + return Err(ProgramError::InvalidAccountData); + } + + let buffer_data = undelegate_buffer.try_borrow_data()?; + let mut delegated_data = delegated_account.try_borrow_mut_data()?; + if delegated_data.len() != buffer_data.len() { + return Err(ProgramError::InvalidAccountData); + } + delegated_data.copy_from_slice(&buffer_data); + Ok(()) +} + +fn assert_custom_error( + error: BanksClientError, + expected: dpi::error::DlpError, +) { + assert!( + matches!( + error, + BanksClientError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(code), + ), + ) if code == expected as u32 + ), + "unexpected error: {error:?}" + ); +}