diff --git a/dlp-api/src/v2/instruction.rs b/dlp-api/src/v2/instruction.rs index 799825a9..c80f685a 100644 --- a/dlp-api/src/v2/instruction.rs +++ b/dlp-api/src/v2/instruction.rs @@ -25,6 +25,8 @@ pub enum DlpV2Instruction { /// InitStateBuffer takes more arguments and AppendStateBuffer takes as less as /// possible. WriteStateBuffer = 107, + /// Applies an approved v2 commitment to the delegated account. + FinalizeCommitment = 108, } impl DlpV2Instruction { diff --git a/dlp-api/src/v2/instruction_builder/finalize_commitment.rs b/dlp-api/src/v2/instruction_builder/finalize_commitment.rs new file mode 100644 index 00000000..ff45c6a0 --- /dev/null +++ b/dlp-api/src/v2/instruction_builder/finalize_commitment.rs @@ -0,0 +1,62 @@ +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; +use solana_sdk_ids::system_program; + +use crate::{ + compat::{Compatize, Modernize}, + pda::{ + delegation_metadata_pda_from_delegated_account, + delegation_record_pda_from_delegated_account, + }, + v2::{ + pda::{pending_commitment_pda, state_buffer_pda}, + DlpV2Instruction, + }, +}; + +/// Builds the instruction that finalizes one approved v2 commitment. +pub fn finalize_commitment( + operator: Pubkey, + account: Pubkey, + commit_id: u64, +) -> Instruction { + Instruction { + program_id: crate::id().modernize(), + accounts: vec![ + AccountMeta::new(operator, true), + AccountMeta::new( + pending_commitment_pda(&account.compatize(), commit_id) + .modernize(), + false, + ), + AccountMeta::new(account, false), + AccountMeta::new( + delegation_record_pda_from_delegated_account( + &account.compatize(), + ) + .modernize(), + false, + ), + AccountMeta::new( + delegation_metadata_pda_from_delegated_account( + &account.compatize(), + ) + .modernize(), + false, + ), + AccountMeta::new_readonly( + state_buffer_pda( + &account.compatize(), + commit_id, + &operator.compatize(), + ) + .modernize(), + false, + ), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: DlpV2Instruction::FinalizeCommitment.to_vec(), + } +} diff --git a/dlp-api/src/v2/instruction_builder/mod.rs b/dlp-api/src/v2/instruction_builder/mod.rs index 124c5254..f75bcf66 100644 --- a/dlp-api/src/v2/instruction_builder/mod.rs +++ b/dlp-api/src/v2/instruction_builder/mod.rs @@ -1,4 +1,5 @@ mod approve_commitment; +mod finalize_commitment; mod init_protocol_config; mod post_commitment; mod register_operator; @@ -8,6 +9,7 @@ mod update_verifier_registry; mod write_state_buffer; pub use approve_commitment::*; +pub use finalize_commitment::*; pub use init_protocol_config::*; pub use post_commitment::*; pub use register_operator::*; diff --git a/src/v2/processor/fraud_proofs/finalize_commitment.rs b/src/v2/processor/fraud_proofs/finalize_commitment.rs new file mode 100644 index 00000000..e189f200 --- /dev/null +++ b/src/v2/processor/fraud_proofs/finalize_commitment.rs @@ -0,0 +1,421 @@ +use dlp_api::{ + error::DlpError, + state::{DelegationMetadataFast, DelegationRecord, UndelegationRequester}, + v2::{ + pda::{PENDING_COMMITMENT_SEED, STATE_BUFFER_SEED}, + PendingCommitment, SelectedVerifier, StateBuffer, + PENDING_COMMITMENT_STATUS_ACTIVE, PENDING_COMMITMENT_STATUS_FINALIZED, + }, +}; +use pinocchio::{ + address::Address, + error::ProgramError, + sysvars::{clock::Clock, rent::Rent, Sysvar}, + AccountView, ProgramResult, +}; +use pinocchio_system::instructions as system; +use wheels::{ + layout::{Decodable, Encodable}, + require_eq, require_eq_keys, require_ge, require_gt, require_n_accounts, + require_signer, +}; + +use crate::{ + processor::fast::{to_pinocchio_program_error, utils::LamportsOperation}, + requires::{ + require_initialized_delegation_metadata, + require_initialized_delegation_record, require_initialized_pda, + require_owned_pda, + }, +}; + +/// Finalize one approved v2 account-state commitment. +/// +/// Accounts: +/// 0: `[signer, writable]` operator identity and lamport settlement account +/// 1: `[writable]` PendingCommitment PDA +/// 2: `[writable]` delegated account +/// 3: `[writable]` DelegationRecord PDA +/// 4: `[writable]` DelegationMetadata PDA +/// 5: `[]` finalized StateBuffer PDA +/// 6: `[]` system program, required by system CPI +#[inline(never)] +pub fn process_finalize_commitment( + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + let [ + operator, // force multi-line + pending_commitment, + delegated_account, + delegation_record, + delegation_metadata, + state_buffer, + _system_program, + ] = require_n_accounts!(accounts, 7); + + require_eq!(data.len(), 0, ProgramError::InvalidInstructionData); + require_signer!(operator); + if !operator.is_writable() || !delegated_account.is_writable() { + return Err(ProgramError::Immutable); + } + + require_owned_pda( + delegated_account, + &crate::fast::ID, + "delegated account", + )?; + require_owned_pda( + pending_commitment, + &crate::fast::ID, + "pending commitment", + )?; + + let mut pending = load_pending_commitment(pending_commitment)?; + validate_pending_commitment( + &pending, + pending_commitment, + operator, + delegated_account, + )?; + + require_initialized_delegation_record( + delegated_account, + delegation_record, + true, + )?; + require_eq_keys!( + &Address::from(pending.delegation_record.to_bytes()), + delegation_record.address(), + ProgramError::InvalidAccountData + ); + require_initialized_delegation_metadata( + delegated_account, + delegation_metadata, + true, + )?; + + let commit_id_bytes = pending.commit_id.to_le_bytes(); + require_initialized_pda( + state_buffer, + &[ + STATE_BUFFER_SEED, + delegated_account.address().as_ref(), + &commit_id_bytes, + operator.address().as_ref(), + ], + &crate::fast::ID, + false, + "state buffer", + )?; + + let record_lamports = + validate_delegation_record(delegation_record, operator, &pending)?; + validate_delegation_metadata(delegation_metadata, pending.commit_id)?; + + let state_buffer_data = state_buffer.try_borrow()?; + let raw_state = validate_state_buffer( + state_buffer_data.as_ref(), + operator, + delegated_account, + &pending, + )?; + + delegated_account.resize(raw_state.len())?; + settle_lamports( + operator, + delegated_account, + record_lamports, + pending.lamports, + )?; + require_rent_exempt(delegated_account)?; + + delegated_account + .try_borrow_mut()? + .as_mut() + .copy_from_slice(raw_state); + drop(state_buffer_data); + + { + let mut delegation_record_data = delegation_record.try_borrow_mut()?; + let delegation_record_state = + DelegationRecord::try_from_bytes_with_discriminator_mut( + &mut delegation_record_data, + ) + .map_err(to_pinocchio_program_error)?; + delegation_record_state.lamports = pending.lamports; + } + + { + let mut metadata = + DelegationMetadataFast::from_account(delegation_metadata)?; + metadata.set_last_commit_id(pending.commit_id); + } + + pending.status = PENDING_COMMITMENT_STATUS_FINALIZED; + pending.encode_to(pending_commitment.try_borrow_mut()?.as_mut())?; + + Ok(()) +} + +fn load_pending_commitment( + pending_commitment: &AccountView, +) -> Result { + let pending_data = pending_commitment.try_borrow()?; + let pending_view = PendingCommitment::decode(pending_data.as_ref())?; + + if pending_view.discriminator() != PendingCommitment::DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData); + } + + Ok(PendingCommitment { + discriminator: PendingCommitment::DISCRIMINATOR, + status: pending_view.status(), + operator_identity: *pending_view.operator_identity(), + operator_bond: *pending_view.operator_bond(), + account_pubkey: *pending_view.account_pubkey(), + commit_id: pending_view.commit_id(), + delegation_record: *pending_view.delegation_record(), + da_pointer_hash: *pending_view.da_pointer_hash(), + account_state_hash: *pending_view.account_state_hash(), + data_hash: *pending_view.data_hash(), + lamports: pending_view.lamports(), + owner: *pending_view.owner(), + state_commitment_hash: *pending_view.state_commitment_hash(), + verifier_registry: *pending_view.verifier_registry(), + challenge_window_id: pending_view.challenge_window_id(), + posted_slot: pending_view.posted_slot(), + activation_slot: pending_view.activation_slot(), + challenge_window_end_slot: pending_view.challenge_window_end_slot(), + approval_count: pending_view.approval_count(), + approval_threshold: pending_view.approval_threshold(), + active_challenge: pending_view.active_challenge().cloned(), + resolved_state_source: pending_view.resolved_state_source(), + er_slot: pending_view.er_slot(), + _pad_before_selected_verifiers: [0; 7], + selected_verifiers: pending_view + .selected_verifiers() + .iter() + .map(|verifier| SelectedVerifier { + verifier_identity: *verifier.verifier_identity(), + approved: verifier.approved(), + _pad_after_approved: [0; 7], + }) + .collect(), + }) +} + +fn validate_pending_commitment( + pending: &PendingCommitment, + pending_commitment: &AccountView, + operator: &AccountView, + delegated_account: &AccountView, +) -> ProgramResult { + let commit_id_bytes = pending.commit_id.to_le_bytes(); + require_initialized_pda( + pending_commitment, + &[ + PENDING_COMMITMENT_SEED, + pending.account_pubkey.as_ref(), + &commit_id_bytes, + ], + &crate::fast::ID, + true, + "pending commitment", + )?; + require_eq_keys!( + &Address::from(pending.operator_identity.to_bytes()), + operator.address(), + DlpError::InvalidAuthority + ); + require_eq_keys!( + &Address::from(pending.account_pubkey.to_bytes()), + delegated_account.address(), + DlpError::InvalidDelegatedAccount + ); + require_eq!( + pending.status, + PENDING_COMMITMENT_STATUS_ACTIVE, + ProgramError::InvalidInstructionData + ); + require_eq!( + pending.active_challenge.is_none(), + true, + ProgramError::InvalidInstructionData + ); + require_eq!( + pending.resolved_state_source.is_none(), + true, + ProgramError::InvalidInstructionData + ); + require_ge!( + pending.approval_count, + pending.approval_threshold, + ProgramError::InvalidInstructionData + ); + require_eq!( + pending.approval_threshold, + 1, + ProgramError::InvalidAccountData + ); + require_eq!( + pending.selected_verifiers.len(), + 1, + ProgramError::InvalidAccountData + ); + require_eq!( + pending + .selected_verifiers + .get(0) + .ok_or(ProgramError::InvalidAccountData)? + .approved, + true, + ProgramError::InvalidInstructionData + ); + require_gt!( + Clock::get()?.slot, + pending.challenge_window_end_slot, + ProgramError::InvalidInstructionData + ); + + Ok(()) +} + +fn validate_delegation_record( + delegation_record: &AccountView, + operator: &AccountView, + pending: &PendingCommitment, +) -> Result { + let delegation_record_data = delegation_record.try_borrow()?; + let delegation_record_state = + DelegationRecord::try_from_bytes_with_discriminator( + &delegation_record_data, + ) + .map_err(to_pinocchio_program_error)?; + + require_eq_keys!( + &Address::from(delegation_record_state.authority.to_bytes()), + operator.address(), + DlpError::InvalidAuthority + ); + require_eq_keys!( + &Address::from(delegation_record_state.owner.to_bytes()), + &Address::from(pending.owner.to_bytes()), + ProgramError::InvalidAccountData + ); + + Ok(delegation_record_state.lamports) +} + +fn validate_delegation_metadata( + delegation_metadata: &AccountView, + commit_id: u64, +) -> ProgramResult { + let metadata = DelegationMetadataFast::from_account(delegation_metadata)?; + let expected_commit_id = metadata + .last_commit_id() + .checked_add(1) + .ok_or(DlpError::Overflow)?; + require_eq!(commit_id, expected_commit_id, DlpError::NonceOutOfOrder); + + match metadata.undelegation_requester()? { + UndelegationRequester::None => Ok(()), + UndelegationRequester::Validator => { + Err(DlpError::AlreadyUndelegated.into()) + } + UndelegationRequester::OwnerProgram => { + Err(DlpError::OwnerRequestedUndelegation.into()) + } + } +} + +fn validate_state_buffer<'a>( + data: &'a [u8], + operator: &AccountView, + delegated_account: &AccountView, + pending: &PendingCommitment, +) -> Result<&'a [u8], ProgramError> { + let state = StateBuffer::decode(data)?; + + if state.discriminator() != StateBuffer::DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData); + } + require_eq_keys!( + state.authority(), + operator.address(), + DlpError::InvalidAuthority + ); + require_eq_keys!( + state.account_pubkey(), + delegated_account.address(), + DlpError::InvalidDelegatedAccount + ); + require_eq!( + state.commit_id(), + pending.commit_id, + ProgramError::InvalidInstructionData + ); + require_eq!( + state.finalized(), + true, + ProgramError::InvalidInstructionData + ); + require_eq!( + state.payload().len(), + state.total_len() as usize, + ProgramError::InvalidInstructionData + ); + require_eq!( + state.data_hash(), + &pending.data_hash, + ProgramError::InvalidInstructionData + ); + + Ok(state.payload().as_slice()) +} + +fn settle_lamports( + operator: &AccountView, + delegated_account: &AccountView, + record_lamports: u64, + committed_lamports: u64, +) -> ProgramResult { + require_ge!( + delegated_account.lamports(), + record_lamports, + DlpError::InvalidDelegatedState + ); + + match committed_lamports.cmp(&record_lamports) { + std::cmp::Ordering::Greater => { + system::Transfer { + from: operator, + to: delegated_account, + lamports: committed_lamports + .checked_sub(record_lamports) + .ok_or(DlpError::Overflow)?, + } + .invoke()?; + } + std::cmp::Ordering::Less => { + let delta = record_lamports + .checked_sub(committed_lamports) + .ok_or(DlpError::Overflow)?; + delegated_account.lamports_decrement_by(delta)?; + operator.lamports_increment_by(delta)?; + } + std::cmp::Ordering::Equal => {} + } + + Ok(()) +} + +fn require_rent_exempt(account: &AccountView) -> ProgramResult { + require_ge!( + account.lamports(), + Rent::get()?.try_minimum_balance(account.data_len())?, + DlpError::InsufficientRent + ); + + Ok(()) +} diff --git a/src/v2/processor/fraud_proofs/mod.rs b/src/v2/processor/fraud_proofs/mod.rs index e92f6667..7c677870 100644 --- a/src/v2/processor/fraud_proofs/mod.rs +++ b/src/v2/processor/fraud_proofs/mod.rs @@ -1,9 +1,11 @@ //! Processors for v2 fraud-proof instructions. mod approve_commitment; +mod finalize_commitment; mod post_commitment; mod write_state_buffer; pub use approve_commitment::*; +pub use finalize_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 06b40e85..ba9f2bf2 100644 --- a/src/v2/processor/mod.rs +++ b/src/v2/processor/mod.rs @@ -39,5 +39,8 @@ pub fn process_instruction( DlpV2Instruction::WriteStateBuffer => { process_write_state_buffer(accounts, data) } + DlpV2Instruction::FinalizeCommitment => { + process_finalize_commitment(accounts, data) + } } } diff --git a/tests/test_v2_finalize_commitment.rs b/tests/test_v2_finalize_commitment.rs new file mode 100644 index 00000000..7c9d9343 --- /dev/null +++ b/tests/test_v2_finalize_commitment.rs @@ -0,0 +1,590 @@ +use dlp_api::{ + pda::{ + delegation_metadata_pda_from_delegated_account, + delegation_record_pda_from_delegated_account, + }, + state::{DelegationMetadata, DelegationRecord}, + v2::{ + instruction_builder::{ + approve_commitment, finalize_commitment, post_commitment, + register_operator, register_verifier, update_verifier_registry, + write_state_buffer, + }, + pda::pending_commitment_pda, + PendingCommitment, PostCommitmentArgs, RegisterOperatorArgs, + RegisterVerifierArgs, WriteStateBufferArgs, + PENDING_COMMITMENT_STATUS_FINALIZED, VERIFIER_REGISTRY_ACTION_ADD, + }, +}; +use solana_program::native_token::LAMPORTS_PER_SOL; +use solana_program_test::{ + BanksClientError, ProgramTest, ProgramTestBanksClientExt, + ProgramTestContext, +}; +use solana_sdk::{ + account::Account, + hash::Hash, + instruction::Instruction, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use solana_sdk_ids::system_program; +use wheels::layout::Decodable; + +mod fixtures; + +use crate::fixtures::{ + create_delegation_metadata_data, create_delegation_record_data, + v2::{initialize_protocol_config, valid_protocol_config_args}, +}; + +#[tokio::test] +async fn test_finalize_commitment() { + let mut env = setup_finalize_commitment_env( + LAMPORTS_PER_SOL, + LAMPORTS_PER_SOL, + false, + ) + .await; + + post_v2_commitment(&mut env).await.unwrap(); + approve_v2_commitment(&mut env).await.unwrap(); + warp_past_challenge_window(&mut env).await; + finalize_v2_commitment(&mut env).await.unwrap(); + + let pending_commitment = read_pending_commitment(&mut env).await; + assert_eq!( + pending_commitment.status, + PENDING_COMMITMENT_STATUS_FINALIZED + ); + + let delegated_account = env + .context + .banks_client + .get_account(env.delegated_account) + .await + .unwrap() + .unwrap(); + assert_eq!(delegated_account.data, env.final_state_data); + assert_eq!(delegated_account.lamports, env.committed_lamports); + + let delegation_record = read_delegation_record(&mut env).await; + assert_eq!(delegation_record.lamports, env.committed_lamports); + + let delegation_metadata = read_delegation_metadata(&mut env).await; + assert_eq!(delegation_metadata.last_commit_id, env.commit_id); +} + +#[tokio::test] +async fn test_finalize_commitment_fails_before_window_closes() { + let mut env = setup_finalize_commitment_env( + LAMPORTS_PER_SOL, + LAMPORTS_PER_SOL, + false, + ) + .await; + + post_v2_commitment(&mut env).await.unwrap(); + approve_v2_commitment(&mut env).await.unwrap(); + + assert!(finalize_v2_commitment(&mut env).await.is_err()); +} + +#[tokio::test] +async fn test_finalize_commitment_fails_without_approval() { + let mut env = setup_finalize_commitment_env( + LAMPORTS_PER_SOL, + LAMPORTS_PER_SOL, + false, + ) + .await; + + post_v2_commitment(&mut env).await.unwrap(); + warp_past_challenge_window(&mut env).await; + + assert!(finalize_v2_commitment(&mut env).await.is_err()); +} + +#[tokio::test] +async fn test_finalize_commitment_fails_with_wrong_operator() { + let mut env = setup_finalize_commitment_env( + LAMPORTS_PER_SOL, + LAMPORTS_PER_SOL, + false, + ) + .await; + let wrong_operator = Keypair::new(); + add_lamport_account_to_context(&mut env.context, wrong_operator.pubkey()) + .await; + + post_v2_commitment(&mut env).await.unwrap(); + approve_v2_commitment(&mut env).await.unwrap(); + warp_past_challenge_window(&mut env).await; + + let ix = finalize_commitment( + wrong_operator.pubkey(), + env.delegated_account, + env.commit_id, + ); + + assert!(process_ix(&mut env.context, ix, &[&wrong_operator]) + .await + .is_err()); +} + +#[tokio::test] +async fn test_finalize_commitment_fails_with_owner_mismatch() { + let mut env = + setup_finalize_commitment_env(LAMPORTS_PER_SOL, LAMPORTS_PER_SOL, true) + .await; + + post_v2_commitment(&mut env).await.unwrap(); + approve_v2_commitment(&mut env).await.unwrap(); + warp_past_challenge_window(&mut env).await; + + assert!(finalize_v2_commitment(&mut env).await.is_err()); +} + +#[tokio::test] +async fn test_finalize_commitment_lamport_increase() { + let mut env = setup_finalize_commitment_env( + LAMPORTS_PER_SOL, + LAMPORTS_PER_SOL + 1_000, + false, + ) + .await; + + post_v2_commitment(&mut env).await.unwrap(); + approve_v2_commitment(&mut env).await.unwrap(); + warp_past_challenge_window(&mut env).await; + + let operator_balance_before = + balance(&mut env.context, env.operator.pubkey()).await; + finalize_v2_commitment(&mut env).await.unwrap(); + + assert_eq!( + balance(&mut env.context, env.operator.pubkey()).await, + operator_balance_before - 1_000 + ); + assert_eq!( + balance(&mut env.context, env.delegated_account).await, + env.committed_lamports + ); + assert_eq!( + read_delegation_record(&mut env).await.lamports, + env.committed_lamports + ); +} + +#[tokio::test] +async fn test_finalize_commitment_lamport_decrease() { + let mut env = setup_finalize_commitment_env( + LAMPORTS_PER_SOL, + LAMPORTS_PER_SOL - 1_000, + false, + ) + .await; + + post_v2_commitment(&mut env).await.unwrap(); + approve_v2_commitment(&mut env).await.unwrap(); + warp_past_challenge_window(&mut env).await; + + let operator_balance_before = + balance(&mut env.context, env.operator.pubkey()).await; + finalize_v2_commitment(&mut env).await.unwrap(); + + assert_eq!( + balance(&mut env.context, env.operator.pubkey()).await, + operator_balance_before + 1_000 + ); + assert_eq!( + balance(&mut env.context, env.delegated_account).await, + env.committed_lamports + ); + assert_eq!( + read_delegation_record(&mut env).await.lamports, + env.committed_lamports + ); +} + +#[tokio::test] +async fn test_finalize_commitment_fails_twice() { + let mut env = setup_finalize_commitment_env( + LAMPORTS_PER_SOL, + LAMPORTS_PER_SOL, + false, + ) + .await; + + post_v2_commitment(&mut env).await.unwrap(); + approve_v2_commitment(&mut env).await.unwrap(); + warp_past_challenge_window(&mut env).await; + finalize_v2_commitment(&mut env).await.unwrap(); + + assert!(finalize_v2_commitment(&mut env).await.is_err()); +} + +struct FinalizeCommitmentEnv { + context: ProgramTestContext, + operator: Keypair, + verifier: Keypair, + delegated_account: Pubkey, + committed_owner: Pubkey, + committed_lamports: u64, + commit_id: u64, + final_state_data: Vec, +} + +async fn setup_finalize_commitment_env( + record_lamports: u64, + committed_lamports: u64, + owner_mismatch: bool, +) -> FinalizeCommitmentEnv { + 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 verifier = Keypair::new(); + let delegated_account = Pubkey::new_unique(); + let account_owner = Pubkey::new_unique(); + let committed_owner = if owner_mismatch { + Pubkey::new_unique() + } else { + account_owner + }; + let commit_id = 1; + let final_state_data = vec![9, 8, 7, 6, 5]; + + add_lamport_account(&mut program_test, authority.pubkey()); + add_lamport_account(&mut program_test, operator.pubkey()); + add_lamport_account(&mut program_test, verifier.pubkey()); + + program_test.add_account( + delegated_account, + Account { + lamports: record_lamports, + data: vec![1, 2], + 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(), + account_owner, + Some(record_lamports), + ), + owner: dlp_api::ID, + executable: false, + rent_epoch: 0, + }, + ); + program_test.add_account( + delegation_metadata_pda_from_delegated_account(&delegated_account), + Account { + lamports: LAMPORTS_PER_SOL, + data: create_delegation_metadata_data( + authority.pubkey(), + &[], + false, + ), + owner: dlp_api::ID, + executable: false, + rent_epoch: 0, + }, + ); + + let mut context = program_test.start_with_context().await; + let config_args = valid_protocol_config_args(); + initialize_protocol_config( + &context.banks_client, + &context.payer, + &authority, + context.last_blockhash, + config_args.clone(), + ) + .await; + + register_v2_operator( + &mut context, + &operator, + &authority, + config_args.min_operator_bond, + ) + .await + .unwrap(); + register_and_add_v2_verifier( + &mut context, + &verifier, + &authority, + config_args.min_verifier_bond, + ) + .await + .unwrap(); + + write_v2_state_buffer( + &mut context, + &operator, + delegated_account, + WriteStateBufferArgs { + commit_id, + total_len: final_state_data.len() as u32, + offset: 0, + chunk: final_state_data.clone(), + }, + ) + .await + .unwrap(); + + FinalizeCommitmentEnv { + context, + operator, + verifier, + delegated_account, + committed_owner, + committed_lamports, + commit_id, + final_state_data, + } +} + +async fn post_v2_commitment( + env: &mut FinalizeCommitmentEnv, +) -> Result<(), BanksClientError> { + let ix = post_commitment( + env.operator.pubkey(), + env.delegated_account, + PostCommitmentArgs { + commit_id: env.commit_id, + lamports: env.committed_lamports, + owner: env.committed_owner, + da_pointer_hash: [9; 32], + er_slot: Some(42), + }, + ); + + process_ix(&mut env.context, ix, &[&env.operator]).await +} + +async fn approve_v2_commitment( + env: &mut FinalizeCommitmentEnv, +) -> Result<(), BanksClientError> { + let ix = approve_commitment( + env.verifier.pubkey(), + env.delegated_account, + env.commit_id, + ); + + process_ix(&mut env.context, ix, &[&env.verifier]).await +} + +async fn finalize_v2_commitment( + env: &mut FinalizeCommitmentEnv, +) -> Result<(), BanksClientError> { + let ix = finalize_commitment( + env.operator.pubkey(), + env.delegated_account, + env.commit_id, + ); + + process_ix(&mut env.context, ix, &[&env.operator]).await +} + +async fn register_v2_operator( + context: &mut ProgramTestContext, + operator: &Keypair, + authority: &Keypair, + amount_lamports: u64, +) -> Result<(), BanksClientError> { + let ix = register_operator( + operator.pubkey(), + authority.pubkey(), + RegisterOperatorArgs { amount_lamports }, + ); + + process_ix(context, ix, &[operator, authority]).await +} + +async fn register_and_add_v2_verifier( + context: &mut ProgramTestContext, + verifier: &Keypair, + authority: &Keypair, + amount_lamports: u64, +) -> Result<(), BanksClientError> { + let ix = register_verifier( + verifier.pubkey(), + authority.pubkey(), + RegisterVerifierArgs { amount_lamports }, + ); + process_ix(context, ix, &[verifier, authority]).await?; + + let ix = update_verifier_registry( + authority.pubkey(), + verifier.pubkey(), + dlp_api::v2::UpdateVerifierRegistryArgs { + action: VERIFIER_REGISTRY_ACTION_ADD, + weight: 1, + }, + ); + + process_ix(context, ix, &[authority]).await +} + +async fn write_v2_state_buffer( + context: &mut ProgramTestContext, + operator: &Keypair, + account: Pubkey, + args: WriteStateBufferArgs, +) -> Result<(), BanksClientError> { + let ix = write_state_buffer( + context.payer.pubkey(), + operator.pubkey(), + account, + args, + ); + + process_ix(context, ix, &[operator]).await +} + +async fn process_ix( + context: &mut ProgramTestContext, + ix: Instruction, + signers: &[&Keypair], +) -> Result<(), BanksClientError> { + let latest_blockhash: Hash = + context.banks_client.get_latest_blockhash().await.unwrap(); + let blockhash = context + .banks_client + .get_new_latest_blockhash(&latest_blockhash) + .await + .unwrap(); + let tx = { + let mut all_signers = Vec::with_capacity(signers.len() + 1); + all_signers.push(&context.payer); + all_signers.extend_from_slice(signers); + + Transaction::new_signed_with_payer( + &[ix], + Some(&context.payer.pubkey()), + &all_signers, + blockhash, + ) + }; + + context.banks_client.process_transaction(tx).await +} + +async fn warp_past_challenge_window(env: &mut FinalizeCommitmentEnv) { + let pending_commitment = read_pending_commitment(env).await; + env.context + .warp_to_slot(pending_commitment.challenge_window_end_slot + 1) + .unwrap(); +} + +struct PendingCommitmentSnapshot { + status: u8, + challenge_window_end_slot: u64, +} + +async fn read_pending_commitment( + env: &mut FinalizeCommitmentEnv, +) -> PendingCommitmentSnapshot { + let pending_commitment_account = env + .context + .banks_client + .get_account(pending_commitment_pda( + &env.delegated_account, + env.commit_id, + )) + .await + .unwrap() + .unwrap(); + + let pending_commitment = ::decode( + &pending_commitment_account.data, + ) + .unwrap(); + + PendingCommitmentSnapshot { + status: pending_commitment.status(), + challenge_window_end_slot: pending_commitment + .challenge_window_end_slot(), + } +} + +async fn read_delegation_record( + env: &mut FinalizeCommitmentEnv, +) -> DelegationRecord { + let account = env + .context + .banks_client + .get_account(delegation_record_pda_from_delegated_account( + &env.delegated_account, + )) + .await + .unwrap() + .unwrap(); + + *DelegationRecord::try_from_bytes_with_discriminator(&account.data).unwrap() +} + +async fn read_delegation_metadata( + env: &mut FinalizeCommitmentEnv, +) -> DelegationMetadata { + let account = env + .context + .banks_client + .get_account(delegation_metadata_pda_from_delegated_account( + &env.delegated_account, + )) + .await + .unwrap() + .unwrap(); + + DelegationMetadata::try_from_bytes_with_discriminator(&account.data) + .unwrap() +} + +async fn balance(context: &mut ProgramTestContext, pubkey: Pubkey) -> u64 { + context.banks_client.get_balance(pubkey).await.unwrap() +} + +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 add_lamport_account_to_context( + context: &mut ProgramTestContext, + pubkey: Pubkey, +) { + let ix = solana_system_interface::instruction::transfer( + &context.payer.pubkey(), + &pubkey, + LAMPORTS_PER_SOL, + ); + let latest_blockhash = + context.banks_client.get_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&context.payer.pubkey()), + &[&context.payer], + latest_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); +}