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