use std::mem::size_of;
use drillx::Solution;
use marsh_api::{
consts::*,
error::MarshError,
event::MineEvent,
instruction::Mine,
loaders::*,
state::{Bus, Config, Proof},
};
use marsh_utils::*;
use solana_program::program::set_return_data;
#[allow(deprecated)]
use solana_program::{
account_info::AccountInfo,
clock::Clock,
entrypoint::ProgramResult,
keccak::hashv,
program_error::ProgramError,
pubkey::Pubkey,
sanitize::SanitizeError,
serialize_utils::{read_pubkey, read_u16},
slot_hashes::SlotHash,
sysvar::{self, Sysvar},
};
pub fn process_mine(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
let args = Mine::try_from_bytes(data)?;
let [signer, bus_info, config_info, proof_info, instructions_sysvar, slot_hashes_sysvar] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
load_signer(signer)?;
load_any_bus(bus_info, true)?;
load_config(config_info, false)?;
load_proof_with_miner(proof_info, signer.key, true)?;
load_sysvar(instructions_sysvar, sysvar::instructions::id())?;
load_sysvar(slot_hashes_sysvar, sysvar::slot_hashes::id())?;
let clock = Clock::get().or(Err(ProgramError::InvalidAccountData))?;
if clock.unix_timestamp.lt(&START_AT) {
return Err(MarshError::NotStarted.into());
}
if clock.unix_timestamp.ge(&END_AT) {
return Err(MarshError::HasEnded.into());
}
authenticate(&instructions_sysvar.data.borrow(), proof_info.key)?;
let config_data = config_info.data.borrow();
let config = Config::try_from_bytes(&config_data)?;
let clock = Clock::get().or(Err(ProgramError::InvalidAccountData))?;
if config
.last_reset_at
.saturating_add(EPOCH_DURATION)
.le(&clock.unix_timestamp)
{
return Err(MarshError::NeedsReset.into());
}
let mut proof_data = proof_info.data.borrow_mut();
let proof = Proof::try_from_bytes_mut(&mut proof_data)?;
let solution = Solution::new(args.digest, args.nonce);
if !solution.is_valid(&proof.challenge) {
return Err(MarshError::HashInvalid.into());
}
let t: i64 = clock.unix_timestamp;
let t_target = proof.last_hash_at.saturating_add(ONE_MINUTE);
let t_spam = t_target.saturating_sub(TOLERANCE);
if t.lt(&t_spam) {
return Err(MarshError::Spam.into());
}
let hash = solution.to_hash();
let difficulty = hash.difficulty();
if difficulty.lt(&(config.min_difficulty as u32)) {
return Err(MarshError::HashTooEasy.into());
}
let normalized_difficulty = difficulty
.checked_sub(config.min_difficulty as u32)
.unwrap();
let mut reward = config
.base_reward_rate
.checked_mul(2u64.checked_pow(normalized_difficulty).unwrap())
.unwrap();
let mut bus_data = bus_info.data.borrow_mut();
let bus = Bus::try_from_bytes_mut(&mut bus_data)?;
if proof.balance.gt(&0) && proof.last_stake_at.saturating_add(ONE_MINUTE).lt(&t) {
if config.top_balance.gt(&0) {
let staking_reward = (reward as u128)
.checked_mul(proof.balance.min(config.top_balance) as u128)
.unwrap()
.checked_div(config.top_balance as u128)
.unwrap() as u64;
reward = reward.checked_add(staking_reward).unwrap();
}
if proof.balance.gt(&bus.top_balance) {
bus.top_balance = proof.balance;
}
}
let t_liveness = t_target.saturating_add(TOLERANCE);
if t.gt(&t_liveness) {
let tardiness = t.saturating_sub(t_target) as u64;
let halvings = tardiness.saturating_div(ONE_MINUTE as u64);
if halvings.gt(&0) {
reward = reward.saturating_div(2u64.saturating_pow(halvings as u32));
}
let remainder_secs = tardiness.saturating_sub(halvings.saturating_mul(ONE_MINUTE as u64));
if remainder_secs.gt(&0) && reward.gt(&0) {
let penalty = reward
.saturating_div(2)
.saturating_mul(remainder_secs)
.saturating_div(ONE_MINUTE as u64);
reward = reward.saturating_sub(penalty);
}
}
let reward_actual = reward.min(bus.rewards).min(TEN_MARSH);
bus.theoretical_rewards = bus.theoretical_rewards.checked_add(reward).unwrap();
bus.rewards = bus.rewards.checked_sub(reward_actual).unwrap();
proof.balance = proof.balance.checked_add(reward_actual).unwrap();
proof.last_hash = hash.h;
proof.challenge = hashv(&[
hash.h.as_slice(),
&slot_hashes_sysvar.data.borrow()[0..size_of::<SlotHash>()],
])
.0;
proof.last_hash_at = t.max(t_target);
proof.total_hashes = proof.total_hashes.saturating_add(1);
proof.total_rewards = proof.total_rewards.saturating_add(reward);
set_return_data(
MineEvent {
difficulty: difficulty as u64,
reward: reward_actual,
timing: t.saturating_sub(t_liveness),
}
.to_bytes(),
);
Ok(())
}
fn authenticate(data: &[u8], proof_address: &Pubkey) -> ProgramResult {
if let Ok(Some(auth_address)) = parse_auth_address(data) {
if proof_address.ne(&auth_address) {
return Err(MarshError::AuthFailed.into());
}
} else {
return Err(MarshError::AuthFailed.into());
}
Ok(())
}
fn parse_auth_address(data: &[u8]) -> Result<Option<Pubkey>, SanitizeError> {
let mut curr = 0;
let num_instructions = read_u16(&mut curr, data)?;
let pc = curr;
for i in 0..num_instructions as usize {
curr = pc + i * 2;
curr = read_u16(&mut curr, data)? as usize;
let num_accounts = read_u16(&mut curr, data)? as usize;
curr += num_accounts * 33;
let program_id = read_pubkey(&mut curr, data)?;
if program_id.eq(&NOOP_PROGRAM_ID) {
curr += 2;
let address = read_pubkey(&mut curr, data)?;
return Ok(Some(address));
}
}
Ok(None)
}