Skip to main content

paralock_core/
adaptor.rs

1//! ECDSA adaptor signatures on secp256k1, for scriptless atomic swaps (a port of `@paralock/adaptor`).
2//!
3//! A pre-signature on digest `m` under key `x`, encrypted to an adaptor point `Y = y·G`:
4//!
5//! ```text
6//! k random, R' = k·G, R = k·Y, r = R.x mod n, s' = k⁻¹(m + r·x) mod n,
7//! plus a DLEQ proof that log_G(R') = log_Y(R).
8//! ```
9//!
10//! Anyone can check it ([`pre_verify`]) without learning `y`; a successful check returns a [`VerifiedPreSignature`],
11//! the only thing [`adapt`] accepts. Whoever knows `y` turns it into a normal ECDSA signature: `s = s'·y⁻¹` (the
12//! effective nonce is `k·y`, so `(r, s)` verifies under `x`). Whoever holds the pre-signature and later sees that
13//! signature learns the secret: `y = s'·s⁻¹` up to sign, because signatures are normalized to low-S — see
14//! [`extract`].
15//!
16//! **Completing a pre-signature hands `y` to its creator** as soon as the signature is public, whether or not the
17//! transaction carrying it succeeds: a reverted EVM claim still publishes `(r, s)` in its calldata. That is why
18//! [`adapt`] takes only verified pre-signatures (so the result is a valid signature on the digest that was checked),
19//! and why an escrow claim must be simulated (`eth_call`) before it is broadcast; see
20//! [`crate::protocol::verify_adaptor_lock`] and [`crate::protocol::complete_escrow_claim`].
21//!
22//! Paralock uses this so that completing a signature on one chain reveals the secret needed on the other chain, with
23//! no hash or other shared value on-chain.
24//!
25//! Wire compatibility: the nonce derivation, the DLEQ challenge and the 162-byte encoding are identical to the
26//! TypeScript reference, so pre-signatures cross languages in both directions. Nonces are hedged:
27//! `sha512(tag ‖ secret ‖ context ‖ 32 random bytes) mod n`, with the randomness drawn from the caller's
28//! [`CryptoRngCore`], so a repeated RNG output still can't produce the same nonce for two different messages.
29
30use core::fmt;
31
32use k256::elliptic_curve::bigint::U512;
33use k256::elliptic_curve::ops::Reduce;
34use k256::{NonZeroScalar, ProjectivePoint, Scalar, WideBytes};
35use rand_core::CryptoRngCore;
36use sha2::{Digest, Sha256, Sha512};
37use subtle::ConstantTimeEq;
38use zeroize::{Zeroize, Zeroizing};
39
40use crate::encoding::{self, EncodingError};
41use crate::keys::{
42    CURVE_ORDER, KeyError, PrivateKey, PublicKey, finalize_wiped, lift_x, nonzero_scalar_from_bytes, reduce32,
43    scalar_to_bytes, x_mod_n,
44};
45
46pub use crate::keys::Signature;
47
48/// Domain tag of the pre-signature nonce.
49pub const NONCE_TAG: &[u8] = b"paralock/adaptor/nonce/v1";
50/// Domain tag of the DLEQ proof nonce.
51pub const DLEQ_NONCE_TAG: &[u8] = b"paralock/adaptor/dleq-nonce/v1";
52/// Domain tag of the DLEQ challenge: `sha256(DLEQ_TAG ‖ G ‖ Y ‖ A ‖ B ‖ T1 ‖ T2) mod n` over 33-byte compressed
53/// points. Changing it would break wire compatibility.
54pub const DLEQ_TAG: &[u8] = b"paralock/adaptor/dleq/v1";
55/// Size of an encoded pre-signature: `R(33) ‖ R'(33) ‖ s'(32) ‖ c(32) ‖ z(32)`.
56pub const PRE_SIGNATURE_SIZE: usize = 162;
57
58/// Bound on nonce retries (each retry has probability ~2⁻¹²⁸ or less); reaching it means the RNG is broken. The
59/// reference's `MAX_NONCE_ATTEMPTS` (spec §5.2).
60const MAX_NONCE_ATTEMPTS: usize = 8;
61
62/// An adaptor-signature error.
63#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
64#[non_exhaustive]
65pub enum Error {
66    /// Malformed hex or length.
67    #[error(transparent)]
68    Encoding(#[from] EncodingError),
69    /// An invalid key or signature.
70    #[error(transparent)]
71    Key(#[from] KeyError),
72    /// The adaptor secret is zero or not below the curve order.
73    #[error("adaptor secret is out of range")]
74    SecretOutOfRange,
75    /// The pre-signature's `s'` is zero or not below the curve order.
76    #[error("pre-signature s is out of range")]
77    PreSignatureOutOfRange,
78    /// The pre-signature's `R` is not a valid compressed point.
79    #[error("pre-signature R is not a valid point")]
80    InvalidPreSignaturePoint,
81    /// The signature's `r` is not the pre-signature's.
82    #[error("signature does not match the pre-signature")]
83    SignatureMismatch,
84    /// Neither sign of `s'·s⁻¹` is the discrete log of the adaptor point.
85    #[error("could not extract the adaptor secret")]
86    ExtractFailed,
87    /// Neither recovery bit recovers the public key.
88    #[error("signature does not recover to the public key")]
89    NoRecoveryId,
90    /// The RNG failed (`try_fill_bytes` returned an error) or kept producing unusable nonces (it is broken).
91    #[error("could not derive a nonce; the random number generator is broken")]
92    NonceGenerationFailed,
93    /// [`pre_verify`] failed: the pre-signature is malformed, or does not complete to a signature by this key on this
94    /// digest for this adaptor point.
95    #[error("pre-signature does not verify")]
96    InvalidPreSignature,
97    /// The adaptor secret is not the discrete log of the pre-signature's adaptor point.
98    #[error("adaptor secret does not match the adaptor point")]
99    WrongAdaptorSecret,
100}
101
102/// An adaptor secret `y` in `[1, n)`. Zeroized on drop, compared in constant time, never printed.
103#[derive(Clone, PartialEq, Eq)]
104pub struct AdaptorSecret(PrivateKey);
105
106impl AdaptorSecret {
107    /// A fresh random secret (panics if the RNG fails; see [`Self::try_random`]).
108    pub fn random<R: CryptoRngCore + ?Sized>(rng: &mut R) -> Self {
109        Self(PrivateKey::random(rng))
110    }
111
112    /// A fresh random secret, or [`Error::NonceGenerationFailed`] if the RNG fails.
113    pub fn try_random<R: CryptoRngCore + ?Sized>(rng: &mut R) -> Result<Self, Error> {
114        PrivateKey::try_random(rng).map(Self).map_err(|_| Error::NonceGenerationFailed)
115    }
116
117    /// A secret from 32 big-endian bytes. Fails unless `1 ≤ y < n`.
118    pub fn from_bytes(bytes: &[u8; 32]) -> Result<Self, Error> {
119        PrivateKey::from_bytes(bytes).map(Self).map_err(|_| Error::SecretOutOfRange)
120    }
121
122    /// A secret from 64 hex digits (with or without `0x`).
123    pub fn from_hex(s: &str) -> Result<Self, Error> {
124        let bytes = Zeroizing::new(encoding::decode_hex(s)?);
125        let array = Zeroizing::new(encoding::to_array::<32>(&bytes, "adaptor secret")?);
126        Self::from_bytes(&array)
127    }
128
129    /// The 32-byte big-endian secret, zeroized when dropped. Handle with care.
130    pub fn to_bytes(&self) -> Zeroizing<[u8; 32]> {
131        self.0.to_bytes()
132    }
133
134    /// Lowercase hex (no `0x`, `secretToHex`), zeroized when dropped.
135    pub fn to_hex(&self) -> Zeroizing<String> {
136        Zeroizing::new(encoding::to_hex(&*self.to_bytes()))
137    }
138
139    /// The adaptor point `Y = y·G`.
140    pub fn point(&self) -> PublicKey {
141        self.0.public_key()
142    }
143
144    fn scalar(&self) -> Zeroizing<Scalar> {
145        Zeroizing::new(*self.0.scalar())
146    }
147}
148
149impl ConstantTimeEq for AdaptorSecret {
150    fn ct_eq(&self, other: &Self) -> subtle::Choice {
151        self.to_bytes().ct_eq(&*other.to_bytes())
152    }
153}
154
155impl fmt::Debug for AdaptorSecret {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        f.write_str("AdaptorSecret(<redacted>)")
158    }
159}
160
161/// A fresh adaptor secret `y` and its point `Y = y·G` (`adaptorKeyPair`).
162pub fn adaptor_key_pair<R: CryptoRngCore + ?Sized>(rng: &mut R) -> (AdaptorSecret, PublicKey) {
163    let secret = AdaptorSecret::random(rng);
164    let point = secret.point();
165    (secret, point)
166}
167
168/// Chaum–Pedersen proof that `log_G(R') = log_Y(R)`: challenge `c` and response `z`, as 32-byte big-endian
169/// integers (range-checked by [`pre_verify`], not when decoded).
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
171pub struct DleqProof {
172    /// Challenge.
173    pub c: [u8; 32],
174    /// Response.
175    pub z: [u8; 32],
176}
177
178/// An ECDSA adaptor pre-signature. Decoding checks only the length; [`pre_verify`] checks the points and ranges,
179/// and must be called on anything received from a counterparty.
180#[derive(Clone, Copy, PartialEq, Eq, Hash)]
181pub struct PreSignature {
182    /// `R = k·Y`, compressed. Its x coordinate is the final signature's `r`.
183    pub r_point: [u8; 33],
184    /// `R' = k·G`, compressed.
185    pub rp_point: [u8; 33],
186    /// `s' = k⁻¹(m + r·x) mod n`, big-endian.
187    pub s: [u8; 32],
188    /// The DLEQ proof.
189    pub proof: DleqProof,
190}
191
192impl PreSignature {
193    /// Wire format: `R(33) ‖ R'(33) ‖ s'(32) ‖ c(32) ‖ z(32)` (`encodePreSignature`, as bytes).
194    pub fn encode(&self) -> [u8; PRE_SIGNATURE_SIZE] {
195        let mut out = [0u8; PRE_SIGNATURE_SIZE];
196        out[..33].copy_from_slice(&self.r_point);
197        out[33..66].copy_from_slice(&self.rp_point);
198        out[66..98].copy_from_slice(&self.s);
199        out[98..130].copy_from_slice(&self.proof.c);
200        out[130..].copy_from_slice(&self.proof.z);
201        out
202    }
203
204    /// Lowercase hex of [`Self::encode`] (no `0x`): the TypeScript `encodePreSignature` output.
205    pub fn to_hex(&self) -> String {
206        encoding::to_hex(&self.encode())
207    }
208
209    /// Parses the 162-byte wire format (`decodePreSignature`). Only the length is checked here.
210    pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
211        let b: [u8; PRE_SIGNATURE_SIZE] = encoding::to_array(bytes, "pre-signature")?;
212        let mut pre =
213            Self { r_point: [0; 33], rp_point: [0; 33], s: [0; 32], proof: DleqProof { c: [0; 32], z: [0; 32] } };
214        pre.r_point.copy_from_slice(&b[..33]);
215        pre.rp_point.copy_from_slice(&b[33..66]);
216        pre.s.copy_from_slice(&b[66..98]);
217        pre.proof.c.copy_from_slice(&b[98..130]);
218        pre.proof.z.copy_from_slice(&b[130..]);
219        Ok(pre)
220    }
221
222    /// Parses hex (with or without `0x`) of the wire format.
223    pub fn from_hex(s: &str) -> Result<Self, Error> {
224        Self::decode(&encoding::decode_hex(s)?)
225    }
226
227    /// The `r` the completed signature will have: `R.x mod n`.
228    pub fn r(&self) -> Result<[u8; 32], Error> {
229        Ok(scalar_to_bytes(&r_of(&self.r_point)?))
230    }
231}
232
233impl fmt::Debug for PreSignature {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        write!(f, "PreSignature({})", self.to_hex())
236    }
237}
238
239fn r_of(r_point: &[u8; 33]) -> Result<Scalar, Error> {
240    let point = PublicKey::parse(r_point, false, "pre-signature R").map_err(|_| Error::InvalidPreSignaturePoint)?;
241    Ok(x_mod_n(point.affine()))
242}
243
244/// `sha512(tag ‖ secret ‖ context… ‖ 32 fresh random bytes) mod n`, retried while zero. The randomness comes from
245/// `try_fill_bytes` (an RNG failure is [`Error::NonceGenerationFailed`], never a panic), and the hasher, whose state
246/// after finalization *is* the nonce's preimage digest, is wiped in place.
247fn hedged_nonce<R: CryptoRngCore + ?Sized>(
248    rng: &mut R,
249    tag: &[u8],
250    secret: &[u8; 32],
251    context: &[&[u8]],
252) -> Result<Zeroizing<NonZeroScalar>, Error> {
253    let mut hasher = Sha512::new();
254    for _ in 0..MAX_NONCE_ATTEMPTS {
255        let mut random = Zeroizing::new([0u8; 32]);
256        rng.try_fill_bytes(&mut *random).map_err(|_| Error::NonceGenerationFailed)?;
257        hasher.update(tag);
258        hasher.update(secret);
259        for part in context {
260            hasher.update(part);
261        }
262        hasher.update(random.as_slice());
263        let mut wide: WideBytes = finalize_wiped(&mut hasher);
264        let mut k = <Scalar as Reduce<U512>>::reduce_bytes(&wide);
265        wide.zeroize();
266        let nonzero = Option::<NonZeroScalar>::from(NonZeroScalar::new(k));
267        k.zeroize();
268        if let Some(k) = nonzero {
269            return Ok(Zeroizing::new(k));
270        }
271    }
272    Err(Error::NonceGenerationFailed)
273}
274
275fn compressed(point: &ProjectivePoint) -> Option<[u8; 33]> {
276    PublicKey::from_projective(point).map(|p| p.to_bytes())
277}
278
279/// `sha256(DLEQ_TAG ‖ points…) mod n`; `None` if a point is the identity (it has no compressed encoding).
280fn challenge(points: &[&ProjectivePoint; 6]) -> Option<Scalar> {
281    let mut hasher = Sha256::new();
282    hasher.update(DLEQ_TAG);
283    for point in points {
284        hasher.update(compressed(point)?);
285    }
286    Some(reduce32(&hasher.finalize().into()))
287}
288
289/// Proof that `A = k·G` and `B = k·Y` share the same `k`.
290fn dleq_prove<R: CryptoRngCore + ?Sized>(
291    rng: &mut R,
292    k: &NonZeroScalar,
293    a: &ProjectivePoint,
294    y: &ProjectivePoint,
295    b: &ProjectivePoint,
296) -> Result<DleqProof, Error> {
297    let (a_bytes, y_bytes, b_bytes) = (
298        compressed(a).ok_or(Error::NonceGenerationFailed)?,
299        compressed(y).ok_or(Error::NonceGenerationFailed)?,
300        compressed(b).ok_or(Error::NonceGenerationFailed)?,
301    );
302    let k_bytes = Zeroizing::new(scalar_to_bytes(k));
303    let t = hedged_nonce(rng, DLEQ_NONCE_TAG, &k_bytes, &[&a_bytes, &y_bytes, &b_bytes])?;
304    let g = ProjectivePoint::GENERATOR;
305    let c = challenge(&[&g, y, a, b, &(g * **t), &(*y * **t)]).ok_or(Error::NonceGenerationFailed)?;
306    let mut z = **t + c * **k;
307    let proof = DleqProof { c: scalar_to_bytes(&c), z: scalar_to_bytes(&z) };
308    z.zeroize();
309    Ok(proof)
310}
311
312/// True if `bytes`, read as a big-endian integer, is below the curve order.
313fn below_order(bytes: &[u8; 32]) -> bool {
314    bytes.as_slice() < CURVE_ORDER.as_slice()
315}
316
317fn dleq_verify(a: &ProjectivePoint, y: &ProjectivePoint, b: &ProjectivePoint, proof: &DleqProof) -> bool {
318    if !below_order(&proof.c) || !below_order(&proof.z) {
319        return false;
320    }
321    let (c, z) = (reduce32(&proof.c), reduce32(&proof.z));
322    let g = ProjectivePoint::GENERATOR;
323    let t1 = g * z - *a * c;
324    let t2 = *y * z - *b * c;
325    match challenge(&[&g, y, a, b, &t1, &t2]) {
326        Some(expected) => bool::from(expected.ct_eq(&c)),
327        None => false,
328    }
329}
330
331/// Pre-signs a 32-byte digest with `private_key`, encrypted to `adaptor_point`. Each call draws a fresh hedged
332/// nonce (random bytes from `rng` mixed with the key, digest and adaptor point), so two pre-signatures never share a
333/// nonce.
334pub fn pre_sign<R: CryptoRngCore + ?Sized>(
335    rng: &mut R,
336    private_key: &PrivateKey,
337    digest: &[u8; 32],
338    adaptor_point: &PublicKey,
339) -> Result<PreSignature, Error> {
340    let x = Zeroizing::new(private_key.scalar());
341    let x_bytes = Zeroizing::new(scalar_to_bytes(&x));
342    let m = reduce32(digest);
343    let m_bytes = scalar_to_bytes(&m);
344    let y_point = adaptor_point.projective();
345    let y_bytes = adaptor_point.to_bytes();
346    for _ in 0..MAX_NONCE_ATTEMPTS {
347        let k = hedged_nonce(rng, NONCE_TAG, &x_bytes, &[&m_bytes, &y_bytes])?;
348        let big_r = y_point * **k;
349        let Some(r_point) = PublicKey::from_projective(&big_r) else {
350            continue;
351        };
352        let r = x_mod_n(r_point.affine());
353        let k_inv = Zeroizing::new(Option::<Scalar>::from(k.invert()).unwrap_or(Scalar::ZERO));
354        let mut s = *k_inv * (m + r * **x);
355        if bool::from(r.is_zero()) || bool::from(s.is_zero()) {
356            continue;
357        }
358        let rp = ProjectivePoint::GENERATOR * **k;
359        let Some(rp_bytes) = compressed(&rp) else { continue };
360        let proof = dleq_prove(rng, &k, &rp, &y_point, &big_r)?;
361        let pre = PreSignature { r_point: r_point.to_bytes(), rp_point: rp_bytes, s: scalar_to_bytes(&s), proof };
362        s.zeroize();
363        return Ok(pre);
364    }
365    Err(Error::NonceGenerationFailed)
366}
367
368/// A pre-signature that passed [`pre_verify`], bound to the public key, digest and adaptor point it was checked
369/// against. It can only be obtained from [`pre_verify`] (or [`crate::protocol::check_evm_pre_signature`] /
370/// [`crate::protocol::verify_adaptor_lock`], which call it), so [`adapt`] can never complete an unchecked
371/// pre-signature.
372#[derive(Clone, Copy, PartialEq, Eq)]
373pub struct VerifiedPreSignature {
374    pre: PreSignature,
375    public_key: PublicKey,
376    digest: [u8; 32],
377    adaptor_point: PublicKey,
378}
379
380impl VerifiedPreSignature {
381    /// The pre-signature.
382    pub const fn pre_signature(&self) -> &PreSignature {
383        &self.pre
384    }
385
386    /// The key it completes to a signature for.
387    pub const fn public_key(&self) -> &PublicKey {
388        &self.public_key
389    }
390
391    /// The digest it signs.
392    pub const fn digest(&self) -> &[u8; 32] {
393        &self.digest
394    }
395
396    /// The adaptor point it is encrypted to.
397    pub const fn adaptor_point(&self) -> &PublicKey {
398        &self.adaptor_point
399    }
400
401    /// The 162-byte wire format, as lowercase hex (no `0x`).
402    pub fn to_hex(&self) -> String {
403        self.pre.to_hex()
404    }
405}
406
407impl fmt::Debug for VerifiedPreSignature {
408    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409        f.debug_struct("VerifiedPreSignature")
410            .field("pre", &self.pre)
411            .field("public_key", &self.public_key)
412            .field("digest", &encoding::to_hex(&self.digest))
413            .field("adaptor_point", &self.adaptor_point)
414            .finish()
415    }
416}
417
418/// Checks that `pre` becomes a valid signature by `public_key` on `digest` once the adaptor secret is applied: the
419/// points are valid, `s'` is in `[1, n)`, `c` and `z` are below `n`, the DLEQ proof holds and
420/// `m·s'⁻¹·G + r·s'⁻¹·X = R'` (spec §5.5). Returns the [`VerifiedPreSignature`] that [`adapt`] requires, or
421/// [`Error::InvalidPreSignature`]. Must be called on every pre-signature received from a counterparty.
422pub fn pre_verify(
423    public_key: &PublicKey,
424    digest: &[u8; 32],
425    adaptor_point: &PublicKey,
426    pre: &PreSignature,
427) -> Result<VerifiedPreSignature, Error> {
428    if !pre_verifies(public_key, digest, adaptor_point, pre) {
429        return Err(Error::InvalidPreSignature);
430    }
431    Ok(VerifiedPreSignature { pre: *pre, public_key: *public_key, digest: *digest, adaptor_point: *adaptor_point })
432}
433
434fn pre_verifies(public_key: &PublicKey, digest: &[u8; 32], adaptor_point: &PublicKey, pre: &PreSignature) -> bool {
435    let (Ok(big_r), Ok(rp)) =
436        (PublicKey::parse(&pre.r_point, false, "R"), PublicKey::parse(&pre.rp_point, false, "R'"))
437    else {
438        return false;
439    };
440    let Some(s) = nonzero_scalar_from_bytes(&pre.s) else {
441        return false;
442    };
443    if !dleq_verify(&rp.projective(), &adaptor_point.projective(), &big_r.projective(), &pre.proof) {
444        return false;
445    }
446    let r = x_mod_n(big_r.affine());
447    if bool::from(r.is_zero()) {
448        return false;
449    }
450    let s_inv = Option::<Scalar>::from(s.invert()).unwrap_or(Scalar::ZERO);
451    let expected = ProjectivePoint::GENERATOR * (reduce32(digest) * s_inv) + public_key.projective() * (r * s_inv);
452    bool::from(expected.to_affine().ct_eq(rp.affine()))
453}
454
455/// Completes a verified pre-signature with the adaptor secret, returning a low-S ECDSA signature by
456/// `pre.public_key()` on `pre.digest()`. Fails with [`Error::WrongAdaptorSecret`] unless `y·G` is the pre-signature's
457/// adaptor point; the completed signature is verified before it is returned.
458///
459/// Whoever created the pre-signature learns `y` from the result ([`extract`]) as soon as it is public, even from a
460/// transaction that fails: never broadcast a claim carrying it without first simulating the claim (`eth_call`) and
461/// re-checking the lock.
462pub fn adapt(pre: &VerifiedPreSignature, adaptor_secret: &AdaptorSecret) -> Result<Signature, Error> {
463    if adaptor_secret.point() != pre.adaptor_point {
464        return Err(Error::WrongAdaptorSecret);
465    }
466    let sig = hazmat::adapt_unchecked(&pre.pre, adaptor_secret)?;
467    // A verified pre-signature completed with its secret always verifies; checked anyway (defense in depth).
468    if !verify(&pre.public_key, &pre.digest, &sig) {
469        return Err(Error::InvalidPreSignature);
470    }
471    Ok(sig)
472}
473
474/// Operations that skip the checks of the safe API. Misuse leaks the adaptor secret.
475pub mod hazmat {
476    use zeroize::Zeroizing;
477
478    use super::{AdaptorSecret, Error, PreSignature, Scalar, Signature, nonzero_scalar_from_bytes, r_of};
479    use crate::keys::scalar_to_bytes;
480
481    /// Completes **any** pre-signature with the adaptor secret (`s = s'·y⁻¹`, normalized to low-S), without checking
482    /// it. The result is a valid signature only if the pre-signature would pass [`super::pre_verify`]; whoever made
483    /// the pre-signature learns `y` from the result either way, so publishing the completion of an unverified
484    /// pre-signature can hand `y` to an attacker. Use [`super::adapt`] unless you have verified it yourself.
485    pub fn adapt_unchecked(pre: &PreSignature, adaptor_secret: &AdaptorSecret) -> Result<Signature, Error> {
486        let s_pre = nonzero_scalar_from_bytes(&pre.s).ok_or(Error::PreSignatureOutOfRange)?;
487        let r = r_of(&pre.r_point)?;
488        let y = adaptor_secret.scalar();
489        let y_inv = Zeroizing::new(Option::<Scalar>::from(y.invert()).unwrap_or(Scalar::ZERO));
490        let s = *s_pre * *y_inv;
491        let sig = Signature::from_scalars(scalar_to_bytes(&r), scalar_to_bytes(&s))?;
492        Ok(sig.normalize_s())
493    }
494}
495
496/// Recovers the adaptor secret from a pre-signature and the completed signature seen on-chain. Exactly one of
497/// `±s'·s⁻¹` can match `Y` (`Y ≠ −Y`), so the result is unambiguous.
498pub fn extract(pre: &PreSignature, sig: &Signature, adaptor_point: &PublicKey) -> Result<AdaptorSecret, Error> {
499    let r = r_of(&pre.r_point)?;
500    if !bool::from(r.ct_eq(&sig.r_scalar())) {
501        return Err(Error::SignatureMismatch);
502    }
503    let s_inv = Option::<Scalar>::from(sig.s_scalar().invert()).ok_or(Error::ExtractFailed)?;
504    let y = Zeroizing::new(reduce32(&pre.s) * s_inv);
505    for candidate in [*y, -*y] {
506        let mut candidate = Zeroizing::new(candidate);
507        if bool::from(candidate.is_zero()) {
508            continue;
509        }
510        let point = ProjectivePoint::GENERATOR * *candidate;
511        if bool::from(point.to_affine().ct_eq(adaptor_point.affine())) {
512            let bytes = Zeroizing::new(scalar_to_bytes(&candidate));
513            candidate.zeroize();
514            return AdaptorSecret::from_bytes(&bytes);
515        }
516    }
517    Err(Error::ExtractFailed)
518}
519
520/// Standard ECDSA verification of a 32-byte digest; low-S required.
521pub fn verify(public_key: &PublicKey, digest: &[u8; 32], sig: &Signature) -> bool {
522    public_key.verify_prehash(digest, sig)
523}
524
525/// 64-byte `r ‖ s` encoding.
526pub fn compact(sig: &Signature) -> [u8; 64] {
527    sig.to_compact()
528}
529
530/// DER encoding, as used in Bitcoin/Zcash scriptSigs (append the sighash type byte yourself).
531pub fn der(sig: &Signature) -> Vec<u8> {
532    sig.to_der()
533}
534
535/// Recovery id (0 or 1) such that `ecrecover(digest, 27 + id, r, s)` returns `public_key`.
536pub fn recovery_id(public_key: &PublicKey, digest: &[u8; 32], sig: &Signature) -> Result<u8, Error> {
537    let r = sig.r_scalar();
538    let r_inv = Option::<Scalar>::from(r.invert()).ok_or(Error::NoRecoveryId)?;
539    let m = reduce32(digest);
540    for bit in [0u8, 1] {
541        let Some(big_r) = lift_x(&sig.r(), bit == 1) else {
542            continue;
543        };
544        let q = (ProjectivePoint::from(big_r) * (sig.s_scalar() * r_inv)) - ProjectivePoint::GENERATOR * (m * r_inv);
545        if PublicKey::from_projective(&q).is_some_and(|q| q == *public_key) {
546            return Ok(bit);
547        }
548    }
549    Err(Error::NoRecoveryId)
550}
551
552/// Recovers the public key of a signature with a recovery id (0 or 1): the core of `ecrecover`.
553pub fn recover_public_key(digest: &[u8; 32], sig: &Signature, recovery_id: u8) -> Result<PublicKey, Error> {
554    if recovery_id > 1 {
555        return Err(Error::NoRecoveryId);
556    }
557    let r_inv = Option::<Scalar>::from(sig.r_scalar().invert()).ok_or(Error::NoRecoveryId)?;
558    let big_r = lift_x(&sig.r(), recovery_id == 1).ok_or(Error::NoRecoveryId)?;
559    let m = reduce32(digest);
560    let q = (ProjectivePoint::from(big_r) * (sig.s_scalar() * r_inv)) - ProjectivePoint::GENERATOR * (m * r_inv);
561    PublicKey::from_projective(&q).ok_or(Error::NoRecoveryId)
562}