1use solana_instruction::AccountMeta;
2use solana_pubkey::Pubkey;
3use solana_sha256_hasher::hashv;
4use wincode::{SchemaRead, SchemaWrite};
5
6pub const MAX_ACTION_OPS: usize = 32;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq, codama_macros::CodamaType, SchemaWrite, SchemaRead)]
14#[wincode(tag_encoding = "u8")]
15pub enum ActionScope {
16 #[wincode(tag = 0)]
17 Manager,
18 #[wincode(tag = 1)]
19 Swap,
20 #[wincode(tag = 2)]
21 AtomicRedeem,
22 #[wincode(tag = 3)]
28 FlashApprove,
29}
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq, codama_macros::CodamaType, SchemaWrite, SchemaRead)]
32#[wincode(tag_encoding = "u8")]
33pub enum Op {
34 #[wincode(tag = 0)]
35 Noop,
36 #[wincode(tag = 1)]
37 IngestInstruction { offset: u16, len: u8 },
38 #[wincode(tag = 2)]
39 IngestAccount { index: u8 },
40 #[wincode(tag = 3)]
41 IngestInstructionDataSize,
42 #[wincode(tag = 4)]
49 IngestSiblingInstruction {
50 relative_index: i8,
51 offset: u8,
52 len: u8,
53 },
54 #[wincode(tag = 5)]
57 IngestSiblingAccount { relative_index: i8, index: u8 },
58}
59
60#[derive(Clone, Copy, Debug, Eq, PartialEq, codama_macros::CodamaType, SchemaWrite, SchemaRead)]
61#[repr(C)]
62pub struct StoredOp {
63 pub kind: u8,
64 pub arg0: u8,
65 pub arg1: u8,
66 pub arg2: u8,
67}
68
69impl StoredOp {
70 pub const fn noop() -> Self {
71 Self {
72 kind: 0,
73 arg0: 0,
74 arg1: 0,
75 arg2: 0,
76 }
77 }
78
79 fn try_to_op(self) -> Result<Op, ActionHashError> {
80 let op = match self.kind {
81 0 if self.arg0 == 0 && self.arg1 == 0 && self.arg2 == 0 => Op::Noop,
82 1 => Op::IngestInstruction {
83 offset: u16::from_le_bytes([self.arg1, self.arg2]),
84 len: self.arg0,
85 },
86 2 if self.arg1 == 0 && self.arg2 == 0 => Op::IngestAccount { index: self.arg0 },
87 3 if self.arg0 == 0 && self.arg1 == 0 && self.arg2 == 0 => {
88 Op::IngestInstructionDataSize
89 }
90 4 => Op::IngestSiblingInstruction {
91 relative_index: self.arg0 as i8,
92 offset: self.arg1,
93 len: self.arg2,
94 },
95 5 if self.arg2 == 0 => Op::IngestSiblingAccount {
96 relative_index: self.arg0 as i8,
97 index: self.arg1,
98 },
99 _ => return Err(ActionHashError::InvalidOp),
100 };
101
102 Ok(op)
103 }
104}
105
106impl Default for StoredOp {
107 fn default() -> Self {
108 Self::noop()
109 }
110}
111
112impl From<Op> for StoredOp {
113 fn from(op: Op) -> Self {
114 match op {
115 Op::Noop => Self::noop(),
116 Op::IngestInstruction { offset, len } => {
117 let [arg1, arg2] = offset.to_le_bytes();
118 Self {
119 kind: 1,
120 arg0: len,
121 arg1,
122 arg2,
123 }
124 }
125 Op::IngestAccount { index } => Self {
126 kind: 2,
127 arg0: index,
128 arg1: 0,
129 arg2: 0,
130 },
131 Op::IngestInstructionDataSize => Self {
132 kind: 3,
133 arg0: 0,
134 arg1: 0,
135 arg2: 0,
136 },
137 Op::IngestSiblingInstruction {
138 relative_index,
139 offset,
140 len,
141 } => Self {
142 kind: 4,
143 arg0: relative_index as u8,
144 arg1: offset,
145 arg2: len,
146 },
147 Op::IngestSiblingAccount {
148 relative_index,
149 index,
150 } => Self {
151 kind: 5,
152 arg0: relative_index as u8,
153 arg1: index,
154 arg2: 0,
155 },
156 }
157 }
158}
159
160#[derive(Clone, Copy, Debug, Eq, PartialEq, codama_macros::CodamaType, SchemaWrite, SchemaRead)]
161#[repr(C)]
162pub struct Ops {
163 pub ops: [StoredOp; 32],
164 pub ops_len: u8,
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use wincode::serialize;
171
172 #[test]
173 fn stored_ops_are_compact() {
174 assert_eq!(core::mem::size_of::<StoredOp>(), 4);
175 assert_eq!(core::mem::size_of::<Ops>(), MAX_ACTION_OPS * 4 + 1);
176 assert_eq!(serialize(&StoredOp::noop()).unwrap().len(), 4);
177 assert_eq!(
178 serialize(&Ops::empty()).unwrap().len(),
179 MAX_ACTION_OPS * 4 + 1
180 );
181 }
182
183 #[test]
184 fn ops_new_enforces_capacity() {
185 let ops = vec![Op::Noop; MAX_ACTION_OPS + 1];
186
187 assert_eq!(Ops::new(ops), Err(ActionHashError::TooManyOps));
188 }
189
190 #[test]
191 fn ops_round_trip_logical_ops() {
192 let ops = Ops::new([
193 Op::Noop,
194 Op::IngestInstruction {
195 offset: 513,
196 len: 7,
197 },
198 Op::IngestAccount { index: 9 },
199 Op::IngestInstructionDataSize,
200 Op::IngestSiblingInstruction {
201 relative_index: -1,
202 offset: 0,
203 len: 8,
204 },
205 Op::IngestSiblingAccount {
206 relative_index: 2,
207 index: 5,
208 },
209 ])
210 .unwrap();
211 let decoded = ops.iter().unwrap().collect::<Result<Vec<_>, _>>().unwrap();
212
213 assert_eq!(
214 decoded,
215 vec![
216 Op::Noop,
217 Op::IngestInstruction {
218 offset: 513,
219 len: 7,
220 },
221 Op::IngestAccount { index: 9 },
222 Op::IngestInstructionDataSize,
223 Op::IngestSiblingInstruction {
224 relative_index: -1,
225 offset: 0,
226 len: 8,
227 },
228 Op::IngestSiblingAccount {
229 relative_index: 2,
230 index: 5,
231 },
232 ]
233 );
234 }
235
236 #[test]
237 fn hash_rejects_corrupt_stored_ops() {
238 let program_id = Pubkey::new_unique();
239 let account_metas = [];
240 let ix_data = [];
241
242 let mut too_many = Ops::empty();
243 too_many.ops_len = u8::try_from(MAX_ACTION_OPS + 1).unwrap();
244 assert_eq!(
245 compute_action_hash_from_metas(&program_id, &too_many, &account_metas, &ix_data, &[]),
246 Err(ActionHashError::TooManyOps)
247 );
248
249 let mut invalid_kind = Ops::empty();
250 invalid_kind.ops[0] = StoredOp {
251 kind: 255,
252 arg0: 0,
253 arg1: 0,
254 arg2: 0,
255 };
256 invalid_kind.ops_len = 1;
257 assert_eq!(
258 compute_action_hash_from_metas(
259 &program_id,
260 &invalid_kind,
261 &account_metas,
262 &ix_data,
263 &[]
264 ),
265 Err(ActionHashError::InvalidOp)
266 );
267
268 let mut non_canonical = Ops::empty();
269 non_canonical.ops[0] = StoredOp {
270 kind: 0,
271 arg0: 1,
272 arg1: 0,
273 arg2: 0,
274 };
275 non_canonical.ops_len = 1;
276 assert_eq!(
277 compute_action_hash_from_metas(
278 &program_id,
279 &non_canonical,
280 &account_metas,
281 &ix_data,
282 &[]
283 ),
284 Err(ActionHashError::InvalidOp)
285 );
286 }
287}
288
289impl Ops {
290 pub const fn empty() -> Self {
291 Self {
292 ops: [StoredOp::noop(); MAX_ACTION_OPS],
293 ops_len: 0,
294 }
295 }
296
297 pub fn new(ops: impl IntoIterator<Item = Op>) -> Result<Self, ActionHashError> {
298 let mut stored_ops = Self::empty();
299
300 for (index, op) in ops.into_iter().enumerate() {
301 if index >= MAX_ACTION_OPS {
302 return Err(ActionHashError::TooManyOps);
303 }
304
305 stored_ops.ops[index] = StoredOp::from(op);
306 stored_ops.ops_len =
307 u8::try_from(index + 1).map_err(|_| ActionHashError::InvalidInstructionData)?;
308 }
309
310 Ok(stored_ops)
311 }
312
313 pub fn len(&self) -> Result<usize, ActionHashError> {
314 let len = usize::from(self.ops_len);
315 if len > MAX_ACTION_OPS {
316 return Err(ActionHashError::TooManyOps);
317 }
318
319 Ok(len)
320 }
321
322 pub fn is_empty(&self) -> Result<bool, ActionHashError> {
323 Ok(self.len()? == 0)
324 }
325
326 pub fn iter(
327 &self,
328 ) -> Result<impl Iterator<Item = Result<Op, ActionHashError>> + '_, ActionHashError> {
329 let len = self.len()?;
330 Ok(self.ops[..len].iter().copied().map(StoredOp::try_to_op))
331 }
332}
333
334impl Default for Ops {
335 fn default() -> Self {
336 Self::empty()
337 }
338}
339
340#[derive(Clone, Copy, Debug, Eq, PartialEq)]
341pub enum ActionHashError {
342 InvalidOp,
343 TooManyOps,
344 InstructionSliceOutOfBounds,
345 AccountIndexOutOfBounds,
346 InvalidInstructionData,
347 MissingSibling,
348}
349
350pub struct ResolvedSibling<'a> {
357 pub relative_index: i8,
358 pub program_id: Pubkey,
359 pub data: &'a [u8],
360 pub accounts: &'a [Pubkey],
361}
362
363pub fn compute_action_hash_from_metas(
364 program_id: &Pubkey,
365 ops: &Ops,
366 accounts: &[AccountMeta],
367 ix_data: &[u8],
368 siblings: &[ResolvedSibling],
369) -> Result<[u8; 32], ActionHashError> {
370 let mut chunks = vec![program_id.to_bytes().to_vec()];
371
372 for op in ops.iter()? {
373 match op? {
374 Op::Noop => chunks.push(vec![0]),
375 Op::IngestInstruction { offset, len } => {
376 let start = usize::from(offset);
377 let length = usize::from(len);
378 let end = start
379 .checked_add(length)
380 .ok_or(ActionHashError::InstructionSliceOutOfBounds)?;
381 let slice = ix_data
382 .get(start..end)
383 .ok_or(ActionHashError::InstructionSliceOutOfBounds)?;
384
385 chunks.push(vec![1]);
386 chunks.push(offset.to_le_bytes().to_vec());
387 chunks.push(vec![len]);
388 chunks.push(slice.to_vec());
389 }
390 Op::IngestAccount { index } => {
391 let account = accounts
392 .get(usize::from(index))
393 .ok_or(ActionHashError::AccountIndexOutOfBounds)?;
394
395 chunks.push(vec![2]);
396 chunks.push(vec![index]);
397 chunks.push(account.pubkey.to_bytes().to_vec());
398 chunks.push(vec![u8::from(account.is_signer)]);
399 chunks.push(vec![u8::from(account.is_writable)]);
400 }
401 Op::IngestInstructionDataSize => {
402 let data_len = u32::try_from(ix_data.len())
403 .map_err(|_| ActionHashError::InvalidInstructionData)?;
404
405 chunks.push(vec![3]);
406 chunks.push(data_len.to_le_bytes().to_vec());
407 }
408 Op::IngestSiblingInstruction {
409 relative_index,
410 offset,
411 len,
412 } => {
413 let sibling = resolve_sibling(siblings, relative_index)?;
414 let start = usize::from(offset);
415 let end = start
416 .checked_add(usize::from(len))
417 .ok_or(ActionHashError::InstructionSliceOutOfBounds)?;
418 let slice = sibling
419 .data
420 .get(start..end)
421 .ok_or(ActionHashError::InstructionSliceOutOfBounds)?;
422
423 chunks.push(vec![4]);
424 chunks.push(vec![relative_index as u8]);
425 chunks.push(sibling.program_id.to_bytes().to_vec());
426 chunks.push(vec![offset]);
427 chunks.push(vec![len]);
428 chunks.push(slice.to_vec());
429 }
430 Op::IngestSiblingAccount {
431 relative_index,
432 index,
433 } => {
434 let sibling = resolve_sibling(siblings, relative_index)?;
435 let account = sibling
436 .accounts
437 .get(usize::from(index))
438 .ok_or(ActionHashError::AccountIndexOutOfBounds)?;
439
440 chunks.push(vec![5]);
441 chunks.push(vec![relative_index as u8]);
442 chunks.push(vec![index]);
443 chunks.push(account.to_bytes().to_vec());
444 }
445 }
446 }
447
448 let refs = chunks.iter().map(Vec::as_slice).collect::<Vec<_>>();
449 Ok(hashv(&refs).to_bytes())
450}
451
452fn resolve_sibling<'a, 'b>(
453 siblings: &'a [ResolvedSibling<'b>],
454 relative_index: i8,
455) -> Result<&'a ResolvedSibling<'b>, ActionHashError> {
456 siblings
457 .iter()
458 .find(|sibling| sibling.relative_index == relative_index)
459 .ok_or(ActionHashError::MissingSibling)
460}