From 40bda91e9b5548a5806ba7d12dfa2e822a09e28e Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sat, 29 Aug 2026 03:43:19 +0530 Subject: [PATCH 1/5] feat(fraud-proofs): Implement ChallengerReveal --- dlp-api/src/v2/args/challenger_reveal.rs | 19 + dlp-api/src/v2/args/mod.rs | 2 + dlp-api/src/v2/instruction.rs | 2 + .../instruction_builder/challenger_reveal.rs | 72 ++ dlp-api/src/v2/instruction_builder/mod.rs | 2 + .../fraud_proofs/challenger_reveal.rs | 501 +++++++++++ src/v2/processor/fraud_proofs/mod.rs | 2 + src/v2/processor/mod.rs | 3 + tests/test_v2_challenger_reveal.rs | 850 ++++++++++++++++++ 9 files changed, 1453 insertions(+) create mode 100644 dlp-api/src/v2/args/challenger_reveal.rs create mode 100644 dlp-api/src/v2/instruction_builder/challenger_reveal.rs create mode 100644 src/v2/processor/fraud_proofs/challenger_reveal.rs create mode 100644 tests/test_v2_challenger_reveal.rs diff --git a/dlp-api/src/v2/args/challenger_reveal.rs b/dlp-api/src/v2/args/challenger_reveal.rs new file mode 100644 index 00000000..8e97b7b3 --- /dev/null +++ b/dlp-api/src/v2/args/challenger_reveal.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 ChallengerRevealArgs { + /// Challenger-revealed account lamports. + pub lamports: u64, + + /// Challenger-revealed account owner. + pub owner: Pubkey, + + /// Hash of the full challenger-uploaded account data. + pub data_hash: [u8; 32], + + /// Salt used to open the challenge hash. + pub salt: [u8; 32], +} diff --git a/dlp-api/src/v2/args/mod.rs b/dlp-api/src/v2/args/mod.rs index 79d708e5..ab1733b1 100644 --- a/dlp-api/src/v2/args/mod.rs +++ b/dlp-api/src/v2/args/mod.rs @@ -1,6 +1,7 @@ // V2 processors decode args from `instruction_data[1..]` after the one-byte // instruction tag, so v2 instruction args use `buffer_offset = 1`. +mod challenger_reveal; mod init_protocol_config; mod post_commitment; mod raise_challenge; @@ -10,6 +11,7 @@ mod update_protocol_config; mod update_verifier_registry; mod write_state_buffer; +pub use challenger_reveal::*; pub use init_protocol_config::*; pub use post_commitment::*; pub use raise_challenge::*; diff --git a/dlp-api/src/v2/instruction.rs b/dlp-api/src/v2/instruction.rs index 80f24fb0..77a0ffa9 100644 --- a/dlp-api/src/v2/instruction.rs +++ b/dlp-api/src/v2/instruction.rs @@ -29,6 +29,8 @@ pub enum DlpV2Instruction { FinalizeCommitment = 108, /// Raises a hash-only challenge against a v2 pending commitment. RaiseChallenge = 109, + /// Reveals challenger state for a v2 challenge. + ChallengerReveal = 110, } impl DlpV2Instruction { diff --git a/dlp-api/src/v2/instruction_builder/challenger_reveal.rs b/dlp-api/src/v2/instruction_builder/challenger_reveal.rs new file mode 100644 index 00000000..7528720c --- /dev/null +++ b/dlp-api/src/v2/instruction_builder/challenger_reveal.rs @@ -0,0 +1,72 @@ +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; +use wheels::layout::Encodable; + +use crate::{ + compat::{Compatize, Modernize}, + pda::fees_vault_pda, + v2::{ + pda::{ + challenge_pda, pending_commitment_pda, protocol_config_pda, + state_buffer_pda, + }, + ChallengerRevealArgs, DlpV2Instruction, + }, +}; + +/// Builds the instruction that reveals challenger state for a v2 challenge. +pub fn challenger_reveal( + challenger: Pubkey, + operator: Pubkey, + account: Pubkey, + commit_id: u64, + args: ChallengerRevealArgs, +) -> Instruction { + Instruction { + program_id: crate::id().modernize(), + accounts: vec![ + AccountMeta::new(challenger, true), + AccountMeta::new( + challenge_pda( + &account.compatize(), + commit_id, + &challenger.compatize(), + ) + .modernize(), + false, + ), + AccountMeta::new( + pending_commitment_pda(&account.compatize(), commit_id) + .modernize(), + false, + ), + AccountMeta::new_readonly( + state_buffer_pda( + &account.compatize(), + commit_id, + &operator.compatize(), + ) + .modernize(), + false, + ), + AccountMeta::new_readonly( + state_buffer_pda( + &account.compatize(), + commit_id, + &challenger.compatize(), + ) + .modernize(), + false, + ), + AccountMeta::new_readonly(protocol_config_pda().modernize(), false), + AccountMeta::new(fees_vault_pda().modernize(), false), + ], + data: [ + DlpV2Instruction::ChallengerReveal.to_vec(), + args.encode().unwrap(), + ] + .concat(), + } +} diff --git a/dlp-api/src/v2/instruction_builder/mod.rs b/dlp-api/src/v2/instruction_builder/mod.rs index 5e9fbe05..4d9d4eff 100644 --- a/dlp-api/src/v2/instruction_builder/mod.rs +++ b/dlp-api/src/v2/instruction_builder/mod.rs @@ -1,4 +1,5 @@ mod approve_commitment; +mod challenger_reveal; mod finalize_commitment; mod init_protocol_config; mod post_commitment; @@ -10,6 +11,7 @@ mod update_verifier_registry; mod write_state_buffer; pub use approve_commitment::*; +pub use challenger_reveal::*; pub use finalize_commitment::*; pub use init_protocol_config::*; pub use post_commitment::*; diff --git a/src/v2/processor/fraud_proofs/challenger_reveal.rs b/src/v2/processor/fraud_proofs/challenger_reveal.rs new file mode 100644 index 00000000..3869afd7 --- /dev/null +++ b/src/v2/processor/fraud_proofs/challenger_reveal.rs @@ -0,0 +1,501 @@ +use dlp_api::{ + error::DlpError, + v2::{ + pda::{ + CHALLENGE_SEED, PENDING_COMMITMENT_SEED, PROTOCOL_CONFIG_SEED, + STATE_BUFFER_SEED, + }, + Challenge, ChallengerRevealArgs, PendingCommitment, ProtocolConfig, + SelectedVerifier, StateBuffer, CHALLENGE_OUTCOME_INVALID_REVEAL, + CHALLENGE_OUTCOME_MATCHING_STATE_CHALLENGER_PENALIZED, + CHALLENGE_OUTCOME_NONE, CHALLENGE_STATUS_AWAITING_RESOLVER, + CHALLENGE_STATUS_AWAITING_REVEAL, CHALLENGE_STATUS_TERMINAL, + PENDING_COMMITMENT_STATUS_ACTIVE, + PENDING_COMMITMENT_STATUS_AWAITING_CHALLENGER_REVEAL, + PENDING_COMMITMENT_STATUS_AWAITING_DISPUTE_RESOLUTION, + }, +}; +use pinocchio::{ + error::ProgramError, + sysvars::{clock::Clock, Sysvar}, + AccountView, ProgramResult, +}; +use wheels::{ + layout::{Decodable, Encodable}, + require_eq, require_eq_keys, require_le, require_n_accounts, + require_signer, +}; + +use crate::requires::{require_initialized_pda, require_owned_pda}; + +/// Reveal challenger state for one v2 challenge. +/// +/// Accounts: +/// 0: `[signer, writable]` challenger identity and refund account +/// 1: `[writable]` Challenge PDA +/// 2: `[writable]` PendingCommitment PDA +/// 3: `[]` operator StateBuffer PDA +/// 4: `[]` challenger StateBuffer PDA +/// 5: `[]` ProtocolConfig PDA +/// 6: `[writable]` protocol fee vault +#[inline(never)] +pub fn process_challenger_reveal( + accounts: &[AccountView], + data: &[u8], +) -> ProgramResult { + let [ + challenger, // force multi-line + challenge, + pending_commitment, + operator_state_buffer, + challenger_state_buffer, + protocol_config, + protocol_fee_vault, + ] = require_n_accounts!(accounts, 7); + + let args = ChallengerRevealArgs::decode(data)?; + + require_signer!(challenger); + if !challenger.is_writable() + || !challenge.is_writable() + || !pending_commitment.is_writable() + || !protocol_fee_vault.is_writable() + { + return Err(ProgramError::Immutable); + } + + require_owned_pda(challenge, &crate::fast::ID, "challenge")?; + require_owned_pda( + pending_commitment, + &crate::fast::ID, + "pending commitment", + )?; + + let (configured_fee_vault, match_penalty_bps) = + load_protocol_config(protocol_config)?; + // CHECKPOINT: v2 treats the fee vault as the configured recipient, not as a + // hard-coded PDA validation here. InitProtocolConfig currently sets it to + // the legacy fees-vault PDA. + require_eq_keys!( + &configured_fee_vault, + protocol_fee_vault.address(), + ProgramError::InvalidAccountData + ); + + let mut pending = load_pending_commitment(pending_commitment)?; + let mut challenge_state = load_challenge(challenge)?; + let clock = Clock::get()?; + + validate_pending_commitment(&pending, pending_commitment, challenge)?; + validate_challenge( + &challenge_state, + challenge, + pending_commitment, + challenger, + &pending, + clock.slot, + )?; + + validate_state_buffer( + operator_state_buffer, + &pending.operator_identity, + &pending.account_pubkey, + pending.commit_id, + &pending.data_hash, + )?; + + let challenger_identity = challenger.address().clone(); + validate_state_buffer( + challenger_state_buffer, + &challenger_identity, + &pending.account_pubkey, + pending.commit_id, + args.data_hash(), + )?; + + // Keep the attempted opening in Challenge state even when the salted + // challenge hash does not match. + challenge_state.challenger_lamports = args.lamports(); + challenge_state.challenger_owner = *args.owner(); + challenge_state.challenger_data_hash = *args.data_hash(); + challenge_state.challenger_state_buffer = + challenger_state_buffer.address().clone(); + + let opened_state_hash = + account_state_hash(args.lamports(), args.owner(), args.data_hash()); + let opened_challenge_hash = + challenge_hash(&pending, &challenge_state, &args); + + // CHECKPOINT: invalid or matching reveals terminate only this challenge and + // reopen the pending commitment. Challenge PDA rent remains until a later + // close instruction, while resolver-worthy mismatches keep the stake locked. + if opened_challenge_hash != challenge_state.challenge_hash { + move_lamports( + challenge, + protocol_fee_vault, + challenge_state.challenger_stake_lamports, + )?; + challenge_state.status = CHALLENGE_STATUS_TERMINAL; + challenge_state.outcome = CHALLENGE_OUTCOME_INVALID_REVEAL; + reopen_pending_commitment(&mut pending); + } else if opened_state_hash == pending.account_state_hash { + let penalty_lamports = match_penalty_lamports( + challenge_state.challenger_stake_lamports, + match_penalty_bps, + )?; + let refund_lamports = challenge_state + .challenger_stake_lamports + .checked_sub(penalty_lamports) + .ok_or(DlpError::Overflow)?; + + move_lamports(challenge, protocol_fee_vault, penalty_lamports)?; + move_lamports(challenge, challenger, refund_lamports)?; + challenge_state.status = CHALLENGE_STATUS_TERMINAL; + challenge_state.outcome = + CHALLENGE_OUTCOME_MATCHING_STATE_CHALLENGER_PENALIZED; + reopen_pending_commitment(&mut pending); + } else { + challenge_state.status = CHALLENGE_STATUS_AWAITING_RESOLVER; + challenge_state.outcome = CHALLENGE_OUTCOME_NONE; + pending.status = PENDING_COMMITMENT_STATUS_AWAITING_DISPUTE_RESOLUTION; + } + + challenge_state.encode_to(challenge.try_borrow_mut()?.as_mut())?; + pending.encode_to(pending_commitment.try_borrow_mut()?.as_mut())?; + + Ok(()) +} + +fn load_protocol_config( + protocol_config: &AccountView, +) -> Result<(dlp_api::compat::Pubkey, u16), ProgramError> { + require_initialized_pda( + protocol_config, + &[PROTOCOL_CONFIG_SEED], + &crate::fast::ID, + false, + "protocol config", + )?; + + let data = protocol_config.try_borrow()?; + let state = ProtocolConfig::decode(data.as_ref())?; + if state.discriminator() != ProtocolConfig::DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData); + } + require_eq!(state.paused(), false, ProgramError::InvalidAccountData); + + Ok((*state.protocol_fee_vault(), state.match_penalty_bps())) +} + +fn load_pending_commitment( + pending_commitment: &AccountView, +) -> Result { + let pending_data = pending_commitment.try_borrow()?; + let pending_view = PendingCommitment::decode(pending_data.as_ref())?; + + if pending_view.discriminator() != PendingCommitment::DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData); + } + + Ok(PendingCommitment { + discriminator: PendingCommitment::DISCRIMINATOR, + status: pending_view.status(), + operator_identity: *pending_view.operator_identity(), + operator_bond: *pending_view.operator_bond(), + account_pubkey: *pending_view.account_pubkey(), + commit_id: pending_view.commit_id(), + delegation_record: *pending_view.delegation_record(), + da_pointer_hash: *pending_view.da_pointer_hash(), + account_state_hash: *pending_view.account_state_hash(), + data_hash: *pending_view.data_hash(), + lamports: pending_view.lamports(), + owner: *pending_view.owner(), + state_commitment_hash: *pending_view.state_commitment_hash(), + verifier_registry: *pending_view.verifier_registry(), + verifier_registry_revision: pending_view.verifier_registry_revision(), + challenge_window_id: pending_view.challenge_window_id(), + posted_slot: pending_view.posted_slot(), + activation_slot: pending_view.activation_slot(), + challenge_window_end_slot: pending_view.challenge_window_end_slot(), + approval_count: pending_view.approval_count(), + approval_threshold: pending_view.approval_threshold(), + active_challenge: pending_view.active_challenge().cloned(), + resolved_state_source: pending_view.resolved_state_source(), + er_slot: pending_view.er_slot(), + _pad_before_selected_verifiers: [0; 7], + selected_verifiers: pending_view + .selected_verifiers() + .iter() + .map(|verifier| SelectedVerifier { + verifier_identity: *verifier.verifier_identity(), + approved: verifier.approved(), + _pad_after_approved: [0; 7], + }) + .collect(), + }) +} + +fn load_challenge(challenge: &AccountView) -> Result { + let challenge_data = challenge.try_borrow()?; + let challenge_view = Challenge::decode(challenge_data.as_ref())?; + + if challenge_view.discriminator() != Challenge::DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData); + } + + Ok(Challenge { + discriminator: Challenge::DISCRIMINATOR, + status: challenge_view.status(), + outcome: challenge_view.outcome(), + _pad_after_outcome: [0; 6], + pending_commitment: *challenge_view.pending_commitment(), + challenger_identity: *challenge_view.challenger_identity(), + state_commitment_hash: *challenge_view.state_commitment_hash(), + challenge_hash: *challenge_view.challenge_hash(), + challenger_lamports: challenge_view.challenger_lamports(), + challenger_owner: *challenge_view.challenger_owner(), + challenger_data_hash: *challenge_view.challenger_data_hash(), + challenger_state_buffer: *challenge_view.challenger_state_buffer(), + challenger_stake_lamports: challenge_view.challenger_stake_lamports(), + raised_slot: challenge_view.raised_slot(), + reveal_deadline_slot: challenge_view.reveal_deadline_slot(), + }) +} + +fn validate_pending_commitment( + pending: &PendingCommitment, + pending_commitment: &AccountView, + challenge: &AccountView, +) -> ProgramResult { + let commit_id_bytes = pending.commit_id.to_le_bytes(); + require_initialized_pda( + pending_commitment, + &[ + PENDING_COMMITMENT_SEED, + pending.account_pubkey.as_ref(), + &commit_id_bytes, + ], + &crate::fast::ID, + true, + "pending commitment", + )?; + + require_eq!( + pending.status, + PENDING_COMMITMENT_STATUS_AWAITING_CHALLENGER_REVEAL, + ProgramError::InvalidInstructionData + ); + let active_challenge = pending + .active_challenge + .as_ref() + .ok_or(ProgramError::InvalidInstructionData)?; + require_eq_keys!( + active_challenge, + challenge.address(), + ProgramError::InvalidInstructionData + ); + require_eq!( + pending.resolved_state_source.is_none(), + true, + ProgramError::InvalidInstructionData + ); + + Ok(()) +} + +fn validate_challenge( + challenge_state: &Challenge, + challenge: &AccountView, + pending_commitment: &AccountView, + challenger: &AccountView, + pending: &PendingCommitment, + current_slot: u64, +) -> ProgramResult { + let commit_id_bytes = pending.commit_id.to_le_bytes(); + require_initialized_pda( + challenge, + &[ + CHALLENGE_SEED, + pending.account_pubkey.as_ref(), + &commit_id_bytes, + challenger.address().as_ref(), + ], + &crate::fast::ID, + true, + "challenge", + )?; + + require_eq!( + challenge_state.status, + CHALLENGE_STATUS_AWAITING_REVEAL, + ProgramError::InvalidInstructionData + ); + require_eq!( + challenge_state.outcome, + CHALLENGE_OUTCOME_NONE, + ProgramError::InvalidInstructionData + ); + require_eq_keys!( + &challenge_state.pending_commitment, + pending_commitment.address(), + ProgramError::InvalidInstructionData + ); + require_eq_keys!( + &challenge_state.challenger_identity, + challenger.address(), + DlpError::InvalidAuthority + ); + require_eq!( + &challenge_state.state_commitment_hash, + &pending.state_commitment_hash, + ProgramError::InvalidInstructionData + ); + require_le!( + current_slot, + challenge_state.reveal_deadline_slot, + ProgramError::InvalidInstructionData + ); + + Ok(()) +} + +fn validate_state_buffer( + state_buffer: &AccountView, + authority: &dlp_api::compat::Pubkey, + account_pubkey: &dlp_api::compat::Pubkey, + commit_id: u64, + expected_data_hash: &[u8; 32], +) -> ProgramResult { + let commit_id_bytes = commit_id.to_le_bytes(); + require_initialized_pda( + state_buffer, + &[ + STATE_BUFFER_SEED, + account_pubkey.as_ref(), + &commit_id_bytes, + authority.as_ref(), + ], + &crate::fast::ID, + false, + "state buffer", + )?; + + let data = state_buffer.try_borrow()?; + if data.len() < StateBuffer::DATA_LEN { + return Err(ProgramError::InvalidAccountData); + } + let state = StateBuffer::decode(&data.as_ref()[..StateBuffer::DATA_LEN])?; + + if state.discriminator() != StateBuffer::DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData); + } + require_eq_keys!(state.authority(), authority, DlpError::InvalidAuthority); + require_eq_keys!( + state.account_pubkey(), + account_pubkey, + ProgramError::InvalidAccountData + ); + require_eq!( + state.commit_id(), + commit_id, + ProgramError::InvalidInstructionData + ); + require_eq!( + state.finalized(), + true, + ProgramError::InvalidInstructionData + ); + require_eq!( + state.written_len(), + state.total_len(), + ProgramError::InvalidInstructionData + ); + require_eq!( + state.data_hash(), + expected_data_hash, + ProgramError::InvalidInstructionData + ); + + let raw_end = StateBuffer::DATA_LEN + .checked_add(state.total_len() as usize) + .ok_or(DlpError::Overflow)?; + if data.len() < raw_end { + return Err(ProgramError::InvalidAccountData); + } + + Ok(()) +} + +fn reopen_pending_commitment(pending: &mut PendingCommitment) { + pending.status = PENDING_COMMITMENT_STATUS_ACTIVE; + pending.active_challenge = None; + pending.resolved_state_source = None; +} + +fn match_penalty_lamports( + stake_lamports: u64, + match_penalty_bps: u16, +) -> Result { + Ok(stake_lamports + .checked_mul(match_penalty_bps as u64) + .ok_or(DlpError::Overflow)? + / 10_000) +} + +fn move_lamports( + from: &AccountView, + to: &AccountView, + amount: u64, +) -> ProgramResult { + if amount == 0 { + return Ok(()); + } + + let from_lamports = from + .lamports() + .checked_sub(amount) + .ok_or(DlpError::Overflow)?; + let to_lamports = to + .lamports() + .checked_add(amount) + .ok_or(DlpError::Overflow)?; + + from.set_lamports(from_lamports); + to.set_lamports(to_lamports); + + Ok(()) +} + +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() +} + +fn challenge_hash( + pending: &PendingCommitment, + challenge: &Challenge, + args: &dlp_api::v2::ChallengerRevealArgsView<'_>, +) -> [u8; 32] { + solana_sha256_hasher::hashv(&[ + b"magicblock.challenge.v1", + &challenge.state_commitment_hash, + pending.operator_identity.as_ref(), + challenge.challenger_identity.as_ref(), + pending.account_pubkey.as_ref(), + &pending.commit_id.to_le_bytes(), + &args.lamports().to_le_bytes(), + args.owner().as_ref(), + args.data_hash(), + args.salt(), + ]) + .to_bytes() +} diff --git a/src/v2/processor/fraud_proofs/mod.rs b/src/v2/processor/fraud_proofs/mod.rs index c585e735..5d10fc64 100644 --- a/src/v2/processor/fraud_proofs/mod.rs +++ b/src/v2/processor/fraud_proofs/mod.rs @@ -1,12 +1,14 @@ //! Processors for v2 fraud-proof instructions. mod approve_commitment; +mod challenger_reveal; mod finalize_commitment; mod post_commitment; mod raise_challenge; mod write_state_buffer; pub use approve_commitment::*; +pub use challenger_reveal::*; pub use finalize_commitment::*; pub use post_commitment::*; pub use raise_challenge::*; diff --git a/src/v2/processor/mod.rs b/src/v2/processor/mod.rs index 396013a9..68c9cbec 100644 --- a/src/v2/processor/mod.rs +++ b/src/v2/processor/mod.rs @@ -45,5 +45,8 @@ pub fn process_instruction( DlpV2Instruction::RaiseChallenge => { process_raise_challenge(accounts, data) } + DlpV2Instruction::ChallengerReveal => { + process_challenger_reveal(accounts, data) + } } } diff --git a/tests/test_v2_challenger_reveal.rs b/tests/test_v2_challenger_reveal.rs new file mode 100644 index 00000000..86f8edf9 --- /dev/null +++ b/tests/test_v2_challenger_reveal.rs @@ -0,0 +1,850 @@ +use dlp_api::{ + pda::{ + delegation_metadata_pda_from_delegated_account, + delegation_record_pda_from_delegated_account, fees_vault_pda, + }, + v2::{ + instruction_builder::{ + challenger_reveal, post_commitment, raise_challenge, + register_operator, register_verifier, update_verifier_registry, + write_state_buffer, + }, + pda::{challenge_pda, pending_commitment_pda, state_buffer_pda}, + Challenge, ChallengerRevealArgs, DlpV2Instruction, PendingCommitment, + PostCommitmentArgs, RaiseChallengeArgs, RegisterOperatorArgs, + RegisterVerifierArgs, WriteStateBufferArgs, + CHALLENGE_OUTCOME_INVALID_REVEAL, + CHALLENGE_OUTCOME_MATCHING_STATE_CHALLENGER_PENALIZED, + CHALLENGE_OUTCOME_NONE, CHALLENGE_STATUS_AWAITING_RESOLVER, + CHALLENGE_STATUS_TERMINAL, PENDING_COMMITMENT_STATUS_ACTIVE, + PENDING_COMMITMENT_STATUS_AWAITING_DISPUTE_RESOLUTION, + VERIFIER_REGISTRY_ACTION_ADD, + }, +}; +use solana_program::{native_token::LAMPORTS_PER_SOL, rent::Rent}; +use solana_program_test::{ + BanksClientError, ProgramTest, ProgramTestBanksClientExt, + ProgramTestContext, +}; +use solana_sdk::{ + account::Account, + hash::Hash, + instruction::Instruction, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use solana_sdk_ids::system_program; +use wheels::layout::{Decodable, Encodable}; + +mod fixtures; + +use crate::fixtures::{ + create_delegation_metadata_data, create_delegation_record_data, + v2::{init_v2, valid_args}, +}; + +const CHALLENGE_STAKE: u64 = 10_000; + +#[test] +fn test_v2_challenger_reveal_instruction_data_uses_one_byte_tag() { + let challenger = Pubkey::new_unique(); + let operator = Pubkey::new_unique(); + let account = Pubkey::new_unique(); + let args = ChallengerRevealArgs { + lamports: 1, + owner: Pubkey::new_unique(), + data_hash: [2; 32], + salt: [3; 32], + }; + let ix = challenger_reveal(challenger, operator, account, 7, args.clone()); + let encoded_args = args.encode().unwrap(); + + assert_eq!(ix.accounts.len(), 7); + assert_eq!(ix.accounts[0].pubkey, challenger); + assert!(ix.accounts[0].is_signer); + assert!(ix.accounts[0].is_writable); + assert_eq!( + ix.accounts[1].pubkey, + challenge_pda(&account, 7, &challenger) + ); + assert!(ix.accounts[1].is_writable); + assert_eq!(ix.accounts[2].pubkey, pending_commitment_pda(&account, 7)); + assert!(ix.accounts[2].is_writable); + assert_eq!( + ix.accounts[3].pubkey, + state_buffer_pda(&account, 7, &operator) + ); + assert!(!ix.accounts[3].is_writable); + assert_eq!( + ix.accounts[4].pubkey, + state_buffer_pda(&account, 7, &challenger) + ); + assert!(!ix.accounts[4].is_writable); + assert_eq!(ix.accounts[6].pubkey, fees_vault_pda()); + assert!(ix.accounts[6].is_writable); + + assert_eq!(ix.data[0], DlpV2Instruction::ChallengerReveal as u8); + assert_eq!(ix.data.len(), 1 + encoded_args.len()); + assert_eq!(&ix.data[1..], encoded_args.as_slice()); + + let decoded = + ::decode(&ix.data[1..]).unwrap(); + assert_eq!(decoded.lamports(), args.lamports); + assert_eq!(*decoded.owner(), args.owner); + assert_eq!(*decoded.data_hash(), args.data_hash); + assert_eq!(*decoded.salt(), args.salt); +} + +#[tokio::test] +async fn test_v2_challenger_reveal_matching_state_penalizes_and_reopens_commitment( +) { + let mut env = setup_challenger_reveal_env().await; + post_v2_commitment(&mut env).await.unwrap(); + + let data_hash = account_data_hash(&env.operator_state_data); + let reveal_args = valid_reveal_args( + env.committed_lamports, + env.committed_owner, + data_hash, + ); + let challenger_data = env.operator_state_data.clone(); + write_challenger_state_buffer(&mut env, challenger_data) + .await + .unwrap(); + + let challenger_before = + account_lamports(&mut env.context, env.challenger.pubkey()).await; + let fee_vault_before = + account_lamports(&mut env.context, fees_vault_pda()).await; + + raise_v2_challenge_for_reveal(&mut env, &reveal_args, CHALLENGE_STAKE) + .await + .unwrap(); + reveal_v2_challenge(&mut env, reveal_args.clone()) + .await + .unwrap(); + + let penalty = + CHALLENGE_STAKE * u64::from(env.config_args.match_penalty_bps) / 10_000; + let challenge_rent = Rent::default().minimum_balance(Challenge::DATA_LEN); + let challenge = read_challenge(&mut env).await; + assert_eq!(challenge.status, CHALLENGE_STATUS_TERMINAL); + assert_eq!( + challenge.outcome, + CHALLENGE_OUTCOME_MATCHING_STATE_CHALLENGER_PENALIZED + ); + assert_eq!(challenge.challenger_lamports, reveal_args.lamports); + assert_eq!(challenge.challenger_owner, reveal_args.owner); + assert_eq!(challenge.challenger_data_hash, reveal_args.data_hash); + assert_eq!( + challenge.challenger_state_buffer, + state_buffer_pda( + &env.delegated_account, + env.commit_id, + &env.challenger.pubkey() + ) + ); + assert_eq!(challenge.account_lamports, challenge_rent); + + let pending = read_pending_commitment(&mut env).await; + assert_eq!(pending.status, PENDING_COMMITMENT_STATUS_ACTIVE); + assert_eq!(pending.active_challenge, None); + assert_eq!(pending.resolved_state_source, None); + assert_eq!( + account_lamports(&mut env.context, env.challenger.pubkey()).await, + challenger_before - challenge_rent - penalty + ); + assert_eq!( + account_lamports(&mut env.context, fees_vault_pda()).await, + fee_vault_before + penalty + ); +} + +#[tokio::test] +async fn test_v2_challenger_reveal_mismatch_moves_to_resolver() { + let mut env = setup_challenger_reveal_env().await; + post_v2_commitment(&mut env).await.unwrap(); + + let challenger_data = vec![4, 3, 2, 1]; + let reveal_args = valid_reveal_args( + env.committed_lamports, + env.committed_owner, + account_data_hash(&challenger_data), + ); + write_challenger_state_buffer(&mut env, challenger_data) + .await + .unwrap(); + + let challenger_before = + account_lamports(&mut env.context, env.challenger.pubkey()).await; + let fee_vault_before = + account_lamports(&mut env.context, fees_vault_pda()).await; + + raise_v2_challenge_for_reveal(&mut env, &reveal_args, CHALLENGE_STAKE) + .await + .unwrap(); + reveal_v2_challenge(&mut env, reveal_args.clone()) + .await + .unwrap(); + + let challenge_rent = Rent::default().minimum_balance(Challenge::DATA_LEN); + let challenge_address = challenge_address(&env); + let challenge = read_challenge(&mut env).await; + assert_eq!(challenge.status, CHALLENGE_STATUS_AWAITING_RESOLVER); + assert_eq!(challenge.outcome, CHALLENGE_OUTCOME_NONE); + assert_eq!(challenge.challenger_lamports, reveal_args.lamports); + assert_eq!(challenge.challenger_owner, reveal_args.owner); + assert_eq!(challenge.challenger_data_hash, reveal_args.data_hash); + assert_eq!(challenge.account_lamports, challenge_rent + CHALLENGE_STAKE); + + let pending = read_pending_commitment(&mut env).await; + assert_eq!( + pending.status, + PENDING_COMMITMENT_STATUS_AWAITING_DISPUTE_RESOLUTION + ); + assert_eq!(pending.active_challenge, Some(challenge_address)); + assert_eq!(pending.resolved_state_source, None); + assert_eq!( + account_lamports(&mut env.context, env.challenger.pubkey()).await, + challenger_before - challenge_rent - CHALLENGE_STAKE + ); + assert_eq!( + account_lamports(&mut env.context, fees_vault_pda()).await, + fee_vault_before + ); +} + +#[tokio::test] +async fn test_v2_challenger_reveal_invalid_hash_slashes_and_reopens_commitment() +{ + let mut env = setup_challenger_reveal_env().await; + post_v2_commitment(&mut env).await.unwrap(); + + let challenger_data = vec![4, 3, 2, 1]; + let raise_reveal_args = valid_reveal_args( + env.committed_lamports, + env.committed_owner, + account_data_hash(&challenger_data), + ); + let mut actual_reveal_args = raise_reveal_args.clone(); + actual_reveal_args.salt[0] ^= 1; + write_challenger_state_buffer(&mut env, challenger_data) + .await + .unwrap(); + + let challenger_before = + account_lamports(&mut env.context, env.challenger.pubkey()).await; + let fee_vault_before = + account_lamports(&mut env.context, fees_vault_pda()).await; + + raise_v2_challenge_for_reveal( + &mut env, + &raise_reveal_args, + CHALLENGE_STAKE, + ) + .await + .unwrap(); + reveal_v2_challenge(&mut env, actual_reveal_args.clone()) + .await + .unwrap(); + + let challenge_rent = Rent::default().minimum_balance(Challenge::DATA_LEN); + let challenge = read_challenge(&mut env).await; + assert_eq!(challenge.status, CHALLENGE_STATUS_TERMINAL); + assert_eq!(challenge.outcome, CHALLENGE_OUTCOME_INVALID_REVEAL); + assert_eq!(challenge.challenger_lamports, actual_reveal_args.lamports); + assert_eq!(challenge.challenger_owner, actual_reveal_args.owner); + assert_eq!(challenge.challenger_data_hash, actual_reveal_args.data_hash); + assert_eq!(challenge.account_lamports, challenge_rent); + + let pending = read_pending_commitment(&mut env).await; + assert_eq!(pending.status, PENDING_COMMITMENT_STATUS_ACTIVE); + assert_eq!(pending.active_challenge, None); + assert_eq!(pending.resolved_state_source, None); + assert_eq!( + account_lamports(&mut env.context, env.challenger.pubkey()).await, + challenger_before - challenge_rent - CHALLENGE_STAKE + ); + assert_eq!( + account_lamports(&mut env.context, fees_vault_pda()).await, + fee_vault_before + CHALLENGE_STAKE + ); +} + +#[tokio::test] +async fn test_v2_challenger_reveal_fails_after_reveal_deadline() { + let mut env = setup_challenger_reveal_env().await; + post_v2_commitment(&mut env).await.unwrap(); + + let data_hash = account_data_hash(&env.operator_state_data); + let reveal_args = valid_reveal_args( + env.committed_lamports, + env.committed_owner, + data_hash, + ); + let challenger_data = env.operator_state_data.clone(); + write_challenger_state_buffer(&mut env, challenger_data) + .await + .unwrap(); + raise_v2_challenge_for_reveal(&mut env, &reveal_args, CHALLENGE_STAKE) + .await + .unwrap(); + + let challenge = read_challenge(&mut env).await; + env.context + .warp_to_slot(challenge.reveal_deadline_slot + 1) + .unwrap(); + + assert!(reveal_v2_challenge(&mut env, reveal_args).await.is_err()); +} + +#[tokio::test] +async fn test_v2_challenger_reveal_fails_with_wrong_challenger_buffer() { + let mut env = setup_challenger_reveal_env().await; + post_v2_commitment(&mut env).await.unwrap(); + + let data_hash = account_data_hash(&env.operator_state_data); + let reveal_args = valid_reveal_args( + env.committed_lamports, + env.committed_owner, + data_hash, + ); + raise_v2_challenge_for_reveal(&mut env, &reveal_args, CHALLENGE_STAKE) + .await + .unwrap(); + + let mut ix = challenger_reveal( + env.challenger.pubkey(), + env.operator.pubkey(), + env.delegated_account, + env.commit_id, + reveal_args, + ); + ix.accounts[4].pubkey = state_buffer_pda( + &env.delegated_account, + env.commit_id, + &env.operator.pubkey(), + ); + + assert!(process_ix(&mut env.context, ix, &[&env.challenger]) + .await + .is_err()); +} + +#[tokio::test] +async fn test_v2_challenger_reveal_fails_with_unfinalized_challenger_buffer() { + let mut env = setup_challenger_reveal_env().await; + post_v2_commitment(&mut env).await.unwrap(); + + let expected_data = [1, 2, 3, 4]; + let reveal_args = valid_reveal_args( + env.committed_lamports, + env.committed_owner, + account_data_hash(&expected_data), + ); + let commit_id = env.commit_id; + write_challenger_state_buffer_args( + &mut env, + WriteStateBufferArgs { + commit_id, + total_len: expected_data.len() as u32, + offset: 0, + chunk: expected_data[..2].to_vec(), + }, + ) + .await + .unwrap(); + raise_v2_challenge_for_reveal(&mut env, &reveal_args, CHALLENGE_STAKE) + .await + .unwrap(); + + assert!(reveal_v2_challenge(&mut env, reveal_args).await.is_err()); +} + +#[tokio::test] +async fn test_v2_challenger_reveal_fails_with_wrong_fee_vault() { + let mut env = setup_challenger_reveal_env().await; + post_v2_commitment(&mut env).await.unwrap(); + + let data_hash = account_data_hash(&env.operator_state_data); + let reveal_args = valid_reveal_args( + env.committed_lamports, + env.committed_owner, + data_hash, + ); + let challenger_data = env.operator_state_data.clone(); + write_challenger_state_buffer(&mut env, challenger_data) + .await + .unwrap(); + raise_v2_challenge_for_reveal(&mut env, &reveal_args, CHALLENGE_STAKE) + .await + .unwrap(); + + let mut ix = challenger_reveal( + env.challenger.pubkey(), + env.operator.pubkey(), + env.delegated_account, + env.commit_id, + reveal_args, + ); + ix.accounts[6].pubkey = env.challenger.pubkey(); + + assert!(process_ix(&mut env.context, ix, &[&env.challenger]) + .await + .is_err()); +} + +struct ChallengerRevealEnv { + context: ProgramTestContext, + operator: Keypair, + challenger: Keypair, + delegated_account: Pubkey, + committed_owner: Pubkey, + committed_lamports: u64, + commit_id: u64, + config_args: dlp_api::v2::InitProtocolConfigArgs, + operator_state_data: Vec, +} + +async fn setup_challenger_reveal_env() -> ChallengerRevealEnv { + 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 verifier = Keypair::new(); + let challenger = Keypair::new(); + let delegated_account = Pubkey::new_unique(); + let committed_owner = Pubkey::new_unique(); + let commit_id = 1; + let operator_state_data = vec![9, 8, 7, 6, 5]; + let record_lamports = LAMPORTS_PER_SOL; + let committed_lamports = LAMPORTS_PER_SOL; + + add_lamport_account(&mut program_test, authority.pubkey()); + add_lamport_account(&mut program_test, operator.pubkey()); + add_lamport_account(&mut program_test, verifier.pubkey()); + add_lamport_account(&mut program_test, challenger.pubkey()); + add_protocol_fee_vault(&mut program_test); + + program_test.add_account( + delegated_account, + Account { + lamports: record_lamports, + data: vec![1, 2], + 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( + operator.pubkey(), + committed_owner, + Some(record_lamports), + ), + owner: dlp_api::ID, + executable: false, + rent_epoch: 0, + }, + ); + program_test.add_account( + delegation_metadata_pda_from_delegated_account(&delegated_account), + Account { + lamports: LAMPORTS_PER_SOL, + data: create_delegation_metadata_data( + authority.pubkey(), + &[], + false, + ), + owner: dlp_api::ID, + executable: false, + rent_epoch: 0, + }, + ); + + let mut context = program_test.start_with_context().await; + let config_args = valid_args(); + init_v2( + &context.banks_client, + &context.payer, + &authority, + context.last_blockhash, + config_args.clone(), + ) + .await; + + register_v2_operator( + &mut context, + &operator, + &authority, + config_args.min_operator_bond, + ) + .await + .unwrap(); + register_and_add_v2_verifier( + &mut context, + &verifier, + &authority, + config_args.min_verifier_bond, + ) + .await + .unwrap(); + + write_v2_state_buffer( + &mut context, + &operator, + delegated_account, + WriteStateBufferArgs { + commit_id, + total_len: operator_state_data.len() as u32, + offset: 0, + chunk: operator_state_data.clone(), + }, + ) + .await + .unwrap(); + + ChallengerRevealEnv { + context, + operator, + challenger, + delegated_account, + committed_owner, + committed_lamports, + commit_id, + config_args, + operator_state_data, + } +} + +async fn post_v2_commitment( + env: &mut ChallengerRevealEnv, +) -> Result<(), BanksClientError> { + let ix = post_commitment( + env.operator.pubkey(), + env.delegated_account, + PostCommitmentArgs { + commit_id: env.commit_id, + lamports: env.committed_lamports, + owner: env.committed_owner, + da_pointer_hash: [9; 32], + er_slot: Some(42), + }, + ); + + process_ix(&mut env.context, ix, &[&env.operator]).await +} + +async fn raise_v2_challenge_for_reveal( + env: &mut ChallengerRevealEnv, + reveal_args: &ChallengerRevealArgs, + stake_lamports: u64, +) -> Result<(), BanksClientError> { + let pending = read_pending_commitment(env).await; + let ix = raise_challenge( + env.challenger.pubkey(), + env.delegated_account, + env.commit_id, + RaiseChallengeArgs { + state_commitment_hash: pending.state_commitment_hash, + challenge_hash: challenge_hash( + pending.state_commitment_hash, + env.operator.pubkey(), + env.challenger.pubkey(), + env.delegated_account, + env.commit_id, + reveal_args, + ), + stake_lamports, + }, + ); + + process_ix(&mut env.context, ix, &[&env.challenger]).await +} + +async fn reveal_v2_challenge( + env: &mut ChallengerRevealEnv, + args: ChallengerRevealArgs, +) -> Result<(), BanksClientError> { + let ix = challenger_reveal( + env.challenger.pubkey(), + env.operator.pubkey(), + env.delegated_account, + env.commit_id, + args, + ); + + process_ix(&mut env.context, ix, &[&env.challenger]).await +} + +async fn write_challenger_state_buffer( + env: &mut ChallengerRevealEnv, + data: Vec, +) -> Result<(), BanksClientError> { + write_challenger_state_buffer_args( + env, + WriteStateBufferArgs { + commit_id: env.commit_id, + total_len: data.len() as u32, + offset: 0, + chunk: data, + }, + ) + .await +} + +async fn write_challenger_state_buffer_args( + env: &mut ChallengerRevealEnv, + args: WriteStateBufferArgs, +) -> Result<(), BanksClientError> { + write_v2_state_buffer( + &mut env.context, + &env.challenger, + env.delegated_account, + args, + ) + .await +} + +async fn register_v2_operator( + context: &mut ProgramTestContext, + operator: &Keypair, + authority: &Keypair, + amount_lamports: u64, +) -> Result<(), BanksClientError> { + let ix = register_operator( + operator.pubkey(), + authority.pubkey(), + RegisterOperatorArgs { amount_lamports }, + ); + + process_ix(context, ix, &[operator, authority]).await +} + +async fn register_and_add_v2_verifier( + context: &mut ProgramTestContext, + verifier: &Keypair, + authority: &Keypair, + amount_lamports: u64, +) -> Result<(), BanksClientError> { + let ix = register_verifier( + verifier.pubkey(), + authority.pubkey(), + RegisterVerifierArgs { amount_lamports }, + ); + process_ix(context, ix, &[verifier, authority]).await?; + + let ix = update_verifier_registry( + authority.pubkey(), + verifier.pubkey(), + dlp_api::v2::UpdateVerifierRegistryArgs { + action: VERIFIER_REGISTRY_ACTION_ADD, + weight: 1, + }, + ); + + process_ix(context, ix, &[authority]).await +} + +async fn write_v2_state_buffer( + context: &mut ProgramTestContext, + authority: &Keypair, + account: Pubkey, + args: WriteStateBufferArgs, +) -> Result<(), BanksClientError> { + let ix = write_state_buffer( + context.payer.pubkey(), + authority.pubkey(), + account, + args, + ); + + process_ix(context, ix, &[authority]).await +} + +async fn process_ix( + context: &mut ProgramTestContext, + ix: Instruction, + signers: &[&Keypair], +) -> Result<(), BanksClientError> { + let latest_blockhash: Hash = + context.banks_client.get_latest_blockhash().await.unwrap(); + let blockhash = context + .banks_client + .get_new_latest_blockhash(&latest_blockhash) + .await + .unwrap(); + let tx = { + let mut all_signers = Vec::with_capacity(signers.len() + 1); + all_signers.push(&context.payer); + all_signers.extend_from_slice(signers); + + Transaction::new_signed_with_payer( + &[ix], + Some(&context.payer.pubkey()), + &all_signers, + blockhash, + ) + }; + + context.banks_client.process_transaction(tx).await +} + +struct PendingCommitmentSnapshot { + status: u8, + active_challenge: Option, + resolved_state_source: Option, + state_commitment_hash: [u8; 32], +} + +async fn read_pending_commitment( + env: &mut ChallengerRevealEnv, +) -> PendingCommitmentSnapshot { + let account = env + .context + .banks_client + .get_account(pending_commitment_pda( + &env.delegated_account, + env.commit_id, + )) + .await + .unwrap() + .unwrap(); + + let pending = + ::decode(&account.data).unwrap(); + + PendingCommitmentSnapshot { + status: pending.status(), + active_challenge: pending.active_challenge().cloned(), + resolved_state_source: pending.resolved_state_source(), + state_commitment_hash: *pending.state_commitment_hash(), + } +} + +struct ChallengeSnapshot { + status: u8, + outcome: u8, + challenger_lamports: u64, + challenger_owner: Pubkey, + challenger_data_hash: [u8; 32], + challenger_state_buffer: Pubkey, + reveal_deadline_slot: u64, + account_lamports: u64, +} + +async fn read_challenge(env: &mut ChallengerRevealEnv) -> ChallengeSnapshot { + let account = env + .context + .banks_client + .get_account(challenge_address(env)) + .await + .unwrap() + .unwrap(); + + let challenge = ::decode(&account.data).unwrap(); + + ChallengeSnapshot { + status: challenge.status(), + outcome: challenge.outcome(), + challenger_lamports: challenge.challenger_lamports(), + challenger_owner: *challenge.challenger_owner(), + challenger_data_hash: *challenge.challenger_data_hash(), + challenger_state_buffer: *challenge.challenger_state_buffer(), + reveal_deadline_slot: challenge.reveal_deadline_slot(), + account_lamports: account.lamports, + } +} + +async fn account_lamports( + context: &mut ProgramTestContext, + pubkey: Pubkey, +) -> u64 { + context + .banks_client + .get_account(pubkey) + .await + .unwrap() + .unwrap() + .lamports +} + +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, + }, + ); +} + +fn add_protocol_fee_vault(program_test: &mut ProgramTest) { + program_test.add_account( + fees_vault_pda(), + Account { + lamports: LAMPORTS_PER_SOL, + data: vec![0; 8], + owner: dlp_api::ID, + executable: false, + rent_epoch: 0, + }, + ); +} + +fn valid_reveal_args( + lamports: u64, + owner: Pubkey, + data_hash: [u8; 32], +) -> ChallengerRevealArgs { + ChallengerRevealArgs { + lamports, + owner, + data_hash, + salt: [7; 32], + } +} + +fn challenge_address(env: &ChallengerRevealEnv) -> Pubkey { + challenge_pda( + &env.delegated_account, + env.commit_id, + &env.challenger.pubkey(), + ) +} + +fn account_data_hash(data: &[u8]) -> [u8; 32] { + solana_sha256_hasher::hashv(&[b"magicblock.account_data.v1", data]) + .to_bytes() +} + +fn challenge_hash( + state_commitment_hash: [u8; 32], + operator: Pubkey, + challenger: Pubkey, + account: Pubkey, + commit_id: u64, + args: &ChallengerRevealArgs, +) -> [u8; 32] { + solana_sha256_hasher::hashv(&[ + b"magicblock.challenge.v1", + &state_commitment_hash, + operator.as_ref(), + challenger.as_ref(), + account.as_ref(), + &commit_id.to_le_bytes(), + &args.lamports.to_le_bytes(), + args.owner.as_ref(), + &args.data_hash, + &args.salt, + ]) + .to_bytes() +} From 3663849bf053905ccf5244207b97d2f709a4a89d Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Sun, 30 Aug 2026 22:18:59 +0530 Subject: [PATCH 2/5] Decode extendable StateBuffer in ChallengerReveal --- .../processor/fraud_proofs/challenger_reveal.rs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/v2/processor/fraud_proofs/challenger_reveal.rs b/src/v2/processor/fraud_proofs/challenger_reveal.rs index 3869afd7..1fadffd3 100644 --- a/src/v2/processor/fraud_proofs/challenger_reveal.rs +++ b/src/v2/processor/fraud_proofs/challenger_reveal.rs @@ -381,10 +381,7 @@ fn validate_state_buffer( )?; let data = state_buffer.try_borrow()?; - if data.len() < StateBuffer::DATA_LEN { - return Err(ProgramError::InvalidAccountData); - } - let state = StateBuffer::decode(&data.as_ref()[..StateBuffer::DATA_LEN])?; + let state = StateBuffer::decode(data.as_ref())?; if state.discriminator() != StateBuffer::DISCRIMINATOR { return Err(ProgramError::InvalidAccountData); @@ -406,8 +403,8 @@ fn validate_state_buffer( ProgramError::InvalidInstructionData ); require_eq!( - state.written_len(), - state.total_len(), + state.payload().len(), + state.total_len() as usize, ProgramError::InvalidInstructionData ); require_eq!( @@ -416,13 +413,6 @@ fn validate_state_buffer( ProgramError::InvalidInstructionData ); - let raw_end = StateBuffer::DATA_LEN - .checked_add(state.total_len() as usize) - .ok_or(DlpError::Overflow)?; - if data.len() < raw_end { - return Err(ProgramError::InvalidAccountData); - } - Ok(()) } From 54d394a3eeffb8de8d03088da31338989ade4cdf Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Mon, 31 Aug 2026 13:29:44 +0530 Subject: [PATCH 3/5] Drop trivial ChallengerReveal instruction data test --- tests/test_v2_challenger_reveal.rs | 58 +++--------------------------- 1 file changed, 4 insertions(+), 54 deletions(-) diff --git a/tests/test_v2_challenger_reveal.rs b/tests/test_v2_challenger_reveal.rs index 86f8edf9..16848789 100644 --- a/tests/test_v2_challenger_reveal.rs +++ b/tests/test_v2_challenger_reveal.rs @@ -10,9 +10,9 @@ use dlp_api::{ write_state_buffer, }, pda::{challenge_pda, pending_commitment_pda, state_buffer_pda}, - Challenge, ChallengerRevealArgs, DlpV2Instruction, PendingCommitment, - PostCommitmentArgs, RaiseChallengeArgs, RegisterOperatorArgs, - RegisterVerifierArgs, WriteStateBufferArgs, + Challenge, ChallengerRevealArgs, PendingCommitment, PostCommitmentArgs, + RaiseChallengeArgs, RegisterOperatorArgs, RegisterVerifierArgs, + WriteStateBufferArgs, CHALLENGE_OUTCOME_INVALID_REVEAL, CHALLENGE_OUTCOME_MATCHING_STATE_CHALLENGER_PENALIZED, CHALLENGE_OUTCOME_NONE, CHALLENGE_STATUS_AWAITING_RESOLVER, @@ -35,7 +35,7 @@ use solana_sdk::{ transaction::Transaction, }; use solana_sdk_ids::system_program; -use wheels::layout::{Decodable, Encodable}; +use wheels::layout::Decodable; mod fixtures; @@ -46,56 +46,6 @@ use crate::fixtures::{ const CHALLENGE_STAKE: u64 = 10_000; -#[test] -fn test_v2_challenger_reveal_instruction_data_uses_one_byte_tag() { - let challenger = Pubkey::new_unique(); - let operator = Pubkey::new_unique(); - let account = Pubkey::new_unique(); - let args = ChallengerRevealArgs { - lamports: 1, - owner: Pubkey::new_unique(), - data_hash: [2; 32], - salt: [3; 32], - }; - let ix = challenger_reveal(challenger, operator, account, 7, args.clone()); - let encoded_args = args.encode().unwrap(); - - assert_eq!(ix.accounts.len(), 7); - assert_eq!(ix.accounts[0].pubkey, challenger); - assert!(ix.accounts[0].is_signer); - assert!(ix.accounts[0].is_writable); - assert_eq!( - ix.accounts[1].pubkey, - challenge_pda(&account, 7, &challenger) - ); - assert!(ix.accounts[1].is_writable); - assert_eq!(ix.accounts[2].pubkey, pending_commitment_pda(&account, 7)); - assert!(ix.accounts[2].is_writable); - assert_eq!( - ix.accounts[3].pubkey, - state_buffer_pda(&account, 7, &operator) - ); - assert!(!ix.accounts[3].is_writable); - assert_eq!( - ix.accounts[4].pubkey, - state_buffer_pda(&account, 7, &challenger) - ); - assert!(!ix.accounts[4].is_writable); - assert_eq!(ix.accounts[6].pubkey, fees_vault_pda()); - assert!(ix.accounts[6].is_writable); - - assert_eq!(ix.data[0], DlpV2Instruction::ChallengerReveal as u8); - assert_eq!(ix.data.len(), 1 + encoded_args.len()); - assert_eq!(&ix.data[1..], encoded_args.as_slice()); - - let decoded = - ::decode(&ix.data[1..]).unwrap(); - assert_eq!(decoded.lamports(), args.lamports); - assert_eq!(*decoded.owner(), args.owner); - assert_eq!(*decoded.data_hash(), args.data_hash); - assert_eq!(*decoded.salt(), args.salt); -} - #[tokio::test] async fn test_v2_challenger_reveal_matching_state_penalizes_and_reopens_commitment( ) { From f2a937f89a16164f0ce2df93da5bd40ab25e229c Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Tue, 1 Sep 2026 05:23:30 +0530 Subject: [PATCH 4/5] Rename challenger reveal tests --- tests/test_v2_challenger_reveal.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/test_v2_challenger_reveal.rs b/tests/test_v2_challenger_reveal.rs index 16848789..1d09b120 100644 --- a/tests/test_v2_challenger_reveal.rs +++ b/tests/test_v2_challenger_reveal.rs @@ -12,8 +12,7 @@ use dlp_api::{ pda::{challenge_pda, pending_commitment_pda, state_buffer_pda}, Challenge, ChallengerRevealArgs, PendingCommitment, PostCommitmentArgs, RaiseChallengeArgs, RegisterOperatorArgs, RegisterVerifierArgs, - WriteStateBufferArgs, - CHALLENGE_OUTCOME_INVALID_REVEAL, + WriteStateBufferArgs, CHALLENGE_OUTCOME_INVALID_REVEAL, CHALLENGE_OUTCOME_MATCHING_STATE_CHALLENGER_PENALIZED, CHALLENGE_OUTCOME_NONE, CHALLENGE_STATUS_AWAITING_RESOLVER, CHALLENGE_STATUS_TERMINAL, PENDING_COMMITMENT_STATUS_ACTIVE, @@ -41,13 +40,13 @@ mod fixtures; use crate::fixtures::{ create_delegation_metadata_data, create_delegation_record_data, - v2::{init_v2, valid_args}, + v2::{initialize_protocol_config, valid_protocol_config_args}, }; const CHALLENGE_STAKE: u64 = 10_000; #[tokio::test] -async fn test_v2_challenger_reveal_matching_state_penalizes_and_reopens_commitment( +async fn test_challenger_reveal_matching_state_penalizes_and_reopens_commitment( ) { let mut env = setup_challenger_reveal_env().await; post_v2_commitment(&mut env).await.unwrap(); @@ -112,7 +111,7 @@ async fn test_v2_challenger_reveal_matching_state_penalizes_and_reopens_commitme } #[tokio::test] -async fn test_v2_challenger_reveal_mismatch_moves_to_resolver() { +async fn test_challenger_reveal_mismatch_moves_to_resolver() { let mut env = setup_challenger_reveal_env().await; post_v2_commitment(&mut env).await.unwrap(); @@ -166,8 +165,7 @@ async fn test_v2_challenger_reveal_mismatch_moves_to_resolver() { } #[tokio::test] -async fn test_v2_challenger_reveal_invalid_hash_slashes_and_reopens_commitment() -{ +async fn test_challenger_reveal_invalid_hash_slashes_and_reopens_commitment() { let mut env = setup_challenger_reveal_env().await; post_v2_commitment(&mut env).await.unwrap(); @@ -223,7 +221,7 @@ async fn test_v2_challenger_reveal_invalid_hash_slashes_and_reopens_commitment() } #[tokio::test] -async fn test_v2_challenger_reveal_fails_after_reveal_deadline() { +async fn test_challenger_reveal_fails_after_reveal_deadline() { let mut env = setup_challenger_reveal_env().await; post_v2_commitment(&mut env).await.unwrap(); @@ -250,7 +248,7 @@ async fn test_v2_challenger_reveal_fails_after_reveal_deadline() { } #[tokio::test] -async fn test_v2_challenger_reveal_fails_with_wrong_challenger_buffer() { +async fn test_challenger_reveal_fails_with_wrong_challenger_buffer() { let mut env = setup_challenger_reveal_env().await; post_v2_commitment(&mut env).await.unwrap(); @@ -283,7 +281,7 @@ async fn test_v2_challenger_reveal_fails_with_wrong_challenger_buffer() { } #[tokio::test] -async fn test_v2_challenger_reveal_fails_with_unfinalized_challenger_buffer() { +async fn test_challenger_reveal_fails_with_unfinalized_challenger_buffer() { let mut env = setup_challenger_reveal_env().await; post_v2_commitment(&mut env).await.unwrap(); @@ -313,7 +311,7 @@ async fn test_v2_challenger_reveal_fails_with_unfinalized_challenger_buffer() { } #[tokio::test] -async fn test_v2_challenger_reveal_fails_with_wrong_fee_vault() { +async fn test_challenger_reveal_fails_with_wrong_fee_vault() { let mut env = setup_challenger_reveal_env().await; post_v2_commitment(&mut env).await.unwrap(); @@ -418,8 +416,8 @@ async fn setup_challenger_reveal_env() -> ChallengerRevealEnv { ); let mut context = program_test.start_with_context().await; - let config_args = valid_args(); - init_v2( + let config_args = valid_protocol_config_args(); + initialize_protocol_config( &context.banks_client, &context.payer, &authority, From 028faa3479c497ae85f2e8d4193ab9ff02989487 Mon Sep 17 00:00:00 2001 From: Sarfaraz Nawaz Date: Wed, 2 Sep 2026 16:11:18 +0530 Subject: [PATCH 5/5] Remove pending revision from reveal load --- src/v2/processor/fraud_proofs/challenger_reveal.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/v2/processor/fraud_proofs/challenger_reveal.rs b/src/v2/processor/fraud_proofs/challenger_reveal.rs index 1fadffd3..9ea28d4a 100644 --- a/src/v2/processor/fraud_proofs/challenger_reveal.rs +++ b/src/v2/processor/fraud_proofs/challenger_reveal.rs @@ -212,7 +212,6 @@ fn load_pending_commitment( owner: *pending_view.owner(), state_commitment_hash: *pending_view.state_commitment_hash(), verifier_registry: *pending_view.verifier_registry(), - verifier_registry_revision: pending_view.verifier_registry_revision(), challenge_window_id: pending_view.challenge_window_id(), posted_slot: pending_view.posted_slot(), activation_slot: pending_view.activation_slot(),