Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add instruction to close violation report #19

Merged
merged 6 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions program/src/address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! Helper struct to derive the seeds for the report PDA

use {
crate::{id, state::ViolationReport},
solana_program::pubkey::Pubkey,
};

pub(crate) struct ViolationReportAddress<'a> {
address: Pubkey,
pubkey_seed: &'a [u8],
slot_seed: &'a [u8; 8],
violation_seed: [u8; 1],
bump_seed: [u8; 1],
}

impl<'a> ViolationReportAddress<'a> {
pub(crate) fn new(report: &'a ViolationReport) -> ViolationReportAddress<'a> {
let pubkey_seed = report.pubkey.as_ref();
let slot_seed = &report.slot.0;
let violation_seed = [report.violation_type];
let (pda, bump) =
Pubkey::find_program_address(&[pubkey_seed, slot_seed, &violation_seed], &id());
let bump_seed = [bump];
Self {
address: pda,
pubkey_seed,
slot_seed,
violation_seed,
bump_seed,
}
}

pub(crate) fn key(&self) -> &Pubkey {
&self.address
}

pub(crate) fn seeds(&self) -> [&[u8]; 4] {
[
self.pubkey_seed,
self.slot_seed,
&self.violation_seed,
&self.bump_seed,
]
}
}
13 changes: 13 additions & 0 deletions program/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ use {
/// Errors that may be returned by the program.
#[derive(Clone, Copy, Debug, Eq, Error, FromPrimitive, PartialEq)]
pub enum SlashingError {
/// Attempting to close a violation report before 3
/// epochs have passed
#[error("Closing violation report too soon")]
CloseViolationReportTooSoon,

/// Destination address is the report account itself
#[error("Destination address is the report account")]
DestinationAddressIsReportAccount,

/// Violation has already been reported
#[error("Duplicate report")]
DuplicateReport,
Expand All @@ -17,6 +26,10 @@ pub enum SlashingError {
#[error("Exceeds statute of limitations")]
ExceedsStatuteOfLimitations,

/// Destination account does not match the key on the report
#[error("Invalid destination account")]
InvalidDestinationAccount,

/// Invalid shred variant
#[error("Invalid shred variant")]
InvalidShredVariant,
Expand Down
43 changes: 41 additions & 2 deletions program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,29 @@ use {
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive, IntoPrimitive)]
pub enum SlashingInstruction {
/// Close the report account that was created after successfully submitting
/// a slashing proof. To ensure that the runtime and indexers have seen the
/// report, we require that at least 3 epochs have passed since creation.
///
/// After closing the account, we credit the lamports to the destination
/// address denoted in the report.
///
/// Accounts expected by this instruction:
/// 0. `[WRITE]` PDA where the violation report is stored, see
/// `[get_violation_report_address]` for the address derivation.
/// 1. `[WRITE]` Destination account which will be credited the lamports
/// from the PDA.
CloseViolationReport,

/// Submit a slashable violation proof for `node_pubkey`, which indicates
/// that they submitted a duplicate block to the network
///
///
/// Accounts expected by this instruction:
/// 0. `[]` Proof account, must be previously initialized with the proof
/// data.
/// 1. `[WRITE]` PDA to store the violation report
/// 1. `[WRITE]` PDA to store the violation report, see
/// `[get_violation_report_address]` for the address derivation.
/// 2. `[]` Instructions sysvar
/// 3. `[]` System program
///
Expand Down Expand Up @@ -141,6 +156,20 @@ pub(crate) fn decode_instruction_data<T: Pod>(input_with_type: &[u8]) -> Result<
}
}

/// Create a `SlashingInstruction::CloseViolationReport` instruction
/// Callers can use `[get_violation_report_address]` to derive the report
/// account address
pub fn close_violation_report(
report_account: &Pubkey,
destination_account: &Pubkey,
) -> Instruction {
let accounts = vec![
AccountMeta::new(*report_account, false),
AccountMeta::new(*destination_account, false),
];
encode_instruction(accounts, SlashingInstruction::CloseViolationReport, &())
}

/// Create a `SlashingInstruction::DuplicateBlockProof` instruction
pub fn duplicate_block_proof(
proof_account: &Pubkey,
Expand Down Expand Up @@ -286,7 +315,7 @@ mod tests {
shred_2_signature,
};
let instruction = duplicate_block_proof(&Pubkey::new_unique(), &instruction_data);
let mut expected = vec![0];
let mut expected = vec![1];
expected.extend_from_slice(&offset.to_le_bytes());
expected.extend_from_slice(&slot.to_le_bytes());
expected.extend_from_slice(&node_pubkey.to_bytes());
Expand Down Expand Up @@ -316,6 +345,16 @@ mod tests {
assert_eq!(instruction_data.shred_2_signature, shred_2_signature);
}

#[test]
fn serialize_close_violation_report() {
let instruction = close_violation_report(&Pubkey::new_unique(), &Pubkey::new_unique());

assert_eq!(
SlashingInstruction::CloseViolationReport,
decode_instruction_type(&instruction.data).unwrap()
);
}

#[test]
fn deserialize_invalid_instruction() {
let mut expected = vec![12];
Expand Down
1 change: 1 addition & 0 deletions program/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Slashing program
#![deny(missing_docs)]

mod address;
pub mod duplicate_block_proof;
mod entrypoint;
pub mod error;
Expand Down
23 changes: 18 additions & 5 deletions program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@

use {
crate::{
check_id,
duplicate_block_proof::DuplicateBlockProofData,
error::SlashingError,
instruction::{
decode_instruction_data, decode_instruction_type, DuplicateBlockProofInstructionData,
SlashingInstruction,
},
state::{
store_violation_report, PodEpoch, ProofType, SlashingAccounts, SlashingProofData,
ViolationReport,
close_violation_report, store_violation_report, PodEpoch, ProofType, SlashingAccounts,
SlashingProofData, ViolationReport,
},
},
solana_program::{
account_info::AccountInfo,
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
Expand Down Expand Up @@ -65,9 +66,21 @@ pub fn process_instruction(
) -> ProgramResult {
let instruction_type = decode_instruction_type(input)?;
let account_info_iter = &mut accounts.iter();
let accounts = SlashingAccounts::new(account_info_iter)?;
match instruction_type {
SlashingInstruction::CloseViolationReport => {
let report_account = next_account_info(account_info_iter)?;
let destination_account = next_account_info(account_info_iter)?;

if !check_id(report_account.owner) || report_account.data_is_empty() {
return Err(ProgramError::from(
SlashingError::InvalidViolationReportAcccount,
));
}

close_violation_report(report_account, destination_account)?;
}
SlashingInstruction::DuplicateBlockProof => {
let accounts = SlashingAccounts::new(account_info_iter)?;
let data = decode_instruction_data::<DuplicateBlockProofInstructionData>(input)?;
let proof_data = &accounts.proof_account.try_borrow_data()?[data.offset()?..];
let violation_report = ViolationReport {
Expand All @@ -86,9 +99,9 @@ pub fn process_instruction(
proof_data,
input,
)?;
Ok(())
}
}
Ok(())
}

#[cfg(test)]
Expand Down
91 changes: 68 additions & 23 deletions program/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
//! Program state
use {
crate::{check_id, duplicate_block_proof::DuplicateBlockProofData, error::SlashingError, id},
crate::{
address::ViolationReportAddress, check_id, duplicate_block_proof::DuplicateBlockProofData,
error::SlashingError, id,
},
bytemuck::{Pod, Zeroable},
solana_program::{
account_info::{next_account_info, AccountInfo},
clock::Slot,
clock::{Epoch, Slot},
msg,
program::invoke_signed,
program_error::ProgramError,
Expand All @@ -13,7 +16,7 @@ use {
system_instruction, system_program,
sysvar::{self, Sysvar},
},
spl_pod::primitives::PodU64,
spl_pod::{bytemuck::pod_from_bytes, primitives::PodU64},
};

const PACKET_DATA_SIZE: usize = 1232;
Expand Down Expand Up @@ -222,16 +225,15 @@ pub(crate) fn store_violation_report<'a, 'b, T>(
where
T: SlashingProofData<'a>,
{
let slot = report.slot;
let pubkey_seed = report.pubkey.as_ref();
let slot_seed = slot.0;
let violation_seed = [report.violation_type];
let mut seeds: Vec<&[u8]> = vec![&pubkey_seed, &slot_seed, &violation_seed];
let (pda, bump) = Pubkey::find_program_address(&seeds, &id());
let bump_seed = [bump];
seeds.push(&bump_seed);

if pda != *accounts.violation_account() {
let report_address = ViolationReportAddress::new(&report);
let report_key = report_address.key();
let seeds = report_address.seeds();
let cpi_accounts = [
accounts.violation_pda_account.clone(),
accounts.system_program_account.clone(),
];

if *report_key != *accounts.violation_account() {
return Err(ProgramError::from(
SlashingError::InvalidViolationReportAcccount,
));
Expand All @@ -242,11 +244,17 @@ where
msg!(
"{} violation verified in slot {} however the violation has already been reported",
T::PROOF_TYPE.violation_str(),
u64::from(slot),
u64::from(report.slot),
);
return Err(ProgramError::from(SlashingError::DuplicateReport));
}

if *report_key == report.destination {
return Err(ProgramError::from(
SlashingError::DestinationAddressIsReportAccount,
));
}

// Check if the account has been prefunded to store the report
let data_len = ViolationReport::packed_len(&proof_data);
let lamports = Rent::get()?.minimum_balance(data_len);
Expand All @@ -255,15 +263,8 @@ where
}

// Assign the slashing program as the owner
let assign_instruction = system_instruction::assign(&pda, &id());
invoke_signed(
&assign_instruction,
&[
accounts.violation_pda_account.clone(),
accounts.system_program_account.clone(),
],
&[&seeds],
)?;
let assign_instruction = system_instruction::assign(report_key, &id());
invoke_signed(&assign_instruction, &cpi_accounts, &[&seeds])?;

// Allocate enough space for the report
accounts.violation_pda_account.realloc(data_len, false)?;
Expand All @@ -282,6 +283,50 @@ where
accounts.write_violation_report(report, proof_data)
}

pub(crate) fn close_violation_report<'a, 'b>(
report_account: &'a AccountInfo<'b>,
destination_account: &'a AccountInfo<'b>,
) -> Result<(), ProgramError> {
let report_data = report_account.try_borrow_data()?;
let report: &ViolationReport =
pod_from_bytes(&report_data[0..std::mem::size_of::<ViolationReport>()])?;
let destination = report.destination;

if Epoch::from(report.epoch).saturating_add(3) > sysvar::clock::Clock::get()?.epoch {
return Err(ProgramError::from(
SlashingError::CloseViolationReportTooSoon,
));
}

if destination != *destination_account.key {
return Err(ProgramError::from(SlashingError::InvalidDestinationAccount));
}

// Drop the report account to close it
drop(report_data);

// Reallocate the account to 0 bytes
report_account.realloc(0, false)?;

// Assign the system program as the owner
report_account.assign(&system_program::id());

// Transfer the lamports to the destination address
let report_lamports = report_account.lamports();
**report_account.try_borrow_mut_lamports()? = 0;

let destination_lamports = destination_account.lamports();
**(destination_account.try_borrow_mut_lamports()?) = destination_lamports
.checked_add(report_lamports)
.ok_or(ProgramError::ArithmeticOverflow)?;

msg!(
"Closed violation report and credited {} lamports to the destination address",
report_lamports
);
Ok(())
}

#[cfg(test)]
mod tests {
use crate::state::PACKET_DATA_SIZE;
Expand Down
Loading
Loading