diff --git a/dlp-api/src/v2/args/mod.rs b/dlp-api/src/v2/args/mod.rs index 9f8ca380..08637186 100644 --- a/dlp-api/src/v2/args/mod.rs +++ b/dlp-api/src/v2/args/mod.rs @@ -4,7 +4,9 @@ mod init_protocol_config; mod register_operator; mod register_verifier; +mod update_verifier_registry; pub use init_protocol_config::*; pub use register_operator::*; pub use register_verifier::*; +pub use update_verifier_registry::*; diff --git a/dlp-api/src/v2/args/update_verifier_registry.rs b/dlp-api/src/v2/args/update_verifier_registry.rs new file mode 100644 index 00000000..94765988 --- /dev/null +++ b/dlp-api/src/v2/args/update_verifier_registry.rs @@ -0,0 +1,21 @@ +use wheels::variable_offset_layout; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[variable_offset_layout(buffer_offset = 1)] +pub struct UpdateVerifierRegistryArgs { + pub action: u8, + pub weight: u64, +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum VerifierRegistryAction { + Add = 1, + Remove = 2, +} + +impl VerifierRegistryAction { + pub const fn value(self) -> u8 { + self as u8 + } +} diff --git a/dlp-api/src/v2/instruction.rs b/dlp-api/src/v2/instruction.rs index b0e1f6df..a8944a32 100644 --- a/dlp-api/src/v2/instruction.rs +++ b/dlp-api/src/v2/instruction.rs @@ -11,6 +11,8 @@ pub enum DlpV2Instruction { RegisterOperator = 101, /// Registers one verifier and deposits its initial stake. RegisterVerifier = 102, + /// Updates the set of verifiers that can be selected. + UpdateVerifierRegistry = 103, } impl DlpV2Instruction { diff --git a/dlp-api/src/v2/instruction_builder/mod.rs b/dlp-api/src/v2/instruction_builder/mod.rs index 5d6f02ab..72621b82 100644 --- a/dlp-api/src/v2/instruction_builder/mod.rs +++ b/dlp-api/src/v2/instruction_builder/mod.rs @@ -1,7 +1,9 @@ mod init_protocol_config; mod register_operator; mod register_verifier; +mod update_verifier_registry; pub use init_protocol_config::*; pub use register_operator::*; pub use register_verifier::*; +pub use update_verifier_registry::*; diff --git a/dlp-api/src/v2/instruction_builder/update_verifier_registry.rs b/dlp-api/src/v2/instruction_builder/update_verifier_registry.rs new file mode 100644 index 00000000..30466fea --- /dev/null +++ b/dlp-api/src/v2/instruction_builder/update_verifier_registry.rs @@ -0,0 +1,40 @@ +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; +use solana_sdk_ids::system_program; +use wheels::layout::Encodable; + +use crate::{ + compat::{Compatize, Modernize}, + v2::{ + pda::{protocol_config_pda, verifier_bond_pda, verifier_registry_pda}, + DlpV2Instruction, UpdateVerifierRegistryArgs, + }, +}; + +/// Builds the instruction that updates the verifier selection registry. +pub fn update_verifier_registry( + authority: Pubkey, + verifier: Pubkey, + args: UpdateVerifierRegistryArgs, +) -> Instruction { + Instruction { + program_id: crate::id().modernize(), + accounts: vec![ + AccountMeta::new(authority, true), + AccountMeta::new_readonly(protocol_config_pda().modernize(), false), + AccountMeta::new(verifier_registry_pda().modernize(), false), + AccountMeta::new_readonly( + verifier_bond_pda(&verifier.compatize()).modernize(), + false, + ), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: [ + DlpV2Instruction::UpdateVerifierRegistry.to_vec(), + args.encode().unwrap(), + ] + .concat(), + } +} diff --git a/src/processor/fast/utils/pda.rs b/src/processor/fast/utils/pda.rs index c08b67ed..08b4e1ec 100644 --- a/src/processor/fast/utils/pda.rs +++ b/src/processor/fast/utils/pda.rs @@ -80,6 +80,30 @@ pub(crate) fn close_pda( target_account.resize(0) } +/// Tops up a PDA to the rent-exempt balance for `space`. +#[inline(always)] +pub(crate) fn top_up_pda_rent( + payer: &AccountView, + target_account: &AccountView, + space: usize, +) -> ProgramResult { + let rent = Rent::get()?; + let rent_exempt_balance = rent + .try_minimum_balance(space)? + .saturating_sub(target_account.lamports()); + + if rent_exempt_balance > 0 { + system::Transfer { + from: payer, + to: target_account, + lamports: rent_exempt_balance, + } + .invoke()?; + } + + Ok(()) +} + /// Close PDA with fees, distributing the fees to the specified addresses in sequence /// The total fees are calculated as `fee_percentage` of the total lamports in the PDA /// Each fee address receives fee_percentage % of the previous fee address's amount diff --git a/src/v2/processor/bootstrap/mod.rs b/src/v2/processor/bootstrap/mod.rs index 5d6f02ab..72621b82 100644 --- a/src/v2/processor/bootstrap/mod.rs +++ b/src/v2/processor/bootstrap/mod.rs @@ -1,7 +1,9 @@ mod init_protocol_config; mod register_operator; mod register_verifier; +mod update_verifier_registry; pub use init_protocol_config::*; pub use register_operator::*; pub use register_verifier::*; +pub use update_verifier_registry::*; diff --git a/src/v2/processor/bootstrap/update_verifier_registry.rs b/src/v2/processor/bootstrap/update_verifier_registry.rs new file mode 100644 index 00000000..97669d1a --- /dev/null +++ b/src/v2/processor/bootstrap/update_verifier_registry.rs @@ -0,0 +1,204 @@ +use dlp_api::{ + error::DlpError, + v2::{ + pda::{ + PROTOCOL_CONFIG_SEED, VERIFIER_BOND_SEED, VERIFIER_REGISTRY_SEED, + }, + ProtocolConfig, ProtocolConfigView, UpdateVerifierRegistryArgs, + UpdateVerifierRegistryArgsView, VerifierBond, VerifierBondView, + VerifierRegistry, VerifierRegistryAction, VerifierRegistryEntry, + VerifierStatus, + }, +}; +use pinocchio::{error::ProgramError, AccountView, ProgramResult}; +use wheels::{ + layout::Decodable, require, require_eq, require_eq_keys, require_ge, + require_n_accounts, require_signer, +}; + +use crate::{ + processor::fast::utils::pda::top_up_pda_rent, + requires::{require_initialized_pda, require_owned_pda, require_pda}, +}; + +/// Update the verifier registry used by v2 verifier selection. +/// +/// Accounts: +/// 0: `[signer, writable]` protocol authority and registry rent payer +/// 1: `[]` ProtocolConfig PDA +/// 2: `[writable]` VerifierRegistry PDA +/// 3: `[]` VerifierBond PDA +/// 4: `[]` system program, required by system CPI +#[inline(never)] +pub fn process_update_verifier_registry( + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + let [ + authority, // force multi-line + protocol_config, + verifier_registry, + verifier_bond, + _system_program, + ] = require_n_accounts!(accounts, 5); + + require_signer!(authority); + + let args = UpdateVerifierRegistryArgs::decode(data)?; + validate_update_args(&args)?; + + require_initialized_pda( + protocol_config, + &[PROTOCOL_CONFIG_SEED], + &crate::fast::ID, + false, + "protocol config", + )?; + require_initialized_pda( + verifier_registry, + &[VERIFIER_REGISTRY_SEED], + &crate::fast::ID, + true, + "verifier registry", + )?; + require_owned_pda(verifier_bond, &crate::fast::ID, "verifier bond")?; + + let verifier_bond_data = verifier_bond.try_borrow()?; + let verifier_bond_state = + VerifierBond::decode(verifier_bond_data.as_ref())?; + validate_verifier_bond(&verifier_bond_state, verifier_bond)?; + + { + let protocol_config_data = protocol_config.try_borrow()?; + let protocol_config_state = + ProtocolConfig::decode(protocol_config_data.as_ref())?; + validate_protocol_config(&protocol_config_state, authority)?; + validate_verifier_can_be_added( + &protocol_config_state, + &verifier_bond_state, + )?; + } + + let verifier_identity = verifier_bond_state.verifier_identity(); + let verifier_bond_key = verifier_bond.address(); + + { + let verifier_registry_data = verifier_registry.try_borrow()?; + let verifier_registry_view = + VerifierRegistry::decode(verifier_registry_data.as_ref())?; + require!( + verifier_registry_view.discriminator() + == VerifierRegistry::DISCRIMINATOR, + ProgramError::InvalidAccountData + ); + // CHECKPOINT: this treats verifier identity and verifier bond as + // separate unique registry keys. Revisit if bond rotation should keep + // the same identity entry instead of rejecting either duplicate. + require!( + !verifier_registry_view.entries().iter().any(|entry| { + entry.verifier_identity() == verifier_identity + || entry.verifier_bond() == verifier_bond_key + }), + ProgramError::AccountAlreadyInitialized + ); + } + + // CHECKPOINT: this single-PDA Vec is only suitable while the verifier set + // is small. Before allowing unbounded growth, cap the entry count or + // replace this with paged storage / Merkle-root based membership. + let old_registry_len = verifier_registry.data_len(); + let mut verifier_registry_state = + VerifierRegistry::decode_mut(verifier_registry)?; + + verifier_registry_state + .entries_mut()? + .push(&VerifierRegistryEntry { + verifier_identity: *verifier_identity, + verifier_bond: *verifier_bond_key, + weight: args.weight(), + })?; + + let new_registry_len = verifier_registry.data_len(); + if new_registry_len > old_registry_len { + top_up_pda_rent(authority, verifier_registry, new_registry_len)?; + } + + Ok(()) +} + +fn validate_update_args( + args: &UpdateVerifierRegistryArgsView<'_>, +) -> ProgramResult { + // CHECKPOINT: implement `VerifierRegistryAction::Remove` when + // withdrawal/removal rules are finalized. + require!( + args.action() == VerifierRegistryAction::Add.value(), + ProgramError::InvalidInstructionData + ); + // MVP verifier selection is equal-weight round-robin, so the only + // meaningful weight until weighted selection exists is 1. + require_eq!(args.weight(), 1_u64, ProgramError::InvalidInstructionData); + + Ok(()) +} + +fn validate_protocol_config( + protocol_config: &ProtocolConfigView<'_>, + authority: &AccountView, +) -> ProgramResult { + require!( + protocol_config.discriminator() == ProtocolConfig::DISCRIMINATOR, + ProgramError::InvalidAccountData + ); + require_eq_keys!( + protocol_config.authority(), + authority.address(), + DlpError::InvalidAuthority + ); + + Ok(()) +} + +fn validate_verifier_bond( + verifier_bond: &VerifierBondView<'_>, + verifier_bond_account: &AccountView, +) -> ProgramResult { + require!( + verifier_bond.discriminator() == VerifierBond::DISCRIMINATOR, + ProgramError::InvalidAccountData + ); + require_pda( + verifier_bond_account, + &[ + VERIFIER_BOND_SEED, + verifier_bond.verifier_identity().as_ref(), + ], + &crate::fast::ID, + false, + "verifier bond", + )?; + + Ok(()) +} + +fn validate_verifier_can_be_added( + protocol_config: &ProtocolConfigView<'_>, + verifier_bond: &VerifierBondView<'_>, +) -> ProgramResult { + require_eq!( + verifier_bond.status(), + VerifierStatus::Active.value(), + ProgramError::InvalidInstructionData + ); + require_ge!( + verifier_bond.stake_lamports(), + protocol_config.min_verifier_bond(), + ProgramError::InvalidInstructionData + ); + require!( + verifier_bond.withdraw_requested_slot().is_none(), + ProgramError::InvalidInstructionData + ); + + Ok(()) +} diff --git a/src/v2/processor/mod.rs b/src/v2/processor/mod.rs index fceada61..3b4f3ea3 100644 --- a/src/v2/processor/mod.rs +++ b/src/v2/processor/mod.rs @@ -22,5 +22,8 @@ pub fn process_instruction( DlpV2Instruction::RegisterVerifier => { process_register_verifier(accounts, data) } + DlpV2Instruction::UpdateVerifierRegistry => { + process_update_verifier_registry(accounts, data) + } } } diff --git a/tests/test_v2_update_verifier_registry.rs b/tests/test_v2_update_verifier_registry.rs new file mode 100644 index 00000000..f6a61a55 --- /dev/null +++ b/tests/test_v2_update_verifier_registry.rs @@ -0,0 +1,347 @@ +use dlp_api::v2::{ + instruction_builder::{register_verifier, update_verifier_registry}, + pda::{verifier_bond_pda, verifier_registry_pda, VERIFIER_REGISTRY_SEED}, + RegisterVerifierArgs, UpdateVerifierRegistryArgs, VerifierRegistry, + VerifierRegistryAction, +}; +use solana_program::{native_token::LAMPORTS_PER_SOL, pubkey::Pubkey}; +use solana_program_test::ProgramTestBanksClientExt; +use solana_sdk::{ + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use solana_system_interface::instruction as system_instruction; +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_verifier_registry_adds_verifier() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + let config_args = valid_protocol_config_args(); + let verifier = Keypair::new(); + let (_, expected_verifier_registry_bump) = + Pubkey::find_program_address(&[VERIFIER_REGISTRY_SEED], &dlp_api::id()); + + initialize_protocol_config( + &banks, + &payer, + &authority, + blockhash, + config_args.clone(), + ) + .await; + fund_and_register_verifier( + &banks, + &payer, + &verifier, + &authority, + config_args.min_verifier_bond, + ) + .await; + + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = update_verifier_registry( + authority.pubkey(), + verifier.pubkey(), + UpdateVerifierRegistryArgs { + action: VerifierRegistryAction::Add.value(), + weight: 1, + }, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &authority], + blockhash, + ); + + assert!(banks.process_transaction(tx).await.is_ok()); + + let verifier_registry_account = banks + .get_account(verifier_registry_pda()) + .await + .unwrap() + .unwrap(); + let verifier_registry = + VerifierRegistry::decode(&verifier_registry_account.data).unwrap(); + + assert_eq!( + verifier_registry.discriminator(), + VerifierRegistry::DISCRIMINATOR + ); + assert_eq!(verifier_registry.bump(), expected_verifier_registry_bump); + assert_eq!(verifier_registry.entries().len(), 1); + let entry = verifier_registry.entries().iter().next().unwrap(); + assert_eq!(*entry.verifier_identity(), verifier.pubkey()); + assert_eq!( + *entry.verifier_bond(), + verifier_bond_pda(&verifier.pubkey()) + ); + assert_eq!(entry.weight(), 1); +} + +#[tokio::test] +async fn test_update_verifier_registry_fails_twice() { + let (mut banks, payer, authority, blockhash) = + setup_program_test_env().await; + let config_args = valid_protocol_config_args(); + let verifier = Keypair::new(); + + initialize_protocol_config( + &banks, + &payer, + &authority, + blockhash, + config_args.clone(), + ) + .await; + fund_and_register_verifier( + &banks, + &payer, + &verifier, + &authority, + config_args.min_verifier_bond, + ) + .await; + add_verifier_to_registry(&banks, &payer, &verifier, &authority, 1).await; + + let latest_blockhash = banks.get_latest_blockhash().await.unwrap(); + let blockhash = banks + .get_new_latest_blockhash(&latest_blockhash) + .await + .unwrap(); + let ix = update_verifier_registry( + authority.pubkey(), + verifier.pubkey(), + UpdateVerifierRegistryArgs { + action: VerifierRegistryAction::Add.value(), + weight: 1, + }, + ); + 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_verifier_registry_fails_with_wrong_authority() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + let config_args = valid_protocol_config_args(); + let verifier = Keypair::new(); + + initialize_protocol_config( + &banks, + &payer, + &authority, + blockhash, + config_args.clone(), + ) + .await; + fund_and_register_verifier( + &banks, + &payer, + &verifier, + &authority, + config_args.min_verifier_bond, + ) + .await; + + let wrong_authority = Keypair::new(); + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = update_verifier_registry( + wrong_authority.pubkey(), + verifier.pubkey(), + UpdateVerifierRegistryArgs { + action: VerifierRegistryAction::Add.value(), + weight: 1, + }, + ); + 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_verifier_registry_fails_with_invalid_weight() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + let config_args = valid_protocol_config_args(); + let verifier = Keypair::new(); + + initialize_protocol_config( + &banks, + &payer, + &authority, + blockhash, + config_args.clone(), + ) + .await; + fund_and_register_verifier( + &banks, + &payer, + &verifier, + &authority, + config_args.min_verifier_bond, + ) + .await; + + { + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = update_verifier_registry( + authority.pubkey(), + verifier.pubkey(), + UpdateVerifierRegistryArgs { + action: VerifierRegistryAction::Add.value(), + weight: 0, + }, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &authority], + blockhash, + ); + + assert!(banks.process_transaction(tx).await.is_err()); + } + + { + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = update_verifier_registry( + authority.pubkey(), + verifier.pubkey(), + UpdateVerifierRegistryArgs { + action: VerifierRegistryAction::Add.value(), + weight: 2, + }, + ); + 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_verifier_registry_fails_with_remove_action() { + let (banks, payer, authority, blockhash) = setup_program_test_env().await; + let config_args = valid_protocol_config_args(); + let verifier = Keypair::new(); + + initialize_protocol_config( + &banks, + &payer, + &authority, + blockhash, + config_args.clone(), + ) + .await; + fund_and_register_verifier( + &banks, + &payer, + &verifier, + &authority, + config_args.min_verifier_bond, + ) + .await; + + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = update_verifier_registry( + authority.pubkey(), + verifier.pubkey(), + UpdateVerifierRegistryArgs { + action: VerifierRegistryAction::Remove.value(), + weight: 1, + }, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &authority], + blockhash, + ); + + assert!(banks.process_transaction(tx).await.is_err()); +} + +async fn fund_and_register_verifier( + banks: &solana_program_test::BanksClient, + payer: &Keypair, + verifier: &Keypair, + authority: &Keypair, + stake_lamports: u64, +) { + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = system_instruction::transfer( + &payer.pubkey(), + &verifier.pubkey(), + LAMPORTS_PER_SOL, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[payer], + blockhash, + ); + banks.process_transaction(tx).await.unwrap(); + + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = register_verifier( + verifier.pubkey(), + authority.pubkey(), + RegisterVerifierArgs { stake_lamports }, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[payer, verifier, authority], + blockhash, + ); + + banks.process_transaction(tx).await.unwrap(); +} + +async fn add_verifier_to_registry( + banks: &solana_program_test::BanksClient, + payer: &Keypair, + verifier: &Keypair, + authority: &Keypair, + weight: u64, +) { + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = update_verifier_registry( + authority.pubkey(), + verifier.pubkey(), + UpdateVerifierRegistryArgs { + action: VerifierRegistryAction::Add.value(), + weight, + }, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[payer, authority], + blockhash, + ); + + banks.process_transaction(tx).await.unwrap(); +}