use cratefield_signer::{EvmTransaction, SignerError};
const TYPE_EIP1559: u8 = 0x02;
pub fn unsigned_transaction(tx: &EvmTransaction) -> Result<Vec<u8>, SignerError> {
let to = match &tx.to {
Some(address) => {
let address = crate::policy::validate_address(address)?;
hex::decode(&address[2..])
.map_err(|_| SignerError::Invalid("an address must be hex".to_owned()))?
}
None => Vec::new(),
};
let mut out = vec![TYPE_EIP1559];
let payload = out.len();
encode_uint(&mut out, u128::from(tx.chain_id));
encode_uint(&mut out, u128::from(tx.nonce));
encode_uint(&mut out, tx.max_priority_fee_per_gas);
encode_uint(&mut out, tx.max_fee_per_gas);
encode_uint(&mut out, tx.gas_limit);
encode_bytes(&mut out, &to);
encode_uint(&mut out, tx.value);
encode_bytes(&mut out, &tx.data);
encode_list(&mut out, &[]);
wrap_list(&mut out, payload);
Ok(out)
}
pub fn decode_signed(signed_hex: &str) -> Result<([u8; 32], [u8; 32], u8), SignerError> {
let malformed = |detail: String| {
SignerError::Provider(format!(
"turnkey returned an unparsable transaction: {detail}"
))
};
let bytes = hex::decode(signed_hex.strip_prefix("0x").unwrap_or(signed_hex))
.map_err(|err| malformed(format!("not hex: {err}")))?;
if bytes.first() != Some(&TYPE_EIP1559) {
return Err(malformed(format!(
"expected the EIP-1559 type byte {TYPE_EIP1559:#04x}, found {:?}",
bytes.first()
)));
}
let outer = rlp_items(&bytes[1..]).map_err(malformed)?;
let [RlpItem::List(payload)] = outer.as_slice() else {
return Err(malformed("expected one transaction list".to_owned()));
};
let items = rlp_items(payload).map_err(malformed)?;
let [.., parity, r, s] = items.as_slice() else {
return Err(malformed(format!(
"expected twelve fields, found {}",
items.len()
)));
};
let RlpItem::Bytes(parity) = parity else {
return Err(malformed("the y-parity is not a byte string".to_owned()));
};
let RlpItem::Bytes(r) = r else {
return Err(malformed("`r` is not a byte string".to_owned()));
};
let RlpItem::Bytes(s) = s else {
return Err(malformed("`s` is not a byte string".to_owned()));
};
let r: [u8; 32] =
left_pad(r).ok_or_else(|| malformed("`r` is longer than 32 bytes".to_owned()))?;
let s: [u8; 32] =
left_pad(s).ok_or_else(|| malformed("`s` is longer than 32 bytes".to_owned()))?;
let y_parity = parity.first().copied().unwrap_or(0);
if y_parity > 1 {
return Err(malformed(format!("the y-parity is {y_parity}")));
}
Ok((r, s, 27 + y_parity))
}
#[derive(Debug, PartialEq, Eq)]
enum RlpItem {
Bytes(Vec<u8>),
List(Vec<u8>),
}
fn rlp_items(bytes: &[u8]) -> Result<Vec<RlpItem>, String> {
let mut items = Vec::new();
let mut rest = bytes;
while !rest.is_empty() {
let (item, tail) = rlp_item(rest)?;
items.push(item);
rest = tail;
}
Ok(items)
}
fn rlp_item(bytes: &[u8]) -> Result<(RlpItem, &[u8]), String> {
let (&first, rest) = bytes.split_first().ok_or("empty RLP stream")?;
let (length, rest) = match first {
0x00..=0x7f => return Ok((RlpItem::Bytes(vec![first]), rest)),
0x80..=0xb7 => {
let len = usize::from(first - 0x80);
(len, rest)
}
0xb8..=0xbf => {
let len_bytes = usize::from(first - 0xb7);
let slice = rest.get(..len_bytes).ok_or("truncated length")?;
let len = be_usize(slice).ok_or("a length past usize")?;
(len, &rest[len_bytes..])
}
0xc0..=0xf7 => {
let len = usize::from(first - 0xc0);
return list_payload(rest, len);
}
0xf8..=0xff => {
let len_bytes = usize::from(first - 0xf7);
let slice = rest.get(..len_bytes).ok_or("truncated length")?;
let len = be_usize(slice).ok_or("a length past usize")?;
let rest = &rest[len_bytes..];
return list_payload(rest, len);
}
};
let payload = rest.get(..length).ok_or("truncated item")?;
Ok((RlpItem::Bytes(payload.to_vec()), &rest[length..]))
}
fn list_payload(rest: &[u8], len: usize) -> Result<(RlpItem, &[u8]), String> {
let payload = rest.get(..len).ok_or("truncated list")?;
rlp_items(payload)?;
Ok((RlpItem::List(payload.to_vec()), &rest[len..]))
}
fn be_usize(bytes: &[u8]) -> Option<usize> {
if bytes.len() > usize::BITS as usize / 8 {
return None;
}
let mut out = 0_usize;
for byte in bytes {
out = (out << 8) | usize::from(*byte);
}
Some(out)
}
fn left_pad(bytes: &[u8]) -> Option<[u8; 32]> {
if bytes.len() > 32 {
return None;
}
let mut out = [0_u8; 32];
out[32 - bytes.len()..].copy_from_slice(bytes);
Some(out)
}
fn encode_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
if bytes.len() == 1 && bytes[0] < 0x80 {
out.push(bytes[0]);
return;
}
length_prefix(out, bytes.len(), 0x80, 0xb7);
out.extend_from_slice(bytes);
}
fn encode_uint(out: &mut Vec<u8>, value: u128) {
let be = value.to_be_bytes();
let first = be.iter().position(|&byte| byte != 0).unwrap_or(be.len());
encode_bytes(out, &be[first..]);
}
fn encode_list(out: &mut Vec<u8>, payload: &[u8]) {
length_prefix(out, payload.len(), 0xc0, 0xf7);
out.extend_from_slice(payload);
}
fn wrap_list(out: &mut Vec<u8>, payload_start: usize) {
let payload = out.split_off(payload_start);
length_prefix(out, payload.len(), 0xc0, 0xf7);
out.extend_from_slice(&payload);
}
fn length_prefix(out: &mut Vec<u8>, len: usize, short_offset: u8, long_offset: u8) {
if len <= 55 {
out.push(short_offset + u8::try_from(len).expect("a short item is at most 55 bytes"));
} else {
let be = (len as u64).to_be_bytes();
let first = be.iter().position(|&byte| byte != 0).unwrap_or(7);
let len_bytes = &be[first..];
out.push(long_offset + u8::try_from(len_bytes.len()).expect("the length is at most 8"));
out.extend_from_slice(len_bytes);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tx() -> EvmTransaction {
EvmTransaction {
chain_id: 4,
nonce: 819,
max_priority_fee_per_gas: 75_853,
max_fee_per_gas: 121_212,
gas_limit: 35_552,
to: Some("0x000000000000000000000000000000000000aaaa".to_owned()),
value: 43_203_529,
data: Vec::new(),
}
}
#[test]
fn the_unsigned_encoding_is_the_ports_signing_preimage() {
use sha3::Digest as _;
let unsigned = unsigned_transaction(&tx()).expect("encodes");
assert_eq!(unsigned[0], TYPE_EIP1559);
let keccak: [u8; 32] = sha3::Keccak256::digest(&unsigned).into();
assert_eq!(
hex::encode(keccak),
hex::encode(tx().signing_hash().expect("the port hashes")),
"this crate's unsigned bytes hash to exactly the port's signing hash"
);
}
#[test]
fn contract_creation_and_large_fields_still_match_the_preimage() {
use sha3::Digest as _;
let tx = EvmTransaction {
to: None,
value: u128::MAX,
data: vec![0xde, 0xad, 0xbe, 0xef],
nonce: u64::MAX,
..tx()
};
let unsigned = unsigned_transaction(&tx).expect("encodes");
let keccak: [u8; 32] = sha3::Keccak256::digest(&unsigned).into();
assert_eq!(
hex::encode(keccak),
hex::encode(tx.signing_hash().expect("the port hashes")),
);
}
#[test]
fn a_signed_transaction_decodes_to_r_s_and_v() {
let unsigned = unsigned_transaction(&tx()).expect("encodes");
assert!(unsigned[1] < 0xf8, "the fixture list is short-form");
let mut body = unsigned[2..].to_vec();
encode_uint(&mut body, 1); encode_uint(&mut body, 0x8000_0000_0000_0001); encode_uint(&mut body, 0x00c0_ffee); let mut signed = vec![TYPE_EIP1559];
signed.extend_from_slice(&body);
wrap_list(&mut signed, 1);
let hex_signed = format!("0x{}", hex::encode(&signed));
let (r, s, v) = decode_signed(&hex_signed).expect("decodes");
let mut expected_r = [0_u8; 32];
expected_r[24..].copy_from_slice(&0x8000_0000_0000_0001_u64.to_be_bytes());
assert_eq!(r, expected_r);
assert_eq!(s[28..], [0x00, 0xc0, 0xff, 0xee]);
assert_eq!(v, 28);
}
#[test]
fn junk_answers_are_provider_errors() {
for junk in ["not hex", "0x00", "0x01c0"] {
let err = decode_signed(junk).expect_err(junk);
assert!(matches!(err, SignerError::Provider(_)), "got: {err:?}");
}
}
}