Skip to content

Commit 97174ec

Browse files
authored
feat: Add CommitDiff Instruction for Efficient Account Updates (#110)
* feat: Add CommitDiff Instruction for Efficient Account Updates * Make CI happy
1 parent 9ea733e commit 97174ec

File tree

11 files changed

+401
-20
lines changed

11 files changed

+401
-20
lines changed

Cargo.lock

Lines changed: 32 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pinocchio-pubkey = "0.3.0"
4848
pinocchio-system = "0.3.0"
4949
rkyv = "0.7.45"
5050
static_assertions = "1.1.0"
51+
strum = { version = "0.27.2", features = ["derive"] }
5152

5253
[dev-dependencies]
5354
base64 = "0.22.1"

src/args/commit_state.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::mem::size_of;
2+
13
use borsh::{BorshDeserialize, BorshSerialize};
24

35
#[derive(Default, Debug, BorshSerialize, BorshDeserialize)]
@@ -23,3 +25,37 @@ pub struct CommitStateFromBufferArgs {
2325
/// Whether the account can be undelegated after the commit completes
2426
pub allow_undelegation: bool,
2527
}
28+
29+
#[derive(Default, Debug, BorshSerialize)]
30+
pub struct CommitDiffArgs {
31+
/// The account diff
32+
/// SAFETY: this must be the FIRST field in the struct because the serialized format
33+
/// is manually split: the diff (with Borsh Vec prefix) followed by the fixed-size
34+
/// fields. The processor uses `data.split_at(data.len() - SIZE_COMMIT_DIFF_ARGS_WITHOUT_DIFF)`
35+
/// to separate them during deserialization.
36+
pub diff: Vec<u8>,
37+
38+
/// "Nonce" of an account. Updates are submitted historically and nonce incremented by 1
39+
/// Deprecated: The ephemeral slot at which the account data is committed
40+
pub nonce: u64,
41+
42+
/// The lamports that the account holds in the ephemeral validator
43+
pub lamports: u64,
44+
45+
/// Whether the account can be undelegated after the commit completes
46+
pub allow_undelegation: bool,
47+
}
48+
49+
#[derive(Default, Debug, BorshDeserialize)]
50+
pub struct CommitDiffArgsWithoutDiff {
51+
/// "Nonce" of an account. Updates are submitted historically and nonce incremented by 1
52+
/// Deprecated: The ephemeral slot at which the account data is committed
53+
pub nonce: u64,
54+
/// The lamports that the account holds in the ephemeral validator
55+
pub lamports: u64,
56+
/// Whether the account can be undelegated after the commit completes
57+
pub allow_undelegation: bool,
58+
}
59+
60+
pub const SIZE_COMMIT_DIFF_ARGS_WITHOUT_DIFF: usize =
61+
size_of::<u64>() + size_of::<u64>() + size_of::<bool>();

src/diff/types.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ impl<'a> DiffSet<'a> {
103103
Ok(this)
104104
}
105105

106+
pub fn try_new_from_borsh_vec(vec_buffer: &'a [u8]) -> Result<Self, ProgramError> {
107+
if vec_buffer.len() < 4 {
108+
return Err(ProgramError::InvalidInstructionData);
109+
}
110+
Self::try_new(&vec_buffer[4..])
111+
}
112+
106113
pub fn raw_diff(&self) -> &'a [u8] {
107114
// SAFETY: it does not do any "computation" as such. It merely reverses try_new
108115
// and get the immutable slice back.

src/discriminator.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use num_enum::TryFromPrimitive;
2+
use strum::IntoStaticStr;
23

34
#[repr(u8)]
4-
#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
5+
#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive, IntoStaticStr)]
56
#[rustfmt::skip]
67
pub enum DlpDiscriminator {
78
/// See [crate::processor::process_delegate] for docs.
@@ -34,11 +35,17 @@ pub enum DlpDiscriminator {
3435
CloseValidatorFeesVault = 14,
3536
/// See [crate::processor::process_call_handler] for docs.
3637
CallHandler = 15,
38+
/// See [crate::processor::process_commit_diff] for docs.
39+
CommitDiff = 16,
3740
}
3841

3942
impl DlpDiscriminator {
4043
pub fn to_vec(self) -> Vec<u8> {
4144
let num = self as u64;
4245
num.to_le_bytes().to_vec()
4346
}
47+
48+
pub fn name(&self) -> &'static str {
49+
self.into()
50+
}
4451
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use borsh::to_vec;
2+
use solana_program::instruction::Instruction;
3+
use solana_program::system_program;
4+
use solana_program::{instruction::AccountMeta, pubkey::Pubkey};
5+
6+
use crate::args::CommitDiffArgs;
7+
use crate::discriminator::DlpDiscriminator;
8+
use crate::pda::{
9+
commit_record_pda_from_delegated_account, commit_state_pda_from_delegated_account,
10+
delegation_metadata_pda_from_delegated_account, delegation_record_pda_from_delegated_account,
11+
program_config_from_program_id, validator_fees_vault_pda_from_validator,
12+
};
13+
14+
/// Builds a commit state instruction.
15+
/// See [crate::processor::fast::process_commit_diff] for docs.
16+
pub fn commit_diff(
17+
validator: Pubkey,
18+
delegated_account: Pubkey,
19+
delegated_account_owner: Pubkey,
20+
commit_args: CommitDiffArgs,
21+
) -> Instruction {
22+
let commit_args = to_vec(&commit_args).unwrap();
23+
let delegation_record_pda = delegation_record_pda_from_delegated_account(&delegated_account);
24+
let commit_state_pda = commit_state_pda_from_delegated_account(&delegated_account);
25+
let commit_record_pda = commit_record_pda_from_delegated_account(&delegated_account);
26+
let validator_fees_vault_pda = validator_fees_vault_pda_from_validator(&validator);
27+
let delegation_metadata_pda =
28+
delegation_metadata_pda_from_delegated_account(&delegated_account);
29+
let program_config_pda = program_config_from_program_id(&delegated_account_owner);
30+
Instruction {
31+
program_id: crate::id(),
32+
accounts: vec![
33+
AccountMeta::new_readonly(validator, true),
34+
AccountMeta::new_readonly(delegated_account, false),
35+
AccountMeta::new(commit_state_pda, false),
36+
AccountMeta::new(commit_record_pda, false),
37+
AccountMeta::new_readonly(delegation_record_pda, false),
38+
AccountMeta::new(delegation_metadata_pda, false),
39+
AccountMeta::new_readonly(validator_fees_vault_pda, false),
40+
AccountMeta::new_readonly(program_config_pda, false),
41+
AccountMeta::new_readonly(system_program::id(), false),
42+
],
43+
data: [DlpDiscriminator::CommitDiff.to_vec(), commit_args].concat(),
44+
}
45+
}

src/instruction_builder/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod call_handler;
22
mod close_ephemeral_balance;
33
mod close_validator_fees_vault;
4+
mod commit_diff;
45
mod commit_state;
56
mod commit_state_from_buffer;
67
mod delegate;
@@ -17,6 +18,7 @@ mod whitelist_validator_for_program;
1718
pub use call_handler::*;
1819
pub use close_ephemeral_balance::*;
1920
pub use close_validator_fees_vault::*;
21+
pub use commit_diff::*;
2022
pub use commit_state::*;
2123
pub use commit_state_from_buffer::*;
2224
pub use delegate::*;

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ pub fn fast_process_instruction(
8282
DlpDiscriminator::CommitStateFromBuffer => Some(
8383
processor::fast::process_commit_state_from_buffer(program_id, accounts, data),
8484
),
85+
DlpDiscriminator::CommitDiff => Some(processor::fast::process_commit_diff(
86+
program_id, accounts, data,
87+
)),
8588
DlpDiscriminator::Finalize => Some(processor::fast::process_finalize(
8689
program_id, accounts, data,
8790
)),

src/processor/fast/commit_diff.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use borsh::BorshDeserialize;
2+
use pinocchio::{
3+
account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult,
4+
};
5+
use pinocchio_log::log;
6+
7+
use crate::args::{CommitDiffArgsWithoutDiff, SIZE_COMMIT_DIFF_ARGS_WITHOUT_DIFF};
8+
use crate::processor::fast::{process_commit_state_internal, CommitStateInternalArgs};
9+
use crate::{apply_diff_copy, DiffSet};
10+
11+
/// Commit diff to a delegated PDA
12+
///
13+
/// Accounts:
14+
///
15+
/// 0: `[signer]` the validator requesting the commit
16+
/// 1: `[]` the delegated account
17+
/// 2: `[writable]` the PDA storing the new state
18+
/// 3: `[writable]` the PDA storing the commit record
19+
/// 4: `[]` the delegation record
20+
/// 5: `[writable]` the delegation metadata
21+
/// 6: `[]` the validator fees vault
22+
/// 7: `[]` the program config account
23+
/// 8: `[]` the system program
24+
///
25+
/// Requirements:
26+
///
27+
/// - The following accounts must be initialized:
28+
/// - delegation record
29+
/// - delegation metadata
30+
/// - validator fees vault
31+
/// - program config
32+
/// - The following accounts must be uninitialized:
33+
/// - commit state
34+
/// - commit record
35+
/// - delegated account holds at least the lamports indicated in the delegation record
36+
/// - account was not committed at a later slot
37+
///
38+
/// Steps:
39+
/// 1. Check that the pda is delegated
40+
/// 2. Init a new PDA to store the new state
41+
/// 3. Copy the new state to the new PDA
42+
/// 4. Init a new PDA to store the record of the new state commitment
43+
pub fn process_commit_diff(
44+
_program_id: &Pubkey,
45+
accounts: &[AccountInfo],
46+
data: &[u8],
47+
) -> ProgramResult {
48+
let [validator, delegated_account, commit_state_account, commit_record_account, delegation_record_account, delegation_metadata_account, validator_fees_vault, program_config_account, _system_program] =
49+
accounts
50+
else {
51+
return Err(ProgramError::NotEnoughAccountKeys);
52+
};
53+
54+
if data.len() < SIZE_COMMIT_DIFF_ARGS_WITHOUT_DIFF {
55+
return Err(ProgramError::InvalidInstructionData);
56+
}
57+
58+
let (diff, data) = data.split_at(data.len() - SIZE_COMMIT_DIFF_ARGS_WITHOUT_DIFF);
59+
60+
let args =
61+
CommitDiffArgsWithoutDiff::try_from_slice(data).map_err(|_| ProgramError::BorshIoError)?;
62+
63+
let diffset = DiffSet::try_new_from_borsh_vec(diff)?;
64+
65+
if diffset.segments_count() == 0 {
66+
log!("WARN: noop; empty diff sent");
67+
}
68+
69+
let commit_record_lamports = args.lamports;
70+
let commit_record_nonce = args.nonce;
71+
let allow_undelegation = args.allow_undelegation;
72+
73+
// TODO (snawaz): the following approach to apply diff works, but it's not efficient.
74+
// It is also problematic for larger account as it allocates memory on the heap.
75+
// It will be fixed in a separate PR.
76+
let original = unsafe { delegated_account.borrow_data_unchecked() };
77+
let changed = apply_diff_copy(original, &diffset)?;
78+
79+
let commit_args = CommitStateInternalArgs {
80+
commit_state_bytes: &changed,
81+
commit_record_lamports,
82+
commit_record_nonce,
83+
allow_undelegation,
84+
validator,
85+
delegated_account,
86+
commit_state_account,
87+
commit_record_account,
88+
delegation_record_account,
89+
delegation_metadata_account,
90+
validator_fees_vault,
91+
program_config_account,
92+
};
93+
94+
process_commit_state_internal(commit_args)
95+
}

src/processor/fast/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
mod commit_diff;
12
mod commit_state;
23
mod commit_state_from_buffer;
34
mod delegate;
45
mod finalize;
56
mod undelegate;
67
mod utils;
78

9+
pub use commit_diff::*;
810
pub use commit_state::*;
911
pub use commit_state_from_buffer::*;
1012
pub use delegate::*;

0 commit comments

Comments
 (0)