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}
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq, codama_macros::CodamaType, SchemaWrite, SchemaRead)]
25#[wincode(tag_encoding = "u8")]
26pub enum Op {
27 #[wincode(tag = 0)]
28 Noop,
29 #[wincode(tag = 1)]
30 IngestInstruction { offset: u16, len: u8 },
31 #[wincode(tag = 2)]
32 IngestAccount { index: u8 },
33 #[wincode(tag = 3)]
34 IngestInstructionDataSize,
35}
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq, codama_macros::CodamaType, SchemaWrite, SchemaRead)]
38#[repr(C)]
39pub struct StoredOp {
40 pub kind: u8,
41 pub arg0: u8,
42 pub arg1: u8,
43 pub arg2: u8,
44}
45
46impl StoredOp {
47 pub const fn noop() -> Self {
48 Self {
49 kind: 0,
50 arg0: 0,
51 arg1: 0,
52 arg2: 0,
53 }
54 }
55
56 fn try_to_op(self) -> Result<Op, ActionHashError> {
57 let op = match self.kind {
58 0 if self.arg0 == 0 && self.arg1 == 0 && self.arg2 == 0 => Op::Noop,
59 1 => Op::IngestInstruction {
60 offset: u16::from_le_bytes([self.arg1, self.arg2]),
61 len: self.arg0,
62 },
63 2 if self.arg1 == 0 && self.arg2 == 0 => Op::IngestAccount { index: self.arg0 },
64 3 if self.arg0 == 0 && self.arg1 == 0 && self.arg2 == 0 => {
65 Op::IngestInstructionDataSize
66 }
67 _ => return Err(ActionHashError::InvalidOp),
68 };
69
70 Ok(op)
71 }
72}
73
74impl Default for StoredOp {
75 fn default() -> Self {
76 Self::noop()
77 }
78}
79
80impl From<Op> for StoredOp {
81 fn from(op: Op) -> Self {
82 match op {
83 Op::Noop => Self::noop(),
84 Op::IngestInstruction { offset, len } => {
85 let [arg1, arg2] = offset.to_le_bytes();
86 Self {
87 kind: 1,
88 arg0: len,
89 arg1,
90 arg2,
91 }
92 }
93 Op::IngestAccount { index } => Self {
94 kind: 2,
95 arg0: index,
96 arg1: 0,
97 arg2: 0,
98 },
99 Op::IngestInstructionDataSize => Self {
100 kind: 3,
101 arg0: 0,
102 arg1: 0,
103 arg2: 0,
104 },
105 }
106 }
107}
108
109#[derive(Clone, Copy, Debug, Eq, PartialEq, codama_macros::CodamaType, SchemaWrite, SchemaRead)]
110#[repr(C)]
111pub struct Ops {
112 pub ops: [StoredOp; 32],
113 pub ops_len: u8,
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use wincode::serialize;
120
121 #[test]
122 fn stored_ops_are_compact() {
123 assert_eq!(core::mem::size_of::<StoredOp>(), 4);
124 assert_eq!(core::mem::size_of::<Ops>(), MAX_ACTION_OPS * 4 + 1);
125 assert_eq!(serialize(&StoredOp::noop()).unwrap().len(), 4);
126 assert_eq!(
127 serialize(&Ops::empty()).unwrap().len(),
128 MAX_ACTION_OPS * 4 + 1
129 );
130 }
131
132 #[test]
133 fn ops_new_enforces_capacity() {
134 let ops = vec![Op::Noop; MAX_ACTION_OPS + 1];
135
136 assert_eq!(Ops::new(ops), Err(ActionHashError::TooManyOps));
137 }
138
139 #[test]
140 fn ops_round_trip_logical_ops() {
141 let ops = Ops::new([
142 Op::Noop,
143 Op::IngestInstruction {
144 offset: 513,
145 len: 7,
146 },
147 Op::IngestAccount { index: 9 },
148 Op::IngestInstructionDataSize,
149 ])
150 .unwrap();
151 let decoded = ops.iter().unwrap().collect::<Result<Vec<_>, _>>().unwrap();
152
153 assert_eq!(
154 decoded,
155 vec![
156 Op::Noop,
157 Op::IngestInstruction {
158 offset: 513,
159 len: 7,
160 },
161 Op::IngestAccount { index: 9 },
162 Op::IngestInstructionDataSize,
163 ]
164 );
165 }
166
167 #[test]
168 fn hash_rejects_corrupt_stored_ops() {
169 let program_id = Pubkey::new_unique();
170 let account_metas = [];
171 let ix_data = [];
172
173 let mut too_many = Ops::empty();
174 too_many.ops_len = u8::try_from(MAX_ACTION_OPS + 1).unwrap();
175 assert_eq!(
176 compute_action_hash_from_metas(&program_id, &too_many, &account_metas, &ix_data),
177 Err(ActionHashError::TooManyOps)
178 );
179
180 let mut invalid_kind = Ops::empty();
181 invalid_kind.ops[0] = StoredOp {
182 kind: 255,
183 arg0: 0,
184 arg1: 0,
185 arg2: 0,
186 };
187 invalid_kind.ops_len = 1;
188 assert_eq!(
189 compute_action_hash_from_metas(&program_id, &invalid_kind, &account_metas, &ix_data),
190 Err(ActionHashError::InvalidOp)
191 );
192
193 let mut non_canonical = Ops::empty();
194 non_canonical.ops[0] = StoredOp {
195 kind: 0,
196 arg0: 1,
197 arg1: 0,
198 arg2: 0,
199 };
200 non_canonical.ops_len = 1;
201 assert_eq!(
202 compute_action_hash_from_metas(&program_id, &non_canonical, &account_metas, &ix_data),
203 Err(ActionHashError::InvalidOp)
204 );
205 }
206}
207
208impl Ops {
209 pub const fn empty() -> Self {
210 Self {
211 ops: [StoredOp::noop(); MAX_ACTION_OPS],
212 ops_len: 0,
213 }
214 }
215
216 pub fn new(ops: impl IntoIterator<Item = Op>) -> Result<Self, ActionHashError> {
217 let mut stored_ops = Self::empty();
218
219 for (index, op) in ops.into_iter().enumerate() {
220 if index >= MAX_ACTION_OPS {
221 return Err(ActionHashError::TooManyOps);
222 }
223
224 stored_ops.ops[index] = StoredOp::from(op);
225 stored_ops.ops_len =
226 u8::try_from(index + 1).map_err(|_| ActionHashError::InvalidInstructionData)?;
227 }
228
229 Ok(stored_ops)
230 }
231
232 pub fn len(&self) -> Result<usize, ActionHashError> {
233 let len = usize::from(self.ops_len);
234 if len > MAX_ACTION_OPS {
235 return Err(ActionHashError::TooManyOps);
236 }
237
238 Ok(len)
239 }
240
241 pub fn is_empty(&self) -> Result<bool, ActionHashError> {
242 Ok(self.len()? == 0)
243 }
244
245 pub fn iter(
246 &self,
247 ) -> Result<impl Iterator<Item = Result<Op, ActionHashError>> + '_, ActionHashError> {
248 let len = self.len()?;
249 Ok(self.ops[..len].iter().copied().map(StoredOp::try_to_op))
250 }
251}
252
253impl Default for Ops {
254 fn default() -> Self {
255 Self::empty()
256 }
257}
258
259#[derive(Clone, Copy, Debug, Eq, PartialEq)]
260pub enum ActionHashError {
261 InvalidOp,
262 TooManyOps,
263 InstructionSliceOutOfBounds,
264 AccountIndexOutOfBounds,
265 InvalidInstructionData,
266}
267
268pub fn compute_action_hash_from_metas(
269 program_id: &Pubkey,
270 ops: &Ops,
271 accounts: &[AccountMeta],
272 ix_data: &[u8],
273) -> Result<[u8; 32], ActionHashError> {
274 let mut chunks = vec![program_id.to_bytes().to_vec()];
275
276 for op in ops.iter()? {
277 match op? {
278 Op::Noop => chunks.push(vec![0]),
279 Op::IngestInstruction { offset, len } => {
280 let start = usize::from(offset);
281 let length = usize::from(len);
282 let end = start
283 .checked_add(length)
284 .ok_or(ActionHashError::InstructionSliceOutOfBounds)?;
285 let slice = ix_data
286 .get(start..end)
287 .ok_or(ActionHashError::InstructionSliceOutOfBounds)?;
288
289 chunks.push(vec![1]);
290 chunks.push(offset.to_le_bytes().to_vec());
291 chunks.push(vec![len]);
292 chunks.push(slice.to_vec());
293 }
294 Op::IngestAccount { index } => {
295 let account = accounts
296 .get(usize::from(index))
297 .ok_or(ActionHashError::AccountIndexOutOfBounds)?;
298
299 chunks.push(vec![2]);
300 chunks.push(vec![index]);
301 chunks.push(account.pubkey.to_bytes().to_vec());
302 chunks.push(vec![u8::from(account.is_signer)]);
303 chunks.push(vec![u8::from(account.is_writable)]);
304 }
305 Op::IngestInstructionDataSize => {
306 let data_len = u32::try_from(ix_data.len())
307 .map_err(|_| ActionHashError::InvalidInstructionData)?;
308
309 chunks.push(vec![3]);
310 chunks.push(data_len.to_le_bytes().to_vec());
311 }
312 }
313 }
314
315 let refs = chunks.iter().map(Vec::as_slice).collect::<Vec<_>>();
316 Ok(hashv(&refs).to_bytes())
317}