1#![cfg(all(feature = "production", feature = "rayon"))]
7
8use crate::activation::{ForkActivationTable, IsForkActive};
9use crate::error::{ConsensusError, Result};
10use crate::script::verify_script_with_context_full;
11use crate::types::{Block, ForkId, Natural, Network};
12use crate::witness::Witness;
13use crate::witness::is_witness_empty;
14use crossbeam_queue::SegQueue;
15
16use std::cell::RefCell;
17use std::sync::atomic::AtomicUsize;
18use std::sync::{Arc, Condvar, Mutex};
19use std::thread::{self, JoinHandle};
20
21fn lock_poison_recovery<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
22 mutex.lock().unwrap_or_else(|e| e.into_inner())
23}
24
25fn condvar_wait_poison_recovery<'a, T>(
26 cv: &Condvar,
27 guard: std::sync::MutexGuard<'a, T>,
28) -> std::sync::MutexGuard<'a, T> {
29 cv.wait(guard).unwrap_or_else(|e| e.into_inner())
30}
31
32const DEFAULT_BATCH_SIZE: usize = 128;
34
35thread_local! {
38 static PREVOUT_BUF: RefCell<Vec<i64>> = const { RefCell::new(Vec::new()) };
39}
40
41#[derive(Clone, Debug)]
44pub struct ScriptCheck {
45 pub tx_ctx_idx: usize,
46 pub input_idx: usize,
47 pub spk_offset: u32,
48 pub spk_len: u32,
49 pub prevout_value: i64,
50}
51
52#[derive(Clone, Debug)]
56pub struct TxScriptContext {
57 pub tx_index: usize,
58 pub prevout_values_range: (usize, usize),
60 pub script_pubkey_indices_range: (usize, usize),
62 pub flags: u32,
63 #[cfg(feature = "production")]
64 pub bip143: Option<crate::transaction_hash::Bip143PrecomputedHashes>,
65 pub loop_idx: usize,
66 pub fee: i64,
67 pub ecdsa_index_base: usize,
68 #[cfg(feature = "production")]
70 pub sighash_midstate_cache: Option<crate::transaction_hash::SighashMidstateCache>,
71}
72
73pub struct BlockSessionContext {
78 pub block: Arc<Block>,
79 pub prevout_values_buffer: Arc<Vec<i64>>,
81 pub script_pubkey_indices_buffer: Arc<Vec<(usize, usize)>>,
83 pub script_pubkey_buffer: Arc<Vec<u8>>,
85 pub witness_buffer: Arc<Vec<Vec<Witness>>>,
87 pub tx_contexts: Arc<Vec<TxScriptContext>>,
88 #[cfg(feature = "production")]
89 pub ecdsa_sub_counters: Arc<Vec<AtomicUsize>>,
90 #[cfg(feature = "production")]
91 pub schnorr_collector: Option<Arc<crate::bip348::SchnorrSignatureCollector>>,
92 pub height: Natural,
93 pub median_time_past: Option<u64>,
94 pub network: Network,
95 pub activation: ForkActivationTable,
97 pub results: Arc<SegQueue<Vec<(usize, bool)>>>,
99 #[cfg(feature = "production")]
102 pub precomputed_sighashes: Arc<Vec<Option<[u8; 32]>>>,
103 #[cfg(feature = "production")]
105 pub precomputed_p2pkh_hashes: Arc<Vec<Option<[u8; 20]>>>,
106}
107
108struct QueueState {
109 checks: Vec<ScriptCheck>,
110 n_todo: usize,
111 n_total: usize,
112 n_idle: usize,
113 n_submitted: usize,
115 error_result: Option<ConsensusError>,
116 request_stop: bool,
117 session: Option<Arc<BlockSessionContext>>,
118}
119
120pub struct ScriptCheckQueue {
122 state: Arc<Mutex<QueueState>>,
123 worker_cv: Arc<Condvar>,
124 master_cv: Arc<Condvar>,
125 control_mutex: Mutex<()>,
126 _workers: Vec<JoinHandle<()>>,
127 batch_size: usize,
128}
129
130impl ScriptCheckQueue {
131 pub fn new(worker_count: usize, batch_size: Option<usize>) -> Self {
134 let batch_size = batch_size
135 .filter(|&b| b > 0 && b <= 1024)
136 .unwrap_or(DEFAULT_BATCH_SIZE);
137 let state = Arc::new(Mutex::new(QueueState {
138 checks: Vec::new(),
139 n_todo: 0,
140 n_total: 0,
141 n_idle: 0,
142 n_submitted: 0,
143 error_result: None,
144 request_stop: false,
145 session: None,
146 }));
147 let worker_cv = Arc::new(Condvar::new());
148 let master_cv = Arc::new(Condvar::new());
149
150 let mut workers = Vec::with_capacity(worker_count);
151 for n in 0..worker_count {
152 let state_clone = Arc::clone(&state);
153 let cv_clone = Arc::clone(&worker_cv);
154 let master_clone = Arc::clone(&master_cv);
155 let bs = batch_size;
156 workers.push(
157 thread::Builder::new()
158 .name(format!("scriptch.{n}"))
159 .spawn(move || {
160 Self::worker_loop(state_clone, &cv_clone, &master_clone, bs);
161 })
162 .expect("scriptch thread spawn"),
163 );
164 }
165
166 Self {
167 state,
168 worker_cv,
169 master_cv,
170 control_mutex: Mutex::new(()),
171 _workers: workers,
172 batch_size,
173 }
174 }
175
176 pub fn run_check_with_refs(
180 check: &ScriptCheck,
181 session: &BlockSessionContext,
182 ctx: &TxScriptContext,
183 refs: &[&[u8]],
184 buffer: &[u8],
185 #[cfg(feature = "production")] p2pkh_hash: Option<[u8; 20]>,
186 script_pubkey_prefetched: Option<&[u8]>,
187 prevout_values_prefetched: Option<&[i64]>,
188 ) -> std::result::Result<bool, ConsensusError> {
189 let tx = &session.block.transactions[ctx.tx_index];
190 let script_pubkey: &[u8] = match script_pubkey_prefetched {
191 Some(s) => s,
192 None => {
193 let spi = session.script_pubkey_indices_buffer.as_slice();
194 let (base, count) = ctx.script_pubkey_indices_range;
195 let (start, len) = if check.input_idx < count {
196 spi[base + check.input_idx]
197 } else {
198 (0, 0)
199 };
200 &buffer[start..start + len]
201 }
202 };
203 let witness_for_script = if !session
204 .activation
205 .is_fork_active(ForkId::SegWit, session.height)
206 {
207 None
208 } else {
209 session
210 .witness_buffer
211 .get(ctx.tx_index)
212 .and_then(|w| w.get(check.input_idx))
213 .and_then(|w| if is_witness_empty(w) { None } else { Some(w) })
214 };
215 let ecdsa_global_idx = ctx.ecdsa_index_base + check.input_idx;
216
217 #[cfg(feature = "production")]
218 let sighash_cache = ctx.sighash_midstate_cache.as_ref();
219
220 #[cfg(feature = "production")]
221 let precomputed_sighash = session
222 .precomputed_sighashes
223 .get(ecdsa_global_idx)
224 .and_then(|s| *s);
225 #[cfg(feature = "production")]
226 let precomputed_p2pkh = match p2pkh_hash {
227 Some(h) => Some(h),
228 None => session
229 .precomputed_p2pkh_hashes
230 .get(ecdsa_global_idx)
231 .and_then(|h| *h),
232 };
233
234 let do_verify = |prevout_values: &[i64]| {
235 verify_script_with_context_full(
236 &tx.inputs[check.input_idx].script_sig,
237 script_pubkey,
238 witness_for_script,
239 ctx.flags,
240 tx,
241 check.input_idx,
242 prevout_values,
243 refs,
244 Some(session.height),
245 session.median_time_past,
246 session.network,
247 crate::script::SigVersion::Base,
248 #[cfg(feature = "production")]
249 session.schnorr_collector.as_deref(),
250 #[cfg(not(feature = "production"))]
251 None,
252 #[cfg(feature = "production")]
253 ctx.bip143.as_ref(),
254 #[cfg(not(feature = "production"))]
255 None,
256 #[cfg(feature = "production")]
257 precomputed_sighash,
258 #[cfg(feature = "production")]
259 sighash_cache,
260 #[cfg(feature = "production")]
261 precomputed_p2pkh,
262 )
263 .map_err(|e| {
264 ConsensusError::BlockValidation(
265 format!(
266 "Script verification failed at tx {} input {}: {}",
267 ctx.tx_index, check.input_idx, e
268 )
269 .into(),
270 )
271 })
272 };
273
274 match prevout_values_prefetched {
275 Some(p) => do_verify(p),
276 None => {
277 let pv = session.prevout_values_buffer.as_slice();
278 let (base, count) = ctx.prevout_values_range;
279 let slice = &pv[base..][..count];
280 PREVOUT_BUF.with(|cell| {
281 let mut v = cell.borrow_mut();
282 v.clear();
283 v.extend_from_slice(slice);
284 do_verify(&v)
285 })
286 }
287 }
288 }
289
290 fn run_check<'a>(
291 check: &ScriptCheck,
292 session: &'a BlockSessionContext,
293 refs_buf: &mut Vec<&'a [u8]>,
294 ) -> std::result::Result<bool, ConsensusError> {
295 let ctx = session
296 .tx_contexts
297 .get(check.tx_ctx_idx)
298 .ok_or_else(|| ConsensusError::BlockValidation("tx_ctx_idx out of range".into()))?;
299 let buffer = session.script_pubkey_buffer.as_slice();
300 let spi = session.script_pubkey_indices_buffer.as_slice();
301 let (base, count) = ctx.script_pubkey_indices_range;
302 refs_buf.clear();
303 refs_buf.extend((0..count).map(|j| {
304 let (s, l) = spi[base + j];
305 buffer[s..s + l].as_ref()
306 }));
307 Self::run_check_with_refs(
308 check, session, ctx, refs_buf, buffer, None, None, None, )
312 }
313
314 fn worker_loop(
315 state: Arc<Mutex<QueueState>>,
316 worker_cv: &Condvar,
317 master_cv: &Condvar,
318 batch_size: usize,
319 ) {
320 let mut n_now: usize = 0;
321 let mut local_error: Option<ConsensusError> = None;
322 let mut batch_buf: Vec<ScriptCheck> = Vec::with_capacity(batch_size);
323
324 loop {
325 let (session_opt, _batch_len) = {
326 let mut guard = lock_poison_recovery(state.as_ref());
327 if n_now > 0 {
328 if let Some(ref err) = local_error {
329 if guard.error_result.is_none() {
330 guard.error_result = Some(err.clone());
331 }
332 }
333 guard.n_todo -= n_now;
334 if guard.n_todo == 0 {
335 master_cv.notify_one();
336 }
337 n_now = 0;
338 local_error = None;
339 } else {
340 guard.n_total += 1;
341 }
342
343 loop {
344 if guard.request_stop {
345 return;
346 }
347 if guard.checks.is_empty() {
348 guard.n_idle += 1;
349 guard = condvar_wait_poison_recovery(worker_cv, guard);
350 guard.n_idle -= 1;
351 continue;
352 }
353 break;
354 }
355
356 let n_total = guard.n_total;
357 let n_idle = guard.n_idle;
358 let divisor = (n_total + n_idle + 1).max(1);
359 n_now = (guard.checks.len() / divisor).clamp(1, batch_size);
360 let drain_len = n_now.min(guard.checks.len());
361 batch_buf.clear();
362 let drain_start = guard.checks.len() - drain_len;
363 batch_buf.extend(guard.checks.drain(drain_start..));
364 let session = guard.session.clone();
365 (session, ())
366 };
367
368 if batch_buf.is_empty() {
369 continue;
370 }
371
372 let session = match session_opt.as_ref() {
373 Some(s) => Arc::clone(s),
374 None => continue,
375 };
376
377 let mut batch_results = Vec::with_capacity(batch_buf.len());
378 #[cfg(all(feature = "production", feature = "profile"))]
379 let t_run_check = std::time::Instant::now();
380 {
381 batch_buf.sort_unstable_by_key(|c| c.tx_ctx_idx);
382 let buffer = session.script_pubkey_buffer.as_slice();
383 let spi = session.script_pubkey_indices_buffer.as_slice();
384 let pv = session.prevout_values_buffer.as_slice();
385 let mut refs_buf: Vec<&[u8]> = Vec::with_capacity(64);
386 let mut cached_ctx_idx: usize = usize::MAX;
387 for c in batch_buf.iter() {
388 let ctx = match session.tx_contexts.get(c.tx_ctx_idx) {
389 Some(ctx) => ctx,
390 None => {
391 local_error = Some(ConsensusError::BlockValidation(
392 "tx_ctx_idx out of range".into(),
393 ));
394 break;
395 }
396 };
397 let s = c.spk_offset as usize;
398 let l = c.spk_len as usize;
399 let script_pubkey = if s + l <= buffer.len() {
400 &buffer[s..s + l]
401 } else {
402 &[]
403 };
404 let (pv_base, pv_count) = ctx.prevout_values_range;
405 let prevout_slice = &pv[pv_base..][..pv_count];
406 if c.tx_ctx_idx != cached_ctx_idx {
407 refs_buf.clear();
408 let (spi_base, spi_count) = ctx.script_pubkey_indices_range;
409 for j in 0..spi_count {
410 let (start, len) = spi[spi_base + j];
411 refs_buf.push(if start + len <= buffer.len() {
412 &buffer[start..start + len]
413 } else {
414 &[]
415 });
416 }
417 cached_ctx_idx = c.tx_ctx_idx;
418 }
419 match Self::run_check_with_refs(
420 c,
421 session.as_ref(),
422 ctx,
423 &refs_buf,
424 buffer,
425 None,
426 Some(script_pubkey),
427 Some(prevout_slice),
428 ) {
429 Ok(valid) => batch_results.push((c.tx_ctx_idx, valid)),
430 Err(e) => {
431 local_error = Some(e);
432 break;
433 }
434 }
435 }
436 }
437 #[cfg(all(feature = "production", feature = "profile"))]
438 crate::script_profile::add_worker_run_check_loop_ns(
439 t_run_check.elapsed().as_nanos() as u64
440 );
441 if !batch_results.is_empty() {
442 #[cfg(all(feature = "production", feature = "profile"))]
443 let t_results = std::time::Instant::now();
444 session.results.push(batch_results);
445 #[cfg(all(feature = "production", feature = "profile"))]
446 crate::script_profile::add_worker_results_extend_ns(
447 t_results.elapsed().as_nanos() as u64
448 );
449 }
450 }
451 }
452
453 pub fn start_session(&self, session: BlockSessionContext) {
455 let mut guard = lock_poison_recovery(&self.state);
456 guard.session = Some(Arc::new(session));
457 guard.checks.clear();
458 guard.n_todo = 0;
459 guard.n_submitted = 0;
460 guard.error_result = None;
461 }
462
463 pub fn add(&self, checks: Vec<ScriptCheck>) {
465 let n = checks.len();
466 if n == 0 {
467 return;
468 }
469 {
470 let mut guard = lock_poison_recovery(&self.state);
471 guard.checks.extend(checks);
472 guard.n_todo += n;
473 guard.n_submitted += n;
474 }
475 if n == 1 {
476 self.worker_cv.notify_one();
477 } else {
478 self.worker_cv.notify_all();
479 }
480 }
481
482 pub fn add_from_slice(&self, checks: &[ScriptCheck]) {
484 let n = checks.len();
485 if n == 0 {
486 return;
487 }
488 {
489 let mut guard = lock_poison_recovery(&self.state);
490 guard.checks.extend(checks.iter().cloned());
491 guard.n_todo += n;
492 guard.n_submitted += n;
493 }
494 if n == 1 {
495 self.worker_cv.notify_one();
496 } else {
497 self.worker_cv.notify_all();
498 }
499 }
500
501 pub fn run_checks_sequential(
503 checks: &[ScriptCheck],
504 session: &BlockSessionContext,
505 ) -> Result<Vec<(usize, bool)>> {
506 let mut results = Vec::with_capacity(checks.len());
507 let mut refs_buf = Vec::with_capacity(256);
508 for c in checks {
509 let valid = Self::run_check(c, session, &mut refs_buf)?;
510 results.push((c.tx_ctx_idx, valid));
511 }
512 Ok(results)
513 }
514
515 pub fn complete(&self) -> Result<Vec<(usize, bool)>> {
517 let _control = lock_poison_recovery(&self.control_mutex);
518 let state = Arc::clone(&self.state);
519 let worker_cv = Arc::clone(&self.worker_cv);
520 let master_cv = Arc::clone(&self.master_cv);
521 let batch_size = self.batch_size;
522
523 let mut n_now: usize = 0;
524 let mut local_error: Option<ConsensusError> = None;
525 let mut session_opt: Option<Arc<BlockSessionContext>> = None;
526 let mut batch_buf: Vec<ScriptCheck> = Vec::with_capacity(batch_size);
527
528 loop {
529 let done = {
530 let mut guard = lock_poison_recovery(state.as_ref());
531 if n_now > 0 {
532 if let Some(ref err) = local_error {
533 if guard.error_result.is_none() {
534 guard.error_result = Some(err.clone());
535 }
536 }
537 guard.n_todo -= n_now;
538 n_now = 0;
539 local_error = None;
540 } else {
541 guard.n_total += 1;
542 }
543
544 loop {
545 if guard.n_todo == 0 {
546 guard.n_total -= 1;
547 let n_expected = guard.n_submitted;
548 guard.n_submitted = 0;
549 let results = guard
550 .session
551 .as_ref()
552 .map(|s| {
553 let mut out = Vec::with_capacity(n_expected.max(64));
554 while let Some(batch) = s.results.pop() {
555 out.extend(batch);
556 }
557 out
558 })
559 .unwrap_or_default();
560 guard.session = None;
561 if let Some(ref e) = guard.error_result {
562 return Err(e.clone());
563 }
564 return Ok(results);
565 }
566 if guard.checks.is_empty() {
567 guard.n_idle += 1;
568 guard = condvar_wait_poison_recovery(master_cv.as_ref(), guard);
569 guard.n_idle -= 1;
570 continue;
571 }
572 break;
573 }
574
575 let n_total = guard.n_total;
576 let n_idle = guard.n_idle;
577 let divisor = (n_total + n_idle + 1).max(1);
578 n_now = (guard.checks.len() / divisor).clamp(1, batch_size);
579 let drain_len = n_now.min(guard.checks.len());
580 batch_buf.clear();
581 let drain_start = guard.checks.len() - drain_len;
582 batch_buf.extend(guard.checks.drain(drain_start..));
583 session_opt = guard.session.clone();
584 false
585 };
586
587 if batch_buf.is_empty() {
588 continue;
589 }
590
591 let session = match session_opt.as_ref() {
592 Some(s) => Arc::clone(s),
593 None => continue,
594 };
595
596 let mut batch_results = Vec::with_capacity(batch_buf.len());
597 #[cfg(all(feature = "production", feature = "profile"))]
598 let t_run_check = std::time::Instant::now();
599 {
600 batch_buf.sort_unstable_by_key(|c| c.tx_ctx_idx);
601 let buffer = session.script_pubkey_buffer.as_slice();
602 let spi = session.script_pubkey_indices_buffer.as_slice();
603 let pv = session.prevout_values_buffer.as_slice();
604 let mut refs_buf: Vec<&[u8]> = Vec::with_capacity(64);
605 let mut cached_ctx_idx: usize = usize::MAX;
606 for c in batch_buf.iter() {
607 let ctx = match session.tx_contexts.get(c.tx_ctx_idx) {
608 Some(ctx) => ctx,
609 None => {
610 local_error = Some(ConsensusError::BlockValidation(
611 "tx_ctx_idx out of range".into(),
612 ));
613 break;
614 }
615 };
616 let s = c.spk_offset as usize;
617 let l = c.spk_len as usize;
618 let script_pubkey = if s + l <= buffer.len() {
619 &buffer[s..s + l]
620 } else {
621 &[]
622 };
623 let (pv_base, pv_count) = ctx.prevout_values_range;
624 let prevout_slice = &pv[pv_base..][..pv_count];
625 if c.tx_ctx_idx != cached_ctx_idx {
626 refs_buf.clear();
627 let (spi_base, spi_count) = ctx.script_pubkey_indices_range;
628 for j in 0..spi_count {
629 let (start, len) = spi[spi_base + j];
630 refs_buf.push(if start + len <= buffer.len() {
631 &buffer[start..start + len]
632 } else {
633 &[]
634 });
635 }
636 cached_ctx_idx = c.tx_ctx_idx;
637 }
638 match Self::run_check_with_refs(
639 c,
640 session.as_ref(),
641 ctx,
642 &refs_buf,
643 buffer,
644 None,
645 Some(script_pubkey),
646 Some(prevout_slice),
647 ) {
648 Ok(valid) => batch_results.push((c.tx_ctx_idx, valid)),
649 Err(e) => {
650 local_error = Some(e);
651 break;
652 }
653 }
654 }
655 }
656 #[cfg(all(feature = "production", feature = "profile"))]
657 crate::script_profile::add_worker_run_check_loop_ns(
658 t_run_check.elapsed().as_nanos() as u64
659 );
660 if !batch_results.is_empty() {
661 #[cfg(all(feature = "production", feature = "profile"))]
662 let t_results = std::time::Instant::now();
663 session.results.push(batch_results);
664 #[cfg(all(feature = "production", feature = "profile"))]
665 crate::script_profile::add_worker_results_extend_ns(
666 t_results.elapsed().as_nanos() as u64
667 );
668 }
669 }
670 }
671}