Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)),
Expand Down
258 changes: 258 additions & 0 deletions src/processor/fast/commit_state.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
2 changes: 2 additions & 0 deletions src/processor/fast/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod commit_state;
mod delegate;
mod undelegate;
mod utils;

pub use commit_state::*;
pub use delegate::*;
pub use undelegate::*;

Expand Down
35 changes: 30 additions & 5 deletions src/processor/fast/utils/requires.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)]
Expand Down Expand Up @@ -159,15 +159,15 @@ pub fn require_initialized_pda(
) -> Result<u8, ProgramError> {
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);
}

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);
}
Expand Down Expand Up @@ -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<bool, ProgramError> {
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(
Expand Down
13 changes: 13 additions & 0 deletions tests/integration/tests/test-delegation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down