/ console / algorithms / src / ecdsa / mod.rs
mod.rs
  1  // Copyright (c) 2019-2025 Alpha-Delta Network Inc.
  2  // This file is part of the alphavm library.
  3  
  4  // Licensed under the Apache License, Version 2.0 (the "License");
  5  // you may not use this file except in compliance with the License.
  6  // You may obtain a copy of the License at:
  7  
  8  // http://www.apache.org/licenses/LICENSE-2.0
  9  
 10  // Unless required by applicable law or agreed to in writing, software
 11  // distributed under the License is distributed on an "AS IS" BASIS,
 12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  // See the License for the specific language governing permissions and
 14  // limitations under the License.
 15  
 16  #[cfg(test)]
 17  pub mod tests;
 18  
 19  mod serialize;
 20  
 21  use super::*;
 22  use alphavm_utilities::bytes_from_bits_le;
 23  
 24  use k256::{
 25      Secp256k1,
 26      ecdsa::{
 27          RecoveryId as ECDSARecoveryId,
 28          Signature,
 29          SigningKey,
 30          VerifyingKey,
 31          signature::hazmat::{PrehashSigner, PrehashVerifier},
 32      },
 33      elliptic_curve::{Curve, generic_array::typenum::Unsigned},
 34  };
 35  
 36  /// The recovery ID for an ECDSA/Secp256k1 signature.
 37  #[derive(Clone, PartialEq, Eq)]
 38  pub struct RecoveryID {
 39      /// The recovery ID.
 40      pub recovery_id: ECDSARecoveryId,
 41      /// The Ethereum chain ID (if applicable).
 42      /// None = non-Ethereum canonical
 43      /// Some(0) = ETH legacy (v = 27 or 28)
 44      /// Some(id>=1) = EIP-155(id).
 45      pub chain_id: Option<u64>,
 46  }
 47  
 48  impl RecoveryID {
 49      /// The offset to add to the recovery ID for EIP-155 signatures (>= 35).
 50      const ETH_EIP155_OFFSET: u8 = 35;
 51      /// The offset to add to the recovery ID for legacy Ethereum signatures (27 or 28).
 52      const ETH_LEGACY_OFFSET: u8 = 27;
 53  
 54      /// For your *byte* encoding (not raw Ethereum tx `v`):
 55      /// - Standard (None): write 0..=3
 56      /// - ETH legacy (Some(0)): write 27/28
 57      /// - EIP-155 (Some(id>=1)): still just write y (0/1); full `v` is derived via `to_eth_v`.
 58      #[inline]
 59      fn encoded_byte(&self) -> Result<u8> {
 60          let recovery_id = self.recovery_id.to_byte();
 61          match self.chain_id {
 62              // Standard
 63              None => Ok(recovery_id),
 64              // ETH legacy
 65              Some(0) => Ok(recovery_id.saturating_add(Self::ETH_LEGACY_OFFSET)), // 27/28
 66              // EIP-155
 67              Some(chain_id) => {
 68                  let recovery_id = (recovery_id as u64)
 69                      .saturating_add(chain_id.saturating_mul(2))
 70                      .saturating_add(Self::ETH_EIP155_OFFSET as u64);
 71                  Ok(u8::try_from(recovery_id)?)
 72              }
 73          }
 74      }
 75  }
 76  
 77  impl ToBytes for RecoveryID {
 78      fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
 79          let encoded_byte = self.encoded_byte().map_err(error)?;
 80          encoded_byte.write_le(&mut writer)
 81      }
 82  }
 83  
 84  impl FromBytes for RecoveryID {
 85      fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
 86          // Read the recovery ID byte.
 87          let recovery_id_byte = u8::read_le(&mut reader)?;
 88  
 89          // Determine if the recovery ID follows the Ethereum convention.
 90          let (recovery_id_without_offset, chain_id) = match recovery_id_byte {
 91              27 | 28 => (recovery_id_byte.saturating_sub(Self::ETH_LEGACY_OFFSET), Some(0u64)),
 92              v if v >= Self::ETH_EIP155_OFFSET => {
 93                  let y = (v.saturating_sub(Self::ETH_EIP155_OFFSET)) % 2;
 94                  let id = (v.saturating_sub(Self::ETH_EIP155_OFFSET)) / 2;
 95                  (y, Some(id as u64))
 96              }
 97              _ => (recovery_id_byte, None),
 98          };
 99  
100          // Construct the recovery ID from the byte.
101          let recovery_id = ECDSARecoveryId::from_byte(recovery_id_without_offset)
102              .ok_or_else(|| error(format!("Invalid recovery ID byte {recovery_id_byte}")))?;
103          Ok(Self { recovery_id, chain_id })
104      }
105  }
106  
107  /// An ECDSA/Secp256k1 signature (r,s) signature along with the recovery ID.
108  #[derive(Clone, PartialEq, Eq)]
109  pub struct ECDSASignature {
110      /// The (r,s) signature.
111      pub signature: Signature,
112      /// The recovery ID.
113      pub recovery_id: RecoveryID,
114  }
115  
116  impl ECDSASignature {
117      ///  The base signature size in bytes for secp256k1.
118      pub const BASE_SIGNATURE_SIZE_IN_BYTES: usize = <Secp256k1 as Curve>::FieldBytesSize::USIZE * 2;
119      /// The size of an Ethereum address in bytes.
120      pub const ETHEREUM_ADDRESS_SIZE_IN_BYTES: usize = 20;
121      /// The prehash size in bytes for secp256k1.
122      pub const PREHASH_SIZE_IN_BYTES: usize = <Secp256k1 as Curve>::FieldBytesSize::USIZE;
123      /// The ECDSA Signature size in bits for secp256k1 (including the one-byte recovery ID).
124      pub const SIGNATURE_SIZE_IN_BYTES: usize = Self::BASE_SIGNATURE_SIZE_IN_BYTES + 1;
125      /// The compressed VerifyingKey size in bytes for secp256k1 (32 byte field + one-byte header).
126      pub const VERIFYING_KEY_SIZE_IN_BYTES: usize = <Secp256k1 as Curve>::FieldBytesSize::USIZE + 1;
127  
128      /// Returns the recovery ID.
129      pub const fn recovery_id(&self) -> ECDSARecoveryId {
130          self.recovery_id.recovery_id
131      }
132  
133      /// Returns a signature on a `message` using the given `signing_key` and hash function.
134      pub fn sign<H: Hash<Output = Vec<bool>>>(
135          signing_key: &SigningKey,
136          hasher: &H,
137          message: &[H::Input],
138      ) -> Result<Self> {
139          // Hash the message.
140          let hash_bits = hasher.hash(message)?;
141          // Convert the hash output to bytes.
142          let hash_bytes = bytes_from_bits_le(&hash_bits);
143  
144          // Sign the prehashed message.
145          signing_key
146              .sign_prehash(&hash_bytes)
147              .map(|(signature, recovery_id)| {
148                  let recovery_id = RecoveryID { recovery_id, chain_id: None };
149                  Self { signature, recovery_id }
150              })
151              .map_err(|e| anyhow!("Failed to sign message: {e:?}"))
152      }
153  
154      /// Recover the public key from `(r,s, recovery_id)` using *your* hasher on `message`.
155      pub fn recover_public_key<H: Hash<Output = Vec<bool>>>(
156          &self,
157          hasher: &H,
158          message: &[H::Input],
159      ) -> Result<VerifyingKey> {
160          // Hash the message.
161          let hash_bits = hasher.hash(message)?;
162  
163          // Recover the public key using the prehash.
164          self.recover_public_key_with_digest(&hash_bits)
165      }
166  
167      /// Recover the public key from `(r,s, recovery_id)` using *your* hasher on `message`.
168      pub fn recover_public_key_with_digest(&self, digest_bits: &[bool]) -> Result<VerifyingKey> {
169          // Convert the digest output to bytes.
170          let digest = bytes_from_bits_le(digest_bits);
171  
172          // Recover the public key using the prehash.
173          VerifyingKey::recover_from_prehash(&digest, &self.signature, self.recovery_id())
174              .map_err(|e| anyhow!("Failed to recover public key: {e:?}"))
175      }
176  
177      /// Verify `(r,s)` against `verifying_key` using *your* hasher on `message`.
178      pub fn verify<H: Hash<Output = Vec<bool>>>(
179          &self,
180          verifying_key: &VerifyingKey,
181          hasher: &H,
182          message: &[H::Input],
183      ) -> Result<()> {
184          // Hash the message.
185          let hash_bits = hasher.hash(message)?;
186  
187          // Verify the signature using the prehash.
188          self.verify_with_digest(verifying_key, &hash_bits)
189      }
190  
191      /// Verify `(r,s)` against `verifying_key` using the provided `digest`.
192      pub fn verify_with_digest(&self, verifying_key: &VerifyingKey, digest_bits: &[bool]) -> Result<()> {
193          // Convert the digest output to bytes.
194          let digest = bytes_from_bits_le(digest_bits);
195  
196          // Verify the signature using the prehash digest.
197          verifying_key.verify_prehash(&digest, &self.signature).map_err(|e| anyhow!("Failed to verify signature: {e:?}"))
198      }
199  
200      /// Verify `(r,s)` against `verifying_key` using *your* hasher on `message`.
201      pub fn verify_ethereum<H: Hash<Output = Vec<bool>>>(
202          &self,
203          ethereum_address: &[u8; Self::ETHEREUM_ADDRESS_SIZE_IN_BYTES],
204          hasher: &H,
205          message: &[H::Input],
206      ) -> Result<()> {
207          // Hash the message.
208          let hash_bits = hasher.hash(message)?;
209  
210          // Ensure that the derived Ethereum address matches the provided one.
211          self.verify_ethereum_with_digest(ethereum_address, &hash_bits)
212      }
213  
214      /// Verify `(r,s)` against `verifying_key` using *your* hasher on `message`.
215      pub fn verify_ethereum_with_digest(
216          &self,
217          ethereum_address: &[u8; Self::ETHEREUM_ADDRESS_SIZE_IN_BYTES],
218          digest_bits: &[bool],
219      ) -> Result<()> {
220          // Derive the verifying key from the signature.
221          let verifying_key = self.recover_public_key_with_digest(digest_bits)?;
222  
223          // Ensure that the derived Ethereum address matches the provided one.
224          let derived_ethereum_address = Self::ethereum_address_from_public_key(&verifying_key)?;
225          ensure!(
226              &derived_ethereum_address == ethereum_address,
227              "Derived Ethereum address does not match the provided address."
228          );
229  
230          Ok(())
231      }
232  
233      /// Converts a VerifyingKey to an Ethereum address (20 bytes).
234      pub fn ethereum_address_from_public_key(
235          verifying_key: &VerifyingKey,
236      ) -> Result<[u8; Self::ETHEREUM_ADDRESS_SIZE_IN_BYTES]> {
237          // Get the uncompressed public key bytes as [0x04, x_bytes..., y_bytes...]
238          let public_key_point = verifying_key.to_encoded_point(false);
239          let public_key_bytes = public_key_point.as_bytes();
240  
241          // Skip the 0x04 prefix, keep only the x and y coordinates (64 bytes)
242          let coordinates_only = &public_key_bytes[1..]; // 32 bytes x + 32 bytes y
243  
244          // Step 3: Hash the coordinates with Keccak256
245          let address_hash = Keccak256::default().hash(&coordinates_only.to_bits_le())?;
246          let address_bytes = bytes_from_bits_le(&address_hash);
247  
248          // Step 4: Take the last 20 bytes as the Ethereum address
249          let mut ethereum_address = [0u8; Self::ETHEREUM_ADDRESS_SIZE_IN_BYTES];
250          ethereum_address.copy_from_slice(&address_bytes[12..32]);
251  
252          Ok(ethereum_address)
253      }
254  
255      /// Parses a verifying key from bytes.
256      pub fn verifying_key_from_bytes(bytes: &[u8]) -> Result<VerifyingKey> {
257          VerifyingKey::from_sec1_bytes(bytes).map_err(|e| anyhow!("Failed to parse verifying key: {e:?}"))
258      }
259  }
260  
261  impl ToBytes for ECDSASignature {
262      fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
263          // Write the signature bytes.
264          self.signature.to_bytes().to_vec().write_le(&mut writer)?;
265          // Write the recovery ID.
266          self.recovery_id.write_le(&mut writer)
267      }
268  }
269  
270  impl FromBytes for ECDSASignature {
271      fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
272          // Read the signature bytes.
273          let mut bytes = vec![0u8; Self::BASE_SIGNATURE_SIZE_IN_BYTES];
274          reader.read_exact(&mut bytes)?;
275          // Construct the signature from the bytes.
276          let signature = Signature::from_slice(&bytes).map_err(error)?;
277  
278          // Read the recovery ID
279          let recovery_id = RecoveryID::read_le(&mut reader)?;
280  
281          Ok(Self { signature, recovery_id })
282      }
283  }
284  
285  impl FromStr for ECDSASignature {
286      type Err = Error;
287  
288      /// Parses a hex-encoded string into an ECDSASignature.
289      fn from_str(signature: &str) -> Result<Self, Self::Err> {
290          let mut s = signature.trim();
291  
292          // Accept optional 0x prefix
293          if let Some(rest) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
294              s = rest;
295          }
296  
297          // Decode the hex string into bytes.
298          let bytes = hex::decode(s)?;
299  
300          // Construct the signature from the bytes.
301          Self::from_bytes_le(&bytes)
302      }
303  }
304  
305  impl Debug for ECDSASignature {
306      fn fmt(&self, f: &mut Formatter) -> fmt::Result {
307          Display::fmt(self, f)
308      }
309  }
310  
311  impl Display for ECDSASignature {
312      /// Writes the signature as a hex string.
313      fn fmt(&self, f: &mut Formatter) -> fmt::Result {
314          write!(f, "{}", hex::encode(self.to_bytes_le().map_err(|_| fmt::Error)?))
315      }
316  }
317  
318  #[cfg(test)]
319  mod test_helpers {
320      use super::*;
321  
322      pub(crate) type DefaultHasher = Keccak256;
323  
324      /// Samples a random ecdsa signature.
325      pub(super) fn sample_ecdsa_signature<H: Hash<Output = Vec<bool>, Input = bool>>(
326          num_bytes: usize,
327          hasher: &H,
328          rng: &mut TestRng,
329      ) -> (SigningKey, Vec<u8>, ECDSASignature) {
330          // Sample a random signing key.
331          let signing_key = SigningKey::random(rng);
332  
333          // Sample a random message.
334          let message: Vec<u8> = (0..num_bytes).map(|_| rng.r#gen()).collect::<Vec<_>>();
335  
336          // Sign the message.
337          let signature = ECDSASignature::sign::<H>(&signing_key, hasher, &message.to_bits_le()).unwrap();
338  
339          // Return the signing key, message, and signature.
340          (signing_key, message, signature)
341      }
342  }