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