Skip to main content

miden_core_lib/handlers/
ecdsa.rs

1//! ECDSA signature verification precompile for the Miden VM.
2//!
3//! This module provides both execution-time and verification-time support for ECDSA signature
4//! verification using the secp256k1 curve with Keccak256 hashing.
5//!
6//! ## Architecture
7//!
8//! ### Event Handler (Execution-Time)
9//! When the VM emits an ECDSA verification event requesting signature validation, the processor
10//! calls [`EcdsaPrecompile`] which reads the public key, message digest, and signature from
11//! memory, performs the verification, provides the boolean result via the advice stack, and logs
12//! the request data for deferred verification.
13//!
14//! ### Precompile Verifier (Verification-Time)
15//! During verification, the [`PrecompileVerifier`] receives the stored request data (public key,
16//! digest, signature), re-performs the ECDSA verification, and generates a commitment
17//! `P2(P2(P2(pk) || P2(digest)) || P2(sig))`, where P2 stands for Poseidon2, with a tag containing
18//! the verification result that validates the computation was performed correctly. Here `pk` is
19//! hashed as affine-coordinate elements; `digest` and `sig` are hashed as u32‑packed field elements
20//! before being merged.
21//!
22//! ### Commitment Tag Format
23//! Each request is tagged as `[event_id, result, 0, 0]` where `result` is 1 for valid signatures
24//! and 0 for invalid ones. This allows the verifier to check that the execution-time result
25//! matches the verification-time result.
26//!
27//! ## Data Format
28//! - **Public Key**: 64 bytes of affine coordinates (`qx_le_u32[8] || qy_le_u32[8]`) packed as 16
29//!   u32 limbs
30//! - **Message Digest**: 32 bytes (Keccak256 hash of the message)
31//! - **Signature**: 65 bytes (implementation‑defined serialization used by
32//!   `miden_crypto::dsa::ecdsa_k256_keccak::Signature`). When packed into u32 elements for VM
33//!   memory, the final word contains 3 zero padding bytes (since 65 ≡ 1 mod 4).
34
35use alloc::{vec, vec::Vec};
36
37use miden_core::{
38    Felt,
39    events::EventName,
40    field::PrimeCharacteristicRing,
41    precompile::{PrecompileCommitment, PrecompileError, PrecompileRequest, PrecompileVerifier},
42    serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
43    utils::{bytes_to_packed_u32_elements, packed_u32_elements_to_bytes},
44};
45use miden_crypto::{
46    SequentialCommit, ZERO,
47    dsa::ecdsa_k256_keccak::{PublicKey, Signature},
48    hash::poseidon2::Poseidon2,
49};
50use miden_processor::{
51    ProcessorState,
52    advice::AdviceMutation,
53    event::{EventError, EventHandler},
54};
55
56use crate::handlers::read_memory_packed_u32;
57
58/// Qualified event name for the ECDSA signature verification event.
59pub const ECDSA_VERIFY_EVENT_NAME: EventName =
60    EventName::new("miden::core::crypto::dsa::ecdsa_k256_keccak::verify");
61
62const PUBLIC_KEY_LEN_FELTS: usize = 16;
63const PUBLIC_KEY_LEN_BYTES: usize = PUBLIC_KEY_LEN_FELTS * 4;
64const MESSAGE_DIGEST_LEN_BYTES: usize = 32;
65const SIGNATURE_LEN_BYTES: usize = 65; // r (32) + s (32) + v (1)
66
67const PRECOMPILE_REQUEST_LEN: usize =
68    PUBLIC_KEY_LEN_BYTES + MESSAGE_DIGEST_LEN_BYTES + SIGNATURE_LEN_BYTES;
69
70/// ECDSA signature verification precompile handler.
71pub struct EcdsaPrecompile;
72
73impl EventHandler for EcdsaPrecompile {
74    /// ECDSA verification event handler called by the processor when the VM emits a signature
75    /// verification request event.
76    ///
77    /// Reads the public key, signature, and message digest from memory, performs ECDSA signature
78    /// verification, provides the result via the advice stack, and stores the request data for
79    /// verification (see [`PrecompileVerifier`]).
80    ///
81    /// ## Input Format
82    /// - **Stack**: `[event_id, ptr_pk, ptr_digest, ptr_sig, ...]` where all pointers are
83    ///   word-aligned (divisible by 4)
84    /// - **Memory**: Data stored as packed u32 field elements (4 bytes per element, little-endian)
85    ///   with unused bytes in the final u32 set to zero
86    ///
87    /// ## Output Format
88    /// - **Advice Stack**: Extended with verification result (1 for valid, 0 for invalid)
89    /// - **Precompile Request**: Stores tag `[event_id, result, 0, 0]` and serialized request data
90    ///   (pk || digest || sig) for verification time
91    fn on_event(&self, process: &ProcessorState) -> Result<Vec<AdviceMutation>, EventError> {
92        // Stack: [event_id, ptr_pk, ptr_digest, ptr_sig, ...]
93        let ptr_pk = process.get_stack_item(1).as_canonical_u64();
94        let ptr_digest = process.get_stack_item(2).as_canonical_u64();
95        let ptr_sig = process.get_stack_item(3).as_canonical_u64();
96
97        let pk = {
98            let data_type = DataType::PublicKey;
99            let bytes = read_memory_packed_u32(process, ptr_pk, PUBLIC_KEY_LEN_BYTES)
100                .map_err(|source| EcdsaError::ReadError { data_type, source })?;
101            public_key_from_affine_coordinate_bytes(&bytes)
102                .map_err(|source| EcdsaError::DeserializeError { data_type, source })?
103        };
104
105        let sig = {
106            let data_type = DataType::Signature;
107            let bytes = read_memory_packed_u32(process, ptr_sig, SIGNATURE_LEN_BYTES)
108                .map_err(|source| EcdsaError::ReadError { data_type, source })?;
109            Signature::read_from_bytes(&bytes)
110                .map_err(|source| EcdsaError::DeserializeError { data_type, source })?
111        };
112
113        let digest = read_memory_packed_u32(process, ptr_digest, MESSAGE_DIGEST_LEN_BYTES)
114            .map_err(|source| EcdsaError::ReadError { data_type: DataType::Digest, source })?
115            .try_into()
116            .expect("digest is exactly 32 bytes");
117
118        let request = EcdsaRequest::new(pk, digest, sig);
119        let result = request.result();
120
121        Ok(vec![
122            AdviceMutation::extend_stack([Felt::from_bool(result)]),
123            AdviceMutation::extend_precompile_requests([request.into()]),
124        ])
125    }
126}
127
128impl PrecompileVerifier for EcdsaPrecompile {
129    /// Verifier for ECDSA signature verification at verification time.
130    ///
131    /// Receives the serialized request data (public key || digest || signature) stored during
132    /// execution (see [`EventHandler::on_event`]), re-performs the ECDSA verification, and
133    /// generates a commitment `P2(P2(P2(pk) || P2(digest)) || P2(sig))` with tag
134    /// `[event_id, result, 0, 0]` that validates against the execution trace. `pk` is hashed as
135    /// affine-coordinate elements.
136    fn verify(&self, calldata: &[u8]) -> Result<PrecompileCommitment, PrecompileError> {
137        let request = EcdsaRequest::read_from_bytes(calldata)?;
138        Ok(request.as_precompile_commitment())
139    }
140}
141
142/// ECDSA signature verification request containing all data needed to verify a signature.
143///
144/// This structure encapsulates a complete ECDSA verification request including the public key,
145/// message digest, and signature. It is used during both execution (via the event handler) and
146/// verification (via the precompile verifier).
147pub struct EcdsaRequest {
148    /// secp256k1 public key
149    pk: PublicKey,
150    /// Message digest (32 bytes, typically Keccak256 hash)
151    digest: [u8; MESSAGE_DIGEST_LEN_BYTES],
152    /// ECDSA signature (serialized by the implementation; 65 bytes in this crate)
153    sig: Signature,
154}
155
156impl EcdsaRequest {
157    /// Creates a new ECDSA verification request.
158    ///
159    /// # Arguments
160    /// * `pk` - The secp256k1 public key
161    /// * `digest` - The message digest (32 bytes)
162    /// * `sig` - The ECDSA signature
163    pub fn new(pk: PublicKey, digest: [u8; MESSAGE_DIGEST_LEN_BYTES], sig: Signature) -> Self {
164        Self { pk, digest, sig }
165    }
166
167    /// Returns a reference to the public key.
168    pub fn pk(&self) -> &PublicKey {
169        &self.pk
170    }
171
172    /// Returns a reference to the digest.
173    pub fn digest(&self) -> &[u8; MESSAGE_DIGEST_LEN_BYTES] {
174        &self.digest
175    }
176
177    /// Returns a reference to the signature.
178    pub fn sig(&self) -> &Signature {
179        &self.sig
180    }
181
182    /// Converts this request into a [`PrecompileRequest`] for deferred verification.
183    ///
184    /// Serializes the request data (public key || digest || signature) and wraps it in a
185    /// PrecompileRequest with the ECDSA event ID.
186    pub fn as_precompile_request(&self) -> PrecompileRequest {
187        let mut calldata = Vec::with_capacity(PRECOMPILE_REQUEST_LEN);
188        self.write_into(&mut calldata);
189        PrecompileRequest::new(ECDSA_VERIFY_EVENT_NAME.to_event_id(), calldata)
190    }
191
192    /// Performs ECDSA signature verification and returns the result.
193    ///
194    /// Returns `true` if the signature is valid for the given public key and digest,
195    /// `false` otherwise.
196    pub fn result(&self) -> bool {
197        self.pk.verify_prehash(self.digest, &self.sig)
198    }
199
200    /// Computes the precompile commitment for this request.
201    ///
202    /// The commitment is `P2(P2(P2(pk) || P2(digest)) || P2(sig))` with tag
203    /// `[event_id, result, 0, 0]`, where `result` is 1 for valid signatures and 0 for
204    /// invalid ones. The public key commitment is `PublicKey::to_commitment()`.
205    ///
206    /// This is called by the [`PrecompileVerifier`] at verification time and must match
207    /// the commitment generated during execution.
208    pub fn as_precompile_commitment(&self) -> PrecompileCommitment {
209        // Compute tag: [event_id, result, 0, 0]
210        let result = Felt::from_bool(self.result());
211        let tag = [ECDSA_VERIFY_EVENT_NAME.to_event_id().as_felt(), result, ZERO, ZERO].into();
212
213        let pk_comm = self.pk.to_commitment();
214        let digest_comm = {
215            // `digest` is a 32‑byte array; hash its u32‑packed representation
216            let felts = bytes_to_packed_u32_elements(&self.digest);
217            Poseidon2::hash_elements(&felts)
218        };
219        let sig_comm = {
220            let felts = bytes_to_packed_u32_elements(&self.sig.to_bytes());
221            Poseidon2::hash_elements(&felts)
222        };
223
224        let commitment = Poseidon2::merge(&[Poseidon2::merge(&[pk_comm, digest_comm]), sig_comm]);
225
226        PrecompileCommitment::new(tag, commitment)
227    }
228}
229
230impl Serializable for EcdsaRequest {
231    fn write_into<W: ByteWriter>(&self, target: &mut W) {
232        target.write_bytes(&packed_u32_elements_to_bytes(&self.pk.to_elements()));
233        self.digest.write_into(target);
234        self.sig.write_into(target);
235    }
236}
237
238impl Deserializable for EcdsaRequest {
239    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
240        let pk_bytes: [u8; PUBLIC_KEY_LEN_BYTES] = source.read_array()?;
241        let pk = public_key_from_affine_coordinate_bytes(&pk_bytes)?;
242        let digest = source.read_array()?;
243        let sig = Signature::read_from(source)?;
244        Ok(Self { pk, digest, sig })
245    }
246}
247
248impl From<EcdsaRequest> for PrecompileRequest {
249    fn from(request: EcdsaRequest) -> Self {
250        request.as_precompile_request()
251    }
252}
253
254fn public_key_from_affine_coordinate_bytes(
255    bytes: &[u8],
256) -> Result<PublicKey, DeserializationError> {
257    if bytes.len() != PUBLIC_KEY_LEN_BYTES {
258        return Err(DeserializationError::InvalidValue("invalid public key length".into()));
259    }
260
261    let mut compressed = [0u8; 33];
262    compressed[0] = if bytes[32] & 1 == 0 { 0x02 } else { 0x03 };
263    for (dst, src) in compressed[1..].iter_mut().zip(bytes[..32].iter().rev()) {
264        *dst = *src;
265    }
266
267    let pk = PublicKey::read_from_bytes(&compressed)?;
268    let pk_bytes = packed_u32_elements_to_bytes(&pk.to_elements());
269    if pk_bytes.as_slice() == bytes {
270        Ok(pk)
271    } else {
272        Err(DeserializationError::InvalidValue("invalid public key coordinates".into()))
273    }
274}
275
276// ERROR TYPES
277// ================================================================================================
278
279/// Type of data being read/processed during ECDSA verification.
280#[derive(Debug, Clone, Copy)]
281pub(crate) enum DataType {
282    PublicKey,
283    Signature,
284    Digest,
285}
286
287impl core::fmt::Display for DataType {
288    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
289        match self {
290            DataType::PublicKey => write!(f, "public key"),
291            DataType::Signature => write!(f, "signature"),
292            DataType::Digest => write!(f, "digest"),
293        }
294    }
295}
296
297/// Error types that can occur during ECDSA signature verification operations.
298#[derive(Debug, thiserror::Error)]
299pub(crate) enum EcdsaError {
300    /// Failed to read data from memory.
301    #[error("failed to read {data_type} from memory")]
302    ReadError {
303        data_type: DataType,
304        #[source]
305        source: crate::handlers::MemoryReadError,
306    },
307
308    /// Failed to deserialize data.
309    #[error("failed to deserialize {data_type}")]
310    DeserializeError {
311        data_type: DataType,
312        #[source]
313        source: DeserializationError,
314    },
315}