Skip to main content

iota_json_rpc/
transaction_execution_api.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{sync::Arc, time::Duration};
6
7use async_trait::async_trait;
8use fastcrypto::encoding::Base64;
9use iota_core::{
10    authority::AuthorityState, authority_client::NetworkAuthorityClient,
11    transaction_orchestrator::TransactionOrchestrator,
12};
13use iota_json::IotaJsonValue;
14use iota_json_rpc_api::{JsonRpcMetrics, WriteApiOpenRpc, WriteApiServer};
15use iota_json_rpc_types::{
16    DevInspectArgs, DevInspectResults, DryRunTransactionBlockResponse,
17    ExecuteTransactionRequestType as ExecuteTransactionRequestTypeSchema, IotaExecutionStatus,
18    IotaMoveViewCallResults, IotaTransactionBlock, IotaTransactionBlockData,
19    IotaTransactionBlockEffects, IotaTransactionBlockEffectsAPI, IotaTransactionBlockEvents,
20    IotaTransactionBlockResponse, IotaTransactionBlockResponseOptions, IotaTypeTag,
21    MoveFunctionName,
22};
23use iota_metrics::spawn_monitored_task;
24use iota_open_rpc::Module;
25use iota_package_resolver::{
26    Package, PackageStore, Resolver, error::Error as PackageResolverError,
27};
28use iota_sdk_types::{
29    Address, GasPayment, ObjectId, Transaction, TransactionDigest, TransactionExpiration,
30    TransactionKind, TransactionV1, UserSignature,
31};
32use iota_transaction_builder::TransactionBuilder;
33use iota_types::{
34    effects::{TransactionEffectsAPI, TransactionEffectsExt},
35    error::IotaError,
36    execution_config_utils::to_binary_config,
37    inner_temporary_store::{
38        ObjectMapPackageStore, PackageStoreWithFallback, TemporaryModuleResolver,
39    },
40    iota_serde::BigInt,
41    quorum_driver_types::{
42        ExecuteTransactionRequestType, ExecuteTransactionRequestV1, ExecuteTransactionResponseV1,
43    },
44    storage::PostExecutionPackageResolver,
45    transaction::{InputObjectKind, TransactionAPI, TransactionEnvelope},
46    transaction_executor::{SimulateTransactionResult, VmChecks},
47};
48use jsonrpsee::{RpcModule, core::RpcResult};
49use tracing::{Instrument, instrument};
50
51use crate::{
52    IotaRpcModule, ObjectProviderCache,
53    authority_state::StateRead,
54    error::{Error, IotaRpcInputError},
55    get_balance_changes_from_effect, get_object_changes,
56    logger::FutureWithTracing,
57    transaction_builder_api::AuthorityStateDataReader,
58};
59
60#[derive(Clone)]
61pub struct TransactionExecutionApi {
62    state: Arc<dyn StateRead>,
63    transaction_orchestrator: Arc<TransactionOrchestrator<NetworkAuthorityClient>>,
64    metrics: Arc<JsonRpcMetrics>,
65    transaction_builder: TransactionBuilder,
66}
67
68impl TransactionExecutionApi {
69    pub fn new(
70        state: Arc<AuthorityState>,
71        transaction_orchestrator: Arc<TransactionOrchestrator<NetworkAuthorityClient>>,
72        metrics: Arc<JsonRpcMetrics>,
73    ) -> Self {
74        let reader = Arc::new(AuthorityStateDataReader::new(state.clone()));
75        Self {
76            state,
77            transaction_orchestrator,
78            metrics,
79            transaction_builder: TransactionBuilder::new(reader),
80        }
81    }
82
83    pub fn convert_bytes<T: serde::de::DeserializeOwned>(
84        &self,
85        tx_bytes: Base64,
86    ) -> Result<T, IotaRpcInputError> {
87        let data: T = bcs::from_bytes(&tx_bytes.to_vec()?)?;
88        Ok(data)
89    }
90
91    #[expect(clippy::type_complexity)]
92    fn prepare_execute_transaction_block(
93        &self,
94        tx_bytes: Base64,
95        signatures: Vec<Base64>,
96        opts: Option<IotaTransactionBlockResponseOptions>,
97    ) -> Result<
98        (
99            ExecuteTransactionRequestV1,
100            IotaTransactionBlockResponseOptions,
101            Address,
102            Vec<InputObjectKind>,
103            TransactionEnvelope,
104            Option<IotaTransactionBlock>,
105            Vec<u8>,
106        ),
107        IotaRpcInputError,
108    > {
109        let opts = opts.unwrap_or_default();
110        let tx: Transaction = self.convert_bytes(tx_bytes)?;
111        let sender = tx.sender();
112        let input_objs = tx.input_objects().unwrap_or_default();
113
114        let mut sigs = Vec::new();
115        for sig in signatures {
116            sigs.push(
117                UserSignature::from_base64(&sig.encoded())
118                    .map_err(|e| IotaRpcInputError::GenericInvalid(e.to_string()))?,
119            );
120        }
121        let txn = TransactionEnvelope::from_user_sig_data(tx, sigs);
122        let raw_transaction = if opts.show_raw_input {
123            bcs::to_bytes(txn.data())?
124        } else {
125            vec![]
126        };
127        let transaction = if opts.show_input {
128            let epoch_store = self.state.load_epoch_store_one_call_per_task();
129
130            Some(IotaTransactionBlock::try_from(
131                txn.data().clone(),
132                epoch_store.module_cache(),
133                *txn.digest(),
134            )?)
135        } else {
136            None
137        };
138
139        let request = ExecuteTransactionRequestV1 {
140            transaction: txn.clone(),
141            include_events: opts.show_events,
142            include_input_objects: opts.show_balance_changes || opts.show_object_changes,
143            include_output_objects: opts.show_balance_changes
144                || opts.show_object_changes
145                // In order to resolve events, we may need access to the newly published packages.
146                || opts.show_events,
147            include_auxiliary_data: false,
148        };
149
150        Ok((
151            request,
152            opts,
153            sender,
154            input_objs,
155            txn,
156            transaction,
157            raw_transaction,
158        ))
159    }
160
161    #[instrument("json_rpc_api_execute_transaction_block", level = "trace", skip_all)]
162    async fn execute_transaction_block(
163        &self,
164        tx_bytes: Base64,
165        signatures: Vec<Base64>,
166        opts: Option<IotaTransactionBlockResponseOptions>,
167        request_type: Option<ExecuteTransactionRequestType>,
168    ) -> Result<IotaTransactionBlockResponse, Error> {
169        let request_type =
170            request_type.unwrap_or(ExecuteTransactionRequestType::WaitForEffectsCert);
171        let (request, opts, sender, input_objs, txn, transaction, raw_transaction) =
172            self.prepare_execute_transaction_block(tx_bytes, signatures, opts)?;
173        let digest = *txn.digest();
174
175        let transaction_orchestrator = self.transaction_orchestrator.clone();
176        let orch_timer = self.metrics.orchestrator_latency_ms.start_timer();
177
178        tracing::trace!(
179            "Spawning transaction orchestrator task for transaction: {}",
180            digest
181        );
182        let (response, is_executed_locally) = spawn_monitored_task!(
183            transaction_orchestrator.execute_transaction_block(request, request_type, None)
184        )
185        .await?
186        .map_err(Error::from)?;
187        drop(orch_timer);
188
189        self.handle_post_orchestration(
190            response,
191            is_executed_locally,
192            opts,
193            digest,
194            input_objs,
195            transaction,
196            raw_transaction,
197            sender,
198        )
199        .await
200    }
201
202    #[instrument(level = "trace", skip_all)]
203    async fn handle_post_orchestration(
204        &self,
205        response: ExecuteTransactionResponseV1,
206        is_executed_locally: bool,
207        opts: IotaTransactionBlockResponseOptions,
208        digest: TransactionDigest,
209        input_objs: Vec<InputObjectKind>,
210        transaction: Option<IotaTransactionBlock>,
211        raw_transaction: Vec<u8>,
212        sender: Address,
213    ) -> Result<IotaTransactionBlockResponse, Error> {
214        let _post_orch_timer = self.metrics.post_orchestrator_latency_ms.start_timer();
215
216        let events = if opts.show_events {
217            tracing::trace!("Resolving events");
218            let epoch_store = self.state.load_epoch_store_one_call_per_task();
219            let backing_package_store = PostExecutionPackageResolver::new(
220                self.state.get_backing_package_store().clone(),
221                &response.output_objects,
222            );
223            let mut layout_resolver = epoch_store
224                .executor()
225                .type_layout_resolver(Box::new(backing_package_store));
226            Some(IotaTransactionBlockEvents::try_from(
227                response.events.unwrap_or_default(),
228                digest,
229                None,
230                layout_resolver.as_mut(),
231            )?)
232        } else {
233            None
234        };
235
236        // Skip cache (and downstream balance/object_changes) when the validator
237        // returned no input/output objects — e.g. the already-executed early-return.
238        // Without this guard, cache misses fall through to a provider lookup that
239        // races with local state and returns "version higher than latest".
240        let object_cache = if (opts.show_balance_changes || opts.show_object_changes)
241            && (response.input_objects.is_some() || response.output_objects.is_some())
242        {
243            let mut object_cache = ObjectProviderCache::new(self.state.clone());
244            if let Some(input_objects) = response.input_objects {
245                object_cache.insert_objects_into_cache(input_objects);
246            }
247            if let Some(output_objects) = response.output_objects {
248                object_cache.insert_objects_into_cache(output_objects);
249            }
250            Some(object_cache)
251        } else {
252            None
253        };
254
255        let balance_changes = match &object_cache {
256            Some(object_cache) if opts.show_balance_changes => Some(
257                get_balance_changes_from_effect(
258                    object_cache,
259                    &response.effects.effects,
260                    input_objs,
261                    None,
262                )
263                .instrument(tracing::trace_span!("resolving balance changes"))
264                .await?,
265            ),
266            _ => None,
267        };
268
269        let object_changes = match &object_cache {
270            Some(object_cache) if opts.show_object_changes => Some(
271                get_object_changes(
272                    object_cache,
273                    sender,
274                    response.effects.effects.modified_at_versions(),
275                    response.effects.effects.all_changed_objects(),
276                    response.effects.effects.all_removed_objects(),
277                )
278                .instrument(tracing::trace_span!("resolving object changes"))
279                .await?,
280            ),
281            _ => None,
282        };
283
284        let raw_effects = if opts.show_raw_effects {
285            bcs::to_bytes(&response.effects.effects)?
286        } else {
287            vec![]
288        };
289        let resolver = Resolver::new(self.clone());
290
291        let effects = if opts.show_effects {
292            Some(
293                IotaTransactionBlockEffects::from_native_with_clever_error(
294                    response.effects.effects,
295                    &resolver,
296                )
297                .await,
298            )
299        } else {
300            None
301        };
302
303        let errors = match effects.as_ref().map(|e| e.status()) {
304            Some(IotaExecutionStatus::Failure { error }) => vec![error.clone()],
305            _ => vec![],
306        };
307
308        Ok(IotaTransactionBlockResponse {
309            digest,
310            transaction,
311            raw_transaction,
312            effects,
313            events,
314            object_changes,
315            balance_changes,
316            timestamp_ms: None,
317            confirmed_local_execution: Some(is_executed_locally),
318            checkpoint: None,
319            errors,
320            raw_effects,
321        })
322    }
323
324    pub fn prepare_dry_run_transaction_block(
325        &self,
326        tx_bytes: Base64,
327    ) -> Result<(Transaction, Vec<InputObjectKind>), IotaRpcInputError> {
328        let tx: Transaction = self.convert_bytes(tx_bytes)?;
329        let input_objs = tx.input_objects()?;
330        Ok((tx, input_objs))
331    }
332
333    /// Report the gas the simulation ran with, in place of whatever the caller
334    /// left unset. Same rule as gRPC `simulate_transactions`, which shares the
335    /// helper.
336    fn report_simulation_gas(
337        transaction: &mut Transaction,
338        simulation: &SimulateTransactionResult,
339    ) {
340        iota_types::gas::report_simulation_gas(
341            transaction.gas_data_mut(),
342            &simulation.gas_data,
343            simulation.effects.gas_cost_summary().gas_used(),
344        );
345    }
346
347    /// The synchronous part of
348    /// [`dry_run_transaction_block`](Self::dry_run_transaction_block): the
349    /// simulation, and the resolution of the response's input and events over
350    /// the objects it wrote. Meant to run on a blocking thread; the async
351    /// object- and balance-change queries stay with the caller.
352    fn dry_run_transaction_block_impl(
353        &self,
354        mut tx: Transaction,
355    ) -> Result<
356        (
357            SimulateTransactionResult,
358            IotaTransactionBlockData,
359            IotaTransactionBlockEvents,
360        ),
361        Error,
362    > {
363        // Hold on to one epoch store for the whole operation, so that the simulation
364        // and the type resolution below observe the same epoch. A full `Arc` rather
365        // than the arc-swap guard: a guard occupies one of arc-swap's scarce
366        // per-thread borrow slots, meant for short borrows, not a whole simulation.
367        let epoch_store = Arc::clone(&self.state.load_epoch_store_one_call_per_task());
368
369        let mut simulation = self.state.simulate_transaction_in_epoch(
370            &epoch_store,
371            tx.clone(),
372            VmChecks::Enabled,
373        )?;
374
375        Self::report_simulation_gas(&mut tx, &simulation);
376
377        let tx_digest = *simulation.effects.transaction_digest();
378        // Resolve types against the objects the simulation wrote before falling back to
379        // the store, so that packages published by the transaction itself are visible.
380        let (input, events) = {
381            let mut layout_resolver = epoch_store.executor().type_layout_resolver(Box::new(
382                PackageStoreWithFallback::new(
383                    ObjectMapPackageStore(&simulation.output_objects),
384                    self.state.get_backing_package_store(),
385                ),
386            ));
387            let module_cache = TemporaryModuleResolver::new(
388                &simulation.output_objects,
389                to_binary_config(epoch_store.protocol_config()),
390                epoch_store.module_cache().clone(),
391            );
392
393            let input =
394                IotaTransactionBlockData::try_from_with_module_cache(tx, &module_cache, tx_digest)
395                    .map_err(|e| IotaError::TransactionSerialization {
396                        error: format!(
397                            "Failed to convert transaction to IotaTransactionBlockData: {e}"
398                        ),
399                    })?;
400            let events = IotaTransactionBlockEvents::try_from(
401                simulation.events.take().unwrap_or_default(),
402                tx_digest,
403                None,
404                layout_resolver.as_mut(),
405            )?;
406
407            (input, events)
408        };
409
410        Ok((simulation, input, events))
411    }
412
413    async fn dry_run_transaction_block(
414        &self,
415        tx_bytes: Base64,
416    ) -> Result<DryRunTransactionBlockResponse, Error> {
417        let (txn_data, input_objs) = self.prepare_dry_run_transaction_block(tx_bytes)?;
418        let sender = txn_data.sender();
419
420        // Use spawn_blocking since simulating a transaction and resolving types
421        // over its output are long-running synchronous operations
422        let (simulation, input, events) = {
423            let this = self.clone();
424            tokio::task::spawn_blocking(move || this.dry_run_transaction_block_impl(txn_data))
425                .await
426                .map_err(Error::from)??
427        };
428
429        let execution_error_source = simulation
430            .execution_result
431            .as_ref()
432            .err()
433            .and_then(|e| e.source().as_ref().map(|e| e.to_string()));
434
435        let object_cache =
436            ObjectProviderCache::new_with_cache(self.state.clone(), &simulation.output_objects);
437        let balance_changes = get_balance_changes_from_effect(
438            &object_cache,
439            &simulation.effects,
440            input_objs,
441            simulation.mock_gas_id,
442        )
443        .await?;
444        let object_changes = get_object_changes(
445            &object_cache,
446            sender,
447            simulation.effects.modified_at_versions(),
448            simulation.effects.all_changed_objects(),
449            simulation.effects.all_removed_objects(),
450        )
451        .await?;
452
453        let resolver = Resolver::new(self.clone());
454        let effects = IotaTransactionBlockEffects::from_native_with_clever_error(
455            simulation.effects,
456            &resolver,
457        )
458        .await;
459
460        Ok(DryRunTransactionBlockResponse {
461            effects,
462            events,
463            object_changes,
464            balance_changes,
465            input,
466            suggested_gas_price: simulation.suggested_gas_price,
467            execution_error_source,
468        })
469    }
470
471    fn dev_inspect_transaction_impl(
472        &self,
473        sender: Address,
474        transaction_kind: TransactionKind,
475        gas_price: Option<u64>,
476        args: DevInspectArgs,
477    ) -> Result<DevInspectResults, Error> {
478        let DevInspectArgs {
479            gas_sponsor,
480            gas_budget,
481            gas_objects,
482            show_raw_txn_data_and_effects,
483            skip_checks,
484        } = args;
485        let show_raw_txn_data_and_effects = show_raw_txn_data_and_effects.unwrap_or(false);
486        let skip_checks = skip_checks.unwrap_or(true);
487
488        // Hold on to one epoch store for the whole operation, so that the simulation
489        // and the type resolution below observe the same epoch. A full `Arc` rather
490        // than the arc-swap guard: a guard occupies one of arc-swap's scarce
491        // per-thread borrow slots, meant for short borrows, not a whole simulation.
492        let epoch_store = Arc::clone(&self.state.load_epoch_store_one_call_per_task());
493
494        let transaction = Transaction::V1(TransactionV1 {
495            kind: transaction_kind,
496            sender,
497            gas_payment: GasPayment {
498                // Any of these the caller leaves out is filled in by the simulation,
499                // whether or not the checks are skipped: an empty payment gets a mock
500                // gas coin, a zero price gets the epoch's reference gas price, and a
501                // zero budget as much as the gas coins can back, up to the protocol
502                // maximum.
503                objects: gas_objects.unwrap_or_default(),
504                owner: gas_sponsor.unwrap_or(sender),
505                price: gas_price.unwrap_or_default(),
506                budget: gas_budget.unwrap_or_default(),
507            },
508            expiration: TransactionExpiration::None,
509        });
510
511        let checks = if skip_checks {
512            VmChecks::Disabled
513        } else {
514            VmChecks::Enabled
515        };
516        // Kept back from the simulation, which consumes the transaction, so that the
517        // reported gas can be filled in from what the simulation charged.
518        let mut reported_transaction = show_raw_txn_data_and_effects.then(|| transaction.clone());
519        let simulation =
520            self.state
521                .simulate_transaction_in_epoch(&epoch_store, transaction, checks)?;
522
523        let raw_txn_data = match reported_transaction.as_mut() {
524            Some(transaction) => {
525                Self::report_simulation_gas(transaction, &simulation);
526                bcs::to_bytes(transaction).map_err(|_| IotaError::TransactionSerialization {
527                    error: "Failed to serialize transaction during dev inspect".to_string(),
528                })?
529            }
530            None => vec![],
531        };
532
533        let raw_effects = if show_raw_txn_data_and_effects {
534            bcs::to_bytes(&simulation.effects).map_err(|_| IotaError::TransactionSerialization {
535                error: "Failed to serialize transaction effects during dev inspect".to_string(),
536            })?
537        } else {
538            vec![]
539        };
540
541        // Resolve types against the objects the simulation wrote before falling back to
542        // the store, so that packages published by the transaction itself are visible.
543        let mut layout_resolver =
544            epoch_store
545                .executor()
546                .type_layout_resolver(Box::new(PackageStoreWithFallback::new(
547                    ObjectMapPackageStore(&simulation.output_objects),
548                    self.state.get_backing_package_store(),
549                )));
550
551        Ok(DevInspectResults::new(
552            simulation.effects,
553            simulation.events.unwrap_or_default(),
554            simulation.execution_result,
555            raw_txn_data,
556            raw_effects,
557            layout_resolver.as_mut(),
558        )?)
559    }
560
561    async fn dev_inspect_transaction(
562        &self,
563        sender: Address,
564        transaction_kind: TransactionKind,
565        gas_price: Option<u64>,
566        args: DevInspectArgs,
567    ) -> Result<DevInspectResults, Error> {
568        // Use spawn_blocking since simulating a transaction is a long-running
569        // synchronous operation
570        let this = self.clone();
571        tokio::task::spawn_blocking(move || {
572            this.dev_inspect_transaction_impl(sender, transaction_kind, gas_price, args)
573        })
574        .await
575        .map_err(Error::from)?
576    }
577}
578
579#[async_trait]
580impl WriteApiServer for TransactionExecutionApi {
581    #[instrument(skip(self, tx_bytes, signatures))]
582    async fn execute_transaction_block(
583        &self,
584        tx_bytes: Base64,
585        signatures: Vec<Base64>,
586        opts: Option<IotaTransactionBlockResponseOptions>,
587        request_type: Option<ExecuteTransactionRequestTypeSchema>,
588    ) -> RpcResult<IotaTransactionBlockResponse> {
589        self.execute_transaction_block(tx_bytes, signatures, opts, request_type.map(Into::into))
590            .trace_timeout(Duration::from_secs(10))
591            .await
592    }
593
594    /// Calls a move view function.
595    #[instrument(skip(self, arguments))]
596    async fn view_function_call(
597        &self,
598        function_name: String,
599        type_args: Option<Vec<IotaTypeTag>>,
600        arguments: Vec<IotaJsonValue>,
601    ) -> RpcResult<IotaMoveViewCallResults> {
602        let MoveFunctionName {
603            package,
604            module,
605            function,
606        } = function_name.as_str().parse().map_err(Error::from)?;
607        let sender = Address::ZERO;
608        let tx_kind = self
609            .transaction_builder
610            .move_view_call_tx_kind(
611                package,
612                &module,
613                &function,
614                type_args.unwrap_or_default(),
615                arguments,
616            )
617            .await
618            .map_err(Error::from)?;
619        let dev_inspect_results = self
620            .dev_inspect_transaction(sender, tx_kind, None, DevInspectArgs::default())
621            .await?;
622        Ok(
623            IotaMoveViewCallResults::from_dev_inspect_results(self.clone(), dev_inspect_results)
624                .await
625                .map_err(Error::from)?,
626        )
627    }
628
629    #[instrument(
630        skip(self, sender_address, tx_bytes, additional_args),
631        fields(sender_address = %sender_address)
632    )]
633    async fn dev_inspect_transaction_block(
634        &self,
635        sender_address: Address,
636        tx_bytes: Base64,
637        gas_price: Option<BigInt<u64>>,
638        _epoch: Option<BigInt<u64>>,
639        additional_args: Option<DevInspectArgs>,
640    ) -> RpcResult<DevInspectResults> {
641        async move {
642            let tx_kind: TransactionKind = self.convert_bytes(tx_bytes)?;
643            self.dev_inspect_transaction(
644                sender_address,
645                tx_kind,
646                gas_price.map(|i| *i),
647                additional_args.unwrap_or_default(),
648            )
649            .await
650        }
651        .trace()
652        .await
653    }
654
655    #[instrument(skip(self, tx_bytes))]
656    async fn dry_run_transaction_block(
657        &self,
658        tx_bytes: Base64,
659    ) -> RpcResult<DryRunTransactionBlockResponse> {
660        self.dry_run_transaction_block(tx_bytes).trace().await
661    }
662}
663
664impl IotaRpcModule for TransactionExecutionApi {
665    fn rpc(self) -> RpcModule<Self> {
666        self.into_rpc()
667    }
668
669    fn rpc_doc_module() -> Module {
670        WriteApiOpenRpc::module_doc()
671    }
672}
673
674#[async_trait]
675impl PackageStore for TransactionExecutionApi {
676    async fn fetch(&self, id: Address) -> Result<Arc<Package>, PackageResolverError> {
677        let backing_store = self.state.get_backing_package_store();
678        match backing_store.get_package_object(&ObjectId::new(id.into_bytes())) {
679            Ok(Some(pkg)) => Ok(Arc::new(Package::read_from_package(pkg.move_package())?)),
680            Ok(None) => Err(PackageResolverError::PackageNotFound(id)),
681            Err(e) => Err(PackageResolverError::Store {
682                store: "Node",
683                source: Arc::new(e),
684            }),
685        }
686    }
687}