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