miclockwork_thread_program/instructions/
thread_exec.rs

1use anchor_lang::{
2    prelude::*,
3    solana_program::{
4        instruction::Instruction,
5        program::{get_return_data, invoke_signed},
6    },
7    AnchorDeserialize, InstructionData,
8};
9use miclockwork_network_program::state::{Fee, Pool, Worker, WorkerAccount};
10use miclockwork_utils::thread::{SerializableInstruction, ThreadResponse, PAYER_PUBKEY};
11
12use crate::{errors::ClockworkError, state::*};
13
14/// The ID of the pool workers must be a member of to collect fees.
15const POOL_ID: u64 = 0;
16
17/// The number of lamports to reimburse the worker with after they've submitted a transaction's worth of exec instructions.
18pub const TRANSACTION_BASE_FEE_REIMBURSEMENT: u64 = 5_000;
19
20/// Accounts required by the `thread_exec` instruction.
21#[derive(Accounts)]
22pub struct ThreadExec<'info> {
23    /// The worker's fee account.
24    #[account(
25        mut,
26        seeds = [
27            miclockwork_network_program::state::SEED_FEE,
28            worker.key().as_ref(),
29        ],
30        bump,
31        seeds::program = miclockwork_network_program::ID,
32        has_one = worker,
33    )]
34    pub fee: Account<'info, Fee>,
35
36    /// The active worker pool.
37    #[account(address = Pool::pubkey(POOL_ID))]
38    pub pool: Box<Account<'info, Pool>>,
39
40    /// The signatory.
41    #[account(mut)]
42    pub signatory: Signer<'info>,
43
44    /// The thread to execute.
45    #[account(
46        mut,
47        seeds = [
48            SEED_THREAD,
49            thread.authority.as_ref(),
50            thread.id.as_slice(),
51        ],
52        bump = thread.bump,
53        constraint = !thread.paused @ ClockworkError::ThreadPaused,
54        constraint = thread.next_instruction.is_some(),
55        constraint = thread.exec_context.is_some()
56    )]
57    pub thread: Box<Account<'info, Thread>>,
58
59    /// The worker.
60    #[account(address = worker.pubkey())]
61    pub worker: Account<'info, Worker>,
62}
63
64pub fn handler(ctx: Context<ThreadExec>) -> Result<()> {
65    // Get accounts
66    let clock = Clock::get().unwrap();
67    let fee = &mut ctx.accounts.fee;
68    let pool = &ctx.accounts.pool;
69    let signatory = &mut ctx.accounts.signatory;
70    let thread = &mut ctx.accounts.thread;
71    let worker = &ctx.accounts.worker;
72
73    // If the rate limit has been met, exit early.
74    if thread.exec_context.unwrap().last_exec_at == clock.slot
75        && thread.exec_context.unwrap().execs_since_slot >= thread.rate_limit
76    {
77        return Err(ClockworkError::RateLimitExeceeded.into());
78    }
79
80    // Record the worker's lamports before invoking inner ixs.
81    let signatory_lamports_pre = signatory.lamports();
82
83    // Get the instruction to execute.
84    // We have already verified that it is not null during account validation.
85    let instruction: &mut SerializableInstruction = &mut thread.next_instruction.clone().unwrap();
86
87    // Inject the signatory's pubkey for the Clockwork payer ID.
88    for acc in instruction.accounts.iter_mut() {
89        if acc.pubkey.eq(&PAYER_PUBKEY) {
90            acc.pubkey = signatory.key();
91        }
92    }
93
94    // Invoke the provided instruction.
95    invoke_signed(
96        &Instruction::from(&*instruction),
97        ctx.remaining_accounts,
98        &[&[
99            SEED_THREAD,
100            thread.authority.as_ref(),
101            thread.id.as_slice(),
102            &[thread.bump],
103        ]],
104    )?;
105
106    // Verify the inner instruction did not write data to the signatory address.
107    require!(signatory.data_is_empty(), ClockworkError::UnauthorizedWrite);
108
109    // Parse the thread response
110    let thread_response: Option<ThreadResponse> = match get_return_data() {
111        None => None,
112        Some((program_id, return_data)) => {
113            require!(
114                program_id.eq(&instruction.program_id),
115                ClockworkError::InvalidThreadResponse
116            );
117            ThreadResponse::try_from_slice(return_data.as_slice()).ok()
118        }
119    };
120
121    // Grab the next instruction from the thread response.
122    let mut close_to = None;
123    let mut next_instruction = None;
124    if let Some(thread_response) = thread_response {
125        close_to = thread_response.close_to;
126        next_instruction = thread_response.dynamic_instruction;
127
128        // Update the trigger.
129        if let Some(trigger) = thread_response.trigger {
130            require!(
131                std::mem::discriminant(&thread.trigger) == std::mem::discriminant(&trigger),
132                ClockworkError::InvalidTriggerVariant
133            );
134            thread.trigger = trigger.clone();
135
136            // If the user updates an account trigger, the trigger context is no longer valid.
137            // Here we reset the trigger context to zero to re-prime the trigger.
138            thread.exec_context = Some(ExecContext {
139                trigger_context: match trigger {
140                    Trigger::Account {
141                        address: _,
142                        offset: _,
143                        size: _,
144                    } => TriggerContext::Account { data_hash: 0 },
145                    _ => thread.exec_context.unwrap().trigger_context,
146                },
147                ..thread.exec_context.unwrap()
148            })
149        }
150    }
151
152    // If there is no dynamic next instruction, get the next instruction from the instruction set.
153    let mut exec_index = thread.exec_context.unwrap().exec_index;
154    if next_instruction.is_none() {
155        if let Some(ix) = thread.instructions.get((exec_index + 1) as usize) {
156            next_instruction = Some(ix.clone());
157            exec_index = exec_index + 1;
158        }
159    }
160
161    // Update the next instruction.
162    if let Some(close_to) = close_to {
163        thread.next_instruction = Some(
164            Instruction {
165                program_id: crate::ID,
166                accounts: crate::accounts::ThreadDelete {
167                    authority: thread.key(),
168                    close_to,
169                    thread: thread.key(),
170                }
171                .to_account_metas(Some(true)),
172                data: crate::instruction::ThreadDelete {}.data(),
173            }
174            .into(),
175        );
176    } else {
177        thread.next_instruction = next_instruction;
178    }
179
180    // Update the exec context.
181    let should_reimburse_transaction = clock.slot > thread.exec_context.unwrap().last_exec_at;
182    thread.exec_context = Some(ExecContext {
183        exec_index,
184        execs_since_slot: if clock.slot == thread.exec_context.unwrap().last_exec_at {
185            thread
186                .exec_context
187                .unwrap()
188                .execs_since_slot
189                .checked_add(1)
190                .unwrap()
191        } else {
192            1
193        },
194        last_exec_at: clock.slot,
195        ..thread.exec_context.unwrap()
196    });
197
198    // Reimbursement signatory for lamports paid during inner ix.
199    let signatory_lamports_post = signatory.lamports();
200    let mut signatory_reimbursement =
201        signatory_lamports_pre.saturating_sub(signatory_lamports_post);
202    if should_reimburse_transaction {
203        signatory_reimbursement = signatory_reimbursement
204            .checked_add(TRANSACTION_BASE_FEE_REIMBURSEMENT)
205            .unwrap();
206    }
207    if signatory_reimbursement.gt(&0) {
208        **thread.to_account_info().try_borrow_mut_lamports()? = thread
209            .to_account_info()
210            .lamports()
211            .checked_sub(signatory_reimbursement)
212            .unwrap();
213        **signatory.to_account_info().try_borrow_mut_lamports()? = signatory
214            .to_account_info()
215            .lamports()
216            .checked_add(signatory_reimbursement)
217            .unwrap();
218    }
219
220    // If the worker is in the pool, debit from the thread account and payout to the worker's fee account.
221    if pool.clone().into_inner().workers.contains(&worker.key()) {
222        **thread.to_account_info().try_borrow_mut_lamports()? = thread
223            .to_account_info()
224            .lamports()
225            .checked_sub(thread.fee)
226            .unwrap();
227        **fee.to_account_info().try_borrow_mut_lamports()? = fee
228            .to_account_info()
229            .lamports()
230            .checked_add(thread.fee)
231            .unwrap();
232    }
233
234    Ok(())
235}