ckb_script/
verify.rs

1#[cfg(not(target_family = "wasm"))]
2use crate::ChunkCommand;
3use crate::scheduler::Scheduler;
4use crate::{
5    error::{ScriptError, TransactionScriptError},
6    syscalls::generator::generate_ckb_syscalls,
7    type_id::TypeIdSystemScript,
8    types::{
9        DebugPrinter, FullSuspendedState, Machine, RunMode, ScriptGroup, ScriptGroupType,
10        ScriptVersion, SgData, SyscallGenerator, TerminatedResult, TransactionState, TxData,
11        VerifyResult,
12    },
13    verify_env::TxVerifyEnv,
14};
15use ckb_chain_spec::consensus::{Consensus, TYPE_ID_CODE_HASH};
16use ckb_error::Error;
17#[cfg(feature = "logging")]
18use ckb_logger::{debug, info};
19use ckb_traits::{CellDataProvider, ExtensionProvider, HeaderProvider};
20use ckb_types::{
21    bytes::Bytes,
22    core::{Cycle, ScriptHashType, cell::ResolvedTransaction},
23    packed::{Byte32, Script},
24};
25#[cfg(not(target_family = "wasm"))]
26use ckb_vm::machine::Pause as VMPause;
27use ckb_vm::{DefaultMachineRunner, Error as VMInternalError};
28use std::sync::Arc;
29#[cfg(not(target_family = "wasm"))]
30use tokio::sync::{
31    oneshot,
32    watch::{self, Receiver},
33};
34
35#[cfg(test)]
36mod tests;
37
38pub enum ChunkState {
39    Suspended(Option<FullSuspendedState>),
40    // (total_cycles, consumed_cycles in last chunk)
41    Completed(Cycle, Cycle),
42}
43
44impl ChunkState {
45    pub fn suspended(state: FullSuspendedState) -> Self {
46        ChunkState::Suspended(Some(state))
47    }
48
49    pub fn suspended_type_id() -> Self {
50        ChunkState::Suspended(None)
51    }
52}
53
54/// This struct leverages CKB VM to verify transaction inputs.
55pub struct TransactionScriptsVerifier<
56    DL: CellDataProvider,
57    V = DebugPrinter,
58    M: DefaultMachineRunner = Machine,
59> {
60    tx_data: Arc<TxData<DL>>,
61    syscall_generator: SyscallGenerator<DL, V, <M as DefaultMachineRunner>::Inner>,
62    syscall_context: V,
63}
64
65impl<DL> TransactionScriptsVerifier<DL>
66where
67    DL: CellDataProvider + HeaderProvider + ExtensionProvider + Send + Sync + Clone + 'static,
68{
69    /// Create a script verifier using default CKB syscalls and a default debug printer
70    pub fn new(
71        rtx: Arc<ResolvedTransaction>,
72        data_loader: DL,
73        consensus: Arc<Consensus>,
74        tx_env: Arc<TxVerifyEnv>,
75    ) -> Self {
76        let debug_printer: DebugPrinter = Arc::new(
77            #[allow(unused_variables)]
78            |hash: &Byte32, message: &str| {
79                #[cfg(feature = "logging")]
80                debug!("script group: {} DEBUG OUTPUT: {}", hash, message);
81            },
82        );
83
84        Self::new_with_debug_printer(rtx, data_loader, consensus, tx_env, debug_printer)
85    }
86
87    /// Create a script verifier using default CKB syscalls and a custom debug printer
88    pub fn new_with_debug_printer(
89        rtx: Arc<ResolvedTransaction>,
90        data_loader: DL,
91        consensus: Arc<Consensus>,
92        tx_env: Arc<TxVerifyEnv>,
93        debug_printer: DebugPrinter,
94    ) -> Self {
95        Self::new_with_generator(
96            rtx,
97            data_loader,
98            consensus,
99            tx_env,
100            generate_ckb_syscalls,
101            debug_printer,
102        )
103    }
104}
105
106impl<DL, V, M> TransactionScriptsVerifier<DL, V, M>
107where
108    DL: CellDataProvider + HeaderProvider + ExtensionProvider + Clone,
109    V: Clone,
110    M: DefaultMachineRunner,
111{
112    /// Creates a script verifier for the transaction.
113    ///
114    /// ## Params
115    ///
116    /// * `rtx` - transaction which cell out points have been resolved.
117    /// * `data_loader` - used to load cell data.
118    /// * `consensus` - consensus parameters.
119    /// * `tx_env` - environment for verifying transaction, such as committed block, etc.
120    /// * `syscall_generator` - a syscall generator for current verifier
121    /// * `syscall_context` - context for syscall generator
122    pub fn new_with_generator(
123        rtx: Arc<ResolvedTransaction>,
124        data_loader: DL,
125        consensus: Arc<Consensus>,
126        tx_env: Arc<TxVerifyEnv>,
127        syscall_generator: SyscallGenerator<DL, V, <M as DefaultMachineRunner>::Inner>,
128        syscall_context: V,
129    ) -> TransactionScriptsVerifier<DL, V, M> {
130        let tx_data = Arc::new(TxData::new(rtx, data_loader, consensus, tx_env));
131
132        TransactionScriptsVerifier {
133            tx_data,
134            syscall_generator,
135            syscall_context,
136        }
137    }
138
139    //////////////////////////////////////////////////////////////////
140    // Functions below have been moved from verifier struct to TxData,
141    // however we still preserve all the public APIs by delegating
142    // them to TxData.
143    //////////////////////////////////////////////////////////////////
144
145    #[inline]
146    #[allow(dead_code)]
147    fn hash(&self) -> Byte32 {
148        self.tx_data.tx_hash()
149    }
150
151    /// Extracts actual script binary either in dep cells.
152    pub fn extract_script(&self, script: &Script) -> Result<Bytes, ScriptError> {
153        self.tx_data.extract_script(script)
154    }
155
156    /// Returns the version of the machine based on the script and the consensus rules.
157    pub fn select_version(&self, script: &Script) -> Result<ScriptVersion, ScriptError> {
158        self.tx_data.select_version(script)
159    }
160
161    /// Returns all script groups.
162    pub fn groups(&self) -> impl Iterator<Item = (&'_ Byte32, &'_ ScriptGroup)> {
163        self.tx_data.groups()
164    }
165
166    /// Returns all script groups with type.
167    pub fn groups_with_type(
168        &self,
169    ) -> impl Iterator<Item = (ScriptGroupType, &'_ Byte32, &'_ ScriptGroup)> {
170        self.tx_data.groups_with_type()
171    }
172
173    /// Finds the script group from cell deps.
174    pub fn find_script_group(
175        &self,
176        script_group_type: ScriptGroupType,
177        script_hash: &Byte32,
178    ) -> Option<&ScriptGroup> {
179        self.tx_data
180            .find_script_group(script_group_type, script_hash)
181    }
182
183    //////////////////////////////////////////////////////////////////
184    // This marks the end of delegated functions.
185    //////////////////////////////////////////////////////////////////
186
187    /// Verifies the transaction by running scripts.
188    ///
189    /// ## Params
190    ///
191    /// * `max_cycles` - Maximum allowed cycles to run the scripts. The verification quits early
192    ///   when the consumed cycles exceed the limit.
193    ///
194    /// ## Returns
195    ///
196    /// It returns the total consumed cycles on success, Otherwise it returns the verification error.
197    pub fn verify(&self, max_cycles: Cycle) -> Result<Cycle, Error> {
198        let mut cycles: Cycle = 0;
199
200        // Now run each script group
201        for (_hash, group) in self.groups() {
202            // max_cycles must reduce by each group exec
203            let used_cycles = self
204                .verify_script_group(group, max_cycles - cycles)
205                .map_err(|e| {
206                    #[cfg(feature = "logging")]
207                    logging::on_script_error(_hash, &self.hash(), &e);
208                    e.source(group)
209                })?;
210
211            cycles = wrapping_cycles_add(cycles, used_cycles, group)?;
212        }
213        Ok(cycles)
214    }
215
216    /// Performing a resumable verification on the transaction scripts.
217    ///
218    /// ## Params
219    ///
220    /// * `limit_cycles` - Maximum allowed cycles to run the scripts. The verification quits early
221    ///   when the consumed cycles exceed the limit.
222    ///
223    /// ## Returns
224    ///
225    /// It returns the total consumed cycles if verification completed,
226    /// If verify is suspended, a state will returned.
227    pub fn resumable_verify(&self, limit_cycles: Cycle) -> Result<VerifyResult, Error> {
228        let mut cycles = 0;
229        let mut current_consumed_cycles = 0;
230
231        let groups: Vec<_> = self.groups().collect();
232        for (idx, (_hash, group)) in groups.iter().enumerate() {
233            // vm should early return invalid cycles
234            let remain_cycles = limit_cycles
235                .checked_sub(current_consumed_cycles)
236                .ok_or_else(|| {
237                    ScriptError::Other(format!("expect invalid cycles {limit_cycles} {cycles}"))
238                        .source(group)
239                })?;
240
241            match self.verify_group_with_chunk(group, remain_cycles, &None) {
242                Ok(ChunkState::Completed(used_cycles, consumed_cycles)) => {
243                    current_consumed_cycles =
244                        wrapping_cycles_add(current_consumed_cycles, consumed_cycles, group)?;
245                    cycles = wrapping_cycles_add(cycles, used_cycles, group)?;
246                }
247                Ok(ChunkState::Suspended(state)) => {
248                    let current = idx;
249                    let state = TransactionState::new(state, current, cycles, remain_cycles);
250                    return Ok(VerifyResult::Suspended(state));
251                }
252                Err(e) => {
253                    #[cfg(feature = "logging")]
254                    logging::on_script_error(_hash, &self.hash(), &e);
255                    return Err(e.source(group).into());
256                }
257            }
258        }
259
260        Ok(VerifyResult::Completed(cycles))
261    }
262
263    /// Resuming an suspended verify from vm state
264    ///
265    /// ## Params
266    ///
267    /// * `state` - vm state.
268    ///
269    /// * `limit_cycles` - Maximum allowed cycles to run the scripts. The verification quits early
270    ///   when the consumed cycles exceed the limit.
271    ///
272    /// ## Returns
273    ///
274    /// It returns the total consumed cycles if verification completed,
275    /// If verify is suspended, a borrowed state will returned.
276    pub fn resume_from_state(
277        &self,
278        state: &TransactionState,
279        limit_cycles: Cycle,
280    ) -> Result<VerifyResult, Error> {
281        let TransactionState {
282            current,
283            state,
284            current_cycles,
285            ..
286        } = state;
287
288        let mut current_used = 0;
289        let mut cycles = *current_cycles;
290
291        let (_hash, current_group) = self.groups().nth(*current).ok_or_else(|| {
292            ScriptError::Other(format!("snapshot group missing {current:?}")).unknown_source()
293        })?;
294
295        let resumed_script_result =
296            self.verify_group_with_chunk(current_group, limit_cycles, state);
297
298        match resumed_script_result {
299            Ok(ChunkState::Completed(used_cycles, consumed_cycles)) => {
300                current_used = wrapping_cycles_add(current_used, consumed_cycles, current_group)?;
301                cycles = wrapping_cycles_add(cycles, used_cycles, current_group)?;
302            }
303            Ok(ChunkState::Suspended(state)) => {
304                let state = TransactionState::new(state, *current, cycles, limit_cycles);
305                return Ok(VerifyResult::Suspended(state));
306            }
307            Err(e) => {
308                #[cfg(feature = "logging")]
309                logging::on_script_error(_hash, &self.hash(), &e);
310                return Err(e.source(current_group).into());
311            }
312        }
313
314        for (idx, (_hash, group)) in self.groups().enumerate().skip(current + 1) {
315            let remain_cycles = limit_cycles.checked_sub(current_used).ok_or_else(|| {
316                ScriptError::Other(format!(
317                    "expect invalid cycles {limit_cycles} {current_used} {cycles}"
318                ))
319                .source(group)
320            })?;
321
322            match self.verify_group_with_chunk(group, remain_cycles, &None) {
323                Ok(ChunkState::Completed(_, consumed_cycles)) => {
324                    current_used = wrapping_cycles_add(current_used, consumed_cycles, group)?;
325                    cycles = wrapping_cycles_add(cycles, consumed_cycles, group)?;
326                }
327                Ok(ChunkState::Suspended(state)) => {
328                    let current = idx;
329                    let state = TransactionState::new(state, current, cycles, remain_cycles);
330                    return Ok(VerifyResult::Suspended(state));
331                }
332                Err(e) => {
333                    #[cfg(feature = "logging")]
334                    logging::on_script_error(_hash, &self.hash(), &e);
335                    return Err(e.source(group).into());
336                }
337            }
338        }
339
340        Ok(VerifyResult::Completed(cycles))
341    }
342
343    /// Complete an suspended verify
344    ///
345    /// ## Params
346    ///
347    /// * `snap` - Captured transaction verification state.
348    ///
349    /// * `max_cycles` - Maximum allowed cycles to run the scripts. The verification quits early
350    ///   when the consumed cycles exceed the limit.
351    ///
352    /// ## Returns
353    ///
354    /// It returns the total consumed cycles on completed, Otherwise it returns the verification error.
355    pub fn complete(&self, snap: &TransactionState, max_cycles: Cycle) -> Result<Cycle, Error> {
356        let mut cycles = snap.current_cycles;
357
358        let (_hash, current_group) = self.groups().nth(snap.current).ok_or_else(|| {
359            ScriptError::Other(format!("snapshot group missing {:?}", snap.current))
360                .unknown_source()
361        })?;
362
363        if max_cycles < cycles {
364            return Err(ScriptError::ExceededMaximumCycles(max_cycles)
365                .source(current_group)
366                .into());
367        }
368
369        // continue snapshot current script
370        // max_cycles - cycles checked
371        match self.verify_group_with_chunk(current_group, max_cycles - cycles, &snap.state) {
372            Ok(ChunkState::Completed(used_cycles, _consumed_cycles)) => {
373                cycles = wrapping_cycles_add(cycles, used_cycles, current_group)?;
374            }
375            Ok(ChunkState::Suspended(_)) => {
376                return Err(ScriptError::ExceededMaximumCycles(max_cycles)
377                    .source(current_group)
378                    .into());
379            }
380            Err(e) => {
381                #[cfg(feature = "logging")]
382                logging::on_script_error(_hash, &self.hash(), &e);
383                return Err(e.source(current_group).into());
384            }
385        }
386
387        for (_hash, group) in self.groups().skip(snap.current + 1) {
388            let remain_cycles = max_cycles.checked_sub(cycles).ok_or_else(|| {
389                ScriptError::Other(format!("expect invalid cycles {max_cycles} {cycles}"))
390                    .source(group)
391            })?;
392
393            match self.verify_group_with_chunk(group, remain_cycles, &None) {
394                Ok(ChunkState::Completed(used_cycles, _consumed_cycles)) => {
395                    cycles = wrapping_cycles_add(cycles, used_cycles, current_group)?;
396                }
397                Ok(ChunkState::Suspended(_)) => {
398                    return Err(ScriptError::ExceededMaximumCycles(max_cycles)
399                        .source(group)
400                        .into());
401                }
402                Err(e) => {
403                    #[cfg(feature = "logging")]
404                    logging::on_script_error(_hash, &self.hash(), &e);
405                    return Err(e.source(group).into());
406                }
407            }
408        }
409
410        Ok(cycles)
411    }
412
413    /// Runs a single script in current transaction, while this is not useful for
414    /// CKB itself, it can be very helpful when building a CKB debugger.
415    pub fn verify_single(
416        &self,
417        script_group_type: ScriptGroupType,
418        script_hash: &Byte32,
419        max_cycles: Cycle,
420    ) -> Result<Cycle, ScriptError> {
421        match self.find_script_group(script_group_type, script_hash) {
422            Some(group) => self.verify_script_group(group, max_cycles),
423            None => Err(ScriptError::ScriptNotFound(script_hash.clone())),
424        }
425    }
426
427    fn verify_script_group(
428        &self,
429        group: &ScriptGroup,
430        max_cycles: Cycle,
431    ) -> Result<Cycle, ScriptError> {
432        if group.script.code_hash() == TYPE_ID_CODE_HASH.into()
433            && Into::<u8>::into(group.script.hash_type()) == Into::<u8>::into(ScriptHashType::Type)
434        {
435            let verifier = TypeIdSystemScript {
436                rtx: &self.tx_data.rtx,
437                script_group: group,
438                max_cycles,
439            };
440            verifier.verify()
441        } else {
442            self.run(group, max_cycles)
443        }
444    }
445
446    fn verify_group_with_chunk(
447        &self,
448        group: &ScriptGroup,
449        max_cycles: Cycle,
450        state: &Option<FullSuspendedState>,
451    ) -> Result<ChunkState, ScriptError> {
452        if group.script.code_hash() == TYPE_ID_CODE_HASH.into()
453            && Into::<u8>::into(group.script.hash_type()) == Into::<u8>::into(ScriptHashType::Type)
454        {
455            let verifier = TypeIdSystemScript {
456                rtx: &self.tx_data.rtx,
457                script_group: group,
458                max_cycles,
459            };
460            match verifier.verify() {
461                Ok(cycles) => Ok(ChunkState::Completed(cycles, cycles)),
462                Err(ScriptError::ExceededMaximumCycles(_)) => Ok(ChunkState::suspended_type_id()),
463                Err(e) => Err(e),
464            }
465        } else {
466            self.chunk_run(group, max_cycles, state)
467        }
468    }
469
470    fn chunk_run(
471        &self,
472        script_group: &ScriptGroup,
473        max_cycles: Cycle,
474        state: &Option<FullSuspendedState>,
475    ) -> Result<ChunkState, ScriptError> {
476        let mut scheduler = if let Some(state) = state {
477            self.resume_scheduler(script_group, state)
478        } else {
479            self.create_scheduler(script_group)
480        }?;
481        let previous_cycles = scheduler.consumed_cycles();
482        let res = scheduler.run(RunMode::LimitCycles(max_cycles));
483        match res {
484            Ok(TerminatedResult {
485                exit_code,
486                consumed_cycles: cycles,
487            }) => {
488                if exit_code == 0 {
489                    Ok(ChunkState::Completed(
490                        cycles,
491                        scheduler.consumed_cycles() - previous_cycles,
492                    ))
493                } else {
494                    Err(ScriptError::validation_failure(
495                        &script_group.script,
496                        exit_code,
497                    ))
498                }
499            }
500            Err(error) => match error {
501                VMInternalError::CyclesExceeded | VMInternalError::Pause => {
502                    let snapshot = scheduler
503                        .suspend()
504                        .map_err(|err| self.map_vm_internal_error(err, max_cycles))?;
505                    Ok(ChunkState::suspended(snapshot))
506                }
507                _ => Err(self.map_vm_internal_error(error, max_cycles)),
508            },
509        }
510    }
511
512    /// Create a scheduler to manage virtual machine instances.
513    pub fn create_scheduler(
514        &self,
515        script_group: &ScriptGroup,
516    ) -> Result<Scheduler<DL, V, M>, ScriptError> {
517        let sg_data = SgData::new(&self.tx_data, script_group)?;
518        Ok(Scheduler::new(
519            sg_data,
520            self.syscall_generator,
521            self.syscall_context.clone(),
522        ))
523    }
524
525    /// Resumes a scheduler from a previous state.
526    pub fn resume_scheduler(
527        &self,
528        script_group: &ScriptGroup,
529        state: &FullSuspendedState,
530    ) -> Result<Scheduler<DL, V, M>, ScriptError> {
531        let sg_data = SgData::new(&self.tx_data, script_group)?;
532        Ok(Scheduler::resume(
533            sg_data,
534            self.syscall_generator,
535            self.syscall_context.clone(),
536            state.clone(),
537        ))
538    }
539
540    /// Runs a single program, then returns the exit code together with the entire
541    /// machine to the caller for more inspections.
542    pub fn detailed_run(
543        &self,
544        script_group: &ScriptGroup,
545        max_cycles: Cycle,
546    ) -> Result<TerminatedResult, ScriptError> {
547        let mut scheduler = self.create_scheduler(script_group)?;
548        scheduler
549            .run(RunMode::LimitCycles(max_cycles))
550            .map_err(|err| self.map_vm_internal_error(err, max_cycles))
551    }
552
553    fn run(&self, script_group: &ScriptGroup, max_cycles: Cycle) -> Result<Cycle, ScriptError> {
554        let result = self.detailed_run(script_group, max_cycles)?;
555
556        if result.exit_code == 0 {
557            Ok(result.consumed_cycles)
558        } else {
559            Err(ScriptError::validation_failure(
560                &script_group.script,
561                result.exit_code,
562            ))
563        }
564    }
565
566    fn map_vm_internal_error(&self, error: VMInternalError, max_cycles: Cycle) -> ScriptError {
567        match error {
568            VMInternalError::CyclesExceeded => ScriptError::ExceededMaximumCycles(max_cycles),
569            VMInternalError::External(reason) if reason.eq("stopped") => ScriptError::Interrupts,
570            _ => ScriptError::VMInternalError(error),
571        }
572    }
573}
574
575#[cfg(not(target_family = "wasm"))]
576impl<DL, V, M> TransactionScriptsVerifier<DL, V, M>
577where
578    DL: CellDataProvider + HeaderProvider + ExtensionProvider + Send + Sync + Clone + 'static,
579    V: Send + Clone + 'static,
580    M: DefaultMachineRunner + Send + 'static,
581{
582    /// Performing a resumable verification on the transaction scripts with signal channel,
583    /// if `Suspend` comes from `command_rx`, the process will be hang up until `Resume` comes,
584    /// otherwise, it will return until the verification is completed.
585    pub async fn resumable_verify_with_signal(
586        &self,
587        limit_cycles: Cycle,
588        command_rx: &mut Receiver<ChunkCommand>,
589    ) -> Result<Cycle, Error> {
590        let mut cycles = 0;
591
592        let groups: Vec<_> = self.groups().collect();
593        for (_hash, group) in groups.iter() {
594            // vm should early return invalid cycles
595            let remain_cycles = limit_cycles.checked_sub(cycles).ok_or_else(|| {
596                ScriptError::Other(format!("expect invalid cycles {limit_cycles} {cycles}"))
597                    .source(group)
598            })?;
599
600            match self
601                .verify_group_with_signal(group, remain_cycles, command_rx)
602                .await
603            {
604                Ok(used_cycles) => {
605                    cycles = wrapping_cycles_add(cycles, used_cycles, group)?;
606                }
607                Err(e) => {
608                    #[cfg(feature = "logging")]
609                    logging::on_script_error(_hash, &self.hash(), &e);
610                    return Err(e.source(group).into());
611                }
612            }
613        }
614
615        Ok(cycles)
616    }
617
618    async fn verify_group_with_signal(
619        &self,
620        group: &ScriptGroup,
621        max_cycles: Cycle,
622        command_rx: &mut Receiver<ChunkCommand>,
623    ) -> Result<Cycle, ScriptError> {
624        if group.script.code_hash() == TYPE_ID_CODE_HASH.into()
625            && Into::<u8>::into(group.script.hash_type()) == Into::<u8>::into(ScriptHashType::Type)
626        {
627            let verifier = TypeIdSystemScript {
628                rtx: &self.tx_data.rtx,
629                script_group: group,
630                max_cycles,
631            };
632            verifier.verify()
633        } else {
634            self.chunk_run_with_signal(group, max_cycles, command_rx)
635                .await
636        }
637    }
638
639    async fn chunk_run_with_signal(
640        &self,
641        script_group: &ScriptGroup,
642        max_cycles: Cycle,
643        signal: &mut Receiver<ChunkCommand>,
644    ) -> Result<Cycle, ScriptError> {
645        let mut scheduler = self.create_scheduler(script_group)?;
646        let mut pause = VMPause::new();
647        let child_pause = pause.clone();
648        let (finish_tx, mut finish_rx) =
649            oneshot::channel::<Result<TerminatedResult, ckb_vm::Error>>();
650
651        // send initial `Resume` command to child
652        // it's maybe useful to set initial command to `signal.borrow().to_owned()`
653        // so that we can control the initial state of child, which is useful for testing purpose
654        let (child_tx, mut child_rx) = watch::channel(ChunkCommand::Resume);
655        let jh = tokio::spawn(async move {
656            child_rx.mark_changed();
657            loop {
658                let pause_cloned = child_pause.clone();
659                let _ = child_rx.changed().await;
660                match *child_rx.borrow() {
661                    ChunkCommand::Stop => {
662                        let exit = Err(ckb_vm::Error::External("stopped".into()));
663                        let _ = finish_tx.send(exit);
664                        return;
665                    }
666                    ChunkCommand::Suspend => {
667                        continue;
668                    }
669                    ChunkCommand::Resume => {
670                        //info!("[verify-test] run_vms_child: resume");
671                        let res = scheduler.run(RunMode::Pause(pause_cloned, max_cycles));
672                        match res {
673                            Ok(_) => {
674                                let _ = finish_tx.send(res);
675                                return;
676                            }
677                            Err(VMInternalError::Pause) => {
678                                // continue to wait for
679                                debug_assert!(
680                                    scheduler.consumed_cycles() <= max_cycles,
681                                    "Consumed cycles ({}) exceeded max_cycles ({})",
682                                    scheduler.consumed_cycles(),
683                                    max_cycles
684                                );
685                            }
686                            _ => {
687                                let _ = finish_tx.send(res);
688                                return;
689                            }
690                        }
691                    }
692                }
693            }
694        });
695
696        loop {
697            tokio::select! {
698                Ok(_) = signal.changed() => {
699                    let command = signal.borrow().to_owned();
700                    //info!("[verify-test] run_vms_with_signal: {:?}", command);
701                    match command {
702                        ChunkCommand::Suspend => {
703                            pause.interrupt();
704                        }
705                        ChunkCommand::Stop => {
706                            pause.interrupt();
707                            let _ = child_tx.send(command);
708                        }
709                        ChunkCommand::Resume => {
710                            pause.free();
711                            let _ = child_tx.send(command);
712                        }
713                    }
714                }
715                Ok(res) = &mut finish_rx => {
716                    let _ = jh.await;
717                    match res {
718                        Ok(TerminatedResult {
719                            exit_code: 0,
720                            consumed_cycles: cycles,
721                        }) => {
722                            return Ok(cycles);
723                        }
724                        Ok(TerminatedResult { exit_code, .. }) => {
725                            return Err(ScriptError::validation_failure(
726                                &script_group.script,
727                                exit_code
728                            ))},
729                        Err(err) => {
730                            return Err(self.map_vm_internal_error(err, max_cycles));
731                        }
732                    }
733
734                }
735                else => { break Err(ScriptError::validation_failure(&script_group.script, 0)) }
736            }
737        }
738    }
739}
740
741fn wrapping_cycles_add(
742    lhs: Cycle,
743    rhs: Cycle,
744    group: &ScriptGroup,
745) -> Result<Cycle, TransactionScriptError> {
746    lhs.checked_add(rhs)
747        .ok_or_else(|| ScriptError::CyclesOverflow(lhs, rhs).source(group))
748}
749
750#[cfg(feature = "logging")]
751mod logging {
752    use super::{Byte32, ScriptError, info};
753
754    pub fn on_script_error(group: &Byte32, tx: &Byte32, error: &ScriptError) {
755        info!(
756            "Error validating script group {} of transaction {}: {}",
757            group, tx, error
758        );
759    }
760}