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 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
98const 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()?, ¶ms)
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()?, ¶ms)
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 Default::default(),
241 );
242
243 Ok(vm_database)
244 }
245
246 fn dummy_tx(gas_limit: u64) -> Script {
247 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 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 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 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 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 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 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 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 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 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()?, ¶ms)
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 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 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 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 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 #[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 #[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 Completed,
634 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}