1use thiserror::Error;
39
40#[derive(Debug, Error, PartialEq)]
42pub enum ExtrinsicError {
43 #[error("signing payload appears to be pre-hashed (32 bytes); cannot decode")]
47 PayloadHashed,
48
49 #[error("no valid extrinsic layout found ({actual} bytes); ensure it is a Substrate V4 signing payload and is not pre-hashed")]
54 InvalidLayout { actual: usize },
55}
56
57#[derive(Debug, Clone, PartialEq)]
61pub struct DecodedSignPayload {
62 pub genesis_hash: [u8; 32],
64 pub block_hash: [u8; 32],
67 pub spec_version: u32,
69 pub tx_version: u32,
71 pub metadata_hash: Option<[u8; 32]>,
73 pub call_data_and_extra: Vec<u8>,
79}
80
81const TAIL_BASE: usize = 4 + 4 + 32 + 32;
84const TAIL_META_NONE: usize = TAIL_BASE + 1;
86const TAIL_META_SOME: usize = TAIL_BASE + 1 + 32;
88
89pub fn decode_sign_payload(payload: &[u8]) -> Result<DecodedSignPayload, ExtrinsicError> {
102 let len = payload.len();
103
104 if len == 32 {
106 return Err(ExtrinsicError::PayloadHashed);
107 }
108
109 if len > TAIL_META_SOME {
112 let option_offset = len - TAIL_META_SOME + TAIL_BASE;
113 if payload[option_offset] == 0x01 {
114 let prefix = &payload[..len - TAIL_META_SOME];
115 if !prefix.is_empty() {
116 return Ok(decode_tail(payload, prefix, len - TAIL_META_SOME, true));
117 }
118 }
119 }
120
121 if len > TAIL_META_NONE {
124 let option_offset = len - TAIL_META_NONE + TAIL_BASE;
125 if payload[option_offset] == 0x00 {
126 let prefix = &payload[..len - TAIL_META_NONE];
127 if !prefix.is_empty() {
128 return Ok(decode_tail(payload, prefix, len - TAIL_META_NONE, false));
129 }
130 }
131 }
132
133 if len > TAIL_BASE {
135 let prefix = &payload[..len - TAIL_BASE];
136 if !prefix.is_empty() {
137 let tail_start = len - TAIL_BASE;
138 let spec_version =
140 u32::from_le_bytes(payload[tail_start..tail_start + 4].try_into().unwrap());
141 let tx_version =
142 u32::from_le_bytes(payload[tail_start + 4..tail_start + 8].try_into().unwrap());
143 let mut genesis_hash = [0u8; 32];
144 genesis_hash.copy_from_slice(&payload[tail_start + 8..tail_start + 40]);
145 let mut block_hash = [0u8; 32];
146 block_hash.copy_from_slice(&payload[tail_start + 40..tail_start + 72]);
147
148 return Ok(DecodedSignPayload {
149 genesis_hash,
150 block_hash,
151 spec_version,
152 tx_version,
153 metadata_hash: None,
154 call_data_and_extra: prefix.to_vec(),
155 });
156 }
157 }
158
159 Err(ExtrinsicError::InvalidLayout { actual: len })
160}
161
162fn decode_tail(
165 payload: &[u8],
166 prefix: &[u8],
167 tail_start: usize,
168 has_metadata_hash: bool,
169) -> DecodedSignPayload {
170 let spec_version = u32::from_le_bytes(payload[tail_start..tail_start + 4].try_into().unwrap());
172 let tx_version =
173 u32::from_le_bytes(payload[tail_start + 4..tail_start + 8].try_into().unwrap());
174 let mut genesis_hash = [0u8; 32];
175 genesis_hash.copy_from_slice(&payload[tail_start + 8..tail_start + 40]);
176 let mut block_hash = [0u8; 32];
177 block_hash.copy_from_slice(&payload[tail_start + 40..tail_start + 72]);
178
179 let metadata_hash = if has_metadata_hash {
180 let mut h = [0u8; 32];
182 h.copy_from_slice(&payload[tail_start + 73..tail_start + 105]);
183 Some(h)
184 } else {
185 None
186 };
187
188 DecodedSignPayload {
189 genesis_hash,
190 block_hash,
191 spec_version,
192 tx_version,
193 metadata_hash,
194 call_data_and_extra: prefix.to_vec(),
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 const SPEC: u32 = 1_002_004;
205 const TX: u32 = 26;
206 const GENESIS: [u8; 32] = [0x91; 32];
207 const BLOCK: [u8; 32] = [0xAB; 32];
208 const META_HASH: [u8; 32] = [0xCC; 32];
209 const PREFIX: &[u8] = &[0x05, 0x03, 0x00, 0x00, 0x00];
211
212 fn make_mode_none(prefix: &[u8]) -> Vec<u8> {
214 let mut v = prefix.to_vec();
215 v.extend_from_slice(&SPEC.to_le_bytes());
216 v.extend_from_slice(&TX.to_le_bytes());
217 v.extend_from_slice(&GENESIS);
218 v.extend_from_slice(&BLOCK);
219 v.push(0x00); v
221 }
222
223 fn make_mode_some(prefix: &[u8], hash: &[u8; 32]) -> Vec<u8> {
225 let mut v = prefix.to_vec();
226 v.extend_from_slice(&SPEC.to_le_bytes());
227 v.extend_from_slice(&TX.to_le_bytes());
228 v.extend_from_slice(&GENESIS);
229 v.extend_from_slice(&BLOCK);
230 v.push(0x01); v.extend_from_slice(hash);
232 v
233 }
234
235 fn make_legacy(prefix: &[u8]) -> Vec<u8> {
237 let mut v = prefix.to_vec();
238 v.extend_from_slice(&SPEC.to_le_bytes());
239 v.extend_from_slice(&TX.to_le_bytes());
240 v.extend_from_slice(&GENESIS);
241 v.extend_from_slice(&BLOCK);
242 v
243 }
244
245 #[test]
250 fn test_decodes_payload_with_metadata_hash_some() {
251 let payload = make_mode_some(PREFIX, &META_HASH);
252 let decoded = decode_sign_payload(&payload).unwrap();
253 assert_eq!(decoded.genesis_hash, GENESIS);
254 assert_eq!(decoded.block_hash, BLOCK);
255 assert_eq!(decoded.spec_version, SPEC);
256 assert_eq!(decoded.tx_version, TX);
257 assert_eq!(decoded.metadata_hash, Some(META_HASH));
258 assert_eq!(decoded.call_data_and_extra, PREFIX);
259 }
260
261 #[test]
262 fn test_decodes_payload_with_metadata_hash_none() {
263 let payload = make_mode_none(PREFIX);
264 let decoded = decode_sign_payload(&payload).unwrap();
265 assert_eq!(decoded.genesis_hash, GENESIS);
266 assert_eq!(decoded.block_hash, BLOCK);
267 assert_eq!(decoded.spec_version, SPEC);
268 assert_eq!(decoded.tx_version, TX);
269 assert_eq!(decoded.metadata_hash, None);
270 assert_eq!(decoded.call_data_and_extra, PREFIX);
271 }
272
273 #[test]
274 fn test_decodes_payload_without_checkmetadatahash_extension() {
275 let payload = make_legacy(PREFIX);
276 let decoded = decode_sign_payload(&payload).unwrap();
277 assert_eq!(decoded.genesis_hash, GENESIS);
278 assert_eq!(decoded.block_hash, BLOCK);
279 assert_eq!(decoded.spec_version, SPEC);
280 assert_eq!(decoded.tx_version, TX);
281 assert_eq!(decoded.metadata_hash, None);
282 assert_eq!(decoded.call_data_and_extra, PREFIX);
283 }
284
285 #[test]
286 fn test_genesis_hash_and_block_hash_are_distinct() {
287 let genesis = [0x91; 32];
288 let block = [0xAB; 32];
289 let mut v = PREFIX.to_vec();
290 v.extend_from_slice(&SPEC.to_le_bytes());
291 v.extend_from_slice(&TX.to_le_bytes());
292 v.extend_from_slice(&genesis);
293 v.extend_from_slice(&block);
294 v.push(0x00);
295 let decoded = decode_sign_payload(&v).unwrap();
296 assert_eq!(decoded.genesis_hash, genesis);
297 assert_eq!(decoded.block_hash, block);
298 assert_ne!(decoded.genesis_hash, decoded.block_hash);
299 }
300
301 #[test]
302 fn test_spec_and_tx_version_are_little_endian() {
303 let payload = make_mode_none(PREFIX);
304 let decoded = decode_sign_payload(&payload).unwrap();
305 assert_eq!(decoded.spec_version, 1_002_004);
306 assert_eq!(decoded.tx_version, 26);
307 }
308
309 #[test]
310 fn test_decodes_minimum_valid_payload_mode_none() {
311 let payload = make_mode_none(&[0xFF]);
313 assert_eq!(payload.len(), 74);
314 let decoded = decode_sign_payload(&payload).unwrap();
315 assert_eq!(decoded.call_data_and_extra, vec![0xFF]);
316 }
317
318 #[test]
319 fn test_decodes_minimum_valid_payload_legacy() {
320 let payload = make_legacy(&[0xFF]);
322 assert_eq!(payload.len(), 73);
323 let decoded = decode_sign_payload(&payload).unwrap();
324 assert_eq!(decoded.call_data_and_extra, vec![0xFF]);
325 }
326
327 #[test]
328 fn test_call_data_and_extra_is_exact_prefix() {
329 let prefix = vec![0x01, 0x02, 0x03, 0x04, 0x05];
330 let payload = make_mode_none(&prefix);
331 let decoded = decode_sign_payload(&payload).unwrap();
332 assert_eq!(decoded.call_data_and_extra, prefix);
333 }
334
335 #[test]
336 fn test_decodes_payload_with_large_call_data() {
337 let prefix = vec![0xAA; 300];
338 let payload = make_mode_none(&prefix);
339 assert!(payload.len() > 256);
340 let decoded = decode_sign_payload(&payload).unwrap();
341 assert_eq!(decoded.call_data_and_extra.len(), 300);
342 assert_eq!(decoded.genesis_hash, GENESIS);
343 }
344
345 #[test]
346 fn test_prefers_metadata_hash_some_over_none_when_ambiguous() {
347 let payload = make_mode_some(PREFIX, &META_HASH);
350 let decoded = decode_sign_payload(&payload).unwrap();
351 assert_eq!(decoded.metadata_hash, Some(META_HASH));
352 }
353
354 #[test]
355 fn test_heuristic_limitation_mode_none_with_spec_version_lsb_0x01() {
356 let spec_with_lsb_01: u32 = 0x00_00_01_01; let mut v = vec![0x05; 34]; v.extend_from_slice(&spec_with_lsb_01.to_le_bytes());
366 v.extend_from_slice(&TX.to_le_bytes());
367 v.extend_from_slice(&GENESIS);
368 v.extend_from_slice(&BLOCK);
369 v.push(0x00); let result = decode_sign_payload(&v);
371 assert!(
374 result.is_ok(),
375 "heuristic edge case must not error: {result:?}"
376 );
377 }
378
379 #[test]
384 fn test_rejects_empty_payload() {
385 let result = decode_sign_payload(&[]);
386 assert_eq!(result, Err(ExtrinsicError::InvalidLayout { actual: 0 }));
387 }
388
389 #[test]
390 fn test_rejects_payload_too_short() {
391 let result = decode_sign_payload(&[0u8; 71]);
392 assert_eq!(result, Err(ExtrinsicError::InvalidLayout { actual: 71 }));
393 }
394
395 #[test]
396 fn test_rejects_hashed_payload() {
397 let result = decode_sign_payload(&[0xAA; 32]);
398 assert_eq!(result, Err(ExtrinsicError::PayloadHashed));
399 }
400
401 #[test]
402 fn test_rejects_payload_with_empty_prefix() {
403 let result = decode_sign_payload(&[0u8; 72]);
407 assert_eq!(result, Err(ExtrinsicError::InvalidLayout { actual: 72 }));
408 }
409
410 #[test]
415 fn test_golden_polkadot_like_payload_mode_none() {
416 let payload = make_mode_none(PREFIX);
417 let decoded = decode_sign_payload(&payload).unwrap();
418 assert_eq!(decoded.spec_version, 1_002_004);
420 assert_eq!(decoded.tx_version, 26);
421 assert_eq!(decoded.genesis_hash, [0x91; 32]);
422 assert_eq!(decoded.block_hash, [0xAB; 32]);
423 assert_eq!(decoded.metadata_hash, None);
424 assert_eq!(
425 decoded.call_data_and_extra,
426 vec![0x05, 0x03, 0x00, 0x00, 0x00]
427 );
428 }
429
430 #[test]
431 fn test_golden_polkadot_like_payload_mode_some() {
432 let payload = make_mode_some(PREFIX, &META_HASH);
433 let decoded = decode_sign_payload(&payload).unwrap();
434 assert_eq!(decoded.spec_version, 1_002_004);
435 assert_eq!(decoded.tx_version, 26);
436 assert_eq!(decoded.genesis_hash, [0x91; 32]);
437 assert_eq!(decoded.block_hash, [0xAB; 32]);
438 assert_eq!(decoded.metadata_hash, Some([0xCC; 32]));
439 assert_eq!(
440 decoded.call_data_and_extra,
441 vec![0x05, 0x03, 0x00, 0x00, 0x00]
442 );
443 }
444
445 #[test]
446 fn test_golden_legacy_payload() {
447 let payload = make_legacy(&[0xFF]);
448 let decoded = decode_sign_payload(&payload).unwrap();
449 assert_eq!(decoded.spec_version, SPEC);
450 assert_eq!(decoded.tx_version, TX);
451 assert_eq!(decoded.genesis_hash, GENESIS);
452 assert_eq!(decoded.block_hash, BLOCK);
453 assert_eq!(decoded.metadata_hash, None);
454 assert_eq!(decoded.call_data_and_extra, vec![0xFF]);
455 }
456
457 #[test]
458 fn test_metadata_hash_all_zeros_is_some() {
459 let zero_hash = [0x00u8; 32];
462 let payload = make_mode_some(PREFIX, &zero_hash);
463 let decoded = decode_sign_payload(&payload).unwrap();
464 assert_eq!(decoded.metadata_hash, Some(zero_hash));
465 }
466}