explorerd/service/
transactions.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use std::collections::HashMap;
20
21use log::{debug, error};
22use smol::io::Cursor;
23use tinyjson::JsonValue;
24
25use darkfi::{
26    blockchain::{
27        BlockInfo, BlockchainOverlay, HeaderHash, SLED_PENDING_TX_ORDER_TREE, SLED_PENDING_TX_TREE,
28        SLED_TX_LOCATION_TREE, SLED_TX_TREE,
29    },
30    error::TxVerifyFailed,
31    runtime::vm_runtime::Runtime,
32    tx::Transaction,
33    util::time::Timestamp,
34    validator::fees::{circuit_gas_use, GasData, PALLAS_SCHNORR_SIGNATURE_FEE},
35    zk::VerifyingKey,
36    Error, Result,
37};
38use darkfi_sdk::{
39    crypto::{ContractId, PublicKey},
40    deploy::DeployParamsV1,
41    pasta::pallas,
42    tx::TransactionHash,
43};
44use darkfi_serial::{deserialize_async, serialize_async, AsyncDecodable, AsyncEncodable};
45
46use crate::{error::ExplorerdError, ExplorerService};
47
48#[derive(Debug, Clone)]
49/// Structure representing a `TRANSACTIONS_TABLE` record.
50pub struct TransactionRecord {
51    /// Transaction hash identifier
52    pub transaction_hash: String,
53    /// Header hash identifier of the block this transaction was included in
54    pub header_hash: String,
55    // TODO: Split the payload into a more easily readable fields
56    /// Transaction payload
57    pub payload: Transaction,
58    /// Time transaction was added to the block
59    pub timestamp: Timestamp,
60    /// Total gas used for processing transaction
61    pub total_gas_used: u64,
62    /// Gas used by WASM
63    pub wasm_gas_used: u64,
64    /// Gas used by ZK circuit operations
65    pub zk_circuit_gas_used: u64,
66    /// Gas used for creating the transaction signature
67    pub signature_gas_used: u64,
68    /// Gas used for deployments
69    pub deployment_gas_used: u64,
70}
71
72impl TransactionRecord {
73    /// Auxiliary function to convert a `TransactionRecord` into a `JsonValue` array.
74    pub fn to_json_array(&self) -> JsonValue {
75        JsonValue::Array(vec![
76            JsonValue::String(self.transaction_hash.clone()),
77            JsonValue::String(self.header_hash.clone()),
78            JsonValue::String(format!("{:?}", self.payload)),
79            JsonValue::String(self.timestamp.to_string()),
80            JsonValue::Number(self.total_gas_used as f64),
81            JsonValue::Number(self.wasm_gas_used as f64),
82            JsonValue::Number(self.zk_circuit_gas_used as f64),
83            JsonValue::Number(self.signature_gas_used as f64),
84            JsonValue::Number(self.deployment_gas_used as f64),
85        ])
86    }
87}
88
89impl ExplorerService {
90    /// Resets transactions in the database by clearing transaction-related trees, returning an Ok result on success.
91    pub fn reset_transactions(&self) -> Result<()> {
92        // Initialize transaction trees to reset
93        let trees_to_reset =
94            [SLED_TX_TREE, SLED_TX_LOCATION_TREE, SLED_PENDING_TX_TREE, SLED_PENDING_TX_ORDER_TREE];
95
96        // Iterate over each associated transaction tree and delete its contents
97        for tree_name in &trees_to_reset {
98            let tree = &self.db.blockchain.sled_db.open_tree(tree_name)?;
99            tree.clear()?;
100            let tree_name_str = std::str::from_utf8(tree_name)?;
101            debug!(target: "explorerd::blocks", "Successfully reset transaction tree: {tree_name_str}");
102        }
103
104        Ok(())
105    }
106
107    /// Provides the transaction count of all the transactions in the explorer database.
108    pub fn get_transaction_count(&self) -> usize {
109        self.db.blockchain.txs_len()
110    }
111
112    /// Fetches all known transactions from the database.
113    ///
114    /// This function retrieves all transactions stored in the database and transforms
115    /// them into a vector of [`TransactionRecord`]s. If no transactions are found,
116    /// it returns an empty vector.
117    pub fn get_transactions(&self) -> Result<Vec<TransactionRecord>> {
118        // Retrieve all transactions and handle any errors encountered
119        let txs = self.db.blockchain.transactions.get_all().map_err(|e| {
120            Error::DatabaseError(format!("[get_transactions] Trxs retrieval: {e:?}"))
121        })?;
122
123        // Transform the found `Transactions` into a vector of `TransactionRecords`
124        let txs_records = txs
125            .iter()
126            .map(|(_, tx)| self.to_tx_record(None, tx))
127            .collect::<Result<Vec<TransactionRecord>>>()?;
128
129        Ok(txs_records)
130    }
131
132    /// Fetches all transactions from the database for the given block `header_hash`.
133    ///
134    /// This function retrieves all transactions associated with the specified
135    /// block header hash. It first parses the header hash and then fetches
136    /// the corresponding [`BlockInfo`]. If the block is found, it transforms its
137    /// transactions into a vector of [`TransactionRecord`]s. If no transactions
138    /// are found, it returns an empty vector.
139    pub fn get_transactions_by_header_hash(
140        &self,
141        header_hash: &str,
142    ) -> Result<Vec<TransactionRecord>> {
143        // Parse header hash, returning an error if parsing fails
144        let header_hash = header_hash
145            .parse::<HeaderHash>()
146            .map_err(|_| ExplorerdError::InvalidHeaderHash(header_hash.to_string()))?;
147
148        // Fetch block by hash and handle encountered errors
149        let block = match self.db.blockchain.get_blocks_by_hash(&[header_hash]) {
150            Ok(blocks) => blocks.first().cloned().unwrap(),
151            Err(Error::BlockNotFound(_)) => return Ok(vec![]),
152            Err(e) => {
153                return Err(Error::DatabaseError(format!(
154                    "[get_transactions_by_header_hash] Block retrieval failed: {e:?}"
155                )))
156            }
157        };
158
159        // Transform block transactions into transaction records
160        block
161            .txs
162            .iter()
163            .map(|tx| self.to_tx_record(self.get_block_info(block.header.hash())?, tx))
164            .collect::<Result<Vec<TransactionRecord>>>()
165    }
166
167    /// Fetches a transaction given its header hash.
168    ///
169    /// This function retrieves the transaction associated with the provided
170    /// [`TransactionHash`] and transforms it into a [`TransactionRecord`] if found.
171    /// If no transaction is found, it returns `None`.
172    pub fn get_transaction_by_hash(
173        &self,
174        tx_hash: &TransactionHash,
175    ) -> Result<Option<TransactionRecord>> {
176        let tx_store = &self.db.blockchain.transactions;
177
178        // Attempt to retrieve the transaction using the provided hash handling any potential errors
179        let tx_opt = &tx_store.get(&[*tx_hash], false).map_err(|e| {
180            Error::DatabaseError(format!(
181                "[get_transaction_by_hash] Transaction retrieval failed: {e:?}"
182            ))
183        })?[0];
184
185        // Transform `Transaction` to a `TransactionRecord`, returning None if no transaction was found
186        tx_opt.as_ref().map(|tx| self.to_tx_record(None, tx)).transpose()
187    }
188
189    /// Fetches the [`BlockInfo`] associated with a given transaction hash.
190    ///
191    /// This auxiliary function first fetches the location of the transaction in the blockchain.
192    /// If the location is found, it retrieves the associated [`HeaderHash`] and then fetches
193    /// the block information corresponding to that header hash. The function returns the
194    /// [`BlockInfo`] if successful, or `None` if no location or header hash is found.
195    fn get_tx_block_info(&self, tx_hash: &TransactionHash) -> Result<Option<BlockInfo>> {
196        // Retrieve the location of the transaction
197        let location =
198            self.db.blockchain.transactions.get_location(&[*tx_hash], false).map_err(|e| {
199                Error::DatabaseError(format!(
200                    "[get_tx_block_info] Location retrieval failed: {e:?}"
201                ))
202            })?[0];
203
204        // Fetch the `HeaderHash` associated with the location
205        let header_hash = match location {
206            None => return Ok(None),
207            Some((block_height, _)) => {
208                self.db.blockchain.blocks.get_order(&[block_height], false).map_err(|e| {
209                    Error::DatabaseError(format!(
210                        "[get_tx_block_info] Block retrieval failed: {e:?}"
211                    ))
212                })?[0]
213            }
214        };
215
216        // Return the associated `BlockInfo` if the header hash is found; otherwise, return `None`.
217        match header_hash {
218            None => Ok(None),
219            Some(header_hash) => self.get_block_info(header_hash).map_err(|e| {
220                Error::DatabaseError(format!(
221                    "[get_tx_block_info] BlockInfo retrieval failed: {e:?}"
222                ))
223            }),
224        }
225    }
226
227    /// Fetches the [`BlockInfo`] associated with a given [`HeaderHash`].
228    ///
229    /// This auxiliary function attempts to retrieve the block information using
230    /// the specified [`HeaderHash`]. It returns the associated [`BlockInfo`] if found,
231    /// or `None` when not found.
232    fn get_block_info(&self, header_hash: HeaderHash) -> Result<Option<BlockInfo>> {
233        match self.db.blockchain.get_blocks_by_hash(&[header_hash]) {
234            Err(Error::BlockNotFound(_)) => Ok(None),
235            Ok(block_info) => Ok(block_info.into_iter().next()),
236            Err(e) => Err(Error::DatabaseError(format!(
237                "[get_transactions_by_header_hash] Block retrieval failed: {e:?}"
238            ))),
239        }
240    }
241
242    /// Calculates the gas data for a given transaction, returning a [`GasData`] instance detailing
243    /// various aspects of the gas usage.
244    pub async fn calculate_tx_gas_data(
245        &self,
246        tx: &Transaction,
247        verify_fee: bool,
248    ) -> Result<GasData> {
249        let tx_hash = tx.hash();
250
251        let overlay = BlockchainOverlay::new(&self.db.blockchain)?;
252
253        // Gas accumulators
254        let mut total_gas_used = 0;
255        let mut zk_circuit_gas_used = 0;
256        let mut wasm_gas_used = 0;
257        let mut deploy_gas_used = 0;
258        let mut gas_paid = 0;
259
260        // Table of public inputs used for ZK proof verification
261        let mut zkp_table = vec![];
262        // Table of public keys used for signature verification
263        let mut sig_table = vec![];
264
265        // Index of the Fee-paying call
266        let fee_call_idx = 0;
267
268        // Write the transaction calls payload data
269        let mut payload = vec![];
270        tx.calls.encode_async(&mut payload).await?;
271
272        // Map of ZK proof verifying keys for the transaction
273        let mut verifying_keys: HashMap<[u8; 32], HashMap<String, VerifyingKey>> = HashMap::new();
274        for call in &tx.calls {
275            verifying_keys.insert(call.data.contract_id.to_bytes(), HashMap::new());
276        }
277
278        let block_target = self.db.blockchain.blocks.get_last()?.0 + 1;
279
280        // We'll also take note of all the circuits in a Vec so we can calculate their verification cost.
281        let mut circuits_to_verify = vec![];
282
283        // Iterate over all calls to get the metadata
284        for (idx, call) in tx.calls.iter().enumerate() {
285            // Transaction must not contain a Pow reward call
286            if call.data.is_money_pow_reward() {
287                error!(target: "explorerd::calculate_tx_gas_data", "Reward transaction detected");
288                return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
289            }
290
291            let wasm = overlay.lock().unwrap().contracts.get(call.data.contract_id)?;
292            let mut runtime = Runtime::new(
293                &wasm,
294                overlay.clone(),
295                call.data.contract_id,
296                block_target,
297                block_target,
298                tx_hash,
299                idx as u8,
300            )?;
301
302            // Retrieve the runtime metadata
303            let metadata = runtime.metadata(&payload)?;
304
305            // Decode the metadata retrieved from the execution
306            let mut decoder = Cursor::new(&metadata);
307
308            // The tuple is (zkas_ns, public_inputs)
309            let zkp_pub: Vec<(String, Vec<pallas::Base>)> =
310                AsyncDecodable::decode_async(&mut decoder).await?;
311            let sig_pub: Vec<PublicKey> = AsyncDecodable::decode_async(&mut decoder).await?;
312
313            if decoder.position() != metadata.len() as u64 {
314                error!(
315                    target: "block_explorer::calculate_tx_gas_data",
316                    "[BLOCK_EXPLORER] Failed decoding entire metadata buffer for {tx_hash}:{idx}"
317                );
318                return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
319            }
320
321            // Here we'll look up verifying keys and insert them into the per-contract map.
322            for (zkas_ns, _) in &zkp_pub {
323                let inner_vk_map =
324                    verifying_keys.get_mut(&call.data.contract_id.to_bytes()).unwrap();
325
326                // TODO: This will be a problem in case of ::deploy, unless we force a different
327                // namespace and disable updating existing circuit. Might be a smart idea to do
328                // so in order to have to care less about being able to verify historical txs.
329                if inner_vk_map.contains_key(zkas_ns.as_str()) {
330                    continue
331                }
332
333                let (zkbin, vk) =
334                    overlay.lock().unwrap().contracts.get_zkas(&call.data.contract_id, zkas_ns)?;
335
336                inner_vk_map.insert(zkas_ns.to_string(), vk);
337                circuits_to_verify.push(zkbin);
338            }
339
340            zkp_table.push(zkp_pub);
341            sig_table.push(sig_pub);
342
343            // Execute the contract
344            let mut state_update = vec![call.data.data[0]];
345            state_update.append(&mut runtime.exec(&payload)?);
346
347            // Apply contract state update
348            runtime.apply(&state_update)?;
349
350            // Contracts are not included within blocks. They need to be deployed so that they can be accessed and utilized for fee data computation
351            if call.data.is_deployment()
352            /* DeployV1 */
353            {
354                // Deserialize the deployment parameters
355                let deploy_params: DeployParamsV1 = deserialize_async(&call.data.data[1..]).await?;
356                let deploy_cid = ContractId::derive_public(deploy_params.public_key);
357
358                // Instantiate the new deployment runtime
359                let mut deploy_runtime = Runtime::new(
360                    &deploy_params.wasm_bincode,
361                    overlay.clone(),
362                    deploy_cid,
363                    block_target,
364                    block_target,
365                    tx_hash,
366                    idx as u8,
367                )?;
368
369                deploy_runtime.deploy(&deploy_params.ix)?;
370
371                deploy_gas_used = deploy_runtime.gas_used();
372
373                // Append the used deployment gas
374                total_gas_used += deploy_gas_used;
375            }
376
377            // At this point we're done with the call and move on to the next one.
378            // Accumulate the WASM gas used.
379            wasm_gas_used = runtime.gas_used();
380
381            // Append the used wasm gas
382            total_gas_used += wasm_gas_used;
383        }
384
385        // The signature fee is tx_size + fixed_sig_fee * n_signatures
386        let signature_gas_used = (PALLAS_SCHNORR_SIGNATURE_FEE * tx.signatures.len() as u64) +
387            serialize_async(tx).await.len() as u64;
388
389        // Append the used signature gas
390        total_gas_used += signature_gas_used;
391
392        // The ZK circuit fee is calculated using a function in validator/fees.rs
393        for zkbin in circuits_to_verify.iter() {
394            zk_circuit_gas_used = circuit_gas_use(zkbin);
395
396            // Append the used zk circuit gas
397            total_gas_used += zk_circuit_gas_used;
398        }
399
400        if verify_fee {
401            // Deserialize the fee call to find the paid fee
402            let fee: u64 = match deserialize_async(&tx.calls[fee_call_idx].data.data[1..9]).await {
403                Ok(v) => v,
404                Err(e) => {
405                    error!(
406                        target: "block_explorer::calculate_tx_gas_data",
407                        "[VALIDATOR] Failed deserializing tx {tx_hash} fee call: {e}"
408                    );
409                    return Err(TxVerifyFailed::InvalidFee.into())
410                }
411            };
412
413            // TODO: This counts 1 gas as 1 token unit. Pricing should be better specified.
414            // Check that enough fee has been paid for the used gas in this transaction.
415            if total_gas_used > fee {
416                error!(
417                    target: "block_explorer::calculate_tx_gas_data",
418                    "[VALIDATOR] Transaction {tx_hash} has insufficient fee. Required: {total_gas_used}, Paid: {fee}");
419                return Err(TxVerifyFailed::InsufficientFee.into())
420            }
421            debug!(target: "block_explorer::calculate_tx_gas_data", "The gas paid for transaction {tx_hash}: {gas_paid}");
422
423            // Store paid fee
424            gas_paid = fee;
425        }
426
427        // Commit changes made to the overlay
428        overlay.lock().unwrap().overlay.lock().unwrap().apply()?;
429
430        let fee_data = GasData {
431            paid: gas_paid,
432            wasm: wasm_gas_used,
433            zk_circuits: zk_circuit_gas_used,
434            signatures: signature_gas_used,
435            deployments: deploy_gas_used,
436        };
437
438        debug!(target: "block_explorer::calculate_tx_gas_data", "The total gas usage for transaction {tx_hash}: {fee_data:?}");
439
440        Ok(fee_data)
441    }
442
443    /// Converts a [`Transaction`] and its associated block information into a [`TransactionRecord`].
444    ///
445    /// This auxiliary function first retrieves the gas data associated with the provided transaction.
446    /// If [`BlockInfo`] is not provided, it attempts to fetch it using the transaction's hash,
447    /// returning an error if the block information cannot be found. Upon success, the function
448    /// returns a [`TransactionRecord`] containing relevant details about the transaction.
449    fn to_tx_record(
450        &self,
451        block_info_opt: Option<BlockInfo>,
452        tx: &Transaction,
453    ) -> Result<TransactionRecord> {
454        // Fetch the gas data associated with the transaction
455        let gas_data_option = self.db.metrics_store.get_tx_gas_data(&tx.hash()).map_err(|e| {
456            Error::DatabaseError(format!(
457                "[to_tx_record] Failed to fetch the gas data associated with transaction {}: {e:?}",
458                tx.hash()
459            ))
460        })?;
461
462        // Unwrap the option, providing a default value when `None`
463        let gas_data = gas_data_option.unwrap_or_else(GasData::default);
464
465        // Process provided block_info option
466        let block_info = match block_info_opt {
467            // Use provided block_info when present
468            Some(block_info) => block_info,
469            // Fetch the block info associated with the transaction when block info not provided
470            None => {
471                match self.get_tx_block_info(&tx.hash())? {
472                    Some(block_info) => block_info,
473                    // If no associated block info found, throw an error as this should not happen
474                    None => {
475                        return Err(Error::BlockNotFound(format!(
476                            "[to_tx_record] Required `BlockInfo` was not found for transaction: {}",
477                            tx.hash()
478                        )))
479                    }
480                }
481            }
482        };
483
484        // Return transformed transaction record
485        Ok(TransactionRecord {
486            transaction_hash: tx.hash().to_string(),
487            header_hash: block_info.hash().to_string(),
488            timestamp: block_info.header.timestamp,
489            payload: tx.clone(),
490            total_gas_used: gas_data.total_gas_used(),
491            wasm_gas_used: gas_data.wasm,
492            zk_circuit_gas_used: gas_data.zk_circuits,
493            signature_gas_used: gas_data.signatures,
494            deployment_gas_used: gas_data.deployments,
495        })
496    }
497}