use eth_valkyoth_hash::{Keccak256, hash_chunks};
use eth_valkyoth_primitives::{Address, B256, ChainId};
#[cfg(feature = "secp256k1-k256")]
use crate::K256Secp256k1Backend;
use crate::{
EthereumSignature, RecoverableSecp256k1, VerifyError, recover_sender_from_digest_with_backend,
};
pub const EIP712_SIGNING_PREFIX: [u8; 2] = [0x19, 0x01];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Eip712Domain {
chain_id: Option<ChainId>,
verifying_contract: Option<Address>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Eip712DomainExpectation {
chain_id: ChainId,
verifying_contract: Address,
}
impl Eip712DomainExpectation {
#[must_use]
pub const fn new(chain_id: ChainId, verifying_contract: Address) -> Self {
Self {
chain_id,
verifying_contract,
}
}
#[must_use]
pub const fn chain_id(self) -> ChainId {
self.chain_id
}
#[must_use]
pub const fn verifying_contract(self) -> Address {
self.verifying_contract
}
}
impl Eip712Domain {
#[must_use]
pub const fn new(chain_id: Option<ChainId>, verifying_contract: Option<Address>) -> Self {
Self {
chain_id,
verifying_contract,
}
}
#[must_use]
pub const fn complete(chain_id: ChainId, verifying_contract: Address) -> Self {
Self::new(Some(chain_id), Some(verifying_contract))
}
#[must_use]
pub const fn chain_id(self) -> Option<ChainId> {
self.chain_id
}
#[must_use]
pub const fn verifying_contract(self) -> Option<Address> {
self.verifying_contract
}
#[must_use]
pub const fn is_complete(self) -> bool {
self.chain_id.is_some() && self.verifying_contract.is_some()
}
}
pub fn require_eip712_domain(
expected_chain: ChainId,
expected_verifying_contract: Address,
domain: Eip712Domain,
) -> Result<(), VerifyError> {
let actual_chain = domain.chain_id().ok_or(VerifyError::MissingEip712ChainId)?;
if actual_chain != expected_chain {
return Err(VerifyError::WrongChain);
}
let actual_contract = domain
.verifying_contract()
.ok_or(VerifyError::MissingEip712VerifyingContract)?;
if actual_contract != expected_verifying_contract {
return Err(VerifyError::WrongVerifyingContract);
}
Ok(())
}
#[must_use]
pub fn eip712_signing_digest<H>(domain_separator: B256, message_hash: B256, hasher: H) -> B256
where
H: Keccak256,
{
let domain_bytes = domain_separator.to_bytes();
let message_bytes = message_hash.to_bytes();
hash_chunks(
hasher,
[
EIP712_SIGNING_PREFIX.as_slice(),
domain_bytes.as_slice(),
message_bytes.as_slice(),
],
)
}
#[cfg(feature = "secp256k1-k256")]
pub fn recover_eip712_sender<DH, AH>(
expected_domain: Eip712DomainExpectation,
domain: Eip712Domain,
domain_separator: B256,
message_hash: B256,
signature: EthereumSignature,
digest_hasher: DH,
address_hasher: AH,
) -> Result<Address, VerifyError>
where
DH: Keccak256,
AH: Keccak256,
{
recover_eip712_sender_with_backend(
expected_domain,
domain,
domain_separator,
message_hash,
signature,
digest_hasher,
K256Secp256k1Backend,
address_hasher,
)
}
#[allow(clippy::too_many_arguments)]
pub fn recover_eip712_sender_with_backend<B, DH, AH>(
expected_domain: Eip712DomainExpectation,
domain: Eip712Domain,
domain_separator: B256,
message_hash: B256,
signature: EthereumSignature,
digest_hasher: DH,
secp256k1_backend: B,
address_hasher: AH,
) -> Result<Address, VerifyError>
where
B: RecoverableSecp256k1,
DH: Keccak256,
AH: Keccak256,
{
require_eip712_domain(
expected_domain.chain_id(),
expected_domain.verifying_contract(),
domain,
)?;
let digest = eip712_signing_digest(domain_separator, message_hash, digest_hasher);
recover_sender_from_digest_with_backend(digest, signature, secp256k1_backend, address_hasher)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_crypto::{RealKeccak, TestSecp256k1Backend};
use k256::ecdsa::SigningKey;
const TEST_PRIVATE_KEY: [u8; 32] = [
0x4c, 0x08, 0x83, 0xa6, 0x91, 0x02, 0x93, 0x7d, 0x62, 0x31, 0x47, 0x1b, 0x5d, 0xbb, 0x62,
0x04, 0xfe, 0x51, 0x29, 0x61, 0x70, 0x82, 0x79, 0x2a, 0xe4, 0x68, 0xd0, 0x1a, 0x3f, 0x36,
0x23, 0x18,
];
const TEST_PRIVATE_KEY_ADDRESS: Address = Address::from_bytes([
0x2c, 0x75, 0x36, 0xe3, 0x60, 0x5d, 0x9c, 0x16, 0xa7, 0xa3, 0xd7, 0xb1, 0x89, 0x8e, 0x52,
0x93, 0x96, 0xa6, 0x5c, 0x23,
]);
use eth_valkyoth_protocol::SignatureYParity;
struct Eip712TranscriptHasher {
state: u8,
valid: bool,
}
struct FixedHasher {
digest: B256,
}
impl Eip712TranscriptHasher {
const fn new() -> Self {
Self {
state: 0,
valid: true,
}
}
}
impl Keccak256 for Eip712TranscriptHasher {
fn update(&mut self, input: &[u8]) {
let expected = match self.state {
0 => EIP712_SIGNING_PREFIX.as_slice(),
1 => &[0x11_u8; 32],
2 => &[0x22_u8; 32],
_ => &[],
};
self.valid &= input == expected;
self.state = self.state.saturating_add(1);
}
fn finalize(self) -> B256 {
let mut bytes = [0_u8; 32];
if self.valid
&& self.state == 3
&& let Some(first) = bytes.first_mut()
{
*first = 0x7a;
}
B256::from_bytes(bytes)
}
}
impl Keccak256 for FixedHasher {
fn update(&mut self, _input: &[u8]) {}
fn finalize(self) -> B256 {
self.digest
}
}
fn expected_chain() -> ChainId {
ChainId::new(1)
}
fn expected_contract() -> Address {
Address::from_bytes([0xcc_u8; 20])
}
fn expected_domain() -> Eip712DomainExpectation {
Eip712DomainExpectation::new(expected_chain(), expected_contract())
}
fn complete_domain() -> Eip712Domain {
Eip712Domain::complete(expected_chain(), expected_contract())
}
fn invalid_signature() -> EthereumSignature {
EthereumSignature::from_parts([0_u8; 32], [0_u8; 32], SignatureYParity::Even)
}
fn signing_key() -> Result<SigningKey, VerifyError> {
SigningKey::from_bytes((&TEST_PRIVATE_KEY).into())
.map_err(|_| VerifyError::InvalidSignature)
}
fn sign_digest(digest: B256) -> Result<EthereumSignature, VerifyError> {
let key = signing_key()?;
let (signature, recovery_id) = key
.sign_prehash_recoverable(&<[u8; 32]>::from(digest))
.map_err(|_| VerifyError::InvalidSignature)?;
let bytes = signature.to_bytes();
let r = bytes
.get(..32)
.and_then(|value| <[u8; 32]>::try_from(value).ok())
.ok_or(VerifyError::InvalidSignature)?;
let s = bytes
.get(32..)
.and_then(|value| <[u8; 32]>::try_from(value).ok())
.ok_or(VerifyError::InvalidSignature)?;
EthereumSignature::try_from_parts_with_y_parity(r, s, recovery_id.to_byte())
}
#[test]
fn complete_domain_exposes_required_fields() {
let domain = complete_domain();
let expected = expected_domain();
assert!(domain.is_complete());
assert_eq!(domain.chain_id(), Some(expected_chain()));
assert_eq!(domain.verifying_contract(), Some(expected_contract()));
assert_eq!(expected.chain_id(), expected_chain());
assert_eq!(expected.verifying_contract(), expected_contract());
}
#[test]
fn eip712_domain_requires_chain_id() {
let domain = Eip712Domain::new(None, Some(expected_contract()));
assert_eq!(
require_eip712_domain(expected_chain(), expected_contract(), domain),
Err(VerifyError::MissingEip712ChainId)
);
}
#[test]
fn eip712_domain_requires_verifying_contract() {
let domain = Eip712Domain::new(Some(expected_chain()), None);
assert_eq!(
require_eip712_domain(expected_chain(), expected_contract(), domain),
Err(VerifyError::MissingEip712VerifyingContract)
);
}
#[test]
fn eip712_domain_rejects_wrong_chain() {
let domain = Eip712Domain::complete(ChainId::new(5), expected_contract());
assert_eq!(
require_eip712_domain(expected_chain(), expected_contract(), domain),
Err(VerifyError::WrongChain)
);
}
#[test]
fn eip712_domain_rejects_wrong_verifying_contract() {
let domain = Eip712Domain::complete(expected_chain(), Address::from_bytes([0xdd_u8; 20]));
assert_eq!(
require_eip712_domain(expected_chain(), expected_contract(), domain),
Err(VerifyError::WrongVerifyingContract)
);
}
#[test]
fn eip712_signing_digest_uses_eip191_prefix_and_32_byte_domains() {
let digest = eip712_signing_digest(
B256::from_bytes([0x11_u8; 32]),
B256::from_bytes([0x22_u8; 32]),
Eip712TranscriptHasher::new(),
);
let mut expected = [0_u8; 32];
if let Some(first) = expected.first_mut() {
*first = 0x7a;
}
assert_eq!(digest, B256::from_bytes(expected));
}
#[test]
fn recovers_known_eip712_vector() -> Result<(), VerifyError> {
let domain_separator = B256::from_bytes([0x11_u8; 32]);
let message_hash = B256::from_bytes([0x22_u8; 32]);
let digest = eip712_signing_digest(domain_separator, message_hash, RealKeccak::default());
let signature = sign_digest(digest)?;
assert_eq!(
recover_eip712_sender_with_backend(
expected_domain(),
complete_domain(),
domain_separator,
message_hash,
signature,
RealKeccak::default(),
TestSecp256k1Backend,
RealKeccak::default(),
),
Ok(TEST_PRIVATE_KEY_ADDRESS)
);
Ok(())
}
#[test]
fn eip712_recovery_checks_domain_before_signature() {
let domain = Eip712Domain::complete(ChainId::new(5), expected_contract());
assert_eq!(
recover_eip712_sender_with_backend(
expected_domain(),
domain,
B256::from_bytes([0x11_u8; 32]),
B256::from_bytes([0x22_u8; 32]),
invalid_signature(),
FixedHasher {
digest: B256::from_bytes([0x33_u8; 32])
},
TestSecp256k1Backend,
FixedHasher {
digest: B256::from_bytes([0x44_u8; 32])
},
),
Err(VerifyError::WrongChain)
);
}
#[test]
fn eip712_recovery_uses_signature_after_complete_domain() {
assert_eq!(
recover_eip712_sender_with_backend(
expected_domain(),
complete_domain(),
B256::from_bytes([0x11_u8; 32]),
B256::from_bytes([0x22_u8; 32]),
invalid_signature(),
FixedHasher {
digest: B256::from_bytes([0x33_u8; 32])
},
TestSecp256k1Backend,
FixedHasher {
digest: B256::from_bytes([0x44_u8; 32])
},
),
Err(VerifyError::InvalidSignature)
);
}
}