fuel_core/schema/
dap.rs

1use crate::{
2    database::{
3        Database,
4        OnChainIterableKeyValueView,
5        database_description::on_chain::OnChain,
6    },
7    fuel_core_graphql_api::api_service::ChainInfoProvider,
8    schema::scalars::{
9        U32,
10        U64,
11    },
12};
13use anyhow::anyhow;
14use async_graphql::{
15    Context,
16    ID,
17    Object,
18    SchemaBuilder,
19};
20use fuel_core_storage::{
21    InterpreterStorage,
22    not_found,
23    transactional::{
24        AtomicView,
25        IntoTransaction,
26        StorageTransaction,
27    },
28    vm_storage::VmStorage,
29};
30use fuel_core_types::{
31    fuel_asm::{
32        Instruction,
33        RegisterId,
34        Word,
35    },
36    fuel_tx::{
37        ConsensusParameters,
38        Executable,
39        Script,
40        Transaction,
41        field::{
42            Policies,
43            ScriptGasLimit,
44            Witnesses,
45        },
46        policies::PolicyType,
47    },
48    fuel_vm::{
49        Interpreter,
50        InterpreterError,
51        checked_transaction::{
52            CheckedTransaction,
53            IntoChecked,
54        },
55        interpreter::{
56            InterpreterParams,
57            MemoryInstance,
58        },
59        state::DebugEval,
60    },
61};
62use futures::lock::Mutex;
63use std::{
64    collections::HashMap,
65    io,
66    sync,
67    sync::Arc,
68};
69use tracing::{
70    debug,
71    trace,
72};
73use uuid::Uuid;
74
75pub struct Config {
76    /// `true` means that debugger functionality is enabled.
77    pub debug_enabled: bool,
78}
79
80pub fn require_debug(ctx: &Context<'_>) -> async_graphql::Result<()> {
81    let config = ctx.data_unchecked::<Config>();
82
83    if config.debug_enabled {
84        Ok(())
85    } else {
86        Err(async_graphql::Error::new("The 'debug' feature is disabled"))
87    }
88}
89
90type FrozenDatabase = VmStorage<StorageTransaction<OnChainIterableKeyValueView>>;
91
92#[derive(Default, Debug)]
93pub struct ConcreteStorage {
94    vm: HashMap<ID, Interpreter<MemoryInstance, FrozenDatabase, Script>>,
95    tx: HashMap<ID, Vec<Script>>,
96}
97
98/// The gas price used for transactions in the debugger. It is set to 0 because
99/// the debugger does not actually execute transactions, but only simulates
100/// their execution.
101const GAS_PRICE: u64 = 0;
102
103impl ConcreteStorage {
104    pub fn new() -> Self {
105        Self::default()
106    }
107
108    pub fn register(&self, id: &ID, register: RegisterId) -> Option<Word> {
109        self.vm
110            .get(id)
111            .and_then(|vm| vm.registers().get(register).copied())
112    }
113
114    pub fn memory(&self, id: &ID, start: usize, size: usize) -> Option<&[u8]> {
115        self.vm
116            .get(id)
117            .and_then(|vm| vm.memory().read(start, size).ok())
118    }
119
120    pub fn init(
121        &mut self,
122        txs: &[Script],
123        params: Arc<ConsensusParameters>,
124        storage: &Database<OnChain>,
125    ) -> anyhow::Result<ID> {
126        let id = Uuid::new_v4();
127        let id = ID::from(id);
128
129        let vm_database = Self::vm_database(storage)?;
130        let tx = Self::dummy_tx(params.tx_params().max_gas_per_tx() / 2);
131        let checked_tx = tx
132            .into_checked_basic(vm_database.block_height()?, &params)
133            .map_err(|e| anyhow::anyhow!("{:?}", e))?;
134        self.tx
135            .get_mut(&id)
136            .map(|tx| tx.extend_from_slice(txs))
137            .unwrap_or_else(|| {
138                self.tx.insert(id.clone(), txs.to_owned());
139            });
140
141        let gas_costs = params.gas_costs();
142        let fee_params = params.fee_params();
143
144        let ready_tx = checked_tx
145            .into_ready(
146                GAS_PRICE,
147                gas_costs,
148                fee_params,
149                Some(vm_database.block_height()?),
150            )
151            .map_err(|e| {
152                anyhow!("Failed to apply dynamic values to checked tx: {:?}", e)
153            })?;
154
155        let interpreter_params = InterpreterParams::new(GAS_PRICE, params.as_ref());
156        let mut vm = Interpreter::with_storage(
157            MemoryInstance::new(),
158            vm_database,
159            interpreter_params,
160        );
161        vm.transact(ready_tx).map_err(|e| anyhow::anyhow!(e))?;
162        self.vm.insert(id.clone(), vm);
163
164        Ok(id)
165    }
166
167    pub fn kill(&mut self, id: &ID) -> bool {
168        self.tx.remove(id);
169        self.vm.remove(id).is_some()
170    }
171
172    pub fn reset(
173        &mut self,
174        id: &ID,
175        params: Arc<ConsensusParameters>,
176        storage: &Database<OnChain>,
177    ) -> anyhow::Result<()> {
178        let vm_database = Self::vm_database(storage)?;
179        let tx = self
180            .tx
181            .get(id)
182            .and_then(|tx| tx.first())
183            .cloned()
184            .unwrap_or(Self::dummy_tx(params.tx_params().max_gas_per_tx() / 2));
185
186        let checked_tx = tx
187            .into_checked_basic(vm_database.block_height()?, &params)
188            .map_err(|e| anyhow::anyhow!("{:?}", e))?;
189
190        let gas_costs = params.gas_costs();
191        let fee_params = params.fee_params();
192
193        let ready_tx = checked_tx
194            .into_ready(
195                GAS_PRICE,
196                gas_costs,
197                fee_params,
198                Some(vm_database.block_height()?),
199            )
200            .map_err(|e| {
201                anyhow!("Failed to apply dynamic values to checked tx: {:?}", e)
202            })?;
203
204        let interpreter_params = InterpreterParams::new(GAS_PRICE, params.as_ref());
205        let mut vm = Interpreter::with_storage(
206            MemoryInstance::new(),
207            vm_database,
208            interpreter_params,
209        );
210        vm.transact(ready_tx).map_err(|e| anyhow::anyhow!(e))?;
211        self.vm.insert(id.clone(), vm).ok_or_else(|| {
212            io::Error::new(io::ErrorKind::NotFound, "The VM instance was not found")
213        })?;
214        Ok(())
215    }
216
217    pub fn exec(&mut self, id: &ID, op: Instruction) -> anyhow::Result<()> {
218        self.vm
219            .get_mut(id)
220            .map(|vm| vm.instruction::<_, false>(op))
221            .transpose()
222            .map_err(|e| anyhow::anyhow!(e))?
223            .map(|_| ())
224            .ok_or_else(|| anyhow::anyhow!("The VM instance was not found"))
225    }
226
227    fn vm_database(storage: &Database<OnChain>) -> anyhow::Result<FrozenDatabase> {
228        let view = storage.latest_view()?;
229        let block = view
230            .get_current_block()?
231            .ok_or(not_found!("Block for VMDatabase"))?;
232
233        let application_header = block.header().as_empty_application_header();
234
235        let vm_database = VmStorage::new(
236            view.into_transaction(),
237            block.header().consensus(),
238            &application_header,
239            // TODO: Use a real coinbase address
240            Default::default(),
241        );
242
243        Ok(vm_database)
244    }
245
246    fn dummy_tx(gas_limit: u64) -> Script {
247        // Create `Script` transaction with dummy coin
248        let mut tx = Script::default();
249        *tx.script_gas_limit_mut() = gas_limit;
250        tx.add_unsigned_coin_input(
251            Default::default(),
252            &Default::default(),
253            Default::default(),
254            Default::default(),
255            Default::default(),
256            Default::default(),
257        );
258        tx.witnesses_mut().push(vec![].into());
259        tx.policies_mut().set(PolicyType::MaxFee, Some(0));
260        tx
261    }
262}
263
264pub type GraphStorage = sync::Arc<Mutex<ConcreteStorage>>;
265
266#[derive(Default)]
267pub struct DapQuery;
268#[derive(Default)]
269pub struct DapMutation;
270
271pub fn init<Q, M, S>(
272    schema: SchemaBuilder<Q, M, S>,
273    debug_enabled: bool,
274) -> SchemaBuilder<Q, M, S> {
275    schema
276        .data(GraphStorage::new(Mutex::new(ConcreteStorage::new())))
277        .data(Config { debug_enabled })
278}
279
280#[Object]
281impl DapQuery {
282    /// Read register value by index.
283    async fn register(
284        &self,
285        ctx: &Context<'_>,
286        id: ID,
287        register: U32,
288    ) -> async_graphql::Result<U64> {
289        require_debug(ctx)?;
290        ctx.data_unchecked::<GraphStorage>()
291            .lock()
292            .await
293            .register(&id, register.0 as RegisterId)
294            .ok_or_else(|| async_graphql::Error::new("Invalid register identifier"))
295            .map(|val| val.into())
296    }
297
298    /// Read read a range of memory bytes.
299    async fn memory(
300        &self,
301        ctx: &Context<'_>,
302        id: ID,
303        start: U32,
304        size: U32,
305    ) -> async_graphql::Result<String> {
306        require_debug(ctx)?;
307        ctx.data_unchecked::<GraphStorage>()
308            .lock()
309            .await
310            .memory(&id, start.0 as usize, size.0 as usize)
311            .ok_or_else(|| async_graphql::Error::new("Invalid memory range"))
312            .and_then(|mem| Ok(serde_json::to_string(mem)?))
313    }
314}
315
316#[Object]
317impl DapMutation {
318    /// Initialize a new debugger session, returning its ID.
319    /// A new VM instance is spawned for each session.
320    /// The session is run in a separate database transaction,
321    /// on top of the most recent node state.
322    async fn start_session(&self, ctx: &Context<'_>) -> async_graphql::Result<ID> {
323        require_debug(ctx)?;
324        trace!("Initializing new interpreter");
325
326        let db = ctx.data_unchecked::<Database>();
327        let params = ctx
328            .data_unchecked::<ChainInfoProvider>()
329            .current_consensus_params();
330
331        let id =
332            ctx.data_unchecked::<GraphStorage>()
333                .lock()
334                .await
335                .init(&[], params, db)?;
336
337        debug!("Session {:?} initialized", id);
338
339        Ok(id)
340    }
341
342    /// End debugger session.
343    async fn end_session(
344        &self,
345        ctx: &Context<'_>,
346        id: ID,
347    ) -> async_graphql::Result<bool> {
348        require_debug(ctx)?;
349        let existed = ctx.data_unchecked::<GraphStorage>().lock().await.kill(&id);
350
351        debug!("Session {:?} dropped with result {}", id, existed);
352
353        Ok(existed)
354    }
355
356    /// Reset the VM instance to the initial state.
357    async fn reset(&self, ctx: &Context<'_>, id: ID) -> async_graphql::Result<bool> {
358        require_debug(ctx)?;
359        let db = ctx.data_unchecked::<Database>();
360        let params = ctx
361            .data_unchecked::<ChainInfoProvider>()
362            .current_consensus_params();
363
364        ctx.data_unchecked::<GraphStorage>()
365            .lock()
366            .await
367            .reset(&id, params, db)?;
368
369        debug!("Session {:?} was reset", id);
370
371        Ok(true)
372    }
373
374    /// Execute a single fuel-asm instruction.
375    async fn execute(
376        &self,
377        ctx: &Context<'_>,
378        id: ID,
379        op: String,
380    ) -> async_graphql::Result<bool> {
381        require_debug(ctx)?;
382        trace!("Execute encoded op {}", op);
383
384        let op: Instruction = serde_json::from_str(op.as_str())?;
385
386        trace!("Op decoded to {:?}", op);
387
388        let storage = ctx.data_unchecked::<GraphStorage>().clone();
389        let result = storage.lock().await.exec(&id, op).is_ok();
390
391        debug!("Op {:?} executed with result {}", op, result);
392
393        Ok(result)
394    }
395
396    /// Set single-stepping mode for the VM instance.
397    async fn set_single_stepping(
398        &self,
399        ctx: &Context<'_>,
400        id: ID,
401        enable: bool,
402    ) -> async_graphql::Result<bool> {
403        require_debug(ctx)?;
404        trace!("Set single stepping to {} for VM {:?}", enable, id);
405
406        let mut locked = ctx.data_unchecked::<GraphStorage>().lock().await;
407        let vm = locked
408            .vm
409            .get_mut(&id)
410            .ok_or_else(|| async_graphql::Error::new("VM not found"))?;
411
412        vm.set_single_stepping(enable);
413        Ok(enable)
414    }
415
416    /// Set a breakpoint for a VM instance.
417    async fn set_breakpoint(
418        &self,
419        ctx: &Context<'_>,
420        id: ID,
421        breakpoint: gql_types::Breakpoint,
422    ) -> async_graphql::Result<bool> {
423        require_debug(ctx)?;
424        trace!("Set breakpoint for VM {:?}", id);
425
426        let mut locked = ctx.data_unchecked::<GraphStorage>().lock().await;
427        let vm = locked
428            .vm
429            .get_mut(&id)
430            .ok_or_else(|| async_graphql::Error::new("VM not found"))?;
431
432        vm.set_breakpoint(breakpoint.into());
433        Ok(true)
434    }
435
436    /// Run a single transaction in given session until it
437    /// hits a breakpoint or completes.
438    async fn start_tx(
439        &self,
440        ctx: &Context<'_>,
441        id: ID,
442        tx_json: String,
443    ) -> async_graphql::Result<gql_types::RunResult> {
444        require_debug(ctx)?;
445        trace!("Spawning a new VM instance");
446
447        let tx: Transaction = serde_json::from_str(&tx_json)
448            .map_err(|_| async_graphql::Error::new("Invalid transaction JSON"))?;
449
450        let mut locked = ctx.data_unchecked::<GraphStorage>().lock().await;
451        let params = ctx
452            .data_unchecked::<ChainInfoProvider>()
453            .current_consensus_params();
454
455        let vm = locked
456            .vm
457            .get_mut(&id)
458            .ok_or_else(|| async_graphql::Error::new("VM not found"))?;
459
460        let checked_tx = tx
461            .into_checked_basic(vm.as_ref().block_height()?, &params)
462            .map_err(|err| anyhow::anyhow!("{:?}", err))?
463            .into();
464
465        let gas_costs = params.gas_costs();
466        let fee_params = params.fee_params();
467
468        match checked_tx {
469            CheckedTransaction::Script(script) => {
470                let ready_tx = script
471                    .into_ready(
472                        GAS_PRICE,
473                        gas_costs,
474                        fee_params,
475                        Some(vm.as_ref().block_height()?),
476                    )
477                    .map_err(|e| {
478                        anyhow!("Failed to apply dynamic values to checked tx: {:?}", e)
479                    })?;
480                let state_ref = vm.transact(ready_tx).map_err(|err| {
481                    async_graphql::Error::new(format!("Transaction failed: {err:?}"))
482                })?;
483
484                let json_receipts = state_ref
485                    .receipts()
486                    .iter()
487                    .map(|r| {
488                        serde_json::to_string(&r).expect("JSON serialization failed")
489                    })
490                    .collect();
491
492                let dbgref = state_ref.state().debug_ref();
493                Ok(gql_types::RunResult {
494                    state: match dbgref {
495                        Some(_) => gql_types::RunState::Breakpoint,
496                        None => gql_types::RunState::Completed,
497                    },
498                    breakpoint: dbgref.and_then(|d| match d {
499                        DebugEval::Continue => None,
500                        DebugEval::Breakpoint(bp) => Some(bp.into()),
501                    }),
502                    json_receipts,
503                })
504            }
505            CheckedTransaction::Create(_) => {
506                Err(async_graphql::Error::new("`Create` is not supported"))
507            }
508            CheckedTransaction::Mint(_) => {
509                Err(async_graphql::Error::new("`Mint` is not supported"))
510            }
511            CheckedTransaction::Upgrade(_) => {
512                Err(async_graphql::Error::new("`Upgrade` is not supported"))
513            }
514            CheckedTransaction::Upload(_) => {
515                Err(async_graphql::Error::new("`Upload` is not supported"))
516            }
517            CheckedTransaction::Blob(_) => {
518                Err(async_graphql::Error::new("`Blob` is not supported"))
519            }
520        }
521    }
522
523    /// Resume execution of the VM instance after a breakpoint.
524    /// Runs until the next breakpoint or until the transaction completes.
525    async fn continue_tx(
526        &self,
527        ctx: &Context<'_>,
528        id: ID,
529    ) -> async_graphql::Result<gql_types::RunResult> {
530        require_debug(ctx)?;
531        trace!("Continue execution of VM {:?}", id);
532
533        let mut locked = ctx.data_unchecked::<GraphStorage>().lock().await;
534        let vm = locked
535            .vm
536            .get_mut(&id)
537            .ok_or_else(|| async_graphql::Error::new("VM not found"))?;
538
539        let receipt_count_before = vm.receipts().len();
540
541        let state = match vm.resume() {
542            Ok(state) => state,
543            // The transaction was already completed earlier, so it cannot be resumed
544            Err(InterpreterError::DebugStateNotInitialized) => {
545                return Ok(gql_types::RunResult {
546                    state: gql_types::RunState::Completed,
547                    breakpoint: None,
548                    json_receipts: Vec::new(),
549                })
550            }
551            // The transaction was already completed earlier, so it cannot be resumed
552            Err(err) => {
553                return Err(async_graphql::Error::new(format!("VM error: {err:?}")))
554            }
555        };
556
557        let json_receipts = vm
558            .receipts()
559            .iter()
560            .skip(receipt_count_before)
561            .map(|r| serde_json::to_string(&r).expect("JSON serialization failed"))
562            .collect();
563
564        let dbgref = state.debug_ref();
565
566        Ok(gql_types::RunResult {
567            state: match dbgref {
568                Some(_) => gql_types::RunState::Breakpoint,
569                None => gql_types::RunState::Completed,
570            },
571            breakpoint: dbgref.and_then(|d| match d {
572                DebugEval::Continue => None,
573                DebugEval::Breakpoint(bp) => Some(bp.into()),
574            }),
575            json_receipts,
576        })
577    }
578}
579
580mod gql_types {
581    //! GraphQL type wrappers
582    use async_graphql::*;
583
584    use crate::schema::scalars::{
585        ContractId,
586        U64,
587    };
588
589    use fuel_core_types::fuel_vm::Breakpoint as FuelBreakpoint;
590
591    /// Breakpoint, defined as a tuple of contract ID and relative PC offset inside it
592    #[derive(Debug, Clone, Copy, InputObject)]
593    pub struct Breakpoint {
594        contract: ContractId,
595        pc: U64,
596    }
597
598    impl From<&FuelBreakpoint> for Breakpoint {
599        fn from(bp: &FuelBreakpoint) -> Self {
600            Self {
601                contract: (*bp.contract()).into(),
602                pc: U64(bp.pc()),
603            }
604        }
605    }
606
607    impl From<Breakpoint> for FuelBreakpoint {
608        fn from(bp: Breakpoint) -> Self {
609            Self::new(bp.contract.into(), bp.pc.0)
610        }
611    }
612
613    /// A separate `Breakpoint` type to be used as an output, as a single
614    /// type cannot act as both input and output type in async-graphql
615    #[derive(Debug, Clone, Copy, SimpleObject)]
616    pub struct OutputBreakpoint {
617        contract: ContractId,
618        pc: U64,
619    }
620
621    impl From<&FuelBreakpoint> for OutputBreakpoint {
622        fn from(bp: &FuelBreakpoint) -> Self {
623            Self {
624                contract: (*bp.contract()).into(),
625                pc: U64(bp.pc()),
626            }
627        }
628    }
629
630    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Enum)]
631    pub enum RunState {
632        /// All breakpoints have been processed, and the program has terminated
633        Completed,
634        /// Stopped on a breakpoint
635        Breakpoint,
636    }
637
638    #[derive(Debug, Clone, SimpleObject)]
639    pub struct RunResult {
640        pub state: RunState,
641        pub breakpoint: Option<OutputBreakpoint>,
642        pub json_receipts: Vec<String>,
643    }
644}