Skip to main content

blvm_consensus/script/
signature.rs

1//! ECDSA signature verification for script execution.
2//!
3//! BIP66 strict DER, BIP62 LOW_S, assumevalid optimization.
4//! Supports two backends (exactly one must be enabled):
5//!   - `blvm-secp256k1`: pure-Rust, no FFI, handles compressed and uncompressed pubkeys
6//!   - `secp256k1-fallback`: crates.io secp256k1 0.28 (libsecp256k1 FFI)
7
8use crate::error::Result;
9use crate::types::Natural;
10use blvm_spec_lock::spec_locked;
11
12use super::SigVersion;
13
14// ---------- backend-specific imports & thread-local context ----------
15
16#[cfg(all(feature = "secp256k1-fallback", not(feature = "blvm-secp256k1")))]
17use secp256k1::Secp256k1;
18
19/// Opaque secp context type. Under `blvm-secp256k1` (default) this is a zero-sized placeholder;
20/// under `secp256k1-fallback` (and NOT `blvm-secp256k1`) it is `secp256k1::Secp256k1<All>`.
21/// Callers treat it identically — they pass it to `verify_signature` which ignores / uses it.
22/// When both features are active `blvm-secp256k1` takes priority.
23#[cfg(all(feature = "secp256k1-fallback", not(feature = "blvm-secp256k1")))]
24pub(crate) type SecpCtx = secp256k1::Secp256k1<secp256k1::All>;
25
26#[cfg(not(all(feature = "secp256k1-fallback", not(feature = "blvm-secp256k1"))))]
27#[derive(Default, Clone, Copy)]
28pub(crate) struct SecpCtx;
29
30/// Construct a fresh context.
31pub(crate) fn new_secp() -> SecpCtx {
32    #[cfg(all(feature = "secp256k1-fallback", not(feature = "blvm-secp256k1")))]
33    {
34        secp256k1::Secp256k1::new()
35    }
36    #[cfg(not(all(feature = "secp256k1-fallback", not(feature = "blvm-secp256k1"))))]
37    {
38        SecpCtx
39    }
40}
41
42#[cfg(feature = "production")]
43use std::thread_local;
44
45#[cfg(feature = "production")]
46thread_local! {
47    static SECP256K1_CONTEXT: SecpCtx = new_secp();
48}
49
50/// Run a closure with the thread-local context (avoids repeated allocation under secp256k1-fallback).
51#[cfg(feature = "production")]
52pub(crate) fn with_secp_context<F, R>(f: F) -> R
53where
54    F: FnOnce(&SecpCtx) -> R,
55{
56    SECP256K1_CONTEXT.with(f)
57}
58
59// ---------- core verifier ----------
60
61/// Verify ECDSA signature.
62/// BIP66: strict DER. BIP62: LOW_S, STRICTENC.
63/// The `_secp` parameter is accepted for call-site uniformity but is ignored when
64/// the `blvm-secp256k1` backend is active.
65#[allow(clippy::too_many_arguments)]
66pub(crate) fn verify_signature(
67    _secp: &SecpCtx,
68    pubkey_bytes: &[u8],
69    signature_bytes: &[u8],
70    sighash: &[u8; 32],
71    flags: u32,
72    height: Natural,
73    network: crate::types::Network,
74    sigversion: SigVersion,
75) -> Result<bool> {
76    if signature_bytes.is_empty() {
77        return Ok(false);
78    }
79    let sig_len = signature_bytes.len();
80    let sighash_byte = signature_bytes[sig_len - 1];
81    let der_sig = &signature_bytes[..sig_len - 1];
82
83    if flags & 0x04 != 0
84        && !crate::bip_validation::check_bip66_network(signature_bytes, height, network)?
85    {
86        return Ok(false);
87    }
88
89    if flags & 0x02 != 0 {
90        let base_sighash = sighash_byte & !0x80;
91        if !(0x01..=0x03).contains(&base_sighash) {
92            return Ok(false);
93        }
94    }
95
96    if flags & 0x02 != 0 {
97        if pubkey_bytes.len() < 33 {
98            return Ok(false);
99        }
100        if pubkey_bytes[0] == 0x04 {
101            if pubkey_bytes.len() != 65 {
102                return Ok(false);
103            }
104        } else if pubkey_bytes[0] == 0x02 || pubkey_bytes[0] == 0x03 {
105            if pubkey_bytes.len() != 33 {
106                return Ok(false);
107            }
108        } else {
109            return Ok(false);
110        }
111    }
112
113    const SCRIPT_VERIFY_WITNESS_PUBKEYTYPE: u32 = 0x8000;
114    if (flags & SCRIPT_VERIFY_WITNESS_PUBKEYTYPE) != 0
115        && sigversion == SigVersion::WitnessV0
116        && !(pubkey_bytes.len() == 33 && (pubkey_bytes[0] == 0x02 || pubkey_bytes[0] == 0x03))
117    {
118        return Ok(false);
119    }
120
121    let strict_der = flags & 0x04 != 0;
122    let enforce_low_s = flags & 0x08 != 0;
123
124    #[cfg(feature = "blvm-secp256k1")]
125    {
126        return Ok(blvm_secp256k1::ecdsa::verify_ecdsa_direct(
127            der_sig,
128            pubkey_bytes,
129            sighash,
130            strict_der,
131            enforce_low_s,
132        )
133        .unwrap_or(false));
134    }
135
136    #[cfg(all(feature = "secp256k1-fallback", not(feature = "blvm-secp256k1")))]
137    {
138        use secp256k1::{ecdsa::Signature, PublicKey};
139
140        let signature = if strict_der {
141            match Signature::from_der(der_sig) {
142                Ok(sig) => sig,
143                Err(_) => return Ok(false),
144            }
145        } else {
146            match Signature::from_der_lax(der_sig) {
147                Ok(sig) => sig,
148                Err(_) => return Ok(false),
149            }
150        };
151
152        if enforce_low_s {
153            let before = signature.serialize_compact();
154            let mut normalized = signature;
155            normalized.normalize_s();
156            if before != normalized.serialize_compact() {
157                return Ok(false);
158            }
159        }
160
161        let pubkey = match PublicKey::from_slice(pubkey_bytes) {
162            Ok(pk) => pk,
163            Err(_) => return Ok(false),
164        };
165
166        let normalized_signature = if enforce_low_s {
167            signature
168        } else {
169            let mut s = signature;
170            s.normalize_s();
171            s
172        };
173
174        let sig_compact = normalized_signature.serialize_compact();
175        let pk_compressed = pubkey.serialize();
176        return crate::secp256k1_backend::verify_ecdsa(sighash, &sig_compact, &pk_compressed);
177    }
178
179    #[allow(unreachable_code)]
180    Ok(false)
181}
182
183/// Verify pre-extracted ECDSA (P2PKH/P2PK) inline without re-parsing script_sig.
184#[cfg(feature = "production")]
185pub fn verify_pre_extracted_ecdsa(
186    pubkey_bytes: &[u8],
187    signature_bytes: &[u8],
188    sighash: &[u8; 32],
189    flags: u32,
190    height: Natural,
191    network: crate::types::Network,
192) -> Result<bool> {
193    with_secp_context(|secp| {
194        verify_signature(
195            secp,
196            pubkey_bytes,
197            signature_bytes,
198            sighash,
199            flags,
200            height,
201            network,
202            SigVersion::Base,
203        )
204    })
205}
206
207#[cfg(feature = "production")]
208#[spec_locked("5.2")]
209pub fn batch_verify_signatures(
210    verification_tasks: &[(&[u8], &[u8], [u8; 32])],
211    flags: u32,
212    height: Natural,
213    network: crate::types::Network,
214) -> Result<Vec<bool>> {
215    #[cfg(feature = "profile")]
216    let _t0 = std::time::Instant::now();
217
218    if verification_tasks.is_empty() {
219        #[cfg(feature = "profile")]
220        crate::script_profile::add_multisig_ns(_t0.elapsed().as_nanos() as u64);
221        return Ok(Vec::new());
222    }
223
224    // Serial verification. The previous rayon `par_iter` path was a
225    // hidden hotspot: it fires inside script execution (per OP_CHECKMULTISIG), and IBD
226    // runs N script verifiers in parallel on N worker threads. Each multisig opcode
227    // pushed up to 20 sig-checks into the global rayon pool, oversubscribing the CPU
228    // and causing IBD workers to stall waiting for their own rayon tasks. CHECKMULTISIG
229    // caps at 20 sigs so serial cost is small relative to the per-worker thread budget.
230    let mut results = Vec::with_capacity(verification_tasks.len());
231    with_secp_context(|secp| -> Result<()> {
232        for (pubkey_bytes, signature_bytes, sighash) in verification_tasks {
233            let result = verify_signature(
234                secp,
235                pubkey_bytes,
236                signature_bytes,
237                sighash,
238                flags,
239                height,
240                network,
241                SigVersion::Base,
242            )?;
243            results.push(result);
244        }
245        Ok(())
246    })?;
247    #[cfg(feature = "profile")]
248    crate::script_profile::add_multisig_ns(_t0.elapsed().as_nanos() as u64);
249    Ok(results)
250}