clockwork_scheduler/state/
task.rs

1use crate::state::FeeAccount;
2
3use {
4    super::{Config, Fee, Queue},
5    crate::{
6        errors::ClockworkError,
7        state::{QueueAccount, QueueStatus},
8    },
9    anchor_lang::{
10        prelude::borsh::BorshSchema, prelude::*, solana_program::instruction::Instruction,
11        AnchorDeserialize,
12    },
13    std::convert::TryFrom,
14};
15
16pub const SEED_TASK: &[u8] = b"task";
17
18/**
19 * Task
20 */
21
22#[account]
23#[derive(Debug)]
24pub struct Task {
25    pub id: u64,
26    pub ixs: Vec<InstructionData>,
27    pub queue: Pubkey,
28}
29
30impl Task {
31    pub fn pubkey(queue: Pubkey, id: u64) -> Pubkey {
32        Pubkey::find_program_address(
33            &[SEED_TASK, queue.as_ref(), id.to_be_bytes().as_ref()],
34            &crate::ID,
35        )
36        .0
37    }
38}
39
40impl TryFrom<Vec<u8>> for Task {
41    type Error = Error;
42    fn try_from(data: Vec<u8>) -> std::result::Result<Self, Self::Error> {
43        Task::try_deserialize(&mut data.as_slice())
44    }
45}
46
47/**
48 * TaskAccount
49 */
50
51pub trait TaskAccount {
52    fn new(&mut self, ixs: Vec<InstructionData>, queue: &mut Account<Queue>) -> Result<()>;
53
54    fn exec(
55        &mut self,
56        account_infos: &Vec<AccountInfo>,
57        config: &Account<Config>,
58        fee: &mut Account<Fee>,
59        queue: &mut Account<Queue>,
60        queue_bump: u8,
61        worker: &mut Signer,
62    ) -> Result<()>;
63}
64
65impl TaskAccount for Account<'_, Task> {
66    fn new(&mut self, ixs: Vec<InstructionData>, queue: &mut Account<Queue>) -> Result<()> {
67        // Reject inner instructions if they have a signer other than the queue or payer
68        for ix in ixs.iter() {
69            for acc in ix.accounts.iter() {
70                if acc.is_signer {
71                    require!(
72                        acc.pubkey == queue.key() || acc.pubkey == crate::payer::ID,
73                        ClockworkError::InvalidSignatory
74                    );
75                }
76            }
77        }
78
79        // Save data
80        self.id = queue.task_count;
81        self.ixs = ixs;
82        self.queue = queue.key();
83
84        // Increment the queue's task count
85        queue.task_count = queue.task_count.checked_add(1).unwrap();
86
87        Ok(())
88    }
89
90    fn exec(
91        &mut self,
92        account_infos: &Vec<AccountInfo>,
93        config: &Account<Config>,
94        fee: &mut Account<Fee>,
95        queue: &mut Account<Queue>,
96        queue_bump: u8,
97        worker: &mut Signer,
98    ) -> Result<()> {
99        // Validate the task id matches the queue's current execution state
100        require!(
101            self.id
102                == match queue.status {
103                    QueueStatus::Processing { task_id } => task_id,
104                    _ => return Err(ClockworkError::InvalidQueueStatus.into()),
105                },
106            ClockworkError::InvalidTask
107        );
108
109        // Validate the worker data is empty
110        require!(worker.data_is_empty(), ClockworkError::WorkerDataNotEmpty);
111
112        // Record the worker's lamports before invoking inner ixs
113        let worker_lamports_pre = worker.lamports();
114
115        // Create an array of dynamic ixs to update the task for the next invocation
116        let dyanmic_ixs: &mut Vec<InstructionData> = &mut vec![];
117
118        // Process all of the task instructions
119        for ix in &self.ixs {
120            // If an inner ix account matches the Clockwork payer address (ClockworkPayer11111111111111111111111111111111),
121            //  then inject the worker in its place. Dapp developers can use the worker as a payer to initialize
122            //  new accounts in their tasks. Workers will be reimbursed for all SOL spent during the inner ixs.
123            //
124            // Because the worker can be injected as the signer on inner ixs (written by presumed malicious parties),
125            //  node operators should not secure any assets or staking positions with their worker wallets other than
126            //  an operational level of lamports needed to submit txns (~0.01 ⊚).
127            let accs: &mut Vec<AccountMetaData> = &mut vec![];
128            ix.accounts.iter().for_each(|acc| {
129                if acc.pubkey == crate::payer::ID {
130                    accs.push(AccountMetaData {
131                        pubkey: worker.key(),
132                        is_signer: acc.is_signer,
133                        is_writable: acc.is_writable,
134                    });
135                } else {
136                    accs.push(acc.clone());
137                }
138            });
139
140            // Execute the inner ix and process the response. Note that even though the queue PDA is a signer
141            //  on this ix, Solana will not allow downstream programs to mutate accounts owned by this program
142            //  and explicitly forbids CPI reentrancy.
143            let exec_response = queue.sign(
144                &account_infos,
145                queue_bump,
146                &InstructionData {
147                    program_id: ix.program_id,
148                    accounts: accs.clone(),
149                    data: ix.data.clone(),
150                },
151            )?;
152
153            // Process the exec response
154            match exec_response {
155                None => (),
156                Some(exec_response) => match exec_response.dynamic_accounts {
157                    None => (),
158                    Some(dynamic_accounts) => {
159                        require!(
160                            dynamic_accounts.len() == ix.accounts.len(),
161                            ClockworkError::InvalidDynamicAccounts
162                        );
163                        dyanmic_ixs.push(InstructionData {
164                            program_id: ix.program_id,
165                            accounts: dynamic_accounts
166                                .iter()
167                                .enumerate()
168                                .map(|(i, pubkey)| {
169                                    let acc = ix.accounts.get(i).unwrap();
170                                    AccountMetaData {
171                                        pubkey: match pubkey {
172                                            _ if *pubkey == worker.key() => crate::payer::ID,
173                                            _ => *pubkey,
174                                        },
175                                        is_signer: acc.is_signer,
176                                        is_writable: acc.is_writable,
177                                    }
178                                })
179                                .collect::<Vec<AccountMetaData>>(),
180                            data: ix.data.clone(),
181                        });
182                    }
183                },
184            }
185        }
186
187        // Verify that inner ixs have not initialized data at the worker address
188        require!(worker.data_is_empty(), ClockworkError::WorkerDataNotEmpty);
189
190        // Update the actions's ixs for the next invocation
191        if !dyanmic_ixs.is_empty() {
192            self.ixs = dyanmic_ixs.clone();
193        }
194
195        // Track how many lamports the worker spent in the inner ixs
196        let worker_lamports_post = worker.lamports();
197        let worker_reimbursement = worker_lamports_pre
198            .checked_sub(worker_lamports_post)
199            .unwrap();
200
201        // Pay worker fees
202        let total_worker_fee = config.worker_fee.checked_add(worker_reimbursement).unwrap();
203        fee.pay_to_worker(total_worker_fee, queue)?;
204
205        // Update the queue status
206        let next_task_id = self.id.checked_add(1).unwrap();
207        if next_task_id == queue.task_count {
208            queue.roll_forward()?;
209        } else {
210            queue.status = QueueStatus::Processing {
211                task_id: next_task_id,
212            };
213        }
214
215        Ok(())
216    }
217}
218
219/**
220 * InstructionData
221 */
222
223#[derive(AnchorDeserialize, AnchorSerialize, BorshSchema, Clone, Debug, PartialEq)]
224pub struct InstructionData {
225    /// Pubkey of the instruction processor that executes this instruction
226    pub program_id: Pubkey,
227    /// Metadata for what accounts should be passed to the instruction processor
228    pub accounts: Vec<AccountMetaData>,
229    /// Opaque data passed to the instruction processor
230    pub data: Vec<u8>,
231}
232
233impl From<Instruction> for InstructionData {
234    fn from(instruction: Instruction) -> Self {
235        InstructionData {
236            program_id: instruction.program_id,
237            accounts: instruction
238                .accounts
239                .iter()
240                .map(|a| AccountMetaData {
241                    pubkey: a.pubkey,
242                    is_signer: a.is_signer,
243                    is_writable: a.is_writable,
244                })
245                .collect(),
246            data: instruction.data,
247        }
248    }
249}
250
251impl From<&InstructionData> for Instruction {
252    fn from(instruction: &InstructionData) -> Self {
253        Instruction {
254            program_id: instruction.program_id,
255            accounts: instruction
256                .accounts
257                .iter()
258                .map(|a| AccountMeta {
259                    pubkey: a.pubkey,
260                    is_signer: a.is_signer,
261                    is_writable: a.is_writable,
262                })
263                .collect(),
264            data: instruction.data.clone(),
265        }
266    }
267}
268
269impl TryFrom<Vec<u8>> for InstructionData {
270    type Error = Error;
271    fn try_from(data: Vec<u8>) -> std::result::Result<Self, Self::Error> {
272        Ok(
273            borsh::try_from_slice_with_schema::<InstructionData>(data.as_slice())
274                .map_err(|_err| ErrorCode::AccountDidNotDeserialize)?,
275        )
276    }
277}
278
279/**
280 * AccountMetaData
281 */
282
283#[derive(AnchorDeserialize, AnchorSerialize, BorshSchema, Clone, Debug, PartialEq)]
284pub struct AccountMetaData {
285    /// An account's public key
286    pub pubkey: Pubkey,
287    /// True if an Instruction requires a Transaction signature matching `pubkey`.
288    pub is_signer: bool,
289    /// True if the `pubkey` can be loaded as a read-write account.
290    pub is_writable: bool,
291}