diff --git a/Cargo.lock b/Cargo.lock index 2fd7f4caba6..20f65e790c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -668,7 +668,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -705,9 +705,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" dependencies = [ "serde", ] @@ -3254,7 +3254,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if 1.0.0", "cfg_aliases 0.1.1", "libc", @@ -3460,7 +3460,7 @@ version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if 1.0.0", "foreign-types", "libc", @@ -3885,7 +3885,7 @@ checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.5.0", + "bitflags 2.6.0", "lazy_static", "num-traits", "rand 0.8.5", @@ -4413,7 +4413,7 @@ version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -4621,9 +4621,9 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" dependencies = [ "serde", ] @@ -5621,7 +5621,7 @@ checksum = "d51f7b0b73e0eb678f0125d90bcccb28905cfd7ba0b98447f81e0a8cf5ab146b" dependencies = [ "assert_matches", "bincode", - "bitflags 2.5.0", + "bitflags 2.6.0", "byteorder", "chrono", "chrono-humanize", @@ -5840,7 +5840,7 @@ dependencies = [ "ark-serialize", "base64 0.22.1", "bincode", - "bitflags 2.5.0", + "bitflags 2.6.0", "blake3", "borsh 0.10.3", "borsh 1.5.1", @@ -6226,7 +6226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f465706f7595391c7b65d19a940ed488fea5dab69eb8c9c211de87a416a0154" dependencies = [ "bincode", - "bitflags 2.5.0", + "bitflags 2.6.0", "borsh 1.5.1", "bs58", "bytemuck", @@ -7424,6 +7424,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "spl-slashing" +version = "0.1.0" +dependencies = [ + "bytemuck", + "num-derive", + "num-traits", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-pod 0.4.0", + "thiserror", +] + [[package]] name = "spl-stake-pool" version = "2.0.0" diff --git a/Cargo.toml b/Cargo.toml index 76890c34176..994dd85ff41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ members = [ "shared-memory/program", "single-pool/cli", "single-pool/program", + "slashing/program", "stake-pool/cli", "stake-pool/program", "stateless-asks/program", diff --git a/slashing/README.md b/slashing/README.md new file mode 100644 index 00000000000..7d2977b762e --- /dev/null +++ b/slashing/README.md @@ -0,0 +1,5 @@ +# Slashing Program + +A program that validates a proof of a slashable event on chain for logging purposes. +Users can create a proof buffer for the flavor of slashable infraction, populate it, +and submit for verification. diff --git a/slashing/program/Cargo.toml b/slashing/program/Cargo.toml new file mode 100644 index 00000000000..8c9c73da5a3 --- /dev/null +++ b/slashing/program/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "spl-slashing" +version = "0.1.0" +description = "Solana Program Library Slashing" +authors = ["Solana Labs Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2021" + +[features] +no-entrypoint = [] +test-sbf = [] + +[dependencies] +bytemuck = { version = "1.19.0", features = ["derive"] } +num-derive = "0.4" +num-traits = "0.2" +solana-program = "2.0.3" +thiserror = "1.0" +spl-pod = { version = "0.4.0", path = "../../libraries/pod" } + +[dev-dependencies] +solana-program-test = "2.0.3" +solana-sdk = "2.0.3" + +[lib] +crate-type = ["cdylib", "lib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/slashing/program/Xargo.toml b/slashing/program/Xargo.toml new file mode 100644 index 00000000000..475fb71ed15 --- /dev/null +++ b/slashing/program/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/slashing/program/program-id.md b/slashing/program/program-id.md new file mode 100644 index 00000000000..ec35ab3a1f3 --- /dev/null +++ b/slashing/program/program-id.md @@ -0,0 +1 @@ +8sT74BE7sanh4iT84EyVUL8b77cVruLHXGjvTyJ4GwCe diff --git a/slashing/program/src/entrypoint.rs b/slashing/program/src/entrypoint.rs new file mode 100644 index 00000000000..62a02f2a465 --- /dev/null +++ b/slashing/program/src/entrypoint.rs @@ -0,0 +1,14 @@ +//! Program entrypoint + +#![cfg(all(target_os = "solana", not(feature = "no-entrypoint")))] + +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; + +solana_program::entrypoint!(process_instruction); +fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + crate::processor::process_instruction(program_id, accounts, instruction_data) +} diff --git a/slashing/program/src/error.rs b/slashing/program/src/error.rs new file mode 100644 index 00000000000..d32444b76ff --- /dev/null +++ b/slashing/program/src/error.rs @@ -0,0 +1,35 @@ +//! Error types + +use { + num_derive::FromPrimitive, + solana_program::{decode_error::DecodeError, program_error::ProgramError}, + thiserror::Error, +}; + +/// Errors that may be returned by the program. +#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] +pub enum SlashingError { + /// Incorrect authority provided on write or close + #[error("Incorrect authority provided on write or close")] + IncorrectAuthority, + + /// Invalid proof type + #[error("Invalid proof type")] + InvalidProofType, + + /// Calculation overflow + #[error("Calculation overflow")] + Overflow, +} + +impl From for ProgramError { + fn from(e: SlashingError) -> Self { + ProgramError::Custom(e as u32) + } +} + +impl DecodeError for SlashingError { + fn type_of() -> &'static str { + "Slashing Error" + } +} diff --git a/slashing/program/src/instruction.rs b/slashing/program/src/instruction.rs new file mode 100644 index 00000000000..b974b3ee54d --- /dev/null +++ b/slashing/program/src/instruction.rs @@ -0,0 +1,213 @@ +//! Program instructions + +use { + crate::{id, state::ProofType}, + solana_program::{ + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + pubkey::Pubkey, + system_program, + }, + std::mem::size_of, +}; + +/// Instructions supported by the program +#[derive(Clone, Debug, PartialEq)] +pub enum SlashingInstruction<'a> { + /// Create a new proof account + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Proof account, must be uninitialized + /// 1. `[signer]` Fee payer for account creation + /// 2. `[]` Proof authority + InitializeProofAccount { + /// [ProofType] indicating the size of the account + proof_type: ProofType, + }, + + /// Write to the provided proof account + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Proof account, must be previously initialized + /// 1. `[signer]` Proof authority + Write { + /// Offset to start writing proof, expressed as `u64`. + offset: u64, + /// Data to replace the existing proof data + data: &'a [u8], + }, + + /// Close the provided proof account, draining lamports to recipient + /// account + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Proof account, must be previously initialized + /// 1. `[signer]` Proof authority + /// 2. `[]` Receiver of account lamports + CloseAccount, +} + +impl<'a> SlashingInstruction<'a> { + /// Unpacks a byte buffer into a [SlashingInstruction]. + pub fn unpack(input: &'a [u8]) -> Result { + const U8_BYTES: usize = 1; + const U32_BYTES: usize = 4; + const U64_BYTES: usize = 8; + + let (&tag, rest) = input + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + Ok(match tag { + 0 => { + let proof_type = rest + .get(..U8_BYTES) + .and_then(|slice| slice.try_into().ok()) + .map(u8::from_le_bytes) + .ok_or(ProgramError::InvalidInstructionData)?; + let proof_type = ProofType::from(proof_type); + Self::InitializeProofAccount { proof_type } + } + 1 => { + let offset = rest + .get(..U64_BYTES) + .and_then(|slice| slice.try_into().ok()) + .map(u64::from_le_bytes) + .ok_or(ProgramError::InvalidInstructionData)?; + let (length, data) = rest[U64_BYTES..].split_at(U32_BYTES); + let length = u32::from_le_bytes( + length + .try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?, + ) as usize; + + Self::Write { + offset, + data: &data[..length], + } + } + 2 => Self::CloseAccount, + _ => return Err(ProgramError::InvalidInstructionData), + }) + } + + /// Packs a [SlashingInstruction] into a byte buffer. + pub fn pack(&self) -> Vec { + let mut buf = Vec::with_capacity(size_of::()); + match self { + Self::InitializeProofAccount { proof_type } => { + let proof_type = u8::from(*proof_type); + buf.push(0); + buf.extend_from_slice(&proof_type.to_le_bytes()); + } + Self::Write { offset, data } => { + buf.push(1); + buf.extend_from_slice(&offset.to_le_bytes()); + buf.extend_from_slice(&(data.len() as u32).to_le_bytes()); + buf.extend_from_slice(data); + } + Self::CloseAccount => buf.push(2), + }; + buf + } +} + +/// Create a `SlashingInstruction::InitializeProofAccount` instruction +pub fn initialize_proof_account( + proof_account: &Pubkey, + proof_type: ProofType, + fee_payer: &Pubkey, + authority: &Pubkey, +) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(*proof_account, true), + AccountMeta::new(*fee_payer, true), + AccountMeta::new_readonly(*authority, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: SlashingInstruction::InitializeProofAccount { proof_type }.pack(), + } +} + +/// Create a `SlashingInstruction::Write` instruction +pub fn write(proof_account: &Pubkey, signer: &Pubkey, offset: u64, data: &[u8]) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(*proof_account, false), + AccountMeta::new_readonly(*signer, true), + ], + data: SlashingInstruction::Write { offset, data }.pack(), + } +} + +/// Create a `SlashingInstruction::CloseAccount` instruction +pub fn close_account(proof_account: &Pubkey, signer: &Pubkey, receiver: &Pubkey) -> Instruction { + Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(*proof_account, false), + AccountMeta::new_readonly(*signer, true), + AccountMeta::new(*receiver, false), + ], + data: SlashingInstruction::CloseAccount.pack(), + } +} + +#[cfg(test)] +mod tests { + use {super::*, crate::state::tests::TEST_BYTES, solana_program::program_error::ProgramError}; + + #[test] + fn serialize_initialize_duplicate_block_proof() { + let instruction = SlashingInstruction::InitializeProofAccount { + proof_type: ProofType::DuplicateBlockProof, + }; + let expected = vec![0, 0]; + assert_eq!(instruction.pack(), expected); + assert_eq!(SlashingInstruction::unpack(&expected).unwrap(), instruction); + } + + #[test] + fn serialize_initialize_invalid_proof() { + let instruction = SlashingInstruction::InitializeProofAccount { + proof_type: ProofType::InvalidType, + }; + let expected = vec![0, u8::MAX]; + assert_eq!(instruction.pack(), expected); + assert_eq!(SlashingInstruction::unpack(&expected).unwrap(), instruction); + } + + #[test] + fn serialize_write() { + let data = &TEST_BYTES; + let offset = 0u64; + let instruction = SlashingInstruction::Write { offset: 0, data }; + let mut expected = vec![1]; + expected.extend_from_slice(&offset.to_le_bytes()); + expected.extend_from_slice(&(data.len() as u32).to_le_bytes()); + expected.extend_from_slice(data); + assert_eq!(instruction.pack(), expected); + assert_eq!(SlashingInstruction::unpack(&expected).unwrap(), instruction); + } + + #[test] + fn serialize_close_account() { + let instruction = SlashingInstruction::CloseAccount; + let expected = vec![2]; + assert_eq!(instruction.pack(), expected); + assert_eq!(SlashingInstruction::unpack(&expected).unwrap(), instruction); + } + + #[test] + fn deserialize_invalid_instruction() { + let mut expected = vec![12]; + expected.extend_from_slice(&TEST_BYTES); + let err: ProgramError = SlashingInstruction::unpack(&expected).unwrap_err(); + assert_eq!(err, ProgramError::InvalidInstructionData); + } +} diff --git a/slashing/program/src/lib.rs b/slashing/program/src/lib.rs new file mode 100644 index 00000000000..d02a323f4d5 --- /dev/null +++ b/slashing/program/src/lib.rs @@ -0,0 +1,14 @@ +//! Slashing program +#![deny(missing_docs)] + +mod entrypoint; +pub mod error; +pub mod instruction; +pub mod processor; +pub mod state; + +// Export current SDK types for downstream users building with a different SDK +// version +pub use solana_program; + +solana_program::declare_id!("8sT74BE7sanh4iT84EyVUL8b77cVruLHXGjvTyJ4GwCe"); diff --git a/slashing/program/src/processor.rs b/slashing/program/src/processor.rs new file mode 100644 index 00000000000..79189c2080a --- /dev/null +++ b/slashing/program/src/processor.rs @@ -0,0 +1,138 @@ +//! Program state processor + +use { + crate::{error::SlashingError, instruction::SlashingInstruction, state::ProofData}, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + program::invoke, + program_error::ProgramError, + program_pack::IsInitialized, + pubkey::Pubkey, + rent::Rent, + system_instruction, system_program, + }, + spl_pod::bytemuck::{pod_from_bytes, pod_from_bytes_mut}, +}; + +fn check_authority(authority_info: &AccountInfo, expected_authority: &Pubkey) -> ProgramResult { + if expected_authority != authority_info.key { + msg!("Incorrect proof authority provided"); + return Err(SlashingError::IncorrectAuthority.into()); + } + if !authority_info.is_signer { + msg!("Proof authority signature missing"); + return Err(ProgramError::MissingRequiredSignature); + } + Ok(()) +} + +/// Instruction processor +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + input: &[u8], +) -> ProgramResult { + let instruction = SlashingInstruction::unpack(input)?; + let account_info_iter = &mut accounts.iter(); + + match instruction { + SlashingInstruction::InitializeProofAccount { proof_type } => { + msg!( + "SlashingInstruction::InitializeProofAccount {:?}", + proof_type + ); + + let proof_data_info = next_account_info(account_info_iter)?; + let payer_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + let system_program_info = next_account_info(account_info_iter)?; + let account_length = proof_type.proof_account_length()?; + + if *system_program_info.key != system_program::ID { + msg!("Missing system program account"); + return Err(ProgramError::InvalidAccountData); + } + + msg!("Creating proof account with size {}", account_length); + invoke( + &system_instruction::create_account( + payer_info.key, + proof_data_info.key, + 1.max(Rent::default().minimum_balance(account_length)), + account_length as u64, + program_id, + ), + &[payer_info.clone(), proof_data_info.clone()], + )?; + + let raw_data = &mut proof_data_info.data.borrow_mut(); + let account_data = + pod_from_bytes_mut::(&mut raw_data[..ProofData::WRITABLE_START_INDEX])?; + + account_data.proof_type = u8::from(proof_type); + account_data.authority = *authority_info.key; + account_data.version = ProofData::CURRENT_VERSION; + Ok(()) + } + + SlashingInstruction::Write { offset, data } => { + msg!("SlashingInstruction::Write"); + let proof_data_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + { + let raw_data = &proof_data_info.data.borrow(); + if raw_data.len() < ProofData::WRITABLE_START_INDEX { + return Err(ProgramError::InvalidAccountData); + } + let proof_data = + pod_from_bytes::(&raw_data[..ProofData::WRITABLE_START_INDEX])?; + if !proof_data.is_initialized() { + msg!("Proof account not initialized"); + return Err(ProgramError::UninitializedAccount); + } + check_authority(authority_info, &proof_data.authority)?; + } + msg!( + "Writing {} bytes at {} into {}", + data.len(), + offset, + proof_data_info.key + ); + let start = ProofData::WRITABLE_START_INDEX.saturating_add(offset as usize); + let end = start.saturating_add(data.len()); + if end > proof_data_info.data.borrow().len() { + Err(ProgramError::AccountDataTooSmall) + } else { + proof_data_info.data.borrow_mut()[start..end].copy_from_slice(data); + Ok(()) + } + } + + SlashingInstruction::CloseAccount => { + msg!("SlashingInstruction::CloseAccount"); + let proof_data_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + let destination_info = next_account_info(account_info_iter)?; + let raw_data = &mut proof_data_info.data.borrow_mut(); + if raw_data.len() < ProofData::WRITABLE_START_INDEX { + return Err(ProgramError::InvalidAccountData); + } + let account_data = + pod_from_bytes_mut::(&mut raw_data[..ProofData::WRITABLE_START_INDEX])?; + if !account_data.is_initialized() { + msg!("Proof Account not initialized"); + return Err(ProgramError::UninitializedAccount); + } + check_authority(authority_info, &account_data.authority)?; + let destination_starting_lamports = destination_info.lamports(); + let data_lamports = proof_data_info.lamports(); + **proof_data_info.lamports.borrow_mut() = 0; + **destination_info.lamports.borrow_mut() = destination_starting_lamports + .checked_add(data_lamports) + .ok_or(SlashingError::Overflow)?; + Ok(()) + } + } +} diff --git a/slashing/program/src/state.rs b/slashing/program/src/state.rs new file mode 100644 index 00000000000..75b07fed9f9 --- /dev/null +++ b/slashing/program/src/state.rs @@ -0,0 +1,121 @@ +//! Program state +use { + crate::error::SlashingError, + bytemuck::{Pod, Zeroable}, + solana_program::{program_pack::IsInitialized, pubkey::Pubkey}, +}; + +const PACKET_DATA_SIZE: usize = 1232; + +/// Types of slashing proofs +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ProofType { + /// Proof consisting of 2 shreds signed by the leader indicating the leader + /// submitted a duplicate block. + DuplicateBlockProof, + /// Invalid proof type + InvalidType, +} + +impl ProofType { + /// Size of the proof account to create in order to hold the proof data + /// header and contents + pub fn proof_account_length(&self) -> Result { + match self { + // Duplicate block proof consists of 2 shreds + Self::DuplicateBlockProof => Ok(2 * PACKET_DATA_SIZE + ProofData::WRITABLE_START_INDEX), + Self::InvalidType => Err(SlashingError::InvalidProofType), + } + } +} + +impl From for u8 { + fn from(value: ProofType) -> u8 { + match value { + ProofType::DuplicateBlockProof => 0, + ProofType::InvalidType => u8::MAX, + } + } +} + +impl From for ProofType { + fn from(value: u8) -> Self { + match value { + 0 => Self::DuplicateBlockProof, + _ => Self::InvalidType, + } + } +} + +/// Header type for proof data +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct ProofData { + /// Struct version, allows for upgrades to the program + pub version: u8, + + /// Type of proof, determines the size + pub proof_type: u8, + + /// The account allowed to update the data + pub authority: Pubkey, +} + +impl ProofData { + /// Version to fill in on new created accounts + pub const CURRENT_VERSION: u8 = 1; + + /// Start of writable account data, after version, proof type and authority + pub const WRITABLE_START_INDEX: usize = 34; +} + +impl IsInitialized for ProofData { + /// Is initialized + fn is_initialized(&self) -> bool { + self.version == Self::CURRENT_VERSION + } +} + +#[cfg(test)] +pub mod tests { + use { + super::*, + solana_program::program_error::ProgramError, + spl_pod::bytemuck::{pod_bytes_of, pod_from_bytes}, + }; + + /// Version for tests + pub const TEST_VERSION: u8 = 1; + /// Proof type for tests + pub const TEST_PROOF_TYPE: u8 = 0; + /// Pubkey for tests + pub const TEST_PUBKEY: Pubkey = Pubkey::new_from_array([100; 32]); + /// Bytes for tests + pub const TEST_BYTES: [u8; 8] = [42; 8]; + /// ProofData for tests + pub const TEST_PROOF_DATA: ProofData = ProofData { + version: TEST_VERSION, + proof_type: TEST_PROOF_TYPE, + authority: TEST_PUBKEY, + }; + + #[test] + fn serialize_data() { + let mut expected = vec![TEST_VERSION, TEST_PROOF_TYPE]; + expected.extend_from_slice(&TEST_PUBKEY.to_bytes()); + assert_eq!(pod_bytes_of(&TEST_PROOF_DATA), expected); + assert_eq!( + *pod_from_bytes::(&expected).unwrap(), + TEST_PROOF_DATA, + ); + } + + #[test] + fn deserialize_invalid_slice() { + let mut expected = vec![TEST_VERSION]; + expected.extend_from_slice(&TEST_PUBKEY.to_bytes()); + expected.extend_from_slice(&TEST_BYTES); + let err: ProgramError = pod_from_bytes::(&expected).unwrap_err(); + assert_eq!(err, ProgramError::InvalidArgument); + } +} diff --git a/slashing/program/tests/proof_account.rs b/slashing/program/tests/proof_account.rs new file mode 100644 index 00000000000..8bcad21392f --- /dev/null +++ b/slashing/program/tests/proof_account.rs @@ -0,0 +1,393 @@ +#![cfg(feature = "test-sbf")] + +use { + solana_program::{ + instruction::{AccountMeta, Instruction, InstructionError}, + pubkey::Pubkey, + rent::Rent, + }, + solana_program_test::*, + solana_sdk::{ + signature::{Keypair, Signer}, + transaction::{Transaction, TransactionError}, + }, + spl_pod::bytemuck::pod_from_bytes, + spl_slashing::{ + error::SlashingError, + id, instruction, + processor::process_instruction, + state::{ProofData, ProofType}, + }, +}; + +fn program_test() -> ProgramTest { + ProgramTest::new("spl_slashing", id(), processor!(process_instruction)) +} + +async fn initialize_proof_account( + context: &mut ProgramTestContext, + authority: &Keypair, + account: &Keypair, + proof_type: ProofType, + data: &[u8], +) { + let transaction = Transaction::new_signed_with_payer( + &[ + instruction::initialize_proof_account( + &account.pubkey(), + proof_type, + &context.payer.pubkey(), + &authority.pubkey(), + ), + instruction::write(&account.pubkey(), &authority.pubkey(), 0, data), + ], + Some(&context.payer.pubkey()), + &[&context.payer, account, authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); +} + +#[tokio::test] +async fn initialize_success() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[111u8; 8]; + let end_index = ProofData::WRITABLE_START_INDEX + data.len(); + initialize_proof_account( + &mut context, + &authority, + &account, + ProofType::DuplicateBlockProof, + data, + ) + .await; + + let account = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap() + .unwrap(); + let account_data = + pod_from_bytes::(&account.data[..ProofData::WRITABLE_START_INDEX]).unwrap(); + assert_eq!( + ProofType::from(account_data.proof_type), + ProofType::DuplicateBlockProof + ); + assert_eq!(account_data.authority, authority.pubkey()); + assert_eq!(account_data.version, ProofData::CURRENT_VERSION); + assert_eq!( + &account.data[ProofData::WRITABLE_START_INDEX..end_index], + data + ); +} + +#[tokio::test] +async fn initialize_twice_fail() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[111u8; 8]; + initialize_proof_account( + &mut context, + &authority, + &account, + ProofType::DuplicateBlockProof, + data, + ) + .await; + let transaction = Transaction::new_signed_with_payer( + &[instruction::initialize_proof_account( + &account.pubkey(), + ProofType::DuplicateBlockProof, + &context.payer.pubkey(), + &authority.pubkey(), + )], + Some(&context.payer.pubkey()), + &[&context.payer, &account], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err(); +} + +#[tokio::test] +async fn write_success() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + initialize_proof_account( + &mut context, + &authority, + &account, + ProofType::DuplicateBlockProof, + data, + ) + .await; + + let new_data = &[200u8; 16]; + let end_index = new_data.len() + ProofData::WRITABLE_START_INDEX; + let transaction = Transaction::new_signed_with_payer( + &[instruction::write( + &account.pubkey(), + &authority.pubkey(), + 0, + new_data, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let account = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap() + .unwrap(); + let account_data = + pod_from_bytes::(&account.data[..ProofData::WRITABLE_START_INDEX]).unwrap(); + assert_eq!(account_data.authority, authority.pubkey()); + assert_eq!(account_data.version, ProofData::CURRENT_VERSION); + assert_eq!( + &account.data[ProofData::WRITABLE_START_INDEX..end_index], + new_data + ); +} + +#[tokio::test] +async fn write_fail_wrong_authority() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + initialize_proof_account( + &mut context, + &authority, + &account, + ProofType::DuplicateBlockProof, + data, + ) + .await; + + let new_data = &[200u8; 8]; + let wrong_authority = Keypair::new(); + let transaction = Transaction::new_signed_with_payer( + &[instruction::write( + &account.pubkey(), + &wrong_authority.pubkey(), + 0, + new_data, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &wrong_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(SlashingError::IncorrectAuthority as u32) + ) + ); +} + +#[tokio::test] +async fn write_fail_unsigned() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + initialize_proof_account( + &mut context, + &authority, + &account, + ProofType::DuplicateBlockProof, + data, + ) + .await; + + let data = &[200u8; 8]; + + let transaction = Transaction::new_signed_with_payer( + &[Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(account.pubkey(), false), + AccountMeta::new_readonly(authority.pubkey(), false), + ], + data: instruction::SlashingInstruction::Write { offset: 0, data }.pack(), + }], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ); +} + +#[tokio::test] +async fn close_account_success() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + let account_length = ProofType::DuplicateBlockProof + .proof_account_length() + .unwrap(); + initialize_proof_account( + &mut context, + &authority, + &account, + ProofType::DuplicateBlockProof, + data, + ) + .await; + let recipient = Pubkey::new_unique(); + + let transaction = Transaction::new_signed_with_payer( + &[instruction::close_account( + &account.pubkey(), + &authority.pubkey(), + &recipient, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let account = context + .banks_client + .get_account(recipient) + .await + .unwrap() + .unwrap(); + assert_eq!( + account.lamports, + 1.max(Rent::default().minimum_balance(account_length)) + ); +} + +#[tokio::test] +async fn close_account_fail_wrong_authority() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8; 8]; + initialize_proof_account( + &mut context, + &authority, + &account, + ProofType::DuplicateBlockProof, + data, + ) + .await; + + let wrong_authority = Keypair::new(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(account.pubkey(), false), + AccountMeta::new_readonly(wrong_authority.pubkey(), true), + AccountMeta::new(Pubkey::new_unique(), false), + ], + data: instruction::SlashingInstruction::CloseAccount.pack(), + }], + Some(&context.payer.pubkey()), + &[&context.payer, &wrong_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(SlashingError::IncorrectAuthority as u32) + ) + ); +} + +#[tokio::test] +async fn close_account_fail_unsigned() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = &[222u8, 8]; + initialize_proof_account( + &mut context, + &authority, + &account, + ProofType::DuplicateBlockProof, + data, + ) + .await; + + let transaction = Transaction::new_signed_with_payer( + &[Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(account.pubkey(), false), + AccountMeta::new_readonly(authority.pubkey(), false), + AccountMeta::new(Pubkey::new_unique(), false), + ], + data: instruction::SlashingInstruction::CloseAccount.pack(), + }], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ); +}