use crate::{Address, Result, Signature};
use sha3::{Digest, Keccak256};
pub trait Eip712Type {
fn type_string() -> &'static str;
fn encode_data(&self) -> Vec<u8>;
fn type_hash() -> [u8; 32] {
keccak256(Self::type_string().as_bytes())
}
fn hash_struct(&self) -> [u8; 32] {
let type_hash = Self::type_hash();
let encoded = self.encode_data();
let mut buf = Vec::with_capacity(32 + encoded.len());
buf.extend_from_slice(&type_hash);
buf.extend_from_slice(&encoded);
keccak256(&buf)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Eip712Domain {
pub name: Option<String>,
pub version: Option<String>,
pub chain_id: Option<u64>,
pub verifying_contract: Option<Address>,
pub salt: Option<[u8; 32]>,
}
impl Eip712Domain {
pub fn new(name: &str, version: &str, chain_id: u64, verifying_contract: Address) -> Self {
Self {
name: Some(name.to_string()),
version: Some(version.to_string()),
chain_id: Some(chain_id),
verifying_contract: Some(verifying_contract),
salt: None,
}
}
pub fn builder() -> Eip712DomainBuilder {
Eip712DomainBuilder::default()
}
pub fn type_string(&self) -> String {
let mut fields = Vec::new();
if self.name.is_some() {
fields.push("string name");
}
if self.version.is_some() {
fields.push("string version");
}
if self.chain_id.is_some() {
fields.push("uint256 chainId");
}
if self.verifying_contract.is_some() {
fields.push("address verifyingContract");
}
if self.salt.is_some() {
fields.push("bytes32 salt");
}
format!("EIP712Domain({})", fields.join(","))
}
pub fn type_hash(&self) -> [u8; 32] {
keccak256(self.type_string().as_bytes())
}
pub fn domain_separator(&self) -> [u8; 32] {
let type_hash = self.type_hash();
let mut buf = Vec::with_capacity(32 * 6);
buf.extend_from_slice(&type_hash);
if let Some(ref name) = self.name {
buf.extend_from_slice(&keccak256(name.as_bytes()));
}
if let Some(ref version) = self.version {
buf.extend_from_slice(&keccak256(version.as_bytes()));
}
if let Some(chain_id) = self.chain_id {
buf.extend_from_slice(&encode_uint64_as_u256(chain_id));
}
if let Some(ref addr) = self.verifying_contract {
buf.extend_from_slice(&encode_address(addr));
}
if let Some(salt) = self.salt {
buf.extend_from_slice(&salt);
}
keccak256(&buf)
}
}
#[derive(Debug, Clone, Default)]
pub struct Eip712DomainBuilder {
name: Option<String>,
version: Option<String>,
chain_id: Option<u64>,
verifying_contract: Option<Address>,
salt: Option<[u8; 32]>,
}
impl Eip712DomainBuilder {
pub fn name(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self
}
pub fn version(mut self, version: &str) -> Self {
self.version = Some(version.to_string());
self
}
pub fn chain_id(mut self, chain_id: u64) -> Self {
self.chain_id = Some(chain_id);
self
}
pub fn verifying_contract(mut self, address: Address) -> Self {
self.verifying_contract = Some(address);
self
}
pub fn salt(mut self, salt: [u8; 32]) -> Self {
self.salt = Some(salt);
self
}
pub fn build(self) -> Eip712Domain {
Eip712Domain {
name: self.name,
version: self.version,
chain_id: self.chain_id,
verifying_contract: self.verifying_contract,
salt: self.salt,
}
}
}
pub fn hash_typed_data<T: Eip712Type>(domain: &Eip712Domain, message: &T) -> [u8; 32] {
let domain_sep = domain.domain_separator();
let struct_hash = message.hash_struct();
let mut buf = [0u8; 66];
buf[0] = 0x19;
buf[1] = 0x01;
buf[2..34].copy_from_slice(&domain_sep);
buf[34..66].copy_from_slice(&struct_hash);
keccak256(&buf)
}
pub fn sign_typed_data<T: Eip712Type>(
signer: &crate::Bip44Signer,
domain: &Eip712Domain,
message: &T,
) -> Result<Signature> {
let hash = hash_typed_data(domain, message);
signer.sign_hash(&hash)
}
pub fn verify_typed_data<T: Eip712Type>(
domain: &Eip712Domain,
message: &T,
signature: &Signature,
expected_signer: Address,
) -> Result<bool> {
let hash = hash_typed_data(domain, message);
let recovered = crate::recover_signer(&hash, signature)?;
Ok(recovered == expected_signer)
}
pub fn encode_address(address: &Address) -> [u8; 32] {
let mut word = [0u8; 32];
word[12..].copy_from_slice(address.as_bytes());
word
}
pub fn encode_uint256(value: u128) -> [u8; 32] {
let mut word = [0u8; 32];
word[16..].copy_from_slice(&value.to_be_bytes());
word
}
pub fn encode_u256_bytes(value: [u8; 32]) -> [u8; 32] {
value
}
pub fn encode_uint64(value: u64) -> [u8; 32] {
let mut word = [0u8; 32];
word[24..].copy_from_slice(&value.to_be_bytes());
word
}
pub fn encode_bool(value: bool) -> [u8; 32] {
let mut word = [0u8; 32];
word[31] = value as u8;
word
}
pub fn encode_bytes32(value: [u8; 32]) -> [u8; 32] {
value
}
pub fn encode_bytes_dynamic(value: &[u8]) -> [u8; 32] {
keccak256(value)
}
fn encode_uint64_as_u256(value: u64) -> [u8; 32] {
encode_uint64(value)
}
pub(crate) fn keccak256(data: &[u8]) -> [u8; 32] {
let mut hasher = Keccak256::new();
hasher.update(data);
hasher.finalize().into()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Bip44Signer;
fn test_address() -> Address {
"0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
.parse()
.unwrap()
}
fn test_domain() -> Eip712Domain {
Eip712Domain::new("TestProtocol", "1", 56, test_address())
}
struct SimpleTransfer {
to: Address,
amount: u64,
}
impl Eip712Type for SimpleTransfer {
fn type_string() -> &'static str {
"Transfer(address to,uint64 amount)"
}
fn encode_data(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(64);
buf.extend_from_slice(&encode_address(&self.to));
buf.extend_from_slice(&encode_uint64(self.amount));
buf
}
}
#[test]
fn test_domain_type_string_full() {
let domain = test_domain();
assert_eq!(
domain.type_string(),
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
}
#[test]
fn test_domain_type_string_minimal() {
let domain = Eip712Domain::builder().name("App").build();
assert_eq!(domain.type_string(), "EIP712Domain(string name)");
}
#[test]
fn test_domain_type_string_with_salt() {
let domain = Eip712Domain::builder()
.name("App")
.chain_id(1)
.salt([0xffu8; 32])
.build();
assert_eq!(
domain.type_string(),
"EIP712Domain(string name,uint256 chainId,bytes32 salt)"
);
}
#[test]
fn test_domain_separator_deterministic() {
assert_eq!(
test_domain().domain_separator(),
test_domain().domain_separator()
);
}
#[test]
fn test_domain_separator_differs_by_chain() {
let d1 = Eip712Domain::new("App", "1", 56, test_address());
let d2 = Eip712Domain::new("App", "1", 97, test_address());
assert_ne!(d1.domain_separator(), d2.domain_separator());
}
#[test]
fn test_domain_separator_differs_by_name() {
let d1 = Eip712Domain::new("App", "1", 56, test_address());
let d2 = Eip712Domain::new("OtherApp", "1", 56, test_address());
assert_ne!(d1.domain_separator(), d2.domain_separator());
}
#[test]
fn test_domain_separator_differs_by_contract() {
let addr2: Address = "0x1111111111111111111111111111111111111111"
.parse()
.unwrap();
let d1 = Eip712Domain::new("App", "1", 56, test_address());
let d2 = Eip712Domain::new("App", "1", 56, addr2);
assert_ne!(d1.domain_separator(), d2.domain_separator());
}
#[test]
fn test_type_hash_deterministic() {
assert_eq!(SimpleTransfer::type_hash(), SimpleTransfer::type_hash());
}
#[test]
fn test_hash_struct_differs_by_field() {
let t1 = SimpleTransfer {
to: test_address(),
amount: 1000,
};
let t2 = SimpleTransfer {
to: test_address(),
amount: 2000,
};
assert_ne!(t1.hash_struct(), t2.hash_struct());
}
#[test]
fn test_hash_typed_data_deterministic() {
let domain = test_domain();
let msg = SimpleTransfer {
to: test_address(),
amount: 500,
};
assert_eq!(
hash_typed_data(&domain, &msg),
hash_typed_data(&domain, &msg)
);
}
#[test]
fn test_hash_typed_data_differs_by_domain() {
let d1 = Eip712Domain::new("App", "1", 56, test_address());
let d2 = Eip712Domain::new("App", "1", 97, test_address());
let msg = SimpleTransfer {
to: test_address(),
amount: 500,
};
assert_ne!(hash_typed_data(&d1, &msg), hash_typed_data(&d2, &msg));
}
#[test]
fn test_hash_typed_data_differs_by_message() {
let domain = test_domain();
let m1 = SimpleTransfer {
to: test_address(),
amount: 100,
};
let m2 = SimpleTransfer {
to: test_address(),
amount: 200,
};
assert_ne!(hash_typed_data(&domain, &m1), hash_typed_data(&domain, &m2));
}
#[test]
fn test_sign_and_verify() {
let signer = Bip44Signer::from_private_key(&[1u8; 32]).unwrap();
let domain = test_domain();
let msg = SimpleTransfer {
to: test_address(),
amount: 1_000_000,
};
let sig = sign_typed_data(&signer, &domain, &msg).unwrap();
let valid = verify_typed_data(&domain, &msg, &sig, signer.address()).unwrap();
assert!(valid);
}
#[test]
fn test_verify_wrong_signer_returns_false() {
let signer1 = Bip44Signer::from_private_key(&[1u8; 32]).unwrap();
let mut key2 = [1u8; 32];
key2[31] = 2;
let signer2 = Bip44Signer::from_private_key(&key2).unwrap();
let domain = test_domain();
let msg = SimpleTransfer {
to: test_address(),
amount: 42,
};
let sig = sign_typed_data(&signer1, &domain, &msg).unwrap();
let valid = verify_typed_data(&domain, &msg, &sig, signer2.address()).unwrap();
assert!(!valid);
}
#[test]
fn test_sign_deterministic() {
let signer = Bip44Signer::from_private_key(&[1u8; 32]).unwrap();
let domain = test_domain();
let msg = SimpleTransfer {
to: test_address(),
amount: 99,
};
let sig1 = sign_typed_data(&signer, &domain, &msg).unwrap();
let sig2 = sign_typed_data(&signer, &domain, &msg).unwrap();
assert_eq!(sig1.r, sig2.r);
assert_eq!(sig1.s, sig2.s);
assert_eq!(sig1.v, sig2.v);
}
#[test]
fn test_cross_domain_signature_invalid() {
let signer = Bip44Signer::from_private_key(&[1u8; 32]).unwrap();
let d1 = Eip712Domain::new("App", "1", 56, test_address());
let d2 = Eip712Domain::new("App", "1", 97, test_address());
let msg = SimpleTransfer {
to: test_address(),
amount: 1,
};
let sig = sign_typed_data(&signer, &d1, &msg).unwrap();
let valid = verify_typed_data(&d2, &msg, &sig, signer.address()).unwrap();
assert!(
!valid,
"Signature from chain 56 must not be valid on chain 97"
);
}
#[test]
fn test_encode_address_length() {
let encoded = encode_address(&test_address());
assert_eq!(encoded.len(), 32);
assert_eq!(&encoded[0..12], &[0u8; 12]);
}
#[test]
fn test_encode_uint256_zero() {
assert_eq!(encode_uint256(0), [0u8; 32]);
}
#[test]
fn test_encode_uint256_max_u128() {
let encoded = encode_uint256(u128::MAX);
assert_eq!(&encoded[0..16], &[0u8; 16]);
assert_eq!(&encoded[16..], &[0xffu8; 16]);
}
#[test]
fn test_encode_uint64_value() {
let encoded = encode_uint64(1u64);
assert_eq!(encoded[31], 1u8);
assert_eq!(&encoded[0..31], &[0u8; 31]);
}
#[test]
fn test_encode_bool_true() {
let encoded = encode_bool(true);
assert_eq!(encoded[31], 1u8);
assert_eq!(&encoded[0..31], &[0u8; 31]);
}
#[test]
fn test_encode_bool_false() {
assert_eq!(encode_bool(false), [0u8; 32]);
}
#[test]
fn test_encode_bytes_dynamic() {
let h1 = encode_bytes_dynamic(b"hello");
let h2 = encode_bytes_dynamic(b"world");
assert_ne!(h1, h2);
assert_eq!(h1.len(), 32);
}
#[test]
fn test_encode_bytes32_roundtrip() {
let raw = [0xabu8; 32];
assert_eq!(encode_bytes32(raw), raw);
}
#[test]
fn test_nested_struct_hashing() {
struct Inner {
value: u64,
}
impl Eip712Type for Inner {
fn type_string() -> &'static str {
"Inner(uint64 value)"
}
fn encode_data(&self) -> Vec<u8> {
encode_uint64(self.value).to_vec()
}
}
struct Outer {
inner: Inner,
label: Vec<u8>,
}
impl Eip712Type for Outer {
fn type_string() -> &'static str {
"Outer(Inner inner,string label)Inner(uint64 value)"
}
fn encode_data(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(64);
buf.extend_from_slice(&self.inner.hash_struct());
buf.extend_from_slice(&encode_bytes_dynamic(&self.label));
buf
}
}
let o1 = Outer {
inner: Inner { value: 1 },
label: b"foo".to_vec(),
};
let o2 = Outer {
inner: Inner { value: 2 },
label: b"foo".to_vec(),
};
assert_ne!(o1.hash_struct(), o2.hash_struct());
}
}