mars/processor/
mine.rs

1use std::mem::size_of;
2
3use solana_program::{
4    account_info::AccountInfo,
5    clock::Clock,
6    entrypoint::ProgramResult,
7    keccak::{hashv, Hash as KeccakHash, HASH_BYTES},
8    program::set_return_data,
9    program_error::ProgramError,
10    program_memory::sol_memcmp,
11    pubkey::Pubkey,
12    slot_hashes::SlotHash,
13    sysvar::{self, Sysvar},
14};
15
16use crate::{
17    error::MarsError,
18    instruction::MineArgs,
19    loaders::*,
20    state::{Bus, Proof, Treasury},
21    utils::AccountDeserialize,
22    EPOCH_DURATION, START_AT, END_AT,
23};
24
25/// Mine is the primary workhorse instruction of the Mars program. Its responsibilities include:
26/// 1. Verify the provided hash is valid.
27/// 2. Increment the user's claimable rewards counter.
28/// 3. Generate a new challenge for the miner.
29/// 4. Update the miner's lifetime stats.
30///
31/// Safety requirements:
32/// - Mine is a permissionless instruction and can be called by any signer.
33/// - Can only succeed if START_AT has passed.
34/// - Can only succeed if END_AT has not reached.
35/// - Can only succeed if the last reset was less than 60 seconds ago.
36/// - Can only succeed if the provided SHA3 hash and nonce are valid and satisfy the difficulty.
37/// - The the provided proof account must be associated with the signer.
38/// - The provided bus, treasury, and slot hash sysvar must be valid.
39pub fn process_mine<'a, 'info>(
40    _program_id: &Pubkey,
41    accounts: &'a [AccountInfo<'info>],
42    data: &[u8],
43) -> ProgramResult {
44    // Parse args
45    let args = MineArgs::try_from_bytes(data)?;
46
47    // Load accounts
48    let [signer, bus_info, proof_info, treasury_info, slot_hashes_info] = accounts else {
49        return Err(ProgramError::NotEnoughAccountKeys);
50    };
51    load_signer(signer)?;
52    load_any_bus(bus_info, true)?;
53    load_proof(proof_info, signer.key, true)?;
54    load_treasury(treasury_info, false)?;
55    load_sysvar(slot_hashes_info, sysvar::slot_hashes::id())?;
56
57    // Validate mining has starting
58    let clock = Clock::get().or(Err(ProgramError::InvalidAccountData))?;
59    if clock.unix_timestamp.lt(&START_AT) {
60        return Err(MarsError::NotStarted.into());
61    }
62    
63    if clock.unix_timestamp.ge(&END_AT) {
64        return Err(MarsError::HasEnded.into());
65    }
66
67    // Validate epoch is active
68    let treasury_data = treasury_info.data.borrow();
69    let treasury = Treasury::try_from_bytes(&treasury_data)?;
70    let threshold = treasury.last_reset_at.saturating_add(EPOCH_DURATION);
71    if clock.unix_timestamp.ge(&threshold) {
72        return Err(MarsError::NeedsReset.into());
73    }
74
75    // Validate provided hash
76    let mut proof_data = proof_info.data.borrow_mut();
77    let proof = Proof::try_from_bytes_mut(&mut proof_data)?;
78    validate_hash(
79        args.hash.into(),
80        proof.hash.into(),
81        *signer.key,
82        u64::from_le_bytes(args.nonce),
83        treasury.difficulty.into(),
84    )?;
85
86    // Update claimable rewards
87    let mut bus_data = bus_info.data.borrow_mut();
88    let bus = Bus::try_from_bytes_mut(&mut bus_data)?;
89    bus.rewards = bus
90        .rewards
91        .checked_sub(treasury.reward_rate)
92        .ok_or(MarsError::BusRewardsInsufficient)?;
93    proof.claimable_rewards = proof.claimable_rewards.saturating_add(treasury.reward_rate);
94
95    // Hash recent slot hash into the next challenge to prevent pre-mining attacks
96    proof.hash = hashv(&[
97        KeccakHash::from(args.hash).as_ref(),
98        &slot_hashes_info.data.borrow()[0..size_of::<SlotHash>()],
99    ])
100    .into();
101
102    // Update lifetime stats
103    proof.total_hashes = proof.total_hashes.saturating_add(1);
104    proof.total_rewards = proof.total_rewards.saturating_add(treasury.reward_rate);
105
106    // Log the mined rewards
107    set_return_data(treasury.reward_rate.to_le_bytes().as_slice());
108
109    Ok(())
110}
111
112/// Validates the provided hash, ensursing it is equal to SHA3(current_hash, singer, nonce).
113/// Fails if the provided hash is valid but does not satisfy the required difficulty.
114pub(crate) fn validate_hash(
115    hash: KeccakHash,
116    current_hash: KeccakHash,
117    signer: Pubkey,
118    nonce: u64,
119    difficulty: KeccakHash,
120) -> Result<(), ProgramError> {
121    // Validate hash correctness
122    let hash_ = hashv(&[
123        current_hash.as_ref(),
124        signer.as_ref(),
125        nonce.to_le_bytes().as_slice(),
126    ]);
127    if sol_memcmp(hash.as_ref(), hash_.as_ref(), HASH_BYTES) != 0 {
128        return Err(MarsError::HashInvalid.into());
129    }
130
131    // Validate hash difficulty
132    if hash.gt(&difficulty) {
133        return Err(MarsError::DifficultyNotSatisfied.into());
134    }
135
136    Ok(())
137}
138
139#[cfg(test)]
140mod tests {
141    use solana_program::{
142        keccak::{hashv, Hash, HASH_BYTES},
143        pubkey::Pubkey,
144    };
145
146    use crate::validate_hash;
147
148    #[test]
149    fn test_validate_hash_pass() {
150        let h1 = Hash::new_from_array([1; HASH_BYTES]);
151        let signer = Pubkey::new_unique();
152        let nonce = 10u64;
153        let difficulty = Hash::new_from_array([255; HASH_BYTES]);
154        let h2 = hashv(&[
155            h1.to_bytes().as_slice(),
156            signer.to_bytes().as_slice(),
157            nonce.to_le_bytes().as_slice(),
158        ]);
159        let res = validate_hash(h2, h1, signer, nonce, difficulty);
160        assert!(res.is_ok());
161    }
162
163    #[test]
164    fn test_validate_hash_fail() {
165        let h1 = Hash::new_from_array([1; HASH_BYTES]);
166        let signer = Pubkey::new_unique();
167        let nonce = 10u64;
168        let difficulty = Hash::new_from_array([255; HASH_BYTES]);
169        let h2 = Hash::new_from_array([2; HASH_BYTES]);
170        let res = validate_hash(h2, h1, signer, nonce, difficulty);
171        assert!(res.is_err());
172    }
173
174    #[test]
175    fn test_validate_hash_fail_difficulty() {
176        let h1 = Hash::new_from_array([1; HASH_BYTES]);
177        let signer = Pubkey::new_unique();
178        let nonce = 10u64;
179        let difficulty = Hash::new_from_array([0; HASH_BYTES]);
180        let h2 = hashv(&[
181            h1.to_bytes().as_slice(),
182            signer.to_bytes().as_slice(),
183            nonce.to_le_bytes().as_slice(),
184        ]);
185        let res = validate_hash(h2, h1, signer, nonce, difficulty);
186        assert!(res.is_err());
187    }
188}