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