1use solana_instruction::AccountMeta;
2use solana_pubkey::Pubkey;
3use solana_sha256_hasher::hash;
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]
176 fn streamed_hash_matches_chunked_reference() {
177 let program_id = Pubkey::new_unique();
178 let ix_data = (0u8..40).collect::<Vec<_>>();
179 let account = AccountMeta::new(Pubkey::new_unique(), true);
180 let sibling_accounts = [Pubkey::new_unique(), Pubkey::new_unique()];
181 let sibling_data = (10u8..30).collect::<Vec<_>>();
182 let siblings = [ResolvedSibling {
183 relative_index: -1,
184 program_id: Pubkey::new_unique(),
185 data: &sibling_data,
186 accounts: &sibling_accounts,
187 }];
188 let ops = Ops::new([
189 Op::Noop,
190 Op::IngestInstruction { offset: 5, len: 7 },
191 Op::IngestAccount { index: 0 },
192 Op::IngestInstructionDataSize,
193 Op::IngestSiblingInstruction {
194 relative_index: -1,
195 offset: 2,
196 len: 8,
197 },
198 Op::IngestSiblingAccount {
199 relative_index: -1,
200 index: 1,
201 },
202 ])
203 .unwrap();
204
205 let mut chunks: Vec<Vec<u8>> = vec![program_id.to_bytes().to_vec()];
207 chunks.push(vec![0]);
208 chunks.push(vec![1]);
209 chunks.push(5u16.to_le_bytes().to_vec());
210 chunks.push(vec![7]);
211 chunks.push(ix_data[5..12].to_vec());
212 chunks.push(vec![2]);
213 chunks.push(vec![0]);
214 chunks.push(account.pubkey.to_bytes().to_vec());
215 chunks.push(vec![1]);
216 chunks.push(vec![1]);
217 chunks.push(vec![3]);
218 chunks.push((ix_data.len() as u32).to_le_bytes().to_vec());
219 chunks.push(vec![4]);
220 chunks.push(vec![(-1i8) as u8]);
221 chunks.push(siblings[0].program_id.to_bytes().to_vec());
222 chunks.push(vec![2]);
223 chunks.push(vec![8]);
224 chunks.push(sibling_data[2..10].to_vec());
225 chunks.push(vec![5]);
226 chunks.push(vec![(-1i8) as u8]);
227 chunks.push(vec![1]);
228 chunks.push(sibling_accounts[1].to_bytes().to_vec());
229 let refs = chunks.iter().map(Vec::as_slice).collect::<Vec<_>>();
230 let reference = solana_sha256_hasher::hashv(&refs).to_bytes();
231
232 let streamed =
233 compute_action_hash_from_metas(&program_id, &ops, &[account], &ix_data, &siblings)
234 .unwrap();
235
236 assert_eq!(streamed, reference);
237 }
238
239 #[test]
240 fn stored_ops_are_compact() {
241 assert_eq!(core::mem::size_of::<StoredOp>(), 4);
242 assert_eq!(core::mem::size_of::<Ops>(), MAX_ACTION_OPS * 4 + 1);
243 assert_eq!(serialize(&StoredOp::noop()).unwrap().len(), 4);
244 assert_eq!(
245 serialize(&Ops::empty()).unwrap().len(),
246 MAX_ACTION_OPS * 4 + 1
247 );
248 }
249
250 #[test]
251 fn ops_new_enforces_capacity() {
252 let ops = vec![Op::Noop; MAX_ACTION_OPS + 1];
253
254 assert_eq!(Ops::new(ops), Err(ActionHashError::TooManyOps));
255 }
256
257 #[test]
258 fn ops_round_trip_logical_ops() {
259 let ops = Ops::new([
260 Op::Noop,
261 Op::IngestInstruction {
262 offset: 513,
263 len: 7,
264 },
265 Op::IngestAccount { index: 9 },
266 Op::IngestInstructionDataSize,
267 Op::IngestSiblingInstruction {
268 relative_index: -1,
269 offset: 0,
270 len: 8,
271 },
272 Op::IngestSiblingAccount {
273 relative_index: 2,
274 index: 5,
275 },
276 ])
277 .unwrap();
278 let decoded = ops.iter().unwrap().collect::<Result<Vec<_>, _>>().unwrap();
279
280 assert_eq!(
281 decoded,
282 vec![
283 Op::Noop,
284 Op::IngestInstruction {
285 offset: 513,
286 len: 7,
287 },
288 Op::IngestAccount { index: 9 },
289 Op::IngestInstructionDataSize,
290 Op::IngestSiblingInstruction {
291 relative_index: -1,
292 offset: 0,
293 len: 8,
294 },
295 Op::IngestSiblingAccount {
296 relative_index: 2,
297 index: 5,
298 },
299 ]
300 );
301 }
302
303 #[test]
304 fn hash_rejects_corrupt_stored_ops() {
305 let program_id = Pubkey::new_unique();
306 let account_metas = [];
307 let ix_data = [];
308
309 let mut too_many = Ops::empty();
310 too_many.ops_len = u8::try_from(MAX_ACTION_OPS + 1).unwrap();
311 assert_eq!(
312 compute_action_hash_from_metas(&program_id, &too_many, &account_metas, &ix_data, &[]),
313 Err(ActionHashError::TooManyOps)
314 );
315
316 let mut invalid_kind = Ops::empty();
317 invalid_kind.ops[0] = StoredOp {
318 kind: 255,
319 arg0: 0,
320 arg1: 0,
321 arg2: 0,
322 };
323 invalid_kind.ops_len = 1;
324 assert_eq!(
325 compute_action_hash_from_metas(
326 &program_id,
327 &invalid_kind,
328 &account_metas,
329 &ix_data,
330 &[]
331 ),
332 Err(ActionHashError::InvalidOp)
333 );
334
335 let mut non_canonical = Ops::empty();
336 non_canonical.ops[0] = StoredOp {
337 kind: 0,
338 arg0: 1,
339 arg1: 0,
340 arg2: 0,
341 };
342 non_canonical.ops_len = 1;
343 assert_eq!(
344 compute_action_hash_from_metas(
345 &program_id,
346 &non_canonical,
347 &account_metas,
348 &ix_data,
349 &[]
350 ),
351 Err(ActionHashError::InvalidOp)
352 );
353 }
354}
355
356impl Ops {
357 pub const fn empty() -> Self {
358 Self {
359 ops: [StoredOp::noop(); MAX_ACTION_OPS],
360 ops_len: 0,
361 }
362 }
363
364 pub fn new(ops: impl IntoIterator<Item = Op>) -> Result<Self, ActionHashError> {
365 let mut stored_ops = Self::empty();
366
367 for (index, op) in ops.into_iter().enumerate() {
368 if index >= MAX_ACTION_OPS {
369 return Err(ActionHashError::TooManyOps);
370 }
371
372 stored_ops.ops[index] = StoredOp::from(op);
373 stored_ops.ops_len =
374 u8::try_from(index + 1).map_err(|_| ActionHashError::InvalidInstructionData)?;
375 }
376
377 Ok(stored_ops)
378 }
379
380 pub fn len(&self) -> Result<usize, ActionHashError> {
381 let len = usize::from(self.ops_len);
382 if len > MAX_ACTION_OPS {
383 return Err(ActionHashError::TooManyOps);
384 }
385
386 Ok(len)
387 }
388
389 pub fn is_empty(&self) -> Result<bool, ActionHashError> {
390 Ok(self.len()? == 0)
391 }
392
393 pub fn iter(
394 &self,
395 ) -> Result<impl Iterator<Item = Result<Op, ActionHashError>> + '_, ActionHashError> {
396 let len = self.len()?;
397 Ok(self.ops[..len].iter().copied().map(StoredOp::try_to_op))
398 }
399}
400
401impl Default for Ops {
402 fn default() -> Self {
403 Self::empty()
404 }
405}
406
407#[derive(Clone, Copy, Debug, Eq, PartialEq)]
408pub enum ActionHashError {
409 InvalidOp,
410 TooManyOps,
411 InstructionSliceOutOfBounds,
412 AccountIndexOutOfBounds,
413 InvalidInstructionData,
414 MissingSibling,
415}
416
417pub struct ResolvedSibling<'a> {
424 pub relative_index: i8,
425 pub program_id: Pubkey,
426 pub data: &'a [u8],
427 pub accounts: &'a [Pubkey],
428}
429
430pub fn compute_action_hash_from_metas(
431 program_id: &Pubkey,
432 ops: &Ops,
433 accounts: &[AccountMeta],
434 ix_data: &[u8],
435 siblings: &[ResolvedSibling],
436) -> Result<[u8; 32], ActionHashError> {
437 let mut preimage = Vec::with_capacity(preimage_len(ops)?);
445 preimage.extend_from_slice(&program_id.to_bytes());
446
447 for op in ops.iter()? {
448 match op? {
449 Op::Noop => preimage.push(0),
450 Op::IngestInstruction { offset, len } => {
451 let slice = instruction_slice(ix_data, usize::from(offset), len)?;
452 preimage.push(1);
453 preimage.extend_from_slice(&offset.to_le_bytes());
454 preimage.push(len);
455 preimage.extend_from_slice(slice);
456 }
457 Op::IngestAccount { index } => {
458 let account = accounts
459 .get(usize::from(index))
460 .ok_or(ActionHashError::AccountIndexOutOfBounds)?;
461
462 preimage.push(2);
463 preimage.push(index);
464 preimage.extend_from_slice(&account.pubkey.to_bytes());
465 preimage.push(u8::from(account.is_signer));
466 preimage.push(u8::from(account.is_writable));
467 }
468 Op::IngestInstructionDataSize => {
469 let data_len = u32::try_from(ix_data.len())
470 .map_err(|_| ActionHashError::InvalidInstructionData)?;
471
472 preimage.push(3);
473 preimage.extend_from_slice(&data_len.to_le_bytes());
474 }
475 Op::IngestSiblingInstruction {
476 relative_index,
477 offset,
478 len,
479 } => {
480 let sibling = resolve_sibling(siblings, relative_index)?;
481 let slice = instruction_slice(sibling.data, usize::from(offset), len)?;
482
483 preimage.push(4);
484 preimage.push(relative_index as u8);
485 preimage.extend_from_slice(&sibling.program_id.to_bytes());
486 preimage.push(offset);
487 preimage.push(len);
488 preimage.extend_from_slice(slice);
489 }
490 Op::IngestSiblingAccount {
491 relative_index,
492 index,
493 } => {
494 let sibling = resolve_sibling(siblings, relative_index)?;
495 let account = sibling
496 .accounts
497 .get(usize::from(index))
498 .ok_or(ActionHashError::AccountIndexOutOfBounds)?;
499
500 preimage.push(5);
501 preimage.push(relative_index as u8);
502 preimage.push(index);
503 preimage.extend_from_slice(&account.to_bytes());
504 }
505 }
506 }
507
508 Ok(hash(&preimage).to_bytes())
509}
510
511fn preimage_len(ops: &Ops) -> Result<usize, ActionHashError> {
516 let mut len = 32; for op in ops.iter()? {
518 len += match op? {
519 Op::Noop => 1,
520 Op::IngestInstruction { len, .. } => 4 + usize::from(len),
521 Op::IngestAccount { .. } => 36,
522 Op::IngestInstructionDataSize => 5,
523 Op::IngestSiblingInstruction { len, .. } => 36 + usize::from(len),
524 Op::IngestSiblingAccount { .. } => 35,
525 };
526 }
527
528 Ok(len)
529}
530
531fn instruction_slice(data: &[u8], offset: usize, len: u8) -> Result<&[u8], ActionHashError> {
532 let end = offset
533 .checked_add(usize::from(len))
534 .ok_or(ActionHashError::InstructionSliceOutOfBounds)?;
535 data.get(offset..end)
536 .ok_or(ActionHashError::InstructionSliceOutOfBounds)
537}
538
539fn resolve_sibling<'a, 'b>(
540 siblings: &'a [ResolvedSibling<'b>],
541 relative_index: i8,
542) -> Result<&'a ResolvedSibling<'b>, ActionHashError> {
543 siblings
544 .iter()
545 .find(|sibling| sibling.relative_index == relative_index)
546 .ok_or(ActionHashError::MissingSibling)
547}