diff --git a/dlp-api/src/v2/args/mod.rs b/dlp-api/src/v2/args/mod.rs index d7ba57a9..dd8ec8d2 100644 --- a/dlp-api/src/v2/args/mod.rs +++ b/dlp-api/src/v2/args/mod.rs @@ -2,6 +2,7 @@ // instruction tag, so v2 instruction args use `buffer_offset = 1`. mod init_protocol_config; +mod post_commitment; mod register_operator; mod register_verifier; mod update_protocol_config; @@ -9,6 +10,7 @@ mod update_verifier_registry; mod write_state_buffer; pub use init_protocol_config::*; +pub use post_commitment::*; pub use register_operator::*; pub use register_verifier::*; pub use update_protocol_config::*; diff --git a/dlp-api/src/v2/args/post_commitment.rs b/dlp-api/src/v2/args/post_commitment.rs new file mode 100644 index 00000000..c42971fd --- /dev/null +++ b/dlp-api/src/v2/args/post_commitment.rs @@ -0,0 +1,19 @@ +use wheels::variable_offset_layout; + +use crate::compat::Pubkey; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[variable_offset_layout(buffer_offset = 1)] +pub struct PostCommitmentArgs { + pub commit_id: u64, + + pub lamports: u64, + + pub owner: Pubkey, + + /// Hash of replay/data-availability pointer bytes. + pub da_pointer_hash: [u8; 32], + + /// ER slot that produced this commitment, when available. + pub er_slot: Option, +} diff --git a/dlp-api/src/v2/instruction.rs b/dlp-api/src/v2/instruction.rs index a18e8434..92ba8dd4 100644 --- a/dlp-api/src/v2/instruction.rs +++ b/dlp-api/src/v2/instruction.rs @@ -15,6 +15,8 @@ pub enum DlpV2Instruction { UpdateVerifierRegistry = 103, /// Updates global v2 config for future commitments. UpdateProtocolConfig = 104, + /// Posts a new v2 account-state commitment. + PostCommitment = 105, /// Writes full account-state bytes into a v2 state buffer. /// /// TODO (snawaz/optimization): we can split this into two instructions such that diff --git a/dlp-api/src/v2/instruction_builder/mod.rs b/dlp-api/src/v2/instruction_builder/mod.rs index 128e03ff..44641566 100644 --- a/dlp-api/src/v2/instruction_builder/mod.rs +++ b/dlp-api/src/v2/instruction_builder/mod.rs @@ -1,4 +1,5 @@ mod init_protocol_config; +mod post_commitment; mod register_operator; mod register_verifier; mod update_protocol_config; @@ -6,6 +7,7 @@ mod update_verifier_registry; mod write_state_buffer; pub use init_protocol_config::*; +pub use post_commitment::*; pub use register_operator::*; pub use register_verifier::*; pub use update_protocol_config::*; diff --git a/dlp-api/src/v2/instruction_builder/post_commitment.rs b/dlp-api/src/v2/instruction_builder/post_commitment.rs new file mode 100644 index 00000000..93ef2a68 --- /dev/null +++ b/dlp-api/src/v2/instruction_builder/post_commitment.rs @@ -0,0 +1,66 @@ +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; +use solana_sdk_ids::system_program; +use wheels::layout::Encodable; + +use crate::{ + compat::{Compatize, Modernize}, + pda::delegation_record_pda_from_delegated_account, + v2::{ + pda::{ + operator_bond_pda, pending_commitment_pda, protocol_config_pda, + state_buffer_pda, verifier_registry_pda, + }, + DlpV2Instruction, PostCommitmentArgs, + }, +}; + +/// Builds the instruction that posts one v2 account-state commitment. +pub fn post_commitment( + operator: Pubkey, + account: Pubkey, + args: PostCommitmentArgs, +) -> Instruction { + Instruction { + program_id: crate::id().modernize(), + accounts: vec![ + AccountMeta::new(operator, true), + AccountMeta::new_readonly( + operator_bond_pda(&operator.compatize()).modernize(), + false, + ), + AccountMeta::new( + pending_commitment_pda(&account.compatize(), args.commit_id) + .modernize(), + false, + ), + AccountMeta::new_readonly( + state_buffer_pda( + &account.compatize(), + args.commit_id, + &operator.compatize(), + ) + .modernize(), + false, + ), + AccountMeta::new_readonly(account, false), + AccountMeta::new_readonly( + delegation_record_pda_from_delegated_account( + &account.compatize(), + ) + .modernize(), + false, + ), + AccountMeta::new_readonly(protocol_config_pda().modernize(), false), + AccountMeta::new(verifier_registry_pda().modernize(), false), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: [ + DlpV2Instruction::PostCommitment.to_vec(), + args.encode().unwrap(), + ] + .concat(), + } +} diff --git a/dlp-api/src/v2/pda.rs b/dlp-api/src/v2/pda.rs index c783480b..7438f320 100644 --- a/dlp-api/src/v2/pda.rs +++ b/dlp-api/src/v2/pda.rs @@ -5,6 +5,7 @@ pub const OPERATOR_BOND_SEED: &[u8] = b"operator-bond"; pub const VERIFIER_BOND_SEED: &[u8] = b"verifier-bond"; pub const VERIFIER_REGISTRY_SEED: &[u8] = b"verifier-registry"; pub const STATE_BUFFER_SEED: &[u8] = b"state-buffer"; +pub const PENDING_COMMITMENT_SEED: &[u8] = b"pending-commitment"; // TODO (snawaz): Precompute these addresses if PDA derivation becomes const-safe. @@ -48,3 +49,15 @@ pub fn state_buffer_pda( ) .0 } + +pub fn pending_commitment_pda(account: &Pubkey, commit_id: u64) -> Pubkey { + Pubkey::find_program_address( + &[ + PENDING_COMMITMENT_SEED, + account.as_ref(), + &commit_id.to_le_bytes(), + ], + &crate::id(), + ) + .0 +} diff --git a/dlp-api/src/v2/state/mod.rs b/dlp-api/src/v2/state/mod.rs index a92b8062..a39d8513 100644 --- a/dlp-api/src/v2/state/mod.rs +++ b/dlp-api/src/v2/state/mod.rs @@ -1,10 +1,12 @@ mod operator_bond; +mod pending_commitment; mod protocol_config; mod state_buffer; mod verifier_bond; mod verifier_registry; pub use operator_bond::*; +pub use pending_commitment::*; pub use protocol_config::*; pub use state_buffer::*; pub use verifier_bond::*; diff --git a/dlp-api/src/v2/state/pending_commitment.rs b/dlp-api/src/v2/state/pending_commitment.rs new file mode 100644 index 00000000..25e04524 --- /dev/null +++ b/dlp-api/src/v2/state/pending_commitment.rs @@ -0,0 +1,118 @@ +use wheels::fixed_offset_layout; + +use crate::compat::Pubkey; + +pub const PENDING_COMMITMENT_STATUS_ACTIVE: u8 = 1; +pub const PENDING_COMMITMENT_STATUS_AWAITING_OPERATOR_RESPONSE: u8 = 2; +pub const PENDING_COMMITMENT_STATUS_AWAITING_CHALLENGER_REVEAL: u8 = 3; +pub const PENDING_COMMITMENT_STATUS_AWAITING_DISPUTE_RESOLUTION: u8 = 4; +pub const PENDING_COMMITMENT_STATUS_RESOLVED_OPERATOR: u8 = 5; +pub const PENDING_COMMITMENT_STATUS_RESOLVED_CHALLENGER: u8 = 6; +pub const PENDING_COMMITMENT_STATUS_FINALIZED: u8 = 7; +pub const PENDING_COMMITMENT_STATUS_EXPIRED: u8 = 8; +pub const PENDING_COMMITMENT_STATUS_CANCELLED: u8 = 9; + +pub const RESOLVED_STATE_SOURCE_OPERATOR_COMMITMENT: u8 = 1; +pub const RESOLVED_STATE_SOURCE_CHALLENGER_REVEAL: u8 = 2; + +/// PDA: `["pending-commitment", account, commit_id]`. +/// Created by `PostCommitment`. +/// Closed by `CloseTerminalAccounts` after finalize, cancel, or expiry. +/// +/// One account per delegated account and commit id. +#[derive(Clone, Debug, PartialEq, Eq)] +#[fixed_offset_layout(buffer_offset = 0)] +pub struct PendingCommitment { + /// Account type marker. + pub discriminator: [u8; 8], + + /// Current state-machine state for this commitment. + pub status: u8, + + /// Operator identity that posted the commitment. + pub operator_identity: Pubkey, + + /// OperatorBond checked when the commitment was posted. + pub operator_bond: Pubkey, + + /// Delegated account whose base-layer state will be finalized. + pub account_pubkey: Pubkey, + + /// Operator-chosen nonce for this account commitment. + pub commit_id: u64, + + /// Delegation record tying this account to the ER context. + pub delegation_record: Pubkey, + + /// Hash of replay/data-availability pointer bytes. + pub da_pointer_hash: [u8; 32], + + /// Hash of lamports, owner, and data_hash. + pub account_state_hash: [u8; 32], + + /// Hash of full account data. + pub data_hash: [u8; 32], + + /// Lamports committed by the operator. + pub lamports: u64, + + /// Owner committed by the operator. + pub owner: Pubkey, + + /// Hash binding operator, account, commit id, delegation, DA, and state. + pub state_commitment_hash: [u8; 32], + + /// Registry account used when this commitment was posted. + pub verifier_registry: Pubkey, + + /// Monotonic id for this approval/challenge window. + pub challenge_window_id: u64, + + /// Slot when the commitment was posted. + pub posted_slot: u64, + + /// Slot when verifier selection and the challenge window started. + pub activation_slot: u64, + + /// Slot when approval/challenge window closes. + pub challenge_window_end_slot: u64, + + /// Number of unique selected verifiers that approved. + pub approval_count: u16, + + /// Threshold copied from ProtocolConfig when the commitment is posted. + pub approval_threshold: u16, + + /// Active Challenge account, if any. + pub active_challenge: Option, + + /// Which opened state finalization must use after dispute resolution. + pub resolved_state_source: Option, + + /// ER slot that produced this commitment, when available. + pub er_slot: Option, + + /// Aligns selected verifier elements after the Vec length prefix. + pub _pad_before_selected_verifiers: [u8; 7], + + /// Verifiers selected by round-robin for this commitment. + #[extendable = 2] + pub selected_verifiers: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +#[fixed_offset_layout(buffer_offset = 2)] +pub struct SelectedVerifier { + /// Selected verifier identity. + pub verifier_identity: Pubkey, + + /// Whether this verifier has approved this commitment. + pub approved: bool, + + /// Keeps each Vec element aligned for fixed-layout decoding. + pub _pad_after_approved: [u8; 7], +} + +impl PendingCommitment { + pub const DISCRIMINATOR: [u8; 8] = *b"v2pend00"; +} diff --git a/src/v2/processor/fraud_proofs/mod.rs b/src/v2/processor/fraud_proofs/mod.rs index bf64af2d..e702dde7 100644 --- a/src/v2/processor/fraud_proofs/mod.rs +++ b/src/v2/processor/fraud_proofs/mod.rs @@ -1,5 +1,7 @@ //! Processors for v2 fraud-proof instructions. +mod post_commitment; mod write_state_buffer; +pub use post_commitment::*; pub use write_state_buffer::*; diff --git a/src/v2/processor/fraud_proofs/post_commitment.rs b/src/v2/processor/fraud_proofs/post_commitment.rs new file mode 100644 index 00000000..dc1b4d66 --- /dev/null +++ b/src/v2/processor/fraud_proofs/post_commitment.rs @@ -0,0 +1,436 @@ +use dlp_api::{ + error::DlpError, + state::DelegationRecord, + v2::{ + pda::{ + OPERATOR_BOND_SEED, PENDING_COMMITMENT_SEED, PROTOCOL_CONFIG_SEED, + STATE_BUFFER_SEED, VERIFIER_REGISTRY_SEED, + }, + OperatorBond, OperatorStatus, PendingCommitment, PostCommitmentArgs, + ProtocolConfig, SelectedVerifier, StateBuffer, VerifierRegistry, + VerifierRegistryEntry, PENDING_COMMITMENT_STATUS_ACTIVE, + }, +}; +use pinocchio::{ + address::Address, + cpi::{Seed, Signer}, + error::ProgramError, + sysvars::{clock::Clock, Sysvar}, + AccountView, ProgramResult, +}; +use wheels::{ + layout::{Decodable, Encodable}, + require_eq, require_eq_keys, require_ge, require_n_accounts, + require_signer, +}; + +use crate::{ + processor::fast::{to_pinocchio_program_error, utils::pda::create_pda}, + requires::{ + require_initialized_delegation_record, require_initialized_pda, + require_owned_pda, require_uninitialized_pda, StandardCtx, + }, +}; + +/// Post one v2 account-state commitment. +/// +/// Accounts: +/// 0: `[signer, writable]` operator identity and pending account rent payer +/// 1: `[]` OperatorBond PDA +/// 2: `[writable]` PendingCommitment PDA +/// 3: `[]` finalized StateBuffer PDA +/// 4: `[]` delegated account +/// 5: `[]` DelegationRecord PDA +/// 6: `[]` ProtocolConfig PDA +/// 7: `[writable]` VerifierRegistry PDA +/// 8: `[]` system program, required by system CPI +#[inline(never)] +pub fn process_post_commitment( + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + let [ + operator, // force multi-line + operator_bond, + pending_commitment, + state_buffer, + delegated_account, + delegation_record, + protocol_config, + verifier_registry, + _system_program, + ] = require_n_accounts!(accounts, 9); + + let args = PostCommitmentArgs::decode(data)?; + + require_signer!(operator); + require_owned_pda( + delegated_account, + &crate::fast::ID, + "delegated account", + )?; + + require_initialized_pda( + operator_bond, + &[OPERATOR_BOND_SEED, operator.address().as_ref()], + &crate::fast::ID, + false, + "operator bond", + )?; + let commit_id_bytes = args.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", + )?; + require_initialized_delegation_record( + delegated_account, + delegation_record, + false, + )?; + require_initialized_pda( + protocol_config, + &[PROTOCOL_CONFIG_SEED], + &crate::fast::ID, + false, + "protocol config", + )?; + require_initialized_pda( + verifier_registry, + &[VERIFIER_REGISTRY_SEED], + &crate::fast::ID, + true, + "verifier registry", + )?; + + let operator_bond_data = operator_bond.try_borrow()?; + let operator_bond_state = + OperatorBond::decode(operator_bond_data.as_ref())?; + validate_operator_bond(&operator_bond_state, operator)?; + + let protocol_config_data = protocol_config.try_borrow()?; + let protocol_config_state = + ProtocolConfig::decode(protocol_config_data.as_ref())?; + validate_protocol_config(&protocol_config_state)?; + + require_ge!( + operator_bond_state.stake_lamports(), + protocol_config_state.min_operator_bond(), + ProgramError::InvalidInstructionData + ); + drop(operator_bond_data); + + let state_buffer_data = state_buffer.try_borrow()?; + let state_buffer_state = StateBuffer::decode(state_buffer_data.as_ref())?; + let data_hash = validate_state_buffer( + &state_buffer_state, + operator, + delegated_account, + args.commit_id(), + )?; + drop(state_buffer_data); + + 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 + ); + drop(delegation_record_data); + + let verifier_registry_data = verifier_registry.try_borrow()?; + let verifier_registry_view = + VerifierRegistry::decode(verifier_registry_data.as_ref())?; + if verifier_registry_view.discriminator() != VerifierRegistry::DISCRIMINATOR + { + return Err(ProgramError::InvalidAccountData); + } + + let entries = copy_verifier_entries(&verifier_registry_view); + let (selected_verifiers, scanned_count) = select_verifiers_round_robin( + &entries, + verifier_registry_view.next_selection_index(), + protocol_config_state.verifiers_per_commitment() as usize, + operator.address(), + )?; + + require_ge!( + selected_verifiers.len(), + protocol_config_state.approval_threshold() as usize, + ProgramError::InvalidInstructionData + ); + + let updated_registry = VerifierRegistry { + discriminator: VerifierRegistry::DISCRIMINATOR, + bump: verifier_registry_view.bump(), + next_selection_index: verifier_registry_view + .next_selection_index() + .checked_add(scanned_count as u64) + .ok_or(DlpError::Overflow)?, + entries, + }; + drop(verifier_registry_data); + + let clock = Clock::get()?; + let challenge_window_id = 0; + let account_state_hash = + account_state_hash(args.lamports(), args.owner(), &data_hash); + let state_commitment_hash = + state_commitment_hash(StateCommitmentHashInput { + operator_identity: operator.address(), + account_pubkey: delegated_account.address(), + commit_id: args.commit_id(), + delegation_record: delegation_record.address(), + da_pointer_hash: args.da_pointer_hash(), + account_state_hash: &account_state_hash, + verifier_registry: verifier_registry.address(), + challenge_window_id, + }); + + let pending_commitment_state = PendingCommitment { + discriminator: PendingCommitment::DISCRIMINATOR, + status: PENDING_COMMITMENT_STATUS_ACTIVE, + operator_identity: operator.address().to_bytes().into(), + operator_bond: operator_bond.address().to_bytes().into(), + account_pubkey: delegated_account.address().to_bytes().into(), + commit_id: args.commit_id(), + delegation_record: delegation_record.address().to_bytes().into(), + da_pointer_hash: *args.da_pointer_hash(), + account_state_hash, + data_hash, + lamports: args.lamports(), + owner: *args.owner(), + state_commitment_hash, + verifier_registry: verifier_registry.address().to_bytes().into(), + challenge_window_id, + posted_slot: clock.slot, + activation_slot: clock.slot, + challenge_window_end_slot: clock + .slot + .checked_add(protocol_config_state.challenge_window_slots()) + .ok_or(DlpError::Overflow)?, + approval_count: 0, + approval_threshold: protocol_config_state.approval_threshold(), + active_challenge: None, + resolved_state_source: None, + er_slot: args.er_slot(), + _pad_before_selected_verifiers: [0; 7], + selected_verifiers, + }; + + drop(protocol_config_data); + + let pending_commitment_bump = require_uninitialized_pda( + pending_commitment, + &[ + PENDING_COMMITMENT_SEED, + delegated_account.address().as_ref(), + &commit_id_bytes, + ], + &crate::fast::ID, + true, + StandardCtx::new("pending commitment"), + )?; + + create_pda( + pending_commitment, + &crate::fast::ID, + pending_commitment_state.encoded_len()?, + &[Signer::from(&[ + Seed::from(PENDING_COMMITMENT_SEED), + Seed::from(delegated_account.address().as_ref()), + Seed::from(&commit_id_bytes), + Seed::from(&[pending_commitment_bump]), + ])], + operator, + )?; + + pending_commitment_state + .encode_to(pending_commitment.try_borrow_mut()?.as_mut())?; + updated_registry.encode_to(verifier_registry.try_borrow_mut()?.as_mut())?; + + Ok(()) +} + +fn validate_operator_bond( + operator_bond: &dlp_api::v2::OperatorBondView<'_>, + operator: &AccountView, +) -> ProgramResult { + if operator_bond.discriminator() != OperatorBond::DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData); + } + require_eq_keys!( + &Address::from(operator_bond.operator_identity().to_bytes()), + operator.address(), + DlpError::InvalidAuthority + ); + require_eq!( + operator_bond.status(), + OperatorStatus::Active.value(), + ProgramError::InvalidInstructionData + ); + require_eq!( + operator_bond.withdraw_requested_slot().is_none(), + true, + ProgramError::InvalidInstructionData + ); + + Ok(()) +} + +fn validate_protocol_config( + protocol_config: &dlp_api::v2::ProtocolConfigView<'_>, +) -> ProgramResult { + if protocol_config.discriminator() != ProtocolConfig::DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData); + } + require_eq!( + protocol_config.paused(), + false, + ProgramError::InvalidAccountData + ); + + Ok(()) +} + +fn validate_state_buffer( + state_buffer: &dlp_api::v2::StateBufferView<'_>, + operator: &AccountView, + delegated_account: &AccountView, + commit_id: u64, +) -> Result<[u8; 32], ProgramError> { + if state_buffer.discriminator() != StateBuffer::DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData); + } + require_eq_keys!( + state_buffer.authority(), + operator.address(), + DlpError::InvalidAuthority + ); + require_eq_keys!( + state_buffer.account_pubkey(), + delegated_account.address(), + ProgramError::InvalidAccountData + ); + require_eq!( + state_buffer.commit_id(), + commit_id, + ProgramError::InvalidInstructionData + ); + require_eq!( + state_buffer.finalized(), + true, + ProgramError::InvalidInstructionData + ); + require_eq!( + state_buffer.payload().len(), + state_buffer.total_len() as usize, + ProgramError::InvalidInstructionData + ); + + Ok(*state_buffer.data_hash()) +} + +fn copy_verifier_entries( + registry: &dlp_api::v2::VerifierRegistryView<'_>, +) -> Vec { + registry + .entries() + .iter() + .map(|entry| VerifierRegistryEntry { + verifier_identity: *entry.verifier_identity(), + verifier_bond: *entry.verifier_bond(), + weight: entry.weight(), + }) + .collect() +} + +fn select_verifiers_round_robin( + entries: &[VerifierRegistryEntry], + next_selection_index: u64, + verifiers_per_commitment: usize, + operator: &Address, +) -> Result<(Vec, usize), ProgramError> { + if entries.is_empty() || verifiers_per_commitment == 0 { + return Err(ProgramError::InvalidInstructionData); + } + + let start = (next_selection_index % entries.len() as u64) as usize; + let mut selected = Vec::new(); + let mut scanned_count = 0; + + while scanned_count < entries.len() + && selected.len() < verifiers_per_commitment + { + let index = (start + scanned_count) % entries.len(); + let entry = &entries[index]; + + if Address::from(entry.verifier_identity.to_bytes()) != *operator { + selected.push(SelectedVerifier { + verifier_identity: entry.verifier_identity, + approved: false, + _pad_after_approved: [0; 7], + }); + } + + scanned_count += 1; + } + + if selected.is_empty() { + return Err(ProgramError::InvalidInstructionData); + } + + Ok((selected, scanned_count)) +} + +fn account_state_hash( + lamports: u64, + owner: &dlp_api::compat::Pubkey, + data_hash: &[u8; 32], +) -> [u8; 32] { + solana_sha256_hasher::hashv(&[ + b"magicblock.account_state.v1", + &lamports.to_le_bytes(), + owner.as_ref(), + data_hash, + ]) + .to_bytes() +} + +struct StateCommitmentHashInput<'a> { + operator_identity: &'a Address, + account_pubkey: &'a Address, + commit_id: u64, + delegation_record: &'a Address, + da_pointer_hash: &'a [u8; 32], + account_state_hash: &'a [u8; 32], + verifier_registry: &'a Address, + challenge_window_id: u64, +} + +fn state_commitment_hash(input: StateCommitmentHashInput<'_>) -> [u8; 32] { + solana_sha256_hasher::hashv(&[ + b"magicblock.state_commitment.v1", + input.operator_identity.as_ref(), + input.account_pubkey.as_ref(), + &input.commit_id.to_le_bytes(), + input.delegation_record.as_ref(), + input.da_pointer_hash, + input.account_state_hash, + input.verifier_registry.as_ref(), + &input.challenge_window_id.to_le_bytes(), + ]) + .to_bytes() +} diff --git a/src/v2/processor/mod.rs b/src/v2/processor/mod.rs index 010d038e..4c2e6333 100644 --- a/src/v2/processor/mod.rs +++ b/src/v2/processor/mod.rs @@ -8,6 +8,7 @@ use pinocchio::{AccountView, ProgramResult}; pub use bootstrap::*; pub use fraud_proofs::*; +#[inline(never)] pub fn process_instruction( accounts: &[AccountView], data: &[u8], @@ -29,6 +30,9 @@ pub fn process_instruction( DlpV2Instruction::UpdateProtocolConfig => { process_update_protocol_config(accounts, data) } + DlpV2Instruction::PostCommitment => { + process_post_commitment(accounts, data) + } DlpV2Instruction::WriteStateBuffer => { process_write_state_buffer(accounts, data) } diff --git a/tests/test_v2_post_commitment.rs b/tests/test_v2_post_commitment.rs new file mode 100644 index 00000000..e872e3c4 --- /dev/null +++ b/tests/test_v2_post_commitment.rs @@ -0,0 +1,435 @@ +use dlp_api::{ + pda::delegation_record_pda_from_delegated_account, + v2::{ + instruction_builder::{ + post_commitment, register_operator, register_verifier, + update_verifier_registry, write_state_buffer, + }, + pda::{pending_commitment_pda, verifier_registry_pda}, + PendingCommitment, PostCommitmentArgs, RegisterOperatorArgs, + RegisterVerifierArgs, VerifierRegistry, WriteStateBufferArgs, + PENDING_COMMITMENT_STATUS_ACTIVE, VERIFIER_REGISTRY_ACTION_ADD, + }, +}; +use solana_program::native_token::LAMPORTS_PER_SOL; +use solana_program_test::{ + BanksClient, ProgramTest, ProgramTestBanksClientExt, +}; +use solana_sdk::{ + account::Account, + hash::Hash, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use solana_sdk_ids::system_program; +use wheels::layout::Decodable; + +mod fixtures; + +use crate::fixtures::{ + create_delegation_record_data, + v2::{initialize_protocol_config, valid_protocol_config_args}, +}; + +#[tokio::test] +async fn test_post_commitment() { + let mut env = setup_post_commitment_env(2, true, false).await; + let args = valid_post_commitment_args(); + + post_v2_commitment(&mut env, args.clone()).await.unwrap(); + + let pending_commitment_account = env + .banks + .get_account(pending_commitment_pda( + &env.delegated_account, + args.commit_id, + )) + .await + .unwrap() + .unwrap(); + let pending_commitment = ::decode( + &pending_commitment_account.data, + ) + .unwrap(); + + assert_eq!( + pending_commitment.discriminator(), + PendingCommitment::DISCRIMINATOR + ); + assert_eq!( + pending_commitment.status(), + PENDING_COMMITMENT_STATUS_ACTIVE + ); + assert_eq!( + *pending_commitment.operator_identity(), + env.operator.pubkey() + ); + assert_eq!(*pending_commitment.account_pubkey(), env.delegated_account); + assert_eq!(pending_commitment.commit_id(), args.commit_id); + assert_eq!(pending_commitment.lamports(), args.lamports); + assert_eq!(*pending_commitment.owner(), args.owner); + assert_eq!( + *pending_commitment.data_hash(), + account_data_hash(&state_data()) + ); + assert_eq!(*pending_commitment.da_pointer_hash(), args.da_pointer_hash); + assert_eq!(pending_commitment.approval_count(), 0); + assert_eq!( + pending_commitment.approval_threshold(), + env.config_args.approval_threshold + ); + assert_eq!( + pending_commitment.challenge_window_end_slot(), + pending_commitment.posted_slot() + + env.config_args.challenge_window_slots + ); + + let selected: Vec<_> = pending_commitment + .selected_verifiers() + .iter() + .map(|entry| (*entry.verifier_identity(), entry.approved())) + .collect(); + assert_eq!(selected.len(), 1); + assert_eq!(selected[0], (env.verifiers[0].pubkey(), false)); + assert!(selected + .iter() + .all(|(verifier, _)| *verifier != env.operator.pubkey())); + + let verifier_registry_account = env + .banks + .get_account(verifier_registry_pda()) + .await + .unwrap() + .unwrap(); + let verifier_registry = ::decode( + &verifier_registry_account.data, + ) + .unwrap(); + + assert_eq!(verifier_registry.next_selection_index(), 2); +} + +#[tokio::test] +async fn test_post_commitment_fails_twice() { + let mut env = setup_post_commitment_env(2, true, false).await; + let args = valid_post_commitment_args(); + + post_v2_commitment(&mut env, args.clone()).await.unwrap(); + + assert!(post_v2_commitment(&mut env, args).await.is_err()); +} + +#[tokio::test] +async fn test_post_commitment_fails_without_operator_signature() { + let env = setup_post_commitment_env(2, false, false).await; + let args = valid_post_commitment_args(); + let mut ix = + post_commitment(env.operator.pubkey(), env.delegated_account, args); + ix.accounts[0].is_signer = false; + + let blockhash = env.banks.get_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&env.payer.pubkey()), + &[&env.payer], + blockhash, + ); + + assert!(env.banks.process_transaction(tx).await.is_err()); +} + +#[tokio::test] +async fn test_post_commitment_fails_without_enough_verifiers() { + let mut env = setup_post_commitment_env(0, true, false).await; + + assert!(post_v2_commitment(&mut env, valid_post_commitment_args()) + .await + .is_err()); +} + +#[tokio::test] +async fn test_post_commitment_fails_without_state_buffer() { + let mut env = setup_post_commitment_env(2, true, false).await; + let mut args = valid_post_commitment_args(); + args.commit_id = 99; + + assert!(post_v2_commitment(&mut env, args).await.is_err()); +} + +#[tokio::test] +async fn test_post_commitment_fails_with_wrong_delegation_authority() { + let mut env = setup_post_commitment_env(2, true, true).await; + + assert!(post_v2_commitment(&mut env, valid_post_commitment_args()) + .await + .is_err()); +} + +struct PostCommitmentEnv { + banks: BanksClient, + payer: Keypair, + operator: Keypair, + verifiers: Vec, + delegated_account: Pubkey, + config_args: dlp_api::v2::InitProtocolConfigArgs, +} + +async fn setup_post_commitment_env( + verifier_count: usize, + register_operator_as_verifier: bool, + wrong_delegation_authority: bool, +) -> PostCommitmentEnv { + 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 verifiers = (0..verifier_count) + .map(|_| Keypair::new()) + .collect::>(); + let delegated_account = Pubkey::new_unique(); + let delegation_authority = if wrong_delegation_authority { + Pubkey::new_unique() + } else { + operator.pubkey() + }; + + add_lamport_account(&mut program_test, authority.pubkey()); + add_lamport_account(&mut program_test, operator.pubkey()); + for verifier in verifiers.iter() { + add_lamport_account(&mut program_test, verifier.pubkey()); + } + + program_test.add_account( + delegated_account, + Account { + lamports: LAMPORTS_PER_SOL, + data: state_data(), + 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( + delegation_authority, + Pubkey::new_unique(), + Some(LAMPORTS_PER_SOL), + ), + owner: dlp_api::ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks, payer, blockhash) = program_test.start().await; + let config_args = valid_protocol_config_args(); + initialize_protocol_config( + &banks, + &payer, + &authority, + blockhash, + config_args.clone(), + ) + .await; + + register_v2_operator( + &banks, + &payer, + &operator, + &authority, + config_args.min_operator_bond, + ) + .await; + + if register_operator_as_verifier { + register_and_add_v2_verifier( + &banks, + &payer, + &operator, + &authority, + config_args.min_verifier_bond, + ) + .await; + } + + for verifier in verifiers.iter() { + register_and_add_v2_verifier( + &banks, + &payer, + verifier, + &authority, + config_args.min_verifier_bond, + ) + .await; + } + + write_v2_state_buffer( + &mut banks, + &payer, + &operator, + delegated_account, + WriteStateBufferArgs { + commit_id: valid_post_commitment_args().commit_id, + total_len: state_data().len() as u32, + offset: 0, + chunk: state_data(), + }, + ) + .await; + + PostCommitmentEnv { + banks, + payer, + operator, + verifiers, + delegated_account, + config_args, + } +} + +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 register_v2_operator( + banks: &BanksClient, + payer: &Keypair, + operator: &Keypair, + authority: &Keypair, + amount_lamports: u64, +) { + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = register_operator( + operator.pubkey(), + authority.pubkey(), + RegisterOperatorArgs { amount_lamports }, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[payer, operator, authority], + blockhash, + ); + + banks.process_transaction(tx).await.unwrap(); +} + +async fn register_and_add_v2_verifier( + banks: &BanksClient, + payer: &Keypair, + verifier: &Keypair, + authority: &Keypair, + amount_lamports: u64, +) { + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = register_verifier( + verifier.pubkey(), + authority.pubkey(), + RegisterVerifierArgs { amount_lamports }, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[payer, verifier, authority], + blockhash, + ); + banks.process_transaction(tx).await.unwrap(); + + let blockhash = banks.get_latest_blockhash().await.unwrap(); + let ix = update_verifier_registry( + authority.pubkey(), + verifier.pubkey(), + dlp_api::v2::UpdateVerifierRegistryArgs { + action: VERIFIER_REGISTRY_ACTION_ADD, + weight: 1, + }, + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[payer, authority], + blockhash, + ); + + banks.process_transaction(tx).await.unwrap(); +} + +fn valid_post_commitment_args() -> PostCommitmentArgs { + PostCommitmentArgs { + commit_id: 1, + lamports: 1_000, + owner: Pubkey::new_unique(), + da_pointer_hash: [9; 32], + er_slot: Some(42), + } +} + +async fn write_v2_state_buffer( + banks: &mut BanksClient, + payer: &Keypair, + operator: &Keypair, + account: Pubkey, + args: WriteStateBufferArgs, +) { + let latest_blockhash: Hash = banks.get_latest_blockhash().await.unwrap(); + let blockhash = banks + .get_new_latest_blockhash(&latest_blockhash) + .await + .unwrap(); + let ix = + write_state_buffer(payer.pubkey(), operator.pubkey(), account, args); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[payer, operator], + blockhash, + ); + + banks.process_transaction(tx).await.unwrap(); +} + +fn state_data() -> Vec { + vec![1, 2, 3, 4] +} + +fn account_data_hash(data: &[u8]) -> [u8; 32] { + solana_sha256_hasher::hashv(&[b"magicblock.account_data.v1", data]) + .to_bytes() +} + +async fn post_v2_commitment( + env: &mut PostCommitmentEnv, + args: PostCommitmentArgs, +) -> Result<(), solana_program_test::BanksClientError> { + let latest_blockhash: Hash = + env.banks.get_latest_blockhash().await.unwrap(); + let blockhash = env + .banks + .get_new_latest_blockhash(&latest_blockhash) + .await + .unwrap(); + let ix = + post_commitment(env.operator.pubkey(), env.delegated_account, args); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&env.payer.pubkey()), + &[&env.payer, &env.operator], + blockhash, + ); + + env.banks.process_transaction(tx).await +}