use alloy_primitives::{Address, B256, Bytes, Signature as PrimSignature, eip191_hash_message};
use alloy_signer::{SignerSync, k256::ecdsa::Error as K256Error};
use alloy_sol_types::SolStruct;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use std::fmt::{self, Debug, Formatter};
use crate::domain::DomainSeparator;
use crate::signing_scheme::{EcdsaSigningScheme, SigningScheme};
pub const EIP1271_MAX_LEN: usize = 32 * 1024;
pub type EcdsaSignature = PrimSignature;
#[derive(Debug, thiserror::Error)]
pub enum SignatureError {
#[error("expected 65 ecdsa signature bytes, got {0}")]
Length(usize),
#[error("presign payload must be empty or a 20-byte owner, got {0} bytes")]
PreSignLength(usize),
#[error("eip1271 signature payload too long: {len} bytes (max {max})")]
Eip1271TooLong {
len: usize,
max: usize,
},
#[error("invalid signature v value: {0}; expected 0, 1, 27 or 28")]
InvalidV(u8),
#[error("ecdsa recovery failed: {0}")]
Recovery(#[from] alloy_primitives::SignatureError),
#[error("k256 signer error: {0}")]
Signer(#[from] K256Error),
#[error("signer error: {0}")]
SignerOther(String),
#[error("signer mismatch: declared {declared}, recovered {recovered}")]
SignerMismatch {
declared: Address,
recovered: Address,
},
}
#[derive(Clone, Eq, PartialEq, Hash)]
pub enum Signature {
Eip712(EcdsaSignature),
EthSign(EcdsaSignature),
Eip1271(Vec<u8>),
PreSign,
}
impl Debug for Signature {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::PreSign => f.write_str("PreSign"),
other => {
let scheme = format!("{:?}", other.scheme());
let bytes = Bytes::from(other.to_bytes()).to_string();
f.debug_tuple(&scheme).field(&bytes).finish()
}
}
}
}
impl Signature {
pub fn empty_for(scheme: SigningScheme) -> Self {
match scheme {
SigningScheme::Eip712 => Self::Eip712(zero_ecdsa()),
SigningScheme::EthSign => Self::EthSign(zero_ecdsa()),
SigningScheme::Eip1271 => Self::Eip1271(Vec::new()),
SigningScheme::PreSign => Self::PreSign,
}
}
pub const fn from_ecdsa(sig: EcdsaSignature, scheme: EcdsaSigningScheme) -> Self {
match scheme {
EcdsaSigningScheme::Eip712 => Self::Eip712(sig),
EcdsaSigningScheme::EthSign => Self::EthSign(sig),
}
}
pub const fn scheme(&self) -> SigningScheme {
match self {
Self::Eip712(_) => SigningScheme::Eip712,
Self::EthSign(_) => SigningScheme::EthSign,
Self::Eip1271(_) => SigningScheme::Eip1271,
Self::PreSign => SigningScheme::PreSign,
}
}
pub fn to_bytes(&self) -> Vec<u8> {
match self {
Self::Eip712(s) | Self::EthSign(s) => s.as_bytes().to_vec(),
Self::Eip1271(bytes) => bytes.clone(),
Self::PreSign => Vec::new(),
}
}
pub fn from_bytes(scheme: SigningScheme, bytes: &[u8]) -> Result<Self, SignatureError> {
match scheme {
SigningScheme::Eip712 => Ok(Self::from_ecdsa(
parse_ecdsa(bytes)?,
EcdsaSigningScheme::Eip712,
)),
SigningScheme::EthSign => Ok(Self::from_ecdsa(
parse_ecdsa(bytes)?,
EcdsaSigningScheme::EthSign,
)),
SigningScheme::Eip1271 => {
if bytes.len() > EIP1271_MAX_LEN {
return Err(SignatureError::Eip1271TooLong {
len: bytes.len(),
max: EIP1271_MAX_LEN,
});
}
Ok(Self::Eip1271(bytes.to_vec()))
}
SigningScheme::PreSign => {
if !(bytes.is_empty() || bytes.len() == 20) {
return Err(SignatureError::PreSignLength(bytes.len()));
}
Ok(Self::PreSign)
}
}
}
pub fn recover<T: SolStruct>(
&self,
domain: &DomainSeparator,
payload: &T,
) -> Result<Option<Recovered>, SignatureError> {
match self {
Self::Eip712(s) => Ok(Some(ecdsa_recover(
s,
EcdsaSigningScheme::Eip712,
domain,
payload,
)?)),
Self::EthSign(s) => Ok(Some(ecdsa_recover(
s,
EcdsaSigningScheme::EthSign,
domain,
payload,
)?)),
Self::Eip1271(_) | Self::PreSign => Ok(None),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Recovered {
pub message: B256,
pub signer: Address,
}
pub fn sign_ecdsa<T: SolStruct, S: SignerSync>(
scheme: EcdsaSigningScheme,
domain: &DomainSeparator,
payload: &T,
signer: &S,
) -> Result<EcdsaSignature, SignatureError> {
let message = signing_message(scheme, domain, payload);
let raw = signer.sign_hash_sync(&message).map_err(|e| match e {
alloy_signer::Error::Ecdsa(k) => SignatureError::Signer(k),
other => SignatureError::SignerOther(other.to_string()),
})?;
parse_ecdsa(&raw.as_bytes())
}
pub fn parse_ecdsa(bytes: &[u8]) -> Result<EcdsaSignature, SignatureError> {
if bytes.len() != 65 {
return Err(SignatureError::Length(bytes.len()));
}
if !matches!(bytes[64], 0 | 1 | 27 | 28) {
return Err(SignatureError::InvalidV(bytes[64]));
}
Ok(PrimSignature::from_raw(bytes)?)
}
pub fn ecdsa_from_components(r: B256, s: B256, v: u8) -> Result<EcdsaSignature, SignatureError> {
let mut bytes = [0u8; 65];
bytes[..32].copy_from_slice(r.as_slice());
bytes[32..64].copy_from_slice(s.as_slice());
bytes[64] = v;
parse_ecdsa(&bytes)
}
pub fn ecdsa_recover<T: SolStruct>(
sig: &EcdsaSignature,
scheme: EcdsaSigningScheme,
domain: &DomainSeparator,
payload: &T,
) -> Result<Recovered, SignatureError> {
let message = signing_message(scheme, domain, payload);
let signer = sig.recover_address_from_prehash(&message)?;
Ok(Recovered { message, signer })
}
fn zero_ecdsa() -> EcdsaSignature {
PrimSignature::from_bytes_and_parity(&[0u8; 64], false)
}
fn signing_message<T: SolStruct>(
signing_scheme: EcdsaSigningScheme,
domain: &DomainSeparator,
payload: &T,
) -> B256 {
let eip712 = payload.eip712_signing_hash(domain);
match signing_scheme {
EcdsaSigningScheme::Eip712 => eip712,
EcdsaSigningScheme::EthSign => eip191_hash_message(eip712),
}
}
pub mod ecdsa_wire {
use super::{EcdsaSignature, parse_ecdsa};
use alloy_primitives::FixedBytes;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
pub fn serialize<S>(sig: &EcdsaSignature, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
FixedBytes::from(sig.as_bytes()).serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<EcdsaSignature, D::Error>
where
D: Deserializer<'de>,
{
let bytes = FixedBytes::<65>::deserialize(deserializer)?;
parse_ecdsa(bytes.as_slice()).map_err(de::Error::custom)
}
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct JsonSignature {
signing_scheme: SigningScheme,
#[serde(deserialize_with = "deserialize_capped_hex")]
signature: Bytes,
}
fn deserialize_capped_hex<'de, D>(deserializer: D) -> Result<Bytes, D::Error>
where
D: Deserializer<'de>,
{
struct CappedHexVisitor;
impl<'de> de::Visitor<'de> for CappedHexVisitor {
type Value = Bytes;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"0x-prefixed hex string of at most {EIP1271_MAX_LEN} bytes",
)
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Bytes, E> {
const MAX_HEX_LEN: usize = 2 + EIP1271_MAX_LEN * 2;
if v.len() > MAX_HEX_LEN {
return Err(E::custom(format!(
"signature hex exceeds {EIP1271_MAX_LEN}-byte cap (got {} chars)",
v.len()
)));
}
v.parse::<Bytes>().map_err(E::custom)
}
}
deserializer.deserialize_str(CappedHexVisitor)
}
impl Serialize for Signature {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
JsonSignature {
signing_scheme: self.scheme(),
signature: Bytes::from(self.to_bytes()),
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Signature {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let json = JsonSignature::deserialize(deserializer)?;
Self::from_bytes(json.signing_scheme, &json.signature).map_err(de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::{U256, hex};
use alloy_signer_local::PrivateKeySigner;
use serde_json::json;
use super::*;
#[test]
fn from_bytes_rejects_wrong_lengths() {
assert!(matches!(
Signature::from_bytes(SigningScheme::Eip712, &[0u8; 20]),
Err(SignatureError::Length(20))
));
assert!(matches!(
Signature::from_bytes(SigningScheme::PreSign, &[0u8; 32]),
Err(SignatureError::PreSignLength(32))
));
}
#[test]
fn ecdsa_default_zero_signature_round_trips() {
let sig = Signature::from_bytes(SigningScheme::Eip712, &[0u8; 65]).unwrap();
assert_eq!(sig, Signature::empty_for(SigningScheme::Eip712));
}
#[test]
fn eip1271_rejects_oversize_payload() {
let oversize = vec![0u8; EIP1271_MAX_LEN + 1];
assert!(matches!(
Signature::from_bytes(SigningScheme::Eip1271, &oversize),
Err(SignatureError::Eip1271TooLong { len, max })
if len == EIP1271_MAX_LEN + 1 && max == EIP1271_MAX_LEN
));
let at_limit = vec![0u8; EIP1271_MAX_LEN];
assert!(Signature::from_bytes(SigningScheme::Eip1271, &at_limit).is_ok());
}
#[test]
fn deserialize_rejects_oversize_eip1271_payload() {
let oversize_hex = format!("0x{}", "00".repeat(EIP1271_MAX_LEN + 1));
let body = json!({
"signingScheme": "eip1271",
"signature": oversize_hex,
});
let err = serde_json::from_value::<Signature>(body)
.expect_err("oversize signature payload must be rejected on deserialise");
let msg = err.to_string();
assert!(
msg.contains("cap") || msg.contains(&EIP1271_MAX_LEN.to_string()),
"error should reference the EIP-1271 length cap, got: {msg}"
);
let at_limit_hex = format!("0x{}", "00".repeat(EIP1271_MAX_LEN));
let body = json!({
"signingScheme": "eip1271",
"signature": at_limit_hex,
});
let sig: Signature = serde_json::from_value(body).unwrap();
assert!(matches!(sig, Signature::Eip1271(ref b) if b.len() == EIP1271_MAX_LEN));
}
#[test]
fn deserialize_rejects_oversize_hex_string_pre_decode() {
let oversize_hex = format!("0x{}", "00".repeat(EIP1271_MAX_LEN + 1));
let payload =
format!("{{\"signingScheme\":\"eip1271\",\"signature\":\"{oversize_hex}\"}}",);
let err = serde_json::from_str::<Signature>(&payload)
.expect_err("oversize hex string must be rejected before decode");
let msg = err.to_string();
assert!(
msg.contains("cap") || msg.contains(&EIP1271_MAX_LEN.to_string()),
"error should mention the byte cap, got: {msg}",
);
}
#[test]
fn deserialize_accepts_at_limit_eip1271_hex() {
let at_limit_hex = format!("0x{}", "00".repeat(EIP1271_MAX_LEN));
assert_eq!(at_limit_hex.len(), EIP1271_MAX_LEN * 2 + 2);
let payload =
format!("{{\"signingScheme\":\"eip1271\",\"signature\":\"{at_limit_hex}\"}}",);
let sig: Signature = serde_json::from_str(&payload).unwrap();
assert!(matches!(sig, Signature::Eip1271(ref b) if b.len() == EIP1271_MAX_LEN));
}
#[test]
fn presign_accepts_empty_and_legacy_20_byte_payloads() {
assert_eq!(
Signature::from_bytes(SigningScheme::PreSign, &[]).unwrap(),
Signature::PreSign
);
assert_eq!(
Signature::from_bytes(SigningScheme::PreSign, &[0xff; 20]).unwrap(),
Signature::PreSign
);
}
#[test]
fn from_bytes_presign_accepts_empty_and_20_bytes_rejects_others() {
assert_eq!(
Signature::from_bytes(SigningScheme::PreSign, &[]).unwrap(),
Signature::PreSign
);
assert_eq!(
Signature::from_bytes(SigningScheme::PreSign, &[0u8; 20]).unwrap(),
Signature::PreSign
);
assert!(matches!(
Signature::from_bytes(SigningScheme::PreSign, &[0u8; 19]),
Err(SignatureError::PreSignLength(19))
));
assert!(matches!(
Signature::from_bytes(SigningScheme::PreSign, &[0u8; 21]),
Err(SignatureError::PreSignLength(21))
));
}
#[test]
fn v_normalisation_matches_services() {
for (raw, expected) in [(0u8, 27u8), (1, 28), (27, 27), (28, 28)] {
let mut bytes = [0u8; 65];
bytes[64] = raw;
let sig = parse_ecdsa(&bytes).unwrap();
assert_eq!(sig.as_bytes()[64], expected);
}
}
#[test]
fn invalid_v_rejected() {
for invalid_v in [2u8, 3, 26, 29, 30, 255] {
let mut bytes = [0u8; 65];
bytes[64] = invalid_v;
assert!(matches!(
parse_ecdsa(&bytes),
Err(SignatureError::InvalidV(v)) if v == invalid_v
));
}
}
#[test]
fn json_round_trip_for_each_scheme() {
for (signature, expected_json) in [
(
Signature::Eip1271(vec![1, 2, 3]),
json!({ "signingScheme": "eip1271", "signature": "0x010203" }),
),
(
Signature::Eip1271(Vec::new()),
json!({ "signingScheme": "eip1271", "signature": "0x" }),
),
(
Signature::PreSign,
json!({ "signingScheme": "presign", "signature": "0x" }),
),
] {
let serialised = serde_json::to_value(&signature).unwrap();
assert_eq!(serialised, expected_json);
let parsed: Signature = serde_json::from_value(expected_json).unwrap();
assert_eq!(parsed, signature);
}
}
alloy_sol_types::sol! {
struct Probe {
bytes32 value;
}
}
fn probe_payload(value: [u8; 32]) -> Probe {
Probe {
value: B256::from(value),
}
}
#[test]
fn ecdsa_sign_recover_round_trip() {
let signer = PrivateKeySigner::from_bytes(&U256::from(1u64).to_be_bytes().into()).unwrap();
let address = signer.address();
let domain = crate::domain::settlement_domain(
1,
alloy_primitives::address!("9008D19f58AAbD9eD0D60971565AA8510560ab41"),
);
let payload = probe_payload(hex!(
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
));
for scheme in [EcdsaSigningScheme::Eip712, EcdsaSigningScheme::EthSign] {
let ecdsa = sign_ecdsa(scheme, &domain, &payload, &signer).unwrap();
let typed = Signature::from_ecdsa(ecdsa, scheme);
let recovered = typed.recover(&domain, &payload).unwrap().unwrap();
assert_eq!(recovered.signer, address);
}
}
#[test]
fn recover_returns_none_for_onchain_schemes() {
let domain = crate::domain::DomainSeparator::default();
let payload = probe_payload([0u8; 32]);
for signature in [Signature::PreSign, Signature::Eip1271(Vec::new())] {
let recovered = signature.recover(&domain, &payload).unwrap();
assert!(recovered.is_none());
}
}
}