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
41use std::thread_local;
42
43thread_local! {
44    static SECP256K1_CONTEXT: SecpCtx = new_secp();
45}
46
47/// Run a closure with the thread-local context (avoids repeated allocation under secp256k1-fallback).
48pub(crate) fn with_secp_context<F, R>(f: F) -> R
49where
50    F: FnOnce(&SecpCtx) -> R,
51{
52    SECP256K1_CONTEXT.with(f)
53}
54
55// ---------- core verifier ----------
56
57pub(crate) fn reject_strictenc(flags: u32, sighash_byte: u8, pubkey_bytes: &[u8]) -> Result<()> {
58    if flags & 0x02 == 0 {
59        return Ok(());
60    }
61    let base_sighash = sighash_byte & !0x80;
62    if !(0x01..=0x03).contains(&base_sighash) {
63        return Err(crate::error::ConsensusError::ScriptErrorWithCode {
64            code: crate::error::ScriptErrorCode::SigHashType,
65            message: "undefined sighash type under STRICTENC".into(),
66        });
67    }
68    reject_strictenc_pubkey(flags, pubkey_bytes)?;
69    Ok(())
70}
71
72pub(crate) fn reject_strictenc_pubkey(flags: u32, pubkey_bytes: &[u8]) -> Result<()> {
73    if flags & 0x02 == 0 {
74        return Ok(());
75    }
76    let compressed =
77        pubkey_bytes.len() == 33 && (pubkey_bytes[0] == 0x02 || pubkey_bytes[0] == 0x03);
78    let uncompressed = pubkey_bytes.len() == 65 && pubkey_bytes[0] == 0x04;
79    if !compressed && !uncompressed {
80        return Err(crate::error::ConsensusError::ScriptErrorWithCode {
81            code: crate::error::ScriptErrorCode::PubkeyType,
82            message: "public key is not compressed or uncompressed under STRICTENC".into(),
83        });
84    }
85    Ok(())
86}
87
88/// STRICTENC requires a DER signature and a defined hashtype. An empty signature
89/// is a failed check, not an encoding error.
90pub(crate) fn reject_strictenc_signature(flags: u32, signature_bytes: &[u8]) -> Result<()> {
91    if flags & 0x02 == 0 || signature_bytes.is_empty() {
92        return Ok(());
93    }
94    if !crate::bip_validation::is_strict_der(signature_bytes)? {
95        return Err(crate::error::ConsensusError::ScriptErrorWithCode {
96            code: crate::error::ScriptErrorCode::SigDer,
97            message: "non-DER signature under STRICTENC".into(),
98        });
99    }
100    let sighash_byte = signature_bytes[signature_bytes.len() - 1];
101    let base_sighash = sighash_byte & !0x80;
102    if !(0x01..=0x03).contains(&base_sighash) {
103        return Err(crate::error::ConsensusError::ScriptErrorWithCode {
104            code: crate::error::ScriptErrorCode::SigHashType,
105            message: "undefined sighash type under STRICTENC".into(),
106        });
107    }
108    Ok(())
109}
110
111/// Verify ECDSA signature.
112/// BIP66: strict DER. BIP62: LOW_S, STRICTENC.
113/// The `_secp` parameter is accepted for call-site uniformity but is ignored when
114/// the `blvm-secp256k1` backend is active.
115#[allow(clippy::too_many_arguments)]
116pub(crate) fn verify_signature(
117    _secp: &SecpCtx,
118    pubkey_bytes: &[u8],
119    signature_bytes: &[u8],
120    sighash: &[u8; 32],
121    flags: u32,
122    height: Natural,
123    network: crate::types::Network,
124    sigversion: SigVersion,
125) -> Result<bool> {
126    let _ = (height, network);
127    reject_strictenc_pubkey(flags, pubkey_bytes)?;
128    if signature_bytes.is_empty() {
129        return Ok(false);
130    }
131    reject_strictenc_signature(flags, signature_bytes)?;
132    let sig_len = signature_bytes.len();
133    let sighash_byte = signature_bytes[sig_len - 1];
134    let der_sig = &signature_bytes[..sig_len - 1];
135
136    // The flag is the decision. BIP66's height gate belongs to the caller that
137    // sets SCRIPT_VERIFY_DERSIG. Checking the height again drops the flag at
138    // regtest height 0, where the buried deployment has not started.
139    if flags & 0x04 != 0 && !crate::bip_validation::is_strict_der(signature_bytes)? {
140        return Err(crate::error::ConsensusError::ScriptErrorWithCode {
141            code: crate::error::ScriptErrorCode::SigDer,
142            message: "non-DER signature".into(),
143        });
144    }
145
146    reject_strictenc(flags, sighash_byte, pubkey_bytes)?;
147
148    if (flags & super::flags::SCRIPT_VERIFY_WITNESS_PUBKEYTYPE) != 0
149        && sigversion == SigVersion::WitnessV0
150        && !(pubkey_bytes.len() == 33 && (pubkey_bytes[0] == 0x02 || pubkey_bytes[0] == 0x03))
151    {
152        return Ok(false);
153    }
154
155    let strict_der = flags & 0x04 != 0;
156    let enforce_low_s = flags & 0x08 != 0;
157
158    #[cfg(feature = "blvm-secp256k1")]
159    {
160        return Ok(blvm_secp256k1::ecdsa::verify_ecdsa_direct(
161            der_sig,
162            pubkey_bytes,
163            sighash,
164            strict_der,
165            enforce_low_s,
166        )
167        .unwrap_or(false));
168    }
169
170    #[cfg(all(feature = "secp256k1-fallback", not(feature = "blvm-secp256k1")))]
171    {
172        use secp256k1::{PublicKey, ecdsa::Signature};
173
174        let signature = if strict_der {
175            match Signature::from_der(der_sig) {
176                Ok(sig) => sig,
177                Err(_) => {
178                    return Err(crate::error::ConsensusError::ScriptErrorWithCode {
179                        code: crate::error::ScriptErrorCode::SigDer,
180                        message: "non-DER signature".into(),
181                    });
182                }
183            }
184        } else {
185            match Signature::from_der_lax(der_sig) {
186                Ok(sig) => sig,
187                Err(_) => return Ok(false),
188            }
189        };
190
191        if enforce_low_s {
192            let before = signature.serialize_compact();
193            let mut normalized = signature;
194            normalized.normalize_s();
195            if before != normalized.serialize_compact() {
196                return Ok(false);
197            }
198        }
199
200        let pubkey = match PublicKey::from_slice(pubkey_bytes) {
201            Ok(pk) => pk,
202            Err(_) => return Ok(false),
203        };
204
205        let normalized_signature = if enforce_low_s {
206            signature
207        } else {
208            let mut s = signature;
209            s.normalize_s();
210            s
211        };
212
213        let sig_compact = normalized_signature.serialize_compact();
214        let pk_compressed = pubkey.serialize();
215        return crate::secp256k1_backend::verify_ecdsa(sighash, &sig_compact, &pk_compressed);
216    }
217
218    #[allow(unreachable_code)]
219    Ok(false)
220}
221
222/// Verify pre-extracted ECDSA (P2PKH/P2PK) inline without re-parsing script_sig.
223#[cfg(feature = "production")]
224pub fn verify_pre_extracted_ecdsa(
225    pubkey_bytes: &[u8],
226    signature_bytes: &[u8],
227    sighash: &[u8; 32],
228    flags: u32,
229    height: Natural,
230    network: crate::types::Network,
231) -> Result<bool> {
232    with_secp_context(|secp| {
233        verify_signature(
234            secp,
235            pubkey_bytes,
236            signature_bytes,
237            sighash,
238            flags,
239            height,
240            network,
241            SigVersion::Base,
242        )
243    })
244}
245
246#[cfg(feature = "production")]
247pub fn batch_verify_signatures(
248    verification_tasks: &[(&[u8], &[u8], [u8; 32])],
249    flags: u32,
250    height: Natural,
251    network: crate::types::Network,
252    sigversion: SigVersion,
253) -> Result<Vec<bool>> {
254    #[cfg(feature = "profile")]
255    let _t0 = std::time::Instant::now();
256
257    if verification_tasks.is_empty() {
258        #[cfg(feature = "profile")]
259        crate::script_profile::add_multisig_ns(_t0.elapsed().as_nanos() as u64);
260        return Ok(Vec::new());
261    }
262
263    // Serial verification. The previous rayon `par_iter` path was a
264    // hidden hotspot: it fires inside script execution (per OP_CHECKMULTISIG), and IBD
265    // runs N script verifiers in parallel on N worker threads. Each multisig opcode
266    // pushed up to 20 sig-checks into the global rayon pool, oversubscribing the CPU
267    // and causing IBD workers to stall waiting for their own rayon tasks. CHECKMULTISIG
268    // caps at 20 sigs so serial cost is small relative to the per-worker thread budget.
269    let mut results = Vec::with_capacity(verification_tasks.len());
270    with_secp_context(|secp| -> Result<()> {
271        for (pubkey_bytes, signature_bytes, sighash) in verification_tasks {
272            let result = verify_signature(
273                secp,
274                pubkey_bytes,
275                signature_bytes,
276                sighash,
277                flags,
278                height,
279                network,
280                sigversion,
281            )?;
282            results.push(result);
283        }
284        Ok(())
285    })?;
286    #[cfg(feature = "profile")]
287    crate::script_profile::add_multisig_ns(_t0.elapsed().as_nanos() as u64);
288    Ok(results)
289}