diff --git a/dlp-api/src/v2/instruction.rs b/dlp-api/src/v2/instruction.rs index 92ba8dd4..799825a9 100644 --- a/dlp-api/src/v2/instruction.rs +++ b/dlp-api/src/v2/instruction.rs @@ -17,6 +17,8 @@ pub enum DlpV2Instruction { UpdateProtocolConfig = 104, /// Posts a new v2 account-state commitment. PostCommitment = 105, + /// Records approval from the selected verifier for a v2 commitment. + ApproveCommitment = 106, /// Writes full account-state bytes into a v2 state buffer. /// /// TODO (snawaz/optimization): we can split this into two instructions such that diff --git a/dlp-api/src/v2/instruction_builder/approve_commitment.rs b/dlp-api/src/v2/instruction_builder/approve_commitment.rs new file mode 100644 index 00000000..f88af384 --- /dev/null +++ b/dlp-api/src/v2/instruction_builder/approve_commitment.rs @@ -0,0 +1,36 @@ +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; + +use crate::{ + compat::{Compatize, Modernize}, + v2::{ + pda::{pending_commitment_pda, verifier_bond_pda}, + DlpV2Instruction, + }, +}; + +/// Builds the instruction that approves one v2 account-state commitment. +pub fn approve_commitment( + verifier: Pubkey, + account: Pubkey, + commit_id: u64, +) -> Instruction { + Instruction { + program_id: crate::id().modernize(), + accounts: vec![ + AccountMeta::new_readonly(verifier, true), + AccountMeta::new_readonly( + verifier_bond_pda(&verifier.compatize()).modernize(), + false, + ), + AccountMeta::new( + pending_commitment_pda(&account.compatize(), commit_id) + .modernize(), + false, + ), + ], + data: DlpV2Instruction::ApproveCommitment.to_vec(), + } +} diff --git a/dlp-api/src/v2/instruction_builder/mod.rs b/dlp-api/src/v2/instruction_builder/mod.rs index 44641566..124c5254 100644 --- a/dlp-api/src/v2/instruction_builder/mod.rs +++ b/dlp-api/src/v2/instruction_builder/mod.rs @@ -1,3 +1,4 @@ +mod approve_commitment; mod init_protocol_config; mod post_commitment; mod register_operator; @@ -6,6 +7,7 @@ mod update_protocol_config; mod update_verifier_registry; mod write_state_buffer; +pub use approve_commitment::*; pub use init_protocol_config::*; pub use post_commitment::*; pub use register_operator::*; diff --git a/src/v2/processor/fraud_proofs/approve_commitment.rs b/src/v2/processor/fraud_proofs/approve_commitment.rs new file mode 100644 index 00000000..cc35d8c7 --- /dev/null +++ b/src/v2/processor/fraud_proofs/approve_commitment.rs @@ -0,0 +1,206 @@ +use dlp_api::{ + error::DlpError, + v2::{ + pda::{PENDING_COMMITMENT_SEED, VERIFIER_BOND_SEED}, + PendingCommitment, SelectedVerifier, VerifierBond, VerifierStatus, + PENDING_COMMITMENT_STATUS_ACTIVE, + }, +}; +use pinocchio::{ + address::Address, + error::ProgramError, + sysvars::{clock::Clock, Sysvar}, + AccountView, ProgramResult, +}; +use wheels::{ + layout::{Decodable, Encodable}, + require_eq, require_eq_keys, require_le, require_n_accounts, + require_signer, +}; + +use crate::requires::{require_initialized_pda, require_owned_pda}; + +/// Approve one v2 account-state commitment. +/// +/// Accounts: +/// 0: `[signer]` selected verifier identity +/// 1: `[]` VerifierBond PDA +/// 2: `[writable]` PendingCommitment PDA +#[inline(never)] +pub fn process_approve_commitment( + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + let [ + verifier, // force multi-line + verifier_bond, + pending_commitment, + ] = require_n_accounts!(accounts, 3); + + require_eq!(data.len(), 0, ProgramError::InvalidInstructionData); + require_signer!(verifier); + + require_initialized_pda( + verifier_bond, + &[VERIFIER_BOND_SEED, verifier.address().as_ref()], + &crate::fast::ID, + false, + "verifier bond", + )?; + require_owned_pda( + pending_commitment, + &crate::fast::ID, + "pending commitment", + )?; + if !pending_commitment.is_writable() { + return Err(ProgramError::Immutable); + } + + 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)?; + drop(verifier_bond_data); + + let pending_data = pending_commitment.try_borrow()?; + let pending_state = PendingCommitment::decode(pending_data.as_ref())?; + validate_pending_commitment(&pending_state, pending_commitment, verifier)?; + + let selected_verifier = pending_state + .selected_verifiers() + .get(0) + .ok_or(ProgramError::InvalidAccountData)?; + if selected_verifier.approved() { + require_eq!( + pending_state.approval_count(), + 1, + ProgramError::InvalidAccountData + ); + return Ok(()); + } + + require_eq!( + pending_state.approval_count(), + 0, + ProgramError::InvalidAccountData + ); + + let updated_pending = PendingCommitment { + discriminator: PendingCommitment::DISCRIMINATOR, + status: pending_state.status(), + operator_identity: *pending_state.operator_identity(), + operator_bond: *pending_state.operator_bond(), + account_pubkey: *pending_state.account_pubkey(), + commit_id: pending_state.commit_id(), + delegation_record: *pending_state.delegation_record(), + da_pointer_hash: *pending_state.da_pointer_hash(), + account_state_hash: *pending_state.account_state_hash(), + data_hash: *pending_state.data_hash(), + lamports: pending_state.lamports(), + owner: *pending_state.owner(), + state_commitment_hash: *pending_state.state_commitment_hash(), + verifier_registry: *pending_state.verifier_registry(), + challenge_window_id: pending_state.challenge_window_id(), + posted_slot: pending_state.posted_slot(), + activation_slot: pending_state.activation_slot(), + challenge_window_end_slot: pending_state.challenge_window_end_slot(), + approval_count: 1, + approval_threshold: pending_state.approval_threshold(), + active_challenge: pending_state.active_challenge().cloned(), + resolved_state_source: pending_state.resolved_state_source(), + er_slot: pending_state.er_slot(), + _pad_before_selected_verifiers: [0; 7], + selected_verifiers: vec![SelectedVerifier { + verifier_identity: *selected_verifier.verifier_identity(), + approved: true, + _pad_after_approved: [0; 7], + }], + }; + drop(pending_data); + + updated_pending.encode_to(pending_commitment.try_borrow_mut()?.as_mut())?; + + Ok(()) +} + +fn validate_verifier_bond( + verifier_bond: &dlp_api::v2::VerifierBondView<'_>, + verifier: &AccountView, +) -> ProgramResult { + if verifier_bond.discriminator() != VerifierBond::DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData); + } + require_eq_keys!( + &Address::from(verifier_bond.verifier_identity().to_bytes()), + verifier.address(), + DlpError::InvalidAuthority + ); + require_eq!( + verifier_bond.status(), + VerifierStatus::Active.value(), + ProgramError::InvalidInstructionData + ); + require_eq!( + verifier_bond.withdraw_requested_slot().is_none(), + true, + ProgramError::InvalidInstructionData + ); + + Ok(()) +} + +fn validate_pending_commitment( + pending_commitment: &dlp_api::v2::PendingCommitmentView<'_>, + pending_commitment_account: &AccountView, + verifier: &AccountView, +) -> ProgramResult { + if pending_commitment.discriminator() != PendingCommitment::DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData); + } + + let commit_id_bytes = pending_commitment.commit_id().to_le_bytes(); + require_initialized_pda( + pending_commitment_account, + &[ + PENDING_COMMITMENT_SEED, + pending_commitment.account_pubkey().as_ref(), + &commit_id_bytes, + ], + &crate::fast::ID, + true, + "pending commitment", + )?; + + require_eq!( + pending_commitment.status(), + PENDING_COMMITMENT_STATUS_ACTIVE, + ProgramError::InvalidInstructionData + ); + require_le!( + Clock::get()?.slot, + pending_commitment.challenge_window_end_slot(), + ProgramError::InvalidInstructionData + ); + require_eq!( + pending_commitment.selected_verifiers().len(), + 1, + ProgramError::InvalidAccountData + ); + require_eq!( + pending_commitment.approval_threshold(), + 1, + ProgramError::InvalidAccountData + ); + + let selected_verifier = pending_commitment + .selected_verifiers() + .get(0) + .ok_or(ProgramError::InvalidAccountData)?; + require_eq_keys!( + &Address::from(selected_verifier.verifier_identity().to_bytes()), + verifier.address(), + DlpError::InvalidAuthority + ); + + Ok(()) +} diff --git a/src/v2/processor/fraud_proofs/mod.rs b/src/v2/processor/fraud_proofs/mod.rs index e702dde7..e92f6667 100644 --- a/src/v2/processor/fraud_proofs/mod.rs +++ b/src/v2/processor/fraud_proofs/mod.rs @@ -1,7 +1,9 @@ //! Processors for v2 fraud-proof instructions. +mod approve_commitment; mod post_commitment; mod write_state_buffer; +pub use approve_commitment::*; pub use post_commitment::*; pub use write_state_buffer::*; diff --git a/src/v2/processor/mod.rs b/src/v2/processor/mod.rs index 4c2e6333..06b40e85 100644 --- a/src/v2/processor/mod.rs +++ b/src/v2/processor/mod.rs @@ -33,6 +33,9 @@ pub fn process_instruction( DlpV2Instruction::PostCommitment => { process_post_commitment(accounts, data) } + DlpV2Instruction::ApproveCommitment => { + process_approve_commitment(accounts, data) + } DlpV2Instruction::WriteStateBuffer => { process_write_state_buffer(accounts, data) } diff --git a/tests/test_v2_approve_commitment.rs b/tests/test_v2_approve_commitment.rs new file mode 100644 index 00000000..954f61bc --- /dev/null +++ b/tests/test_v2_approve_commitment.rs @@ -0,0 +1,509 @@ +use dlp_api::{ + pda::delegation_record_pda_from_delegated_account, + v2::{ + instruction_builder::{ + approve_commitment, post_commitment, register_operator, + register_verifier, update_verifier_registry, write_state_buffer, + }, + pda::pending_commitment_pda, + PendingCommitment, PostCommitmentArgs, RegisterOperatorArgs, + RegisterVerifierArgs, WriteStateBufferArgs, + VERIFIER_REGISTRY_ACTION_ADD, + }, +}; +use solana_program::native_token::LAMPORTS_PER_SOL; +use solana_program_test::{ + BanksClient, ProgramTest, ProgramTestBanksClientExt, +}; +use solana_sdk::{ + account::Account, + hash::Hash, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use solana_sdk_ids::system_program; +use wheels::layout::Decodable; + +mod fixtures; + +use crate::fixtures::{ + create_delegation_record_data, + v2::{initialize_protocol_config, valid_protocol_config_args}, +}; + +#[tokio::test] +async fn test_approve_commitment() { + let mut env = setup_approve_commitment_env(1).await; + let args = valid_post_commitment_args(); + + post_v2_commitment( + &mut env.banks, + &env.payer, + &env.operator, + env.delegated_account, + args.clone(), + ) + .await + .unwrap(); + approve_v2_commitment( + &mut env.banks, + &env.payer, + &env.verifiers[0], + env.delegated_account, + args.commit_id, + ) + .await + .unwrap(); + + let pending_commitment_data = read_pending_commitment_data( + &mut env.banks, + env.delegated_account, + args.commit_id, + ) + .await; + let pending_commitment = + ::decode(&pending_commitment_data) + .unwrap(); + assert_eq!(pending_commitment.approval_count(), 1); + assert_eq!(pending_commitment.selected_verifiers().len(), 1); + + let selected_verifier = + pending_commitment.selected_verifiers().get(0).unwrap(); + assert_eq!( + *selected_verifier.verifier_identity(), + env.verifiers[0].pubkey() + ); + assert!(selected_verifier.approved()); +} + +#[tokio::test] +async fn test_approve_commitment_duplicate_is_noop() { + let mut env = setup_approve_commitment_env(1).await; + let args = valid_post_commitment_args(); + + post_v2_commitment( + &mut env.banks, + &env.payer, + &env.operator, + env.delegated_account, + args.clone(), + ) + .await + .unwrap(); + approve_v2_commitment( + &mut env.banks, + &env.payer, + &env.verifiers[0], + env.delegated_account, + args.commit_id, + ) + .await + .unwrap(); + approve_v2_commitment( + &mut env.banks, + &env.payer, + &env.verifiers[0], + env.delegated_account, + args.commit_id, + ) + .await + .unwrap(); + + let pending_commitment_data = read_pending_commitment_data( + &mut env.banks, + env.delegated_account, + args.commit_id, + ) + .await; + let pending_commitment = + ::decode(&pending_commitment_data) + .unwrap(); + assert_eq!(pending_commitment.approval_count(), 1); + assert!(pending_commitment + .selected_verifiers() + .get(0) + .unwrap() + .approved()); +} + +#[tokio::test] +async fn test_approve_commitment_fails_with_wrong_verifier() { + let mut env = setup_approve_commitment_env(2).await; + let args = valid_post_commitment_args(); + + post_v2_commitment( + &mut env.banks, + &env.payer, + &env.operator, + env.delegated_account, + args.clone(), + ) + .await + .unwrap(); + + assert!(approve_v2_commitment( + &mut env.banks, + &env.payer, + &env.verifiers[1], + env.delegated_account, + args.commit_id, + ) + .await + .is_err()); +} + +#[tokio::test] +async fn test_approve_commitment_fails_without_verifier_signature() { + let mut env = setup_approve_commitment_env(1).await; + let args = valid_post_commitment_args(); + + post_v2_commitment( + &mut env.banks, + &env.payer, + &env.operator, + env.delegated_account, + args.clone(), + ) + .await + .unwrap(); + + let mut ix = approve_commitment( + env.verifiers[0].pubkey(), + env.delegated_account, + args.commit_id, + ); + ix.accounts[0].is_signer = false; + + let blockhash = env.banks.get_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&env.payer.pubkey()), + &[&env.payer], + blockhash, + ); + + assert!(env.banks.process_transaction(tx).await.is_err()); +} + +#[tokio::test] +async fn test_approve_commitment_fails_with_instruction_data() { + let mut env = setup_approve_commitment_env(1).await; + let args = valid_post_commitment_args(); + + post_v2_commitment( + &mut env.banks, + &env.payer, + &env.operator, + env.delegated_account, + args.clone(), + ) + .await + .unwrap(); + + let mut ix = approve_commitment( + env.verifiers[0].pubkey(), + env.delegated_account, + args.commit_id, + ); + ix.data.push(1); + + let blockhash = env.banks.get_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&env.payer.pubkey()), + &[&env.payer, &env.verifiers[0]], + blockhash, + ); + + assert!(env.banks.process_transaction(tx).await.is_err()); +} + +struct ApproveCommitmentEnv { + banks: BanksClient, + payer: Keypair, + operator: Keypair, + verifiers: Vec, + delegated_account: Pubkey, +} + +async fn setup_approve_commitment_env( + verifier_count: usize, +) -> ApproveCommitmentEnv { + let mut program_test = ProgramTest::new("dlp", dlp_api::ID, None); + program_test.prefer_bpf(true); + + let authority = Keypair::new(); + let operator = Keypair::new(); + let verifiers = (0..verifier_count) + .map(|_| Keypair::new()) + .collect::>(); + let delegated_account = Pubkey::new_unique(); + + add_lamport_account(&mut program_test, authority.pubkey()); + add_lamport_account(&mut program_test, operator.pubkey()); + for verifier in verifiers.iter() { + add_lamport_account(&mut program_test, verifier.pubkey()); + } + + program_test.add_account( + delegated_account, + Account { + lamports: LAMPORTS_PER_SOL, + data: state_data(), + owner: dlp_api::ID, + executable: false, + rent_epoch: 0, + }, + ); + program_test.add_account( + delegation_record_pda_from_delegated_account(&delegated_account), + Account { + lamports: LAMPORTS_PER_SOL, + data: create_delegation_record_data( + operator.pubkey(), + Pubkey::new_unique(), + Some(LAMPORTS_PER_SOL), + ), + owner: dlp_api::ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks, payer, blockhash) = program_test.start().await; + let config_args = valid_protocol_config_args(); + initialize_protocol_config( + &banks, + &payer, + &authority, + blockhash, + config_args.clone(), + ) + .await; + + register_v2_operator( + &banks, + &payer, + &operator, + &authority, + config_args.min_operator_bond, + ) + .await; + register_and_add_v2_verifier( + &banks, + &payer, + &operator, + &authority, + config_args.min_verifier_bond, + ) + .await; + + for verifier in verifiers.iter() { + register_and_add_v2_verifier( + &banks, + &payer, + verifier, + &authority, + config_args.min_verifier_bond, + ) + .await; + } + + write_v2_state_buffer( + &mut banks, + &payer, + &operator, + delegated_account, + WriteStateBufferArgs { + commit_id: valid_post_commitment_args().commit_id, + total_len: state_data().len() as u32, + offset: 0, + chunk: state_data(), + }, + ) + .await; + + ApproveCommitmentEnv { + banks, + payer, + operator, + verifiers, + delegated_account, + } +} + +fn add_lamport_account(program_test: &mut ProgramTest, pubkey: Pubkey) { + program_test.add_account( + pubkey, + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); +} + +async fn register_v2_operator( + banks: &BanksClient, + payer: &Keypair, + operator: &Keypair, + authority: &Keypair, + amount_lamports: u64, +) { + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = register_operator( + operator.pubkey(), + authority.pubkey(), + RegisterOperatorArgs { amount_lamports }, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[payer, operator, authority], + blockhash, + ); + + banks.process_transaction(tx).await.unwrap(); +} + +async fn register_and_add_v2_verifier( + banks: &BanksClient, + payer: &Keypair, + verifier: &Keypair, + authority: &Keypair, + amount_lamports: u64, +) { + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = register_verifier( + verifier.pubkey(), + authority.pubkey(), + RegisterVerifierArgs { amount_lamports }, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[payer, verifier, authority], + blockhash, + ); + banks.process_transaction(tx).await.unwrap(); + + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = update_verifier_registry( + authority.pubkey(), + verifier.pubkey(), + dlp_api::v2::UpdateVerifierRegistryArgs { + action: VERIFIER_REGISTRY_ACTION_ADD, + weight: 1, + }, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[payer, authority], + blockhash, + ); + + banks.process_transaction(tx).await.unwrap(); +} + +fn valid_post_commitment_args() -> PostCommitmentArgs { + PostCommitmentArgs { + commit_id: 1, + lamports: 1_000, + owner: Pubkey::new_unique(), + da_pointer_hash: [9; 32], + er_slot: Some(42), + } +} + +async fn write_v2_state_buffer( + banks: &mut BanksClient, + payer: &Keypair, + operator: &Keypair, + account: Pubkey, + args: WriteStateBufferArgs, +) { + let latest_blockhash: Hash = banks.get_latest_blockhash().await.unwrap(); + let blockhash = banks + .get_new_latest_blockhash(&latest_blockhash) + .await + .unwrap(); + let ix = + write_state_buffer(payer.pubkey(), operator.pubkey(), account, args); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[payer, operator], + blockhash, + ); + + banks.process_transaction(tx).await.unwrap(); +} + +fn state_data() -> Vec { + vec![1, 2, 3, 4] +} + +async fn post_v2_commitment( + banks: &mut BanksClient, + payer: &Keypair, + operator: &Keypair, + delegated_account: Pubkey, + args: PostCommitmentArgs, +) -> Result<(), solana_program_test::BanksClientError> { + let latest_blockhash: Hash = banks.get_latest_blockhash().await.unwrap(); + let blockhash = banks + .get_new_latest_blockhash(&latest_blockhash) + .await + .unwrap(); + let ix = post_commitment(operator.pubkey(), delegated_account, args); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[payer, operator], + blockhash, + ); + + banks.process_transaction(tx).await +} + +async fn approve_v2_commitment( + banks: &mut BanksClient, + payer: &Keypair, + verifier: &Keypair, + delegated_account: Pubkey, + commit_id: u64, +) -> Result<(), solana_program_test::BanksClientError> { + let latest_blockhash: Hash = banks.get_latest_blockhash().await.unwrap(); + let blockhash = banks + .get_new_latest_blockhash(&latest_blockhash) + .await + .unwrap(); + let ix = + approve_commitment(verifier.pubkey(), delegated_account, commit_id); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[payer, verifier], + blockhash, + ); + + banks.process_transaction(tx).await +} + +async fn read_pending_commitment_data( + banks: &mut BanksClient, + delegated_account: Pubkey, + commit_id: u64, +) -> Vec { + let pending_commitment_account = banks + .get_account(pending_commitment_pda(&delegated_account, commit_id)) + .await + .unwrap() + .unwrap(); + + pending_commitment_account.data +}