use thiserror::Error;
#[derive(Debug, Error, PartialEq)]
pub enum ExtrinsicError {
#[error("signing payload appears to be pre-hashed (32 bytes); cannot decode")]
PayloadHashed,
#[error("no valid extrinsic layout found ({actual} bytes); ensure it is a Substrate V4 signing payload and is not pre-hashed")]
InvalidLayout { actual: usize },
}
#[derive(Debug, Clone, PartialEq)]
pub struct DecodedSignPayload {
pub genesis_hash: [u8; 32],
pub block_hash: [u8; 32],
pub spec_version: u32,
pub tx_version: u32,
pub metadata_hash: Option<[u8; 32]>,
pub call_data_and_extra: Vec<u8>,
}
const TAIL_BASE: usize = 4 + 4 + 32 + 32;
const TAIL_META_NONE: usize = TAIL_BASE + 1;
const TAIL_META_SOME: usize = TAIL_BASE + 1 + 32;
pub fn decode_sign_payload(payload: &[u8]) -> Result<DecodedSignPayload, ExtrinsicError> {
let len = payload.len();
if len == 32 {
return Err(ExtrinsicError::PayloadHashed);
}
if len > TAIL_META_SOME {
let option_offset = len - TAIL_META_SOME + TAIL_BASE;
if payload[option_offset] == 0x01 {
let prefix = &payload[..len - TAIL_META_SOME];
if !prefix.is_empty() {
return Ok(decode_tail(payload, prefix, len - TAIL_META_SOME, true));
}
}
}
if len > TAIL_META_NONE {
let option_offset = len - TAIL_META_NONE + TAIL_BASE;
if payload[option_offset] == 0x00 {
let prefix = &payload[..len - TAIL_META_NONE];
if !prefix.is_empty() {
return Ok(decode_tail(payload, prefix, len - TAIL_META_NONE, false));
}
}
}
if len > TAIL_BASE {
let prefix = &payload[..len - TAIL_BASE];
if !prefix.is_empty() {
let tail_start = len - TAIL_BASE;
let spec_version =
u32::from_le_bytes(payload[tail_start..tail_start + 4].try_into().unwrap());
let tx_version =
u32::from_le_bytes(payload[tail_start + 4..tail_start + 8].try_into().unwrap());
let mut genesis_hash = [0u8; 32];
genesis_hash.copy_from_slice(&payload[tail_start + 8..tail_start + 40]);
let mut block_hash = [0u8; 32];
block_hash.copy_from_slice(&payload[tail_start + 40..tail_start + 72]);
return Ok(DecodedSignPayload {
genesis_hash,
block_hash,
spec_version,
tx_version,
metadata_hash: None,
call_data_and_extra: prefix.to_vec(),
});
}
}
Err(ExtrinsicError::InvalidLayout { actual: len })
}
fn decode_tail(
payload: &[u8],
prefix: &[u8],
tail_start: usize,
has_metadata_hash: bool,
) -> DecodedSignPayload {
let spec_version = u32::from_le_bytes(payload[tail_start..tail_start + 4].try_into().unwrap());
let tx_version =
u32::from_le_bytes(payload[tail_start + 4..tail_start + 8].try_into().unwrap());
let mut genesis_hash = [0u8; 32];
genesis_hash.copy_from_slice(&payload[tail_start + 8..tail_start + 40]);
let mut block_hash = [0u8; 32];
block_hash.copy_from_slice(&payload[tail_start + 40..tail_start + 72]);
let metadata_hash = if has_metadata_hash {
let mut h = [0u8; 32];
h.copy_from_slice(&payload[tail_start + 73..tail_start + 105]);
Some(h)
} else {
None
};
DecodedSignPayload {
genesis_hash,
block_hash,
spec_version,
tx_version,
metadata_hash,
call_data_and_extra: prefix.to_vec(),
}
}
#[cfg(test)]
mod tests {
use super::*;
const SPEC: u32 = 1_002_004;
const TX: u32 = 26;
const GENESIS: [u8; 32] = [0x91; 32];
const BLOCK: [u8; 32] = [0xAB; 32];
const META_HASH: [u8; 32] = [0xCC; 32];
const PREFIX: &[u8] = &[0x05, 0x03, 0x00, 0x00, 0x00];
fn make_mode_none(prefix: &[u8]) -> Vec<u8> {
let mut v = prefix.to_vec();
v.extend_from_slice(&SPEC.to_le_bytes());
v.extend_from_slice(&TX.to_le_bytes());
v.extend_from_slice(&GENESIS);
v.extend_from_slice(&BLOCK);
v.push(0x00); v
}
fn make_mode_some(prefix: &[u8], hash: &[u8; 32]) -> Vec<u8> {
let mut v = prefix.to_vec();
v.extend_from_slice(&SPEC.to_le_bytes());
v.extend_from_slice(&TX.to_le_bytes());
v.extend_from_slice(&GENESIS);
v.extend_from_slice(&BLOCK);
v.push(0x01); v.extend_from_slice(hash);
v
}
fn make_legacy(prefix: &[u8]) -> Vec<u8> {
let mut v = prefix.to_vec();
v.extend_from_slice(&SPEC.to_le_bytes());
v.extend_from_slice(&TX.to_le_bytes());
v.extend_from_slice(&GENESIS);
v.extend_from_slice(&BLOCK);
v
}
#[test]
fn test_decodes_payload_with_metadata_hash_some() {
let payload = make_mode_some(PREFIX, &META_HASH);
let decoded = decode_sign_payload(&payload).unwrap();
assert_eq!(decoded.genesis_hash, GENESIS);
assert_eq!(decoded.block_hash, BLOCK);
assert_eq!(decoded.spec_version, SPEC);
assert_eq!(decoded.tx_version, TX);
assert_eq!(decoded.metadata_hash, Some(META_HASH));
assert_eq!(decoded.call_data_and_extra, PREFIX);
}
#[test]
fn test_decodes_payload_with_metadata_hash_none() {
let payload = make_mode_none(PREFIX);
let decoded = decode_sign_payload(&payload).unwrap();
assert_eq!(decoded.genesis_hash, GENESIS);
assert_eq!(decoded.block_hash, BLOCK);
assert_eq!(decoded.spec_version, SPEC);
assert_eq!(decoded.tx_version, TX);
assert_eq!(decoded.metadata_hash, None);
assert_eq!(decoded.call_data_and_extra, PREFIX);
}
#[test]
fn test_decodes_payload_without_checkmetadatahash_extension() {
let payload = make_legacy(PREFIX);
let decoded = decode_sign_payload(&payload).unwrap();
assert_eq!(decoded.genesis_hash, GENESIS);
assert_eq!(decoded.block_hash, BLOCK);
assert_eq!(decoded.spec_version, SPEC);
assert_eq!(decoded.tx_version, TX);
assert_eq!(decoded.metadata_hash, None);
assert_eq!(decoded.call_data_and_extra, PREFIX);
}
#[test]
fn test_genesis_hash_and_block_hash_are_distinct() {
let genesis = [0x91; 32];
let block = [0xAB; 32];
let mut v = PREFIX.to_vec();
v.extend_from_slice(&SPEC.to_le_bytes());
v.extend_from_slice(&TX.to_le_bytes());
v.extend_from_slice(&genesis);
v.extend_from_slice(&block);
v.push(0x00);
let decoded = decode_sign_payload(&v).unwrap();
assert_eq!(decoded.genesis_hash, genesis);
assert_eq!(decoded.block_hash, block);
assert_ne!(decoded.genesis_hash, decoded.block_hash);
}
#[test]
fn test_spec_and_tx_version_are_little_endian() {
let payload = make_mode_none(PREFIX);
let decoded = decode_sign_payload(&payload).unwrap();
assert_eq!(decoded.spec_version, 1_002_004);
assert_eq!(decoded.tx_version, 26);
}
#[test]
fn test_decodes_minimum_valid_payload_mode_none() {
let payload = make_mode_none(&[0xFF]);
assert_eq!(payload.len(), 74);
let decoded = decode_sign_payload(&payload).unwrap();
assert_eq!(decoded.call_data_and_extra, vec![0xFF]);
}
#[test]
fn test_decodes_minimum_valid_payload_legacy() {
let payload = make_legacy(&[0xFF]);
assert_eq!(payload.len(), 73);
let decoded = decode_sign_payload(&payload).unwrap();
assert_eq!(decoded.call_data_and_extra, vec![0xFF]);
}
#[test]
fn test_call_data_and_extra_is_exact_prefix() {
let prefix = vec![0x01, 0x02, 0x03, 0x04, 0x05];
let payload = make_mode_none(&prefix);
let decoded = decode_sign_payload(&payload).unwrap();
assert_eq!(decoded.call_data_and_extra, prefix);
}
#[test]
fn test_decodes_payload_with_large_call_data() {
let prefix = vec![0xAA; 300];
let payload = make_mode_none(&prefix);
assert!(payload.len() > 256);
let decoded = decode_sign_payload(&payload).unwrap();
assert_eq!(decoded.call_data_and_extra.len(), 300);
assert_eq!(decoded.genesis_hash, GENESIS);
}
#[test]
fn test_prefers_metadata_hash_some_over_none_when_ambiguous() {
let payload = make_mode_some(PREFIX, &META_HASH);
let decoded = decode_sign_payload(&payload).unwrap();
assert_eq!(decoded.metadata_hash, Some(META_HASH));
}
#[test]
fn test_heuristic_limitation_mode_none_with_spec_version_lsb_0x01() {
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());
v.extend_from_slice(&TX.to_le_bytes());
v.extend_from_slice(&GENESIS);
v.extend_from_slice(&BLOCK);
v.push(0x00); let result = decode_sign_payload(&v);
assert!(
result.is_ok(),
"heuristic edge case must not error: {result:?}"
);
}
#[test]
fn test_rejects_empty_payload() {
let result = decode_sign_payload(&[]);
assert_eq!(result, Err(ExtrinsicError::InvalidLayout { actual: 0 }));
}
#[test]
fn test_rejects_payload_too_short() {
let result = decode_sign_payload(&[0u8; 71]);
assert_eq!(result, Err(ExtrinsicError::InvalidLayout { actual: 71 }));
}
#[test]
fn test_rejects_hashed_payload() {
let result = decode_sign_payload(&[0xAA; 32]);
assert_eq!(result, Err(ExtrinsicError::PayloadHashed));
}
#[test]
fn test_rejects_payload_with_empty_prefix() {
let result = decode_sign_payload(&[0u8; 72]);
assert_eq!(result, Err(ExtrinsicError::InvalidLayout { actual: 72 }));
}
#[test]
fn test_golden_polkadot_like_payload_mode_none() {
let payload = make_mode_none(PREFIX);
let decoded = decode_sign_payload(&payload).unwrap();
assert_eq!(decoded.spec_version, 1_002_004);
assert_eq!(decoded.tx_version, 26);
assert_eq!(decoded.genesis_hash, [0x91; 32]);
assert_eq!(decoded.block_hash, [0xAB; 32]);
assert_eq!(decoded.metadata_hash, None);
assert_eq!(
decoded.call_data_and_extra,
vec![0x05, 0x03, 0x00, 0x00, 0x00]
);
}
#[test]
fn test_golden_polkadot_like_payload_mode_some() {
let payload = make_mode_some(PREFIX, &META_HASH);
let decoded = decode_sign_payload(&payload).unwrap();
assert_eq!(decoded.spec_version, 1_002_004);
assert_eq!(decoded.tx_version, 26);
assert_eq!(decoded.genesis_hash, [0x91; 32]);
assert_eq!(decoded.block_hash, [0xAB; 32]);
assert_eq!(decoded.metadata_hash, Some([0xCC; 32]));
assert_eq!(
decoded.call_data_and_extra,
vec![0x05, 0x03, 0x00, 0x00, 0x00]
);
}
#[test]
fn test_golden_legacy_payload() {
let payload = make_legacy(&[0xFF]);
let decoded = decode_sign_payload(&payload).unwrap();
assert_eq!(decoded.spec_version, SPEC);
assert_eq!(decoded.tx_version, TX);
assert_eq!(decoded.genesis_hash, GENESIS);
assert_eq!(decoded.block_hash, BLOCK);
assert_eq!(decoded.metadata_hash, None);
assert_eq!(decoded.call_data_and_extra, vec![0xFF]);
}
#[test]
fn test_metadata_hash_all_zeros_is_some() {
let zero_hash = [0x00u8; 32];
let payload = make_mode_some(PREFIX, &zero_hash);
let decoded = decode_sign_payload(&payload).unwrap();
assert_eq!(decoded.metadata_hash, Some(zero_hash));
}
}