diff --git a/dlp-api/src/v2/args/mod.rs b/dlp-api/src/v2/args/mod.rs index 08637186..1c6dd317 100644 --- a/dlp-api/src/v2/args/mod.rs +++ b/dlp-api/src/v2/args/mod.rs @@ -4,9 +4,11 @@ mod init_protocol_config; mod register_operator; mod register_verifier; +mod update_protocol_config; mod update_verifier_registry; pub use init_protocol_config::*; pub use register_operator::*; pub use register_verifier::*; +pub use update_protocol_config::*; pub use update_verifier_registry::*; diff --git a/dlp-api/src/v2/args/update_protocol_config.rs b/dlp-api/src/v2/args/update_protocol_config.rs new file mode 100644 index 00000000..4722206d --- /dev/null +++ b/dlp-api/src/v2/args/update_protocol_config.rs @@ -0,0 +1,5 @@ +// Update uses the same payload as init because every init-time config value in +// this args type is also authority-updatable. Account identity fields such as +// authority, fee vault, pause state, discriminator, and bump are preserved by +// the processor instead of coming from instruction data. +pub type UpdateProtocolConfigArgs = super::InitProtocolConfigArgs; diff --git a/dlp-api/src/v2/instruction.rs b/dlp-api/src/v2/instruction.rs index a8944a32..03e19eb7 100644 --- a/dlp-api/src/v2/instruction.rs +++ b/dlp-api/src/v2/instruction.rs @@ -13,6 +13,8 @@ pub enum DlpV2Instruction { RegisterVerifier = 102, /// Updates the set of verifiers that can be selected. UpdateVerifierRegistry = 103, + /// Updates global v2 config for future commitments. + UpdateProtocolConfig = 104, } impl DlpV2Instruction { diff --git a/dlp-api/src/v2/instruction_builder/mod.rs b/dlp-api/src/v2/instruction_builder/mod.rs index 72621b82..d93de000 100644 --- a/dlp-api/src/v2/instruction_builder/mod.rs +++ b/dlp-api/src/v2/instruction_builder/mod.rs @@ -1,9 +1,11 @@ mod init_protocol_config; mod register_operator; mod register_verifier; +mod update_protocol_config; mod update_verifier_registry; pub use init_protocol_config::*; pub use register_operator::*; pub use register_verifier::*; +pub use update_protocol_config::*; pub use update_verifier_registry::*; diff --git a/dlp-api/src/v2/instruction_builder/update_protocol_config.rs b/dlp-api/src/v2/instruction_builder/update_protocol_config.rs new file mode 100644 index 00000000..2fcb8341 --- /dev/null +++ b/dlp-api/src/v2/instruction_builder/update_protocol_config.rs @@ -0,0 +1,31 @@ +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; +use wheels::layout::Encodable; + +use crate::{ + compat::Modernize, + v2::{ + pda::protocol_config_pda, DlpV2Instruction, UpdateProtocolConfigArgs, + }, +}; + +/// Builds the instruction that updates global v2 config for future work. +pub fn update_protocol_config( + authority: Pubkey, + args: UpdateProtocolConfigArgs, +) -> Instruction { + Instruction { + program_id: crate::id().modernize(), + accounts: vec![ + AccountMeta::new_readonly(authority, true), + AccountMeta::new(protocol_config_pda().modernize(), false), + ], + data: [ + DlpV2Instruction::UpdateProtocolConfig.to_vec(), + args.encode().unwrap(), + ] + .concat(), + } +} diff --git a/src/v2/processor/bootstrap/mod.rs b/src/v2/processor/bootstrap/mod.rs index 72621b82..d93de000 100644 --- a/src/v2/processor/bootstrap/mod.rs +++ b/src/v2/processor/bootstrap/mod.rs @@ -1,9 +1,11 @@ mod init_protocol_config; mod register_operator; mod register_verifier; +mod update_protocol_config; mod update_verifier_registry; pub use init_protocol_config::*; pub use register_operator::*; pub use register_verifier::*; +pub use update_protocol_config::*; pub use update_verifier_registry::*; diff --git a/src/v2/processor/bootstrap/update_protocol_config.rs b/src/v2/processor/bootstrap/update_protocol_config.rs new file mode 100644 index 00000000..38fbb8ed --- /dev/null +++ b/src/v2/processor/bootstrap/update_protocol_config.rs @@ -0,0 +1,77 @@ +use dlp_api::{ + error::DlpError, + v2::{pda::PROTOCOL_CONFIG_SEED, ProtocolConfig, UpdateProtocolConfigArgs}, +}; +use pinocchio::{error::ProgramError, AccountView, ProgramResult}; +use wheels::{ + layout::{Decodable, Encodable}, + require_eq_keys, require_n_accounts, require_signer, +}; + +use crate::requires::require_initialized_pda; + +/// Update global v2 config for future commitments. +/// +/// Accounts: +/// 0: `[signer]` protocol authority +/// 1: `[writable]` ProtocolConfig PDA +#[inline(never)] +pub fn process_update_protocol_config( + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + let [ + authority, // force multi-line + protocol_config, + ] = require_n_accounts!(accounts, 2); + + let args = UpdateProtocolConfigArgs::decode(data)?; + super::validate_protocol_config_args(&args)?; + + require_signer!(authority); + require_initialized_pda( + protocol_config, + &[PROTOCOL_CONFIG_SEED], + &crate::fast::ID, + true, + "protocol config", + )?; + + let protocol_config_data = protocol_config.try_borrow()?; + let current = ProtocolConfig::decode(protocol_config_data.as_ref())?; + if current.discriminator() != ProtocolConfig::DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData); + } + require_eq_keys!( + current.authority(), + authority.address(), + DlpError::InvalidAuthority + ); + + let updated = ProtocolConfig { + discriminator: ProtocolConfig::DISCRIMINATOR, + bump: current.bump(), + authority: *current.authority(), + // CHECKPOINT: pause/unpause remains a separate design decision; this + // instruction deliberately preserves the current emergency-stop state. + paused: current.paused(), + resolver: *args.resolver(), + protocol_fee_vault: *current.protocol_fee_vault(), + 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(), + }; + drop(protocol_config_data); + + updated.encode_to(protocol_config.try_borrow_mut()?.as_mut())?; + + Ok(()) +} diff --git a/src/v2/processor/mod.rs b/src/v2/processor/mod.rs index 3b4f3ea3..b796cbd8 100644 --- a/src/v2/processor/mod.rs +++ b/src/v2/processor/mod.rs @@ -25,5 +25,8 @@ pub fn process_instruction( DlpV2Instruction::UpdateVerifierRegistry => { process_update_verifier_registry(accounts, data) } + DlpV2Instruction::UpdateProtocolConfig => { + process_update_protocol_config(accounts, data) + } } } diff --git a/tests/test_v2_update_protocol_config.rs b/tests/test_v2_update_protocol_config.rs new file mode 100644 index 00000000..4d30bab5 --- /dev/null +++ b/tests/test_v2_update_protocol_config.rs @@ -0,0 +1,216 @@ +use dlp_api::{ + pda::fees_vault_pda, + v2::{ + instruction_builder::update_protocol_config, + pda::{protocol_config_pda, PROTOCOL_CONFIG_SEED}, + ProtocolConfig, UpdateProtocolConfigArgs, + }, +}; +use solana_sdk::{ + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use wheels::layout::Decodable; + +mod fixtures; + +use crate::fixtures::v2::{ + initialize_protocol_config, setup_program_test_env, + valid_protocol_config_args, +}; + +#[tokio::test] +async fn test_update_protocol_config() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + let initial_args = valid_protocol_config_args(); + + initialize_protocol_config( + &banks, + &payer, + &authority, + blockhash, + initial_args.clone(), + ) + .await; + + let update_args = UpdateProtocolConfigArgs { + resolver: Pubkey::new_unique(), + min_operator_bond: initial_args.min_operator_bond + 10, + min_verifier_bond: initial_args.min_verifier_bond + 12, + min_challenger_stake: initial_args.min_challenger_stake + 16, + challenge_window_slots: initial_args.challenge_window_slots + 9, + operator_response_timeout_slots: initial_args + .operator_response_timeout_slots + + 13, + challenger_reveal_timeout_slots: initial_args + .challenger_reveal_timeout_slots + + 19, + payout_timelock_slots: initial_args.payout_timelock_slots + 21, + verifiers_per_commitment: initial_args.verifiers_per_commitment, + approval_threshold: initial_args.approval_threshold, + max_window_extensions: initial_args.max_window_extensions + 1, + match_penalty_bps: initial_args.match_penalty_bps + 200, + }; + + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = update_protocol_config(authority.pubkey(), update_args.clone()); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &authority], + blockhash, + ); + + assert!(banks.process_transaction(tx).await.is_ok()); + + 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(), update_args.resolver); + assert_eq!(*protocol_config.protocol_fee_vault(), fees_vault_pda()); + assert_eq!( + protocol_config.min_operator_bond(), + update_args.min_operator_bond + ); + assert_eq!( + protocol_config.min_verifier_bond(), + update_args.min_verifier_bond + ); + assert_eq!( + protocol_config.min_challenger_stake(), + update_args.min_challenger_stake + ); + assert_eq!( + protocol_config.challenge_window_slots(), + update_args.challenge_window_slots + ); + assert_eq!( + protocol_config.operator_response_timeout_slots(), + update_args.operator_response_timeout_slots + ); + assert_eq!( + protocol_config.challenger_reveal_timeout_slots(), + update_args.challenger_reveal_timeout_slots + ); + assert_eq!( + protocol_config.payout_timelock_slots(), + update_args.payout_timelock_slots + ); + assert_eq!( + protocol_config.verifiers_per_commitment(), + update_args.verifiers_per_commitment + ); + assert_eq!( + protocol_config.approval_threshold(), + update_args.approval_threshold + ); + assert_eq!( + protocol_config.max_window_extensions(), + update_args.max_window_extensions + ); + assert_eq!( + protocol_config.match_penalty_bps(), + update_args.match_penalty_bps + ); +} + +#[tokio::test] +async fn test_update_protocol_config_fails_with_wrong_authority() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + + initialize_protocol_config( + &banks, + &payer, + &authority, + blockhash, + valid_protocol_config_args(), + ) + .await; + + let wrong_authority = Keypair::new(); + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = update_protocol_config( + wrong_authority.pubkey(), + valid_protocol_config_args(), + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &wrong_authority], + blockhash, + ); + + assert!(banks.process_transaction(tx).await.is_err()); +} + +#[tokio::test] +async fn test_update_protocol_config_fails_with_invalid_args() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + + initialize_protocol_config( + &banks, + &payer, + &authority, + blockhash, + valid_protocol_config_args(), + ) + .await; + + let mut args = valid_protocol_config_args(); + args.approval_threshold = args.verifiers_per_commitment + 1; + + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = update_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_update_protocol_config_fails_with_wrong_protocol_config_pda() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + + initialize_protocol_config( + &banks, + &payer, + &authority, + blockhash, + valid_protocol_config_args(), + ) + .await; + + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let mut ix = update_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()); +}