Skip to main content

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, Machine, RunMode, ScriptGroup, ScriptGroupType, ScriptVersion, SgData,
10        SyscallGenerator, TerminatedResult, TxData,
11    },
12    verify_env::TxVerifyEnv,
13};
14use ckb_chain_spec::consensus::{Consensus, TYPE_ID_CODE_HASH};
15use ckb_error::Error;
16#[cfg(feature = "logging")]
17use ckb_logger::{debug, info};
18use ckb_traits::{CellDataProvider, ExtensionProvider, HeaderProvider};
19use ckb_types::{
20    bytes::Bytes,
21    core::{Cycle, ScriptHashType, cell::ResolvedTransaction},
22    packed::{Byte32, Script},
23};
24#[cfg(not(target_family = "wasm"))]
25use ckb_vm::machine::Pause as VMPause;
26use ckb_vm::{DefaultMachineRunner, Error as VMInternalError};
27use std::sync::Arc;
28#[cfg(not(target_family = "wasm"))]
29use tokio::sync::{
30    oneshot,
31    watch::{self, Receiver},
32};
33
34#[cfg(test)]
35mod tests;
36
37/// This struct leverages CKB VM to verify transaction inputs.
38pub struct TransactionScriptsVerifier<
39    DL: CellDataProvider,
40    V = DebugPrinter,
41    M: DefaultMachineRunner = Machine,
42> {
43    tx_data: Arc<TxData<DL>>,
44    syscall_generator: SyscallGenerator<DL, V, <M as DefaultMachineRunner>::Inner>,
45    syscall_context: V,
46}
47
48impl<DL> TransactionScriptsVerifier<DL>
49where
50    DL: CellDataProvider + HeaderProvider + ExtensionProvider + Send + Sync + Clone + 'static,
51{
52    /// Create a script verifier using default CKB syscalls and a default debug printer
53    pub fn new(
54        rtx: Arc<ResolvedTransaction>,
55        data_loader: DL,
56        consensus: Arc<Consensus>,
57        tx_env: Arc<TxVerifyEnv>,
58    ) -> Self {
59        let debug_printer: DebugPrinter = Arc::new(
60            #[allow(unused_variables)]
61            |hash: &Byte32, message: &str| {
62                #[cfg(feature = "logging")]
63                debug!("script group: {} DEBUG OUTPUT: {}", hash, message);
64            },
65        );
66
67        Self::new_with_debug_printer(rtx, data_loader, consensus, tx_env, debug_printer)
68    }
69
70    /// Create a script verifier using default CKB syscalls and a custom debug printer
71    pub fn new_with_debug_printer(
72        rtx: Arc<ResolvedTransaction>,
73        data_loader: DL,
74        consensus: Arc<Consensus>,
75        tx_env: Arc<TxVerifyEnv>,
76        debug_printer: DebugPrinter,
77    ) -> Self {
78        Self::new_with_generator(
79            rtx,
80            data_loader,
81            consensus,
82            tx_env,
83            generate_ckb_syscalls,
84            debug_printer,
85        )
86    }
87}
88
89impl<DL, V, M> TransactionScriptsVerifier<DL, V, M>
90where
91    DL: CellDataProvider + HeaderProvider + ExtensionProvider + Clone,
92    V: Clone,
93    M: DefaultMachineRunner,
94{
95    /// Creates a script verifier for the transaction.
96    ///
97    /// ## Params
98    ///
99    /// * `rtx` - transaction which cell out points have been resolved.
100    /// * `data_loader` - used to load cell data.
101    /// * `consensus` - consensus parameters.
102    /// * `tx_env` - environment for verifying transaction, such as committed block, etc.
103    /// * `syscall_generator` - a syscall generator for current verifier
104    /// * `syscall_context` - context for syscall generator
105    pub fn new_with_generator(
106        rtx: Arc<ResolvedTransaction>,
107        data_loader: DL,
108        consensus: Arc<Consensus>,
109        tx_env: Arc<TxVerifyEnv>,
110        syscall_generator: SyscallGenerator<DL, V, <M as DefaultMachineRunner>::Inner>,
111        syscall_context: V,
112    ) -> TransactionScriptsVerifier<DL, V, M> {
113        let tx_data = Arc::new(TxData::new(rtx, data_loader, consensus, tx_env));
114
115        TransactionScriptsVerifier {
116            tx_data,
117            syscall_generator,
118            syscall_context,
119        }
120    }
121
122    //////////////////////////////////////////////////////////////////
123    // Functions below have been moved from verifier struct to TxData,
124    // however we still preserve all the public APIs by delegating
125    // them to TxData.
126    //////////////////////////////////////////////////////////////////
127
128    #[inline]
129    #[allow(dead_code)]
130    fn hash(&self) -> Byte32 {
131        self.tx_data.tx_hash()
132    }
133
134    /// Extracts actual script binary either in dep cells.
135    pub fn extract_script(&self, script: &Script) -> Result<Bytes, ScriptError> {
136        self.tx_data.extract_script(script)
137    }
138
139    /// Returns the version of the machine based on the script and the consensus rules.
140    pub fn select_version(&self, script: &Script) -> Result<ScriptVersion, ScriptError> {
141        self.tx_data.select_version(script)
142    }
143
144    /// Returns all script groups.
145    pub fn groups(&self) -> impl Iterator<Item = (&'_ Byte32, &'_ ScriptGroup)> {
146        self.tx_data.groups()
147    }
148
149    /// Returns all script groups with type.
150    pub fn groups_with_type(
151        &self,
152    ) -> impl Iterator<Item = (ScriptGroupType, &'_ Byte32, &'_ ScriptGroup)> {
153        self.tx_data.groups_with_type()
154    }
155
156    /// Finds the script group from cell deps.
157    pub fn find_script_group(
158        &self,
159        script_group_type: ScriptGroupType,
160        script_hash: &Byte32,
161    ) -> Option<&ScriptGroup> {
162        self.tx_data
163            .find_script_group(script_group_type, script_hash)
164    }
165
166    //////////////////////////////////////////////////////////////////
167    // This marks the end of delegated functions.
168    //////////////////////////////////////////////////////////////////
169
170    /// Verifies the transaction by running scripts.
171    ///
172    /// ## Params
173    ///
174    /// * `max_cycles` - Maximum allowed cycles to run the scripts. The verification quits early
175    ///   when the consumed cycles exceed the limit.
176    ///
177    /// ## Returns
178    ///
179    /// It returns the total consumed cycles on success, Otherwise it returns the verification error.
180    pub fn verify(&self, max_cycles: Cycle) -> Result<Cycle, Error> {
181        let mut cycles: Cycle = 0;
182
183        // Now run each script group
184        for (_hash, group) in self.groups() {
185            // max_cycles must reduce by each group exec
186            let used_cycles = self
187                .verify_script_group(group, max_cycles - cycles)
188                .map_err(|e| {
189                    #[cfg(feature = "logging")]
190                    logging::on_script_error(_hash, &self.hash(), &e);
191                    e.source(group)
192                })?;
193
194            cycles = wrapping_cycles_add(cycles, used_cycles, group)?;
195        }
196        Ok(cycles)
197    }
198
199    /// Runs a single script in current transaction, while this is not useful for
200    /// CKB itself, it can be very helpful when building a CKB debugger.
201    pub fn verify_single(
202        &self,
203        script_group_type: ScriptGroupType,
204        script_hash: &Byte32,
205        max_cycles: Cycle,
206    ) -> Result<Cycle, ScriptError> {
207        match self.find_script_group(script_group_type, script_hash) {
208            Some(group) => self.verify_script_group(group, max_cycles),
209            None => Err(ScriptError::ScriptNotFound(script_hash.clone())),
210        }
211    }
212
213    fn verify_script_group(
214        &self,
215        group: &ScriptGroup,
216        max_cycles: Cycle,
217    ) -> Result<Cycle, ScriptError> {
218        if group.script.code_hash() == TYPE_ID_CODE_HASH.into()
219            && Into::<u8>::into(group.script.hash_type()) == Into::<u8>::into(ScriptHashType::Type)
220        {
221            let verifier = TypeIdSystemScript {
222                rtx: &self.tx_data.rtx,
223                script_group: group,
224                max_cycles,
225            };
226            verifier.verify()
227        } else {
228            self.run(group, max_cycles)
229        }
230    }
231
232    /// Create a scheduler to manage virtual machine instances.
233    pub fn create_scheduler(
234        &self,
235        script_group: &ScriptGroup,
236    ) -> Result<Scheduler<DL, V, M>, ScriptError> {
237        let sg_data = SgData::new(&self.tx_data, script_group)?;
238        Ok(Scheduler::new(
239            sg_data,
240            self.syscall_generator,
241            self.syscall_context.clone(),
242        ))
243    }
244
245    /// Runs a single program, then returns the exit code together with the entire
246    /// machine to the caller for more inspections.
247    pub fn detailed_run(
248        &self,
249        script_group: &ScriptGroup,
250        max_cycles: Cycle,
251    ) -> Result<TerminatedResult, ScriptError> {
252        let mut scheduler = self.create_scheduler(script_group)?;
253        scheduler
254            .run(RunMode::LimitCycles(max_cycles))
255            .map_err(|err| self.map_vm_internal_error(err, max_cycles))
256    }
257
258    fn run(&self, script_group: &ScriptGroup, max_cycles: Cycle) -> Result<Cycle, ScriptError> {
259        let result = self.detailed_run(script_group, max_cycles)?;
260
261        if result.exit_code == 0 {
262            Ok(result.consumed_cycles)
263        } else {
264            Err(ScriptError::validation_failure(
265                &script_group.script,
266                result.exit_code,
267            ))
268        }
269    }
270
271    fn map_vm_internal_error(&self, error: VMInternalError, max_cycles: Cycle) -> ScriptError {
272        match error {
273            VMInternalError::CyclesExceeded => ScriptError::ExceededMaximumCycles(max_cycles),
274            VMInternalError::External(reason) if reason.eq("stopped") => ScriptError::Interrupts,
275            _ => ScriptError::VMInternalError(error),
276        }
277    }
278}
279
280#[cfg(not(target_family = "wasm"))]
281impl<DL, V, M> TransactionScriptsVerifier<DL, V, M>
282where
283    DL: CellDataProvider + HeaderProvider + ExtensionProvider + Send + Sync + Clone + 'static,
284    V: Send + Clone + 'static,
285    M: DefaultMachineRunner + Send + 'static,
286{
287    /// Performing a resumable verification on the transaction scripts with signal channel,
288    /// if `Suspend` comes from `command_rx`, the process will be hang up until `Resume` comes,
289    /// otherwise, it will return until the verification is completed.
290    pub async fn resumable_verify_with_signal(
291        &self,
292        limit_cycles: Cycle,
293        command_rx: &mut Receiver<ChunkCommand>,
294    ) -> Result<Cycle, Error> {
295        let mut cycles = 0;
296
297        let groups: Vec<_> = self.groups().collect();
298        for (_hash, group) in groups.iter() {
299            // vm should early return invalid cycles
300            let remain_cycles = limit_cycles.checked_sub(cycles).ok_or_else(|| {
301                ScriptError::Other(format!("expect invalid cycles {limit_cycles} {cycles}"))
302                    .source(group)
303            })?;
304
305            match self
306                .verify_group_with_signal(group, remain_cycles, command_rx)
307                .await
308            {
309                Ok(used_cycles) => {
310                    cycles = wrapping_cycles_add(cycles, used_cycles, group)?;
311                }
312                Err(e) => {
313                    #[cfg(feature = "logging")]
314                    logging::on_script_error(_hash, &self.hash(), &e);
315                    return Err(e.source(group).into());
316                }
317            }
318        }
319
320        Ok(cycles)
321    }
322
323    async fn verify_group_with_signal(
324        &self,
325        group: &ScriptGroup,
326        max_cycles: Cycle,
327        command_rx: &mut Receiver<ChunkCommand>,
328    ) -> Result<Cycle, ScriptError> {
329        if group.script.code_hash() == TYPE_ID_CODE_HASH.into()
330            && Into::<u8>::into(group.script.hash_type()) == Into::<u8>::into(ScriptHashType::Type)
331        {
332            let verifier = TypeIdSystemScript {
333                rtx: &self.tx_data.rtx,
334                script_group: group,
335                max_cycles,
336            };
337            verifier.verify()
338        } else {
339            self.chunk_run_with_signal(group, max_cycles, command_rx)
340                .await
341        }
342    }
343
344    async fn chunk_run_with_signal(
345        &self,
346        script_group: &ScriptGroup,
347        max_cycles: Cycle,
348        signal: &mut Receiver<ChunkCommand>,
349    ) -> Result<Cycle, ScriptError> {
350        let mut scheduler = self.create_scheduler(script_group)?;
351        let mut pause = VMPause::new();
352        let child_pause = pause.clone();
353        let (finish_tx, mut finish_rx) =
354            oneshot::channel::<Result<TerminatedResult, ckb_vm::Error>>();
355
356        // send initial `Resume` command to child
357        // it's maybe useful to set initial command to `signal.borrow().to_owned()`
358        // so that we can control the initial state of child, which is useful for testing purpose
359        let (child_tx, mut child_rx) = watch::channel(ChunkCommand::Resume);
360        let jh = tokio::spawn(async move {
361            child_rx.mark_changed();
362            loop {
363                let pause_cloned = child_pause.clone();
364                let _ = child_rx.changed().await;
365                match *child_rx.borrow() {
366                    ChunkCommand::Stop => {
367                        let exit = Err(ckb_vm::Error::External("stopped".into()));
368                        let _ = finish_tx.send(exit);
369                        return;
370                    }
371                    ChunkCommand::Suspend => {
372                        continue;
373                    }
374                    ChunkCommand::Resume => {
375                        //info!("[verify-test] run_vms_child: resume");
376                        let res = scheduler.run(RunMode::Pause(pause_cloned, max_cycles));
377                        match res {
378                            Ok(_) => {
379                                let _ = finish_tx.send(res);
380                                return;
381                            }
382                            Err(VMInternalError::Pause) => {
383                                // continue to wait for
384                                debug_assert!(
385                                    scheduler.consumed_cycles() <= max_cycles,
386                                    "Consumed cycles ({}) exceeded max_cycles ({})",
387                                    scheduler.consumed_cycles(),
388                                    max_cycles
389                                );
390                            }
391                            _ => {
392                                let _ = finish_tx.send(res);
393                                return;
394                            }
395                        }
396                    }
397                }
398            }
399        });
400
401        loop {
402            tokio::select! {
403                Ok(_) = signal.changed() => {
404                    let command = signal.borrow().to_owned();
405                    //info!("[verify-test] run_vms_with_signal: {:?}", command);
406                    match command {
407                        ChunkCommand::Suspend => {
408                            pause.interrupt();
409                        }
410                        ChunkCommand::Stop => {
411                            pause.interrupt();
412                            let _ = child_tx.send(command);
413                        }
414                        ChunkCommand::Resume => {
415                            pause.free();
416                            let _ = child_tx.send(command);
417                        }
418                    }
419                }
420                Ok(res) = &mut finish_rx => {
421                    let _ = jh.await;
422                    match res {
423                        Ok(TerminatedResult {
424                            exit_code: 0,
425                            consumed_cycles: cycles,
426                        }) => {
427                            return Ok(cycles);
428                        }
429                        Ok(TerminatedResult { exit_code, .. }) => {
430                            return Err(ScriptError::validation_failure(
431                                &script_group.script,
432                                exit_code
433                            ))},
434                        Err(err) => {
435                            return Err(self.map_vm_internal_error(err, max_cycles));
436                        }
437                    }
438
439                }
440                else => { break Err(ScriptError::validation_failure(&script_group.script, 0)) }
441            }
442        }
443    }
444}
445
446fn wrapping_cycles_add(
447    lhs: Cycle,
448    rhs: Cycle,
449    group: &ScriptGroup,
450) -> Result<Cycle, TransactionScriptError> {
451    lhs.checked_add(rhs)
452        .ok_or_else(|| ScriptError::CyclesOverflow(lhs, rhs).source(group))
453}
454
455#[cfg(feature = "logging")]
456mod logging {
457    use super::{Byte32, ScriptError, info};
458
459    pub fn on_script_error(group: &Byte32, tx: &Byte32, error: &ScriptError) {
460        info!(
461            "Error validating script group {} of transaction {}: {}",
462            group, tx, error
463        );
464    }
465}