Skip to main content

miden_standards/account/auth/
eip712.rs

1//! EIP-712 encoding for Miden transaction-summary signatures.
2//!
3//! The signed typed data is:
4//! `MidenTransaction(bytes32 txSummaryHash)` in the domain
5//! `EIP712Domain(string name,string version)` with name `Miden Transaction` and version `1`.
6//! The domain deliberately omits `chainId` and `verifyingContract`; the transaction-summary hash
7//! already commits to the Miden network state and executing account. `txSummaryHash` is encoded by
8//! [`Word::as_bytes`]: four field elements in word order, each as a little-endian `u64`.
9//!
10//! The witness advice-map key is
11//! `h(h(PK_COMM, txSummaryHash), [0x323137504945, 0, 0, 0])`, where the tag is little-endian ASCII
12//! `EIP712`.
13//!
14//! A signature witness is stored under the key returned by
15//! [`Eip712TransactionSummary::eip712_signature_key`] and contains the encoded secp256k1 public
16//! key followed by the ECDSA signature, as expected by Miden's `ecdsa_k256_keccak` verifier.
17
18use alloc::vec::Vec;
19
20use miden_core_lib::dsa::ecdsa_k256_keccak::encode_signature;
21use miden_protocol::account::auth::PublicKeyCommitment;
22use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{PublicKey, Signature};
23use miden_protocol::crypto::hash::keccak::Keccak256;
24use miden_protocol::transaction::TransactionSummary;
25use miden_protocol::{Felt, Hasher, Word};
26
27// EIP-191 message prefix.
28const EIP191_PREFIX: u8 = 0x19;
29
30// EIP-191 version byte assigned to EIP-712 typed data.
31const EIP712_VERSION: u8 = 0x01;
32
33// hashStruct(EIP712Domain({ name: "Miden Transaction", version: "1" })).
34const DOMAIN_SEPARATOR: [u8; 32] = [
35    0xd2, 0x99, 0x3b, 0x31, 0x72, 0x06, 0xdc, 0x17, 0xb9, 0x59, 0x70, 0x00, 0x08, 0x48, 0x82, 0x92,
36    0x43, 0x11, 0xd2, 0x36, 0xaa, 0xa8, 0xfc, 0xcd, 0x54, 0xd6, 0xce, 0x4f, 0xaa, 0xce, 0xc6, 0xb7,
37];
38
39// keccak256("MidenTransaction(bytes32 txSummaryHash)").
40const TRANSACTION_TYPE_HASH: [u8; 32] = [
41    0xd4, 0x6c, 0xfc, 0xb2, 0xf8, 0x1c, 0xad, 0x54, 0x42, 0x3e, 0x73, 0x1d, 0x56, 0x4b, 0xb1, 0xa2,
42    0x60, 0x60, 0xc1, 0xfe, 0xa0, 0x1f, 0xff, 0x7f, 0x86, 0xa0, 0xcd, 0x79, 0x6c, 0xaf, 0x2b, 0x63,
43];
44
45/// Must match `SIGNATURE_KEY_DOMAIN` in the MASM transaction-summary adapter.
46const SIGNATURE_KEY_DOMAIN: u64 = u64::from_le_bytes(*b"EIP712\0\0");
47
48/// A 32-byte EIP-712 signing digest.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub struct Eip712Digest([u8; 32]);
51
52impl Eip712Digest {
53    /// Returns the raw digest bytes.
54    pub const fn as_bytes(&self) -> &[u8; 32] {
55        &self.0
56    }
57
58    /// Consumes the digest and returns its raw bytes.
59    pub const fn into_bytes(self) -> [u8; 32] {
60        self.0
61    }
62}
63
64/// EIP-712 signing helpers for a Miden transaction summary.
65pub trait Eip712TransactionSummary {
66    /// Computes the [`Eip712Digest`] for this transaction summary.
67    fn eip712_hash(&self) -> Eip712Digest;
68
69    /// Computes the advice-map key for this transaction summary and public key.
70    fn eip712_signature_key(&self, public_key: PublicKeyCommitment) -> Word;
71
72    /// Builds the advice-map entry for an EIP-712 signature over this transaction summary.
73    ///
74    /// The returned tuple contains the advice-map key followed by the encoded public-key and
75    /// signature witness. `signature` must sign the digest returned by [`Self::eip712_hash`] using
76    /// the secret key corresponding to `public_key`.
77    fn eip712_signature_advice(
78        &self,
79        public_key: &PublicKey,
80        signature: &Signature,
81    ) -> (Word, Vec<Felt>) {
82        let key = self.eip712_signature_key(public_key.to_commitment().into());
83        (key, encode_signature(public_key, signature))
84    }
85}
86
87impl Eip712TransactionSummary for TransactionSummary {
88    fn eip712_hash(&self) -> Eip712Digest {
89        let mut struct_preimage = [0u8; 64];
90        struct_preimage[..32].copy_from_slice(&TRANSACTION_TYPE_HASH);
91        struct_preimage[32..].copy_from_slice(&self.to_commitment().as_bytes());
92        let struct_hash: [u8; 32] = Keccak256::hash(&struct_preimage).into();
93
94        let mut digest_preimage = [0u8; 66];
95        digest_preimage[..2].copy_from_slice(&[EIP191_PREFIX, EIP712_VERSION]);
96        digest_preimage[2..34].copy_from_slice(&DOMAIN_SEPARATOR);
97        digest_preimage[34..].copy_from_slice(&struct_hash);
98
99        Eip712Digest(Keccak256::hash(&digest_preimage).into())
100    }
101
102    fn eip712_signature_key(&self, public_key: PublicKeyCommitment) -> Word {
103        let raw_signature_key = Hasher::merge(&[public_key.into(), self.to_commitment()]);
104        let domain = Word::new([
105            Felt::new_unchecked(SIGNATURE_KEY_DOMAIN),
106            Felt::ZERO,
107            Felt::ZERO,
108            Felt::ZERO,
109        ]);
110        Hasher::merge(&[raw_signature_key, domain])
111    }
112}