diff --git a/src/discriminator.rs b/src/discriminator.rs index 3258e791..1af1d58d 100644 --- a/src/discriminator.rs +++ b/src/discriminator.rs @@ -50,6 +50,9 @@ pub enum DlpDiscriminator { /// See [crate::processor::process_commit_finalize] for docs. CommitFinalize = 21, + + /// See [crate::processor::process_commit_finalize_from_buffer] for docs. + CommitFinalizeFromBuffer = 22, } impl DlpDiscriminator { diff --git a/src/instruction_builder/commit_finalize_from_buffer.rs b/src/instruction_builder/commit_finalize_from_buffer.rs new file mode 100644 index 00000000..2df4e084 --- /dev/null +++ b/src/instruction_builder/commit_finalize_from_buffer.rs @@ -0,0 +1,87 @@ +use solana_program::instruction::Instruction; +use solana_program::system_program; +use solana_program::{instruction::AccountMeta, pubkey::Pubkey}; + +use crate::args::{CommitBumps, CommitFinalizeArgs}; +use crate::discriminator::DlpDiscriminator; +use crate::pod_view::PodView; +use crate::{ + delegation_metadata_seeds_from_delegated_account, + delegation_record_seeds_from_delegated_account, total_size_budget, + validator_fees_vault_seeds_from_validator, AccountSizeClass, DLP_PROGRAM_DATA_SIZE_CLASS, +}; + +/// Builds a commit state from buffer instruction. +/// See [crate::processor::process_commit_diff_from_buffer] for docs. +pub fn commit_finalize_from_buffer( + validator: Pubkey, + delegated_account: Pubkey, + data_buffer: Pubkey, + commit_args: &mut CommitFinalizeArgs, +) -> (Instruction, super::CommitPDAs) { + let delegation_record = Pubkey::find_program_address( + delegation_record_seeds_from_delegated_account!(delegated_account), + &crate::id(), + ); + + let validator_fees_vault = Pubkey::find_program_address( + validator_fees_vault_seeds_from_validator!(validator), + &crate::id(), + ); + + let delegation_metadata = Pubkey::find_program_address( + delegation_metadata_seeds_from_delegated_account!(delegated_account), + &crate::id(), + ); + + // save the bumps in the args + commit_args.bumps = CommitBumps { + delegation_record: delegation_record.1, + delegation_metadata: delegation_metadata.1, + validator_fees_vault: validator_fees_vault.1, + }; + + ( + Instruction { + program_id: crate::id(), + accounts: vec![ + AccountMeta::new_readonly(validator, true), + AccountMeta::new(delegated_account, false), + AccountMeta::new_readonly(delegation_record.0, false), + AccountMeta::new(delegation_metadata.0, false), + AccountMeta::new_readonly(data_buffer, false), + AccountMeta::new_readonly(validator_fees_vault.0, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: [ + DlpDiscriminator::CommitFinalizeFromBuffer.to_vec(), + commit_args.to_bytes(), + ] + .concat(), + }, + super::CommitPDAs { + delegation_record: delegation_record.0, + delegation_metadata: delegation_metadata.0, + validator_fees_vault: validator_fees_vault.0, + }, + ) +} + +/// +/// Returns accounts-data-size budget for commit_diff_from_buffer instruction. +/// +/// This value can be used with ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit +/// +pub fn commit_finalize_from_buffer_size_budget(delegated_account: AccountSizeClass) -> u32 { + total_size_budget(&[ + DLP_PROGRAM_DATA_SIZE_CLASS, + AccountSizeClass::Tiny, // validator + delegated_account, // delegated_account + AccountSizeClass::Tiny, // delegation_record_pda + AccountSizeClass::Tiny, // delegation_metadata_pda + delegated_account, // data_buffer + AccountSizeClass::Tiny, // validator_fees_vault_pda + AccountSizeClass::Tiny, // program_config_pda + AccountSizeClass::Tiny, // system_program + ]) +} diff --git a/src/instruction_builder/mod.rs b/src/instruction_builder/mod.rs index 6d3ea505..66eef03b 100644 --- a/src/instruction_builder/mod.rs +++ b/src/instruction_builder/mod.rs @@ -5,6 +5,7 @@ mod close_validator_fees_vault; mod commit_diff; mod commit_diff_from_buffer; mod commit_finalize; +mod commit_finalize_from_buffer; mod commit_state; mod commit_state_from_buffer; mod delegate; @@ -26,6 +27,7 @@ pub use close_validator_fees_vault::*; pub use commit_diff::*; pub use commit_diff_from_buffer::*; pub use commit_finalize::*; +pub use commit_finalize_from_buffer::*; pub use commit_state::*; pub use commit_state_from_buffer::*; pub use delegate::*; diff --git a/src/lib.rs b/src/lib.rs index a93e3f1c..7e6cf1fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,6 +121,9 @@ pub fn fast_process_instruction( DlpDiscriminator::CommitFinalize => Some(processor::fast::process_commit_finalize( program_id, accounts, data, )), + DlpDiscriminator::CommitFinalizeFromBuffer => Some( + processor::fast::process_commit_finalize_from_buffer(program_id, accounts, data), + ), DlpDiscriminator::Finalize => Some(processor::fast::process_finalize( program_id, accounts, data, )), diff --git a/src/processor/fast/commit_finalize_from_buffer.rs b/src/processor/fast/commit_finalize_from_buffer.rs new file mode 100644 index 00000000..50738c72 --- /dev/null +++ b/src/processor/fast/commit_finalize_from_buffer.rs @@ -0,0 +1,81 @@ +use pinocchio::{account_info::AccountInfo, pubkey::Pubkey, ProgramResult}; +use pinocchio_log::log; + +use crate::args::CommitFinalizeArgs; +use crate::pod_view::PodView; +use crate::processor::fast::internal::{ + process_commit_finalize_internal, CommitFinalizeInternalArgs, +}; +use crate::processor::fast::NewState; +use crate::{require_n_accounts, DiffSet}; + +/// Commit a new state of a delegated PDA +/// +/// Accounts: +/// +/// 0: `[signer]` the validator requesting the commit +/// 1: `[]` the delegated account +/// 2: `[writable]` the PDA storing the new state +/// 3: `[writable]` the PDA storing the commit record +/// 4: `[]` the delegation record +/// 5: `[writable]` the delegation metadata +/// 6: `[]` the validator fees vault +/// +/// Instruction Data: CommitFinalizeArgs +/// +/// Requirements: +/// +/// - delegation record is initialized +/// - delegation metadata is initialized +/// - validator fees vault is initialized +/// - program config is initialized +/// - commit state is uninitialized +/// - commit record is uninitialized +/// - delegated account holds at least the lamports indicated in the delegation record +/// - account was not committed at a later slot +/// +/// Steps: +/// 1. Check that the pda is delegated +/// 2. Init a new PDA to store the new state +/// 3. Copy the new state to the new PDA +/// 4. Init a new PDA to store the record of the new state commitment +pub fn process_commit_finalize_from_buffer( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let [ + validator, // force multi-line + delegated_account, + delegation_record_account, + delegation_metadata_account, + data_account, // full bytes or diff + validator_fees_vault, + _system_program, + ] = require_n_accounts!(accounts, 7); + + let args = CommitFinalizeArgs::try_view_from(data)?; + + let data = data_account.try_borrow_data()?; + let commit_args = CommitFinalizeInternalArgs { + bumps: &args.bumps, + new_state: if args.data_is_diff.is_true() { + let diffset = DiffSet::try_new(data.as_ref())?; + if diffset.segments_count() == 0 { + log!("WARN: noop; empty diff sent"); + } + NewState::Diff(diffset) + } else { + NewState::FullBytes(&data) + }, + commit_id: args.commit_id, + allow_undelegation: args.allow_undelegation.is_true(), + validator, + delegated_account, + delegation_record_account, + delegation_metadata_account, + validator_fees_vault, + }; + + process_commit_finalize_internal(commit_args) +} diff --git a/src/processor/fast/mod.rs b/src/processor/fast/mod.rs index 64cbd7b4..02be473c 100644 --- a/src/processor/fast/mod.rs +++ b/src/processor/fast/mod.rs @@ -1,6 +1,7 @@ mod commit_diff; mod commit_diff_from_buffer; mod commit_finalize; +mod commit_finalize_from_buffer; mod commit_state; mod commit_state_from_buffer; mod delegate; @@ -14,6 +15,7 @@ pub(crate) mod internal; pub use commit_diff::*; pub use commit_diff_from_buffer::*; pub use commit_finalize::*; +pub use commit_finalize_from_buffer::*; pub use commit_state::*; pub use commit_state_from_buffer::*; pub use delegate::*; diff --git a/src/processor/fast/utils/requires.rs b/src/processor/fast/utils/requires.rs index 63373efa..19c66c2a 100644 --- a/src/processor/fast/utils/requires.rs +++ b/src/processor/fast/utils/requires.rs @@ -367,6 +367,8 @@ pub fn require_owned_pda( if !pubkey_eq(info.owner(), owner) { log!("Invalid account owner for {}:", label); pubkey::log(info.key()); + pubkey::log(info.owner()); + pubkey::log(owner); return Err(ProgramError::InvalidAccountOwner); } Ok(()) diff --git a/src/processor/init_validator_fees_vault.rs b/src/processor/init_validator_fees_vault.rs index 4ecd2b9b..e83f63e0 100644 --- a/src/processor/init_validator_fees_vault.rs +++ b/src/processor/init_validator_fees_vault.rs @@ -16,7 +16,7 @@ use crate::validator_fees_vault_seeds_from_validator; /// Accounts: /// /// 0; `[signer]` payer -/// 1; `[signer]` admin that controls the vault +/// 1; `[signer]` magicblock admin that controls the vault /// 2; `[]` validator_identity /// 3; `[]` validator_fees_vault_pda /// 4; `[]` system_program diff --git a/tests/test_commit_finalize.rs b/tests/test_commit_finalize.rs index ff8829fe..bc0b3bfc 100644 --- a/tests/test_commit_finalize.rs +++ b/tests/test_commit_finalize.rs @@ -22,7 +22,7 @@ mod fixtures; #[tokio::test] async fn test_commit_finalize_data_perf() { - run_test_commit_finalize(vec![0; 10240], vec![1; 10240], false, 1100).await; + run_test_commit_finalize(vec![0; 10240], vec![1; 10240], false, 1150).await; } #[tokio::test] diff --git a/tests/test_commit_finalize_from_buffer.rs b/tests/test_commit_finalize_from_buffer.rs new file mode 100644 index 00000000..bdd741fe --- /dev/null +++ b/tests/test_commit_finalize_from_buffer.rs @@ -0,0 +1,232 @@ +use dlp::args::CommitFinalizeArgs; +use dlp::pda::{ + delegation_metadata_pda_from_delegated_account, delegation_record_pda_from_delegated_account, + validator_fees_vault_pda_from_validator, +}; +use dlp::state::DelegationMetadata; +use solana_program::rent::Rent; +use solana_program::{hash::Hash, native_token::LAMPORTS_PER_SOL, system_program}; +use solana_program_test::{BanksClient, BanksTransactionResultWithMetadata, ProgramTest}; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::{ + account::Account, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +use crate::fixtures::{ + get_delegation_metadata_data, get_delegation_record_data, DELEGATED_PDA_ID, TEST_AUTHORITY, +}; + +mod fixtures; + +#[tokio::test] +async fn test_commit_finalize_from_buffer_perf() { + // Setup + let new_state = vec![1; 10240]; + + let (banks, _, authority, blockhash) = + setup_program_test_env(vec![0; 10240], new_state.clone()).await; + + let new_account_balance = 1_000_000; + let state_buffer_pda = Pubkey::find_program_address(&[b"state_buffer"], &authority.pubkey()).0; + + let (ix, pdas) = dlp::instruction_builder::commit_finalize_from_buffer( + authority.pubkey(), + DELEGATED_PDA_ID, + state_buffer_pda, + &mut CommitFinalizeArgs { + commit_id: 1, + allow_undelegation: true.into(), + data_is_diff: false.into(), + lamports: new_account_balance, + bumps: Default::default(), + reserved_padding: Default::default(), + }, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + // execute CommitFinalize and validate CU performmace + { + let BanksTransactionResultWithMetadata { + result: _, + metadata, + } = banks.process_transaction_with_metadata(tx).await.unwrap(); + + let metadata = metadata.unwrap(); + + assertables::assert_lt!(metadata.compute_units_consumed, 1150); + + assert_eq!( + metadata.log_messages.len(), + 3, + "CommitFinalize must not log anything in OK scenario" + ); + } + + let delegated_account = banks.get_account(DELEGATED_PDA_ID).await.unwrap().unwrap(); + assert_eq!(delegated_account.data, new_state); + + let delegation_metadata_account = banks + .get_account(pdas.delegation_metadata) + .await + .unwrap() + .unwrap(); + + let delegation_metadata = + DelegationMetadata::try_from_bytes_with_discriminator(&delegation_metadata_account.data) + .unwrap(); + + assert_eq!(delegation_metadata.is_undelegatable, true); +} + +#[tokio::test] +async fn test_commit_finalize_from_buffer_out_of_order() { + // Setup + let (banks, _, authority, blockhash) = + setup_program_test_env(vec![], vec![0, 1, 2, 9, 9, 9, 6, 7, 8, 9]).await; + + let state_buffer_pda = Pubkey::find_program_address(&[b"state_buffer"], &authority.pubkey()).0; + let new_account_balance = 1_000_000; + + let (ix, _pdas) = dlp::instruction_builder::commit_finalize_from_buffer( + authority.pubkey(), + DELEGATED_PDA_ID, + state_buffer_pda, + &mut CommitFinalizeArgs { + commit_id: 2, // this is the min value which will cause NonceOutOfOrder + allow_undelegation: true.into(), + data_is_diff: false.into(), + lamports: new_account_balance, + bumps: Default::default(), + reserved_padding: Default::default(), + }, + ); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&authority.pubkey()), + &[&authority], + blockhash, + ); + + let BanksTransactionResultWithMetadata { result, metadata } = + banks.process_transaction_with_metadata(tx).await.unwrap(); + + let metadata = metadata.unwrap(); + + let log = metadata + .log_messages + .iter() + .find(|log| log.contains("require")) + .unwrap(); + + assert_eq!( + log, + "Program log: require_eq!(args.commit_id, prev_id + 1) failed: 2 == 1" + ); + + assert!( + metadata + .log_messages + .iter() + .any(|log| log.contains("NonceOutOfOrder")), + "{:#?}", + metadata + ); + + assert_eq!( + result.unwrap_err().to_string(), + "Error processing Instruction 0: custom program error: 0xc" + ); +} + +async fn setup_program_test_env( + pda_data: Vec, + pda_new_state: Vec, +) -> (BanksClient, Keypair, Keypair, Hash) { + let mut program_test = ProgramTest::new("dlp", dlp::ID, None); + program_test.prefer_bpf(true); + + let validator_keypair = Keypair::from_bytes(&TEST_AUTHORITY).unwrap(); + + program_test.add_account( + validator_keypair.pubkey(), + Account { + lamports: 10 * LAMPORTS_PER_SOL, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup a delegated PDA + program_test.add_account( + DELEGATED_PDA_ID, + Account { + lamports: LAMPORTS_PER_SOL, + data: pda_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated account metadata PDA + let delegation_metadata_data = get_delegation_metadata_data(validator_keypair.pubkey(), None); + program_test.add_account( + delegation_metadata_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(delegation_metadata_data.len()), + data: delegation_metadata_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the delegated record PDA + let delegation_record_data = get_delegation_record_data(validator_keypair.pubkey(), None); + program_test.add_account( + delegation_record_pda_from_delegated_account(&DELEGATED_PDA_ID), + Account { + lamports: Rent::default().minimum_balance(delegation_record_data.len()), + data: delegation_record_data, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Setup the validator fees vault + program_test.add_account( + validator_fees_vault_pda_from_validator(&validator_keypair.pubkey()), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![], + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + program_test.add_account( + Pubkey::find_program_address(&[b"state_buffer"], &validator_keypair.pubkey()).0, + Account { + lamports: LAMPORTS_PER_SOL, + data: pda_new_state, + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (banks, payer, blockhash) = program_test.start().await; + (banks, payer, validator_keypair, blockhash) +}