diff --git a/src/lib.rs b/src/lib.rs index 73aa1b8d..1ff540f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,6 +109,9 @@ pub fn fast_process_instruction( discriminator::DlpDiscriminator::Delegate => Some(processor::fast::process_delegate( program_id, accounts, data, )), + discriminator::DlpDiscriminator::CommitState => Some( + processor::fast::process_commit_state(program_id, accounts, data), + ), discriminator::DlpDiscriminator::Undelegate => Some(processor::fast::process_undelegate( program_id, accounts, data, )), diff --git a/src/processor/fast/commit_state.rs b/src/processor/fast/commit_state.rs new file mode 100644 index 00000000..2a324007 --- /dev/null +++ b/src/processor/fast/commit_state.rs @@ -0,0 +1,258 @@ +use borsh::BorshDeserialize; +use pinocchio::instruction::Signer; +use pinocchio::pubkey::{self, pubkey_eq}; +use pinocchio::seeds; +use pinocchio::{ + account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; +use pinocchio_log::log; +use pinocchio_system::instructions as system; + +use crate::args::CommitStateArgs; +use crate::error::DlpError; +use crate::pda; +use crate::processor::fast::utils::{ + pda::create_pda, + requires::{ + require_initialized_delegation_metadata, require_initialized_delegation_record, + require_initialized_validator_fees_vault, require_owned_pda, require_program, + require_program_config, require_signer, require_uninitialized_pda, + }, +}; +use crate::state::{CommitRecord, DelegationMetadata, DelegationRecord, ProgramConfig}; + +use super::to_pinocchio_program_error; + +pub fn process_commit_state( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let args = CommitStateArgs::try_from_slice(data).map_err(|_| ProgramError::BorshIoError)?; + + let commit_state_bytes: &[u8] = args.data.as_ref(); + let commit_record_lamports = args.lamports; + let commit_record_nonce = args.nonce; + let allow_undelegation = args.allow_undelegation; + + let [validator, delegated_account, commit_state_account, commit_record_account, delegation_record_account, delegation_metadata_account, validator_fees_vault, program_config_account, system_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let commit_args = CommitStateInternalArgs { + commit_state_bytes, + commit_record_lamports, + commit_record_nonce, + allow_undelegation, + validator, + delegated_account, + commit_state_account, + commit_record_account, + delegation_record_account, + delegation_metadata_account, + validator_fees_vault, + program_config_account, + system_program, + }; + + process_commit_state_internal(commit_args) +} + +/// Arguments for the commit state internal function +pub(crate) struct CommitStateInternalArgs<'a> { + pub(crate) commit_state_bytes: &'a [u8], + pub(crate) commit_record_lamports: u64, + pub(crate) commit_record_nonce: u64, + pub(crate) allow_undelegation: bool, + pub(crate) validator: &'a AccountInfo, + pub(crate) delegated_account: &'a AccountInfo, + pub(crate) commit_state_account: &'a AccountInfo, + pub(crate) commit_record_account: &'a AccountInfo, + pub(crate) delegation_record_account: &'a AccountInfo, + pub(crate) delegation_metadata_account: &'a AccountInfo, + pub(crate) validator_fees_vault: &'a AccountInfo, + pub(crate) program_config_account: &'a AccountInfo, + pub(crate) system_program: &'a AccountInfo, +} + +/// Commit a new state of a delegated Pda +pub(crate) fn process_commit_state_internal( + args: CommitStateInternalArgs, +) -> Result<(), ProgramError> { + // Check that the origin account is delegated + require_owned_pda( + args.delegated_account, + &crate::fast::ID, + "delegated account", + )?; + require_signer(args.validator, "validator account")?; + require_initialized_delegation_record( + args.delegated_account, + args.delegation_record_account, + false, + )?; + require_initialized_delegation_metadata( + args.delegated_account, + args.delegation_metadata_account, + true, + )?; + require_initialized_validator_fees_vault(args.validator, args.validator_fees_vault, false)?; + require_program(args.system_program, &pinocchio_system::ID, "system program")?; + + // Read delegation metadata + let mut delegation_metadata_data = args.delegation_metadata_account.try_borrow_mut_data()?; + let mut delegation_metadata = + DelegationMetadata::try_from_bytes_with_discriminator(&delegation_metadata_data) + .map_err(to_pinocchio_program_error)?; + + // To preserve correct history of account updates we require sequential commits + if args.commit_record_nonce != delegation_metadata.last_update_nonce + 1 { + log!( + "Nonce {} is incorrect, previous nonce is {}. Rejecting commit", + args.commit_record_nonce, + delegation_metadata.last_update_nonce + ); + return Err(DlpError::NonceOutOfOrder.into()); + } + + // Once the account is marked as undelegatable, any subsequent commit should fail + if delegation_metadata.is_undelegatable { + log!("delegation metadata is already undelegated: "); + pubkey::log(args.delegation_metadata_account.key()); + return Err(DlpError::AlreadyUndelegated.into()); + } + + // Update delegation metadata undelegation flag + delegation_metadata.is_undelegatable = args.allow_undelegation; + delegation_metadata + .to_bytes_with_discriminator(&mut delegation_metadata_data.as_mut()) + .map_err(to_pinocchio_program_error)?; + + // Load delegation record + let delegation_record_data = args.delegation_record_account.try_borrow_data()?; + let delegation_record = + DelegationRecord::try_from_bytes_with_discriminator(&delegation_record_data) + .map_err(to_pinocchio_program_error)?; + + // Check that the authority is allowed to commit + if !pubkey_eq(delegation_record.authority.as_array(), args.validator.key()) + && !pubkey_eq(delegation_record.authority.as_array(), &Pubkey::default()) + { + log!("validator is not the delegation authority. validator: "); + pubkey::log(args.validator.key()); + log!("delegation authority: "); + pubkey::log(delegation_record.authority.as_array()); + return Err(DlpError::InvalidAuthority.into()); + } + + // If there was an issue with the lamport accounting in the past, abort (this should never happen) + if args.delegated_account.lamports() < delegation_record.lamports { + log!( + "delegated account has less lamports than the delegation record indicates. delegation account: "); + pubkey::log(args.delegated_account.key()); + return Err(DlpError::InvalidDelegatedState.into()); + } + + // If committed lamports are more than the previous lamports balance, deposit the difference in the commitment account + // If committed lamports are less than the previous lamports balance, we have collateral to settle the balance at state finalization + // We need to do that so that the finalizer already have all the lamports from the validators ready at finalize time + // The finalizer can return any extra lamport to the validator during finalize, but this acts as the validator's proof of collateral + if args.commit_record_lamports > delegation_record.lamports { + let extra_lamports = args + .commit_record_lamports + .checked_sub(delegation_record.lamports) + .ok_or(DlpError::Overflow)?; + + system::Transfer { + from: args.validator, + to: args.commit_state_account, + lamports: extra_lamports, + } + .invoke()?; + } + + // Load the program configuration and validate it, if any + let has_program_config = require_program_config( + args.program_config_account, + delegation_record.owner.as_array(), + false, + )?; + if has_program_config { + let program_config_data = args.program_config_account.try_borrow_data()?; + + let program_config = ProgramConfig::try_from_bytes_with_discriminator(&program_config_data) + .map_err(to_pinocchio_program_error)?; + if !program_config + .approved_validators + .contains(&(*args.validator.key()).into()) + { + log!("validator is not whitelisted in the program config: "); + pubkey::log(args.validator.key()); + return Err(DlpError::InvalidWhitelistProgramConfig.into()); + } + } + + // Load the uninitialized PDAs + let commit_state_bump = require_uninitialized_pda( + args.commit_state_account, + &[pda::COMMIT_STATE_TAG, args.delegated_account.key()], + &crate::fast::ID, + true, + "commit state account", + )?; + let commit_record_bump = require_uninitialized_pda( + args.commit_record_account, + &[pda::COMMIT_RECORD_TAG, args.delegated_account.key()], + &crate::fast::ID, + true, + "commit record", + )?; + + // Initialize the PDA containing the new committed state + create_pda( + args.commit_state_account, + &crate::fast::ID, + args.commit_state_bytes.len(), + &[Signer::from(&seeds!( + pda::COMMIT_STATE_TAG, + args.delegated_account.key(), + &[commit_state_bump] + ))], + args.validator, + )?; + + // Initialize the PDA containing the record of the committed state + create_pda( + args.commit_record_account, + &crate::fast::ID, + CommitRecord::size_with_discriminator(), + &[Signer::from(&seeds!( + pda::COMMIT_RECORD_TAG, + args.delegated_account.key(), + &[commit_record_bump] + ))], + args.validator, + )?; + + // Initialize the commit record + let commit_record = CommitRecord { + identity: (*args.validator.key()).into(), + account: (*args.delegated_account.key()).into(), + nonce: args.commit_record_nonce, + lamports: args.commit_record_lamports, + }; + let mut commit_record_data = args.commit_record_account.try_borrow_mut_data()?; + commit_record + .to_bytes_with_discriminator(&mut commit_record_data) + .map_err(to_pinocchio_program_error)?; + + // Copy the new state to the initialized PDA + let mut commit_state_data = args.commit_state_account.try_borrow_mut_data()?; + (*commit_state_data).copy_from_slice(args.commit_state_bytes); + + // TODO - Add additional validation for the commitment, e.g. sufficient validator stake + + Ok(()) +} diff --git a/src/processor/fast/mod.rs b/src/processor/fast/mod.rs index a7bdba61..cc342996 100644 --- a/src/processor/fast/mod.rs +++ b/src/processor/fast/mod.rs @@ -1,7 +1,9 @@ +mod commit_state; mod delegate; mod undelegate; mod utils; +pub use commit_state::*; pub use delegate::*; pub use undelegate::*; diff --git a/src/processor/fast/utils/requires.rs b/src/processor/fast/utils/requires.rs index da002a26..35d1bdfe 100644 --- a/src/processor/fast/utils/requires.rs +++ b/src/processor/fast/utils/requires.rs @@ -3,6 +3,9 @@ use pinocchio::program_error::ProgramError; use pinocchio::pubkey::{pubkey_eq, Pubkey}; use pinocchio_log::log; +use crate::error::DlpError; +use crate::pda::{self, program_config_from_program_id, validator_fees_vault_pda_from_validator}; + #[cfg(not(feature = "log-cost"))] use pinocchio::pubkey; @@ -24,9 +27,6 @@ mod pubkey { } } -use crate::error::DlpError; -use crate::pda::{self, validator_fees_vault_pda_from_validator}; - /// Errors if: /// - Account is not owned by expected program. #[inline(always)] @@ -159,7 +159,7 @@ pub fn require_initialized_pda( ) -> Result { let pda = pubkey::find_program_address(seeds, program_id); if !pubkey_eq(info.key(), &pda.0) { - log!("Invalid seeds for account: "); + log!("Invalid seeds (label: {}) for account ", label); pubkey::log(info.key()); return Err(ProgramError::InvalidSeeds); } @@ -167,7 +167,7 @@ pub fn require_initialized_pda( require_owned_pda(info, program_id, label)?; if is_writable && !info.is_writable() { - log!("Account is not writable: "); + log!("Account needs to be writable. label: {}, account: ", label); pubkey::log(info.key()); return Err(ProgramError::InvalidAccountData); } @@ -238,6 +238,31 @@ pub fn require_initialized_validator_fees_vault( Ok(()) } +/// Load program config PDA +/// - Program config PDA must be initialized with the expected seeds and owner, or not exists +pub fn require_program_config( + program_config: &AccountInfo, + program: &Pubkey, + is_writable: bool, +) -> Result { + let pda = program_config_from_program_id(&(*program).into()); + if !pubkey_eq(pda.as_array(), program_config.key()) { + log!("Invalid program config PDA, expected: "); + pubkey::log(pda.as_array()); + log!("but got: "); + pubkey::log(program_config.key()); + return Err(DlpError::InvalidAuthority.into()); + } + require_pda( + program_config, + &[pda::PROGRAM_CONFIG_TAG, program], + &crate::fast::ID, + is_writable, + "program config", + )?; + Ok(!pubkey_eq(program_config.owner(), &pinocchio_system::ID)) +} + /// Load initialized delegation record /// - Delegation record must be derived from the delegated account pub fn require_initialized_delegation_record( diff --git a/tests/integration/tests/test-delegation.ts b/tests/integration/tests/test-delegation.ts index e149a301..7a83da00 100644 --- a/tests/integration/tests/test-delegation.ts +++ b/tests/integration/tests/test-delegation.ts @@ -226,6 +226,19 @@ describe("TestDelegation", () => { ); const txId = await processInstruction(ix); console.log("Commit state signature", txId); + + const tx = await fetchTransaction(txId); + console.log(tx.meta.logMessages); + + const consumedLog = tx.meta.logMessages.find((m) => + m.includes("DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh consumed") + ); + + assert.isAtMost( + parseInt(consumedLog.split(" ").at(3)), + 32000, + "commit instruction must consume less than 32000" + ); }); it("Finalize account state", async () => {