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