use anyhow::anyhow;
use cid::Cid;
use ethers_core::types as et;
use ethers_core::types::transaction::eip2718::TypedTransaction;
use fvm_ipld_encoding::tuple::{Deserialize_tuple, Serialize_tuple};
use fvm_shared::address::{Address, Payload};
use fvm_shared::chainid::ChainID;
use fvm_shared::crypto::signature::ops::recover_secp_public_key;
use fvm_shared::crypto::signature::{Signature, SignatureType, SECP_SIG_LEN};
use fvm_shared::message::Message;
use recall_fendermint_crypto::{PublicKey, SecretKey};
use recall_fendermint_vm_actor_interface::eam::EthAddress;
use recall_fendermint_vm_actor_interface::{eam, evm};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::conv::from_fvm;
enum Signable {
Ethereum((et::H256, et::H160)),
Regular(Vec<u8>),
RegularFromEth((Vec<u8>, et::H160)),
}
#[derive(Error, Debug)]
pub enum SignedMessageError {
#[error("message cannot be serialized")]
Ipld(#[from] fvm_ipld_encoding::Error),
#[error("invalid signature: {0}")]
InvalidSignature(String),
#[error("message cannot be converted to ethereum: {0}")]
Ethereum(#[from] anyhow::Error),
}
#[derive(Debug, Clone)]
pub enum DomainHash {
Eth([u8; 32]),
}
#[derive(PartialEq, Clone, Debug, Serialize_tuple, Deserialize_tuple, Hash, Eq)]
pub struct SignedMessage {
pub origin_kind: OriginKind,
pub message: Message,
pub signature: Signature,
}
#[repr(u8)]
#[derive(PartialEq, Clone, Copy, Debug, Serialize, Deserialize, Hash, Eq)]
pub enum OriginKind {
Fvm = 0,
EthereumLegacy = 1,
EthereumEIP1559 = 2,
}
impl From<u8> for OriginKind {
fn from(value: u8) -> Self {
match value {
0 => Self::Fvm,
1 => Self::EthereumLegacy,
_ => Self::EthereumEIP1559,
}
}
}
impl SignedMessage {
pub fn new_unchecked(
origin_kind: OriginKind,
message: Message,
signature: Signature,
) -> SignedMessage {
SignedMessage {
origin_kind,
message,
signature,
}
}
pub fn new_secp256k1(
message: Message,
sk: &SecretKey,
chain_id: &ChainID,
) -> Result<Self, SignedMessageError> {
let (signature, origin_kind) = match Self::signable(&message, chain_id)? {
Signable::Ethereum((hash, _)) => (sign_eth(sk, hash), OriginKind::EthereumEIP1559),
Signable::Regular(data) => (sign_regular(sk, &data), OriginKind::Fvm),
Signable::RegularFromEth((data, _)) => {
(sign_regular(sk, &data), OriginKind::EthereumEIP1559)
}
};
Ok(Self {
origin_kind,
message,
signature,
})
}
pub fn cid(message: &Message) -> Result<Cid, fvm_ipld_encoding::Error> {
crate::cid(message)
}
fn signable(message: &Message, chain_id: &ChainID) -> Result<Signable, SignedMessageError> {
match maybe_eth_address(&message.from) {
Some(addr) if is_eth_addr_compat_no_masked(&message.to) => {
let tx: TypedTransaction = from_fvm::to_eth_eip1559_request(message, chain_id)
.map_err(SignedMessageError::Ethereum)?
.into();
Ok(Signable::Ethereum((tx.sighash(), addr)))
}
Some(addr) => {
let mut data = Self::cid(message)?.to_bytes();
data.extend(chain_id_bytes(chain_id).iter());
Ok(Signable::RegularFromEth((data, addr)))
}
None => {
let mut data = Self::cid(message)?.to_bytes();
data.extend(chain_id_bytes(chain_id).iter());
Ok(Signable::Regular(data))
}
}
}
pub fn verify_signature(
origin_kind: OriginKind,
message: &Message,
signature: &Signature,
chain_id: &ChainID,
) -> Result<(), SignedMessageError> {
match origin_kind {
OriginKind::Fvm => Self::verify_fvm_signature(message, chain_id, signature),
OriginKind::EthereumLegacy => Self::verify_ethereum_signature(
message,
chain_id,
signature,
|message, chain_id| {
let tx = from_fvm::to_eth_legacy_request(message, chain_id)
.map_err(SignedMessageError::Ethereum)?;
Ok(TypedTransaction::Legacy(tx))
},
),
OriginKind::EthereumEIP1559 => Self::verify_ethereum_signature(
message,
chain_id,
signature,
|message, chain_id| {
Ok(from_fvm::to_eth_eip1559_request(message, chain_id)
.map_err(SignedMessageError::Ethereum)?
.into())
},
),
}
}
fn verify_ethereum_signature<F: Fn(&Message, &ChainID) -> anyhow::Result<TypedTransaction>>(
message: &Message,
chain_id: &ChainID,
signature: &Signature,
to_eth_txn: F,
) -> anyhow::Result<(), SignedMessageError> {
let Some(from) = maybe_eth_address(&message.from) else {
return Err(SignedMessageError::Ethereum(anyhow!(
"sender not ethereum address"
)));
};
if !is_eth_addr_compat_no_masked(&message.to) {
let mut data = Self::cid(message)?.to_bytes();
data.extend(chain_id_bytes(chain_id).iter());
let rec = recover_secp256k1(signature, &data)
.map_err(SignedMessageError::InvalidSignature)?;
let rec_addr = EthAddress::from(rec);
return if rec_addr.0 == from.0 {
Ok(())
} else {
Err(SignedMessageError::InvalidSignature("the Ethereum delegated address did not match the one recovered from the signature".to_string()))
};
}
let hash = to_eth_txn(message, chain_id)
.map_err(SignedMessageError::Ethereum)?
.sighash();
let sig =
from_fvm::to_eth_signature(signature, true).map_err(SignedMessageError::Ethereum)?;
let rec = sig
.recover(hash)
.map_err(|e| SignedMessageError::Ethereum(anyhow!(e)))?;
if rec != from {
return Err(SignedMessageError::InvalidSignature(format!("the Ethereum delegated address did not match the one recovered from the signature (sighash = {:?})", hash)));
}
verify_eth_method(message)
}
fn verify_fvm_signature(
message: &Message,
chain_id: &ChainID,
signature: &Signature,
) -> anyhow::Result<(), SignedMessageError> {
let mut data = Self::cid(message)?.to_bytes();
data.extend(chain_id_bytes(chain_id).iter());
signature
.verify(&data, &message.from)
.map_err(SignedMessageError::InvalidSignature)
}
pub fn domain_hash(
&self,
chain_id: &ChainID,
) -> Result<Option<DomainHash>, SignedMessageError> {
if is_eth_addr_deleg(&self.message.from) && is_eth_addr_compat(&self.message.to) {
let tx = from_fvm::to_eth_typed_transaction(self.origin_kind, self.message(), chain_id)
.map_err(SignedMessageError::Ethereum)?;
let sig = from_fvm::to_eth_signature(self.signature(), true)
.map_err(SignedMessageError::Ethereum)?;
Ok(Some(DomainHash::Eth(tx.hash(&sig).0)))
} else {
Ok(None)
}
}
pub fn verify(&self, chain_id: &ChainID) -> Result<(), SignedMessageError> {
Self::verify_signature(self.origin_kind, &self.message, &self.signature, chain_id)
}
pub fn message(&self) -> &Message {
&self.message
}
pub fn signature(&self) -> &Signature {
&self.signature
}
pub fn into_message(self) -> Message {
self.message
}
pub fn is_bls(&self) -> bool {
self.signature.signature_type() == SignatureType::BLS
}
pub fn is_secp256k1(&self) -> bool {
self.signature.signature_type() == SignatureType::Secp256k1
}
}
fn sign_regular(sk: &SecretKey, data: &[u8]) -> Signature {
let hash: [u8; 32] = blake2b_simd::Params::new()
.hash_length(32)
.to_state()
.update(data)
.finalize()
.as_bytes()
.try_into()
.unwrap();
sign_secp256k1(sk, &hash)
}
fn sign_eth(sk: &SecretKey, hash: et::H256) -> Signature {
sign_secp256k1(sk, &hash.0)
}
pub fn chain_id_bytes(chain_id: &ChainID) -> [u8; 8] {
u64::from(*chain_id).to_be_bytes()
}
fn maybe_eth_address(addr: &Address) -> Option<et::H160> {
match addr.payload() {
Payload::Delegated(addr)
if addr.namespace() == eam::EAM_ACTOR_ID && addr.subaddress().len() == 20 =>
{
Some(et::H160::from_slice(addr.subaddress()))
}
_ => None,
}
}
fn is_eth_addr_compat(addr: &Address) -> bool {
from_fvm::to_eth_address(addr, true).is_ok()
}
fn is_eth_addr_compat_no_masked(addr: &Address) -> bool {
from_fvm::to_eth_address(addr, false).is_ok()
}
fn is_eth_addr_deleg(addr: &Address) -> bool {
maybe_eth_address(addr).is_some()
}
#[allow(dead_code)]
fn verify_eth_method(msg: &Message) -> Result<(), SignedMessageError> {
if msg.to == eam::EAM_ACTOR_ADDR {
if msg.method_num != eam::Method::CreateExternal as u64 {
return Err(SignedMessageError::Ethereum(anyhow!(
"The EAM actor can only be called with CreateExternal; got {}",
msg.method_num
)));
}
} else if msg.method_num != evm::Method::InvokeContract as u64 {
return Err(SignedMessageError::Ethereum(anyhow!(
"An EVM actor can only be called with InvokeContract; got {} - {}",
msg.to,
msg.method_num
)));
}
Ok(())
}
pub fn sign_secp256k1(sk: &SecretKey, hash: &[u8; 32]) -> Signature {
let (sig, recovery_id) = sk.sign(hash);
let mut signature = [0u8; SECP_SIG_LEN];
signature[..64].copy_from_slice(&sig.serialize());
signature[64] = recovery_id.serialize();
Signature {
sig_type: SignatureType::Secp256k1,
bytes: signature.to_vec(),
}
}
fn recover_secp256k1(signature: &Signature, data: &[u8]) -> Result<PublicKey, String> {
let signature = &signature.bytes;
if signature.len() != SECP_SIG_LEN {
return Err(format!(
"Invalid Secp256k1 signature length. Was {}, must be 65",
signature.len()
));
}
let hash = blake2b_simd::Params::new()
.hash_length(32)
.to_state()
.update(data)
.finalize();
let mut sig = [0u8; SECP_SIG_LEN];
sig[..].copy_from_slice(signature);
let rec_key =
recover_secp_public_key(hash.as_bytes().try_into().expect("fixed array size"), &sig)
.map_err(|e| e.to_string())?;
Ok(rec_key)
}
#[cfg(feature = "arb")]
mod arb {
use crate::signed::OriginKind;
use fvm_shared::crypto::signature::Signature;
use recall_fendermint_testing::arb::ArbMessage;
use super::SignedMessage;
impl quickcheck::Arbitrary for SignedMessage {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
Self {
origin_kind: OriginKind::from(u8::arbitrary(g) % 3),
message: ArbMessage::arbitrary(g).0,
signature: Signature::arbitrary(g),
}
}
}
}
#[cfg(test)]
mod tests {
use fvm_shared::{
address::{Address, Payload, Protocol},
chainid::ChainID,
};
use quickcheck_macros::quickcheck;
use recall_fendermint_vm_actor_interface::eam::EthAddress;
use crate::conv::tests::{EthMessage, KeyPair};
use super::SignedMessage;
#[quickcheck]
fn chain_id_in_signature(
msg: SignedMessage,
chain_id: u64,
key: KeyPair,
) -> Result<(), String> {
let KeyPair { sk, pk } = key;
let chain_id0 = ChainID::from(chain_id);
let chain_id1 = ChainID::from(chain_id.overflowing_add(1).0);
let mut msg = msg.into_message();
msg.from = Address::new_secp256k1(&pk.serialize())
.map_err(|e| format!("failed to conver to address: {e}"))?;
let signed = SignedMessage::new_secp256k1(msg, &sk, &chain_id0)
.map_err(|e| format!("signing failed: {e}"))?;
signed
.verify(&chain_id0)
.map_err(|e| format!("verifying failed: {e}"))?;
if signed.verify(&chain_id1).is_ok() {
return Err("verifying with a different chain ID should fail".into());
}
Ok(())
}
#[quickcheck]
fn eth_sign_and_verify(msg: EthMessage, chain_id: u64, key: KeyPair) -> Result<(), String> {
let chain_id = ChainID::from(chain_id);
let KeyPair { sk, pk } = key;
let ea = EthAddress::from(pk);
let mut msg = msg.0;
msg.from = Address::from(ea);
let signed =
SignedMessage::new_secp256k1(msg, &sk, &chain_id).map_err(|e| e.to_string())?;
signed.verify(&chain_id).map_err(|e| e.to_string())
}
#[quickcheck]
fn eth_sign_and_tamper(msg: EthMessage, chain_id: u64, key: KeyPair) -> Result<(), String> {
let chain_id = ChainID::from(chain_id);
let KeyPair { sk, pk } = key;
let ea = EthAddress::from(pk);
let mut msg = msg.0;
msg.from = Address::from(ea);
let mut signed =
SignedMessage::new_secp256k1(msg, &sk, &chain_id).map_err(|e| e.to_string())?;
let Payload::Delegated(da) = signed.message.to.payload() else {
return Err("expected delegated addresss".to_string());
};
let mut bz = da.subaddress().to_vec();
bz.insert(0, Protocol::Secp256k1 as u8);
signed.message.to = Address::from_bytes(&bz).map_err(|e| e.to_string())?;
if signed.verify(&chain_id).is_ok() {
return Err("signature verification should have failed".to_string());
}
Ok(())
}
#[quickcheck]
fn eth_to_non_eth_sign_and_verify(msg: EthMessage, chain_id: u64, from: KeyPair, to: KeyPair) {
let chain_id = ChainID::from(chain_id);
let mut msg = msg.0;
msg.from = Address::from(EthAddress::from(from.pk));
msg.to = Address::new_secp256k1(&to.pk.serialize()).expect("f1 address");
let signed =
SignedMessage::new_secp256k1(msg, &from.sk, &chain_id).expect("message can be signed");
signed.verify(&chain_id).expect("signature should be valid")
}
}