darkfid/rpc/
blockchain.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2026 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::str::FromStr;
20
21use darkfi_sdk::{
22    crypto::contract_id::{ContractId, SMART_CONTRACT_ZKAS_DB_NAME},
23    tx::TransactionHash,
24};
25use darkfi_serial::{deserialize_async, serialize_async};
26use tinyjson::JsonValue;
27use tracing::{debug, error};
28
29use darkfi::{
30    rpc::jsonrpc::{
31        ErrorCode::{InternalError, InvalidParams, ParseError},
32        JsonError, JsonResponse, JsonResult,
33    },
34    util::encoding::base64,
35};
36
37use crate::{server_error, DarkfiNode, RpcError};
38
39impl DarkfiNode {
40    // RPCAPI:
41    // Queries the blockchain database for a block in the given height.
42    // Returns a readable block upon success.
43    //
44    // **Params:**
45    // * `array[0]`: `u32` block height
46    //
47    // **Returns:**
48    // * `BlockInfo` serialized into base64.
49    //
50    // ```rust,no_run,noplayground
51    // {{#include ../../../src/blockchain/block_store.rs:blockinfo}}
52    // ```
53    //
54    // --> {"jsonrpc": "2.0", "method": "blockchain.get_block", "params": [0], "id": 1}
55    // <-- {"jsonrpc": "2.0", "result": "base64encodedblock", "id": 1}
56    pub async fn blockchain_get_block(&self, id: u16, params: JsonValue) -> JsonResult {
57        let Some(params) = params.get::<Vec<JsonValue>>() else {
58            return JsonError::new(InvalidParams, None, id).into()
59        };
60        if params.len() != 1 || !params[0].is_number() {
61            return JsonError::new(InvalidParams, None, id).into()
62        }
63
64        let block_height = *params[0].get::<f64>().unwrap() as u32;
65
66        let blocks = match self
67            .validator
68            .read()
69            .await
70            .blockchain
71            .get_blocks_by_heights(&[block_height])
72        {
73            Ok(v) => v,
74            Err(e) => {
75                error!(target: "darkfid::rpc::blockchain_get_block", "Failed fetching block by height: {e}");
76                return JsonError::new(InternalError, None, id).into()
77            }
78        };
79
80        if blocks.is_empty() {
81            return server_error(RpcError::UnknownBlockHeight, id, None)
82        }
83
84        let block = base64::encode(&serialize_async(&blocks[0]).await);
85        JsonResponse::new(JsonValue::String(block), id).into()
86    }
87
88    // RPCAPI:
89    // Queries the blockchain database for a given transaction.
90    // Returns a base64 encoded `Transaction` object.
91    //
92    // **Params:**
93    // * `array[0]`: Hex-encoded transaction hash string
94    //
95    // **Returns:**
96    // * `Transaction serialized into base64.
97    //
98    // ```rust,no_run,noplayground
99    // {{#include ../../../src/tx/mod.rs:transaction-struct}}
100    // ```
101    //
102    // --> {"jsonrpc": "2.0", "method": "blockchain.get_tx", "params": ["TxHash"], "id": 1}
103    // <-- {"jsonrpc": "2.0", "result": "base64encodedtx", "id": 1}
104    pub async fn blockchain_get_tx(&self, id: u16, params: JsonValue) -> JsonResult {
105        let Some(params) = params.get::<Vec<JsonValue>>() else {
106            return JsonError::new(InvalidParams, None, id).into()
107        };
108        if params.len() != 1 || !params[0].is_string() {
109            return JsonError::new(InvalidParams, None, id).into()
110        }
111
112        let tx_hash = params[0].get::<String>().unwrap();
113        let tx_hash = match TransactionHash::from_str(tx_hash) {
114            Ok(v) => v,
115            Err(_) => return JsonError::new(ParseError, None, id).into(),
116        };
117
118        let txs = match self.validator.read().await.blockchain.transactions.get(&[tx_hash], true) {
119            Ok(txs) => txs,
120            Err(e) => {
121                error!(target: "darkfid::rpc::blockchain_get_tx", "Failed fetching tx by hash: {e}");
122                return JsonError::new(InternalError, None, id).into()
123            }
124        };
125        // This would be an logic error somewhere
126        assert_eq!(txs.len(), 1);
127        // and strict was used during .get()
128        let tx = txs[0].as_ref().unwrap();
129
130        let tx_enc = base64::encode(&serialize_async(tx).await);
131        JsonResponse::new(JsonValue::String(tx_enc), id).into()
132    }
133
134    // RPCAPI:
135    // Queries the blockchain database to fetch the difficulty and cumulative
136    // difficulty for a specific block height.
137    //
138    // **Params:**
139    // * `array[0]`: Block height
140    //
141    // **Returns:**
142    // * `difficulty`: Block difficulty as integer
143    // * `cumulative_difficulty`: Cumulative block difficulty as integer
144    //
145    // --> {"jsonrpc": "2.0", "method": "blockchain.get_difficulty", "params": [1], "id": 1}
146    // <-- {"jsonrpc": "2.0", "result": [123, 456], "id": 1}
147    pub async fn blockchain_get_difficulty(&self, id: u16, params: JsonValue) -> JsonResult {
148        let Some(params) = params.get::<Vec<JsonValue>>() else {
149            return JsonError::new(InvalidParams, None, id).into()
150        };
151        if params.len() != 1 || !params[0].is_number() {
152            return JsonError::new(InvalidParams, None, id).into()
153        }
154
155        let height = *params[0].get::<f64>().unwrap() as u32;
156
157        if height == 0 {
158            return JsonResponse::new(JsonValue::Array(vec![1_f64.into(), 1_f64.into()]), id).into()
159        }
160
161        let Ok(diff) =
162            self.validator.read().await.blockchain.blocks.get_difficulty(&[height], true)
163        else {
164            return server_error(RpcError::UnknownBlockHeight, id, None)
165        };
166
167        let block_diff = diff[0].clone().unwrap();
168
169        let difficulty: f64 = block_diff.difficulty.to_string().parse().unwrap();
170        let cumulative: f64 = block_diff.cumulative_difficulty.to_string().parse().unwrap();
171
172        JsonResponse::new(JsonValue::Array(vec![difficulty.into(), cumulative.into()]), id).into()
173    }
174
175    // RPCAPI:
176    // Queries the blockchain database to find the last confirmed block.
177    //
178    // **Params:**
179    // * Empty
180    //
181    // **Returns:**
182    // * `f64`   : Height of the last confirmed block
183    // * `String`: Header hash of the last confirmed block
184    //
185    // --> {"jsonrpc": "2.0", "method": "blockchain.last_confirmed_block", "params": [], "id": 1}
186    // <-- {"jsonrpc": "2.0", "result": [1234, "HeaderHash"], "id": 1}
187    pub async fn blockchain_last_confirmed_block(&self, id: u16, params: JsonValue) -> JsonResult {
188        let Some(params) = params.get::<Vec<JsonValue>>() else {
189            return JsonError::new(InvalidParams, None, id).into()
190        };
191        if !params.is_empty() {
192            return JsonError::new(InvalidParams, None, id).into()
193        }
194
195        let Ok((height, hash)) = self.validator.read().await.blockchain.last() else {
196            return JsonError::new(InternalError, None, id).into()
197        };
198
199        JsonResponse::new(
200            JsonValue::Array(vec![
201                JsonValue::Number(height as f64),
202                JsonValue::String(hash.to_string()),
203            ]),
204            id,
205        )
206        .into()
207    }
208
209    // RPCAPI:
210    // Queries the validator to find the current best fork next block height.
211    //
212    // **Params:**
213    // * Empty
214    //
215    // **Returns:**
216    // * `f64`: Current best fork next block height
217    //
218    // --> {"jsonrpc": "2.0", "method": "blockchain.best_fork_next_block_height", "params": [], "id": 1}
219    // <-- {"jsonrpc": "2.0", "result": 1234, "id": 1}
220    pub async fn blockchain_best_fork_next_block_height(
221        &self,
222        id: u16,
223        params: JsonValue,
224    ) -> JsonResult {
225        let Some(params) = params.get::<Vec<JsonValue>>() else {
226            return JsonError::new(InvalidParams, None, id).into()
227        };
228        if !params.is_empty() {
229            return JsonError::new(InvalidParams, None, id).into()
230        }
231
232        let Ok(next_block_height) = self.validator.read().await.best_fork_next_block_height().await
233        else {
234            return JsonError::new(InternalError, None, id).into()
235        };
236
237        JsonResponse::new(JsonValue::Number(next_block_height as f64), id).into()
238    }
239
240    // RPCAPI:
241    // Queries the validator to get the currently configured block target time.
242    //
243    // **Params:**
244    // * Empty
245    //
246    // **Returns:**
247    // * `f64`: Current block target time
248    //
249    // --> {"jsonrpc": "2.0", "method": "blockchain.block_target", "params": [], "id": 1}
250    // <-- {"jsonrpc": "2.0", "result": 120, "id": 1}
251    pub async fn blockchain_block_target(&self, id: u16, params: JsonValue) -> JsonResult {
252        let Some(params) = params.get::<Vec<JsonValue>>() else {
253            return JsonError::new(InvalidParams, None, id).into()
254        };
255        if !params.is_empty() {
256            return JsonError::new(InvalidParams, None, id).into()
257        }
258
259        let block_target = self.validator.read().await.consensus.module.target;
260
261        JsonResponse::new(JsonValue::Number(block_target as f64), id).into()
262    }
263
264    // RPCAPI:
265    // Initializes a subscription to new incoming blocks.
266    //
267    // Once a subscription is established, `darkfid` will send JSON-RPC notifications of
268    // new incoming blocks to the subscriber.
269    //
270    // The notifications contain base64-encoded `BlockInfo` structs.
271    //
272    // ```rust,no_run,noplayground
273    // {{#include ../../../src/blockchain/block_store.rs:blockinfo}}
274    // ```
275    //
276    // --> {"jsonrpc": "2.0", "method": "blockchain.subscribe_blocks", "params": [], "id": 1}
277    // <-- {"jsonrpc": "2.0", "method": "blockchain.subscribe_blocks", "params": ["base64encodedblock"]}
278    pub async fn blockchain_subscribe_blocks(&self, id: u16, params: JsonValue) -> JsonResult {
279        let Some(params) = params.get::<Vec<JsonValue>>() else {
280            return JsonError::new(InvalidParams, None, id).into()
281        };
282        if !params.is_empty() {
283            return JsonError::new(InvalidParams, None, id).into()
284        }
285
286        self.subscribers.get("blocks").unwrap().clone().into()
287    }
288
289    // RPCAPI:
290    // Initializes a subscription to new incoming transactions.
291    //
292    // Once a subscription is established, `darkfid` will send JSON-RPC notifications of
293    // new incoming transactions to the subscriber.
294    //
295    // The notifications contain hex-encoded transaction hashes.
296    //
297    // --> {"jsonrpc": "2.0", "method": "blockchain.subscribe_txs", "params": [], "id": 1}
298    // <-- {"jsonrpc": "2.0", "method": "blockchain.subscribe_txs", "params": ["tx_hash"]}
299    pub async fn blockchain_subscribe_txs(&self, id: u16, params: JsonValue) -> JsonResult {
300        let Some(params) = params.get::<Vec<JsonValue>>() else {
301            return JsonError::new(InvalidParams, None, id).into()
302        };
303        if !params.is_empty() {
304            return JsonError::new(InvalidParams, None, id).into()
305        }
306
307        self.subscribers.get("txs").unwrap().clone().into()
308    }
309
310    // RPCAPI:
311    // Initializes a subscription to new incoming proposals. Once a subscription is established,
312    // `darkfid` will send JSON-RPC notifications of new incoming proposals to the subscriber.
313    //
314    // The notifications contain base64-encoded `BlockInfo` structs.
315    //
316    // ```rust,no_run,noplayground
317    // {{#include ../../../src/blockchain/block_store.rs:blockinfo}}
318    // ```
319    //
320    // --> {"jsonrpc": "2.0", "method": "blockchain.subscribe_proposals", "params": [], "id": 1}
321    // <-- {"jsonrpc": "2.0", "method": "blockchain.subscribe_proposals", "params": ["base64encodedblock"]}
322    pub async fn blockchain_subscribe_proposals(&self, id: u16, params: JsonValue) -> JsonResult {
323        let Some(params) = params.get::<Vec<JsonValue>>() else {
324            return JsonError::new(InvalidParams, None, id).into()
325        };
326        if !params.is_empty() {
327            return JsonError::new(InvalidParams, None, id).into()
328        }
329
330        self.subscribers.get("proposals").unwrap().clone().into()
331    }
332
333    // RPCAPI:
334    // Performs a lookup of zkas bincodes for a given contract ID and returns all of
335    // them, including their namespace.
336    //
337    // **Params:**
338    // * `array[0]`: base58-encoded contract ID string
339    //
340    // **Returns:**
341    // * `array[n]`: Pairs of: `zkas_namespace` strings and base64-encoded
342    //   `ZkBinary` objects.
343    //
344    // ```rust,no_run,noplayground
345    // {{#include ../../../src/zkas/decoder.rs:zkbinary-struct}}
346    // ```
347    //
348    // --> {"jsonrpc": "2.0", "method": "blockchain.lookup_zkas", "params": ["BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o"], "id": 1}
349    // <-- {"jsonrpc": "2.0", "result": [["Foo", "ABCD..."], ["Bar", "EFGH..."]], "id": 1}
350    pub async fn blockchain_lookup_zkas(&self, id: u16, params: JsonValue) -> JsonResult {
351        let Some(params) = params.get::<Vec<JsonValue>>() else {
352            return JsonError::new(InvalidParams, None, id).into()
353        };
354        if params.len() != 1 || !params[0].is_string() {
355            return JsonError::new(InvalidParams, None, id).into()
356        }
357
358        let contract_id = params[0].get::<String>().unwrap();
359        let contract_id = match ContractId::from_str(contract_id) {
360            Ok(v) => v,
361            Err(e) => {
362                error!(target: "darkfid::rpc::blockchain_lookup_zkas", "Error decoding string to ContractId: {e}");
363                return JsonError::new(InvalidParams, None, id).into()
364            }
365        };
366
367        let validator = self.validator.read().await;
368        let Ok(zkas_db) = validator.blockchain.contracts.lookup(
369            &validator.blockchain.sled_db,
370            &contract_id,
371            SMART_CONTRACT_ZKAS_DB_NAME,
372        ) else {
373            error!(target: "darkfid::rpc::blockchain_lookup_zkas", "Did not find zkas db for ContractId: {contract_id}");
374            return server_error(RpcError::ContractZkasDbNotFound, id, None)
375        };
376        drop(validator);
377
378        let mut ret = vec![];
379
380        for i in zkas_db.iter() {
381            debug!(target: "darkfid::rpc::blockchain_lookup_zkas", "Iterating over zkas db");
382            let Ok((zkas_ns, zkas_bytes)) = i else {
383                error!(target: "darkfid::rpc::blockchain_lookup_zkas", "Internal sled error iterating db");
384                return JsonError::new(InternalError, None, id).into()
385            };
386
387            let Ok(zkas_ns) = deserialize_async(&zkas_ns).await else {
388                return JsonError::new(InternalError, None, id).into()
389            };
390
391            let (zkbin, _): (Vec<u8>, Vec<u8>) = match deserialize_async(&zkas_bytes).await {
392                Ok(pair) => pair,
393                Err(_) => return JsonError::new(InternalError, None, id).into(),
394            };
395
396            let zkas_bincode = base64::encode(&zkbin);
397            ret.push(JsonValue::Array(vec![
398                JsonValue::String(zkas_ns),
399                JsonValue::String(zkas_bincode),
400            ]));
401        }
402
403        JsonResponse::new(JsonValue::Array(ret), id).into()
404    }
405
406    // RPCAPI:
407    // Perform a lookup of a WASM contract binary deployed on-chain and
408    // return the base64-encoded binary.
409    //
410    // **Params:**
411    // * `array[0]`: base58-encoded contract ID string
412    //
413    // **Returns:**
414    // * `String`: base64-encoded WASM binary
415    //
416    // --> {"jsonrpc": "2.0", "method": "blockchain.lookup_wasm", "params": ["BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o"], "id": 1}
417    // <-- {"jsonrpc": "2.0", "result": "ABCD...", "id": 1}
418    pub async fn blockchain_lookup_wasm(&self, id: u16, params: JsonValue) -> JsonResult {
419        let Some(params) = params.get::<Vec<JsonValue>>() else {
420            return JsonError::new(InvalidParams, None, id).into()
421        };
422        if params.len() != 1 || !params[0].is_string() {
423            return JsonError::new(InvalidParams, None, id).into()
424        }
425
426        let contract_id = params[0].get::<String>().unwrap();
427        let Ok(contract_id) = ContractId::from_str(contract_id) else {
428            return server_error(RpcError::ParseError, id, None)
429        };
430
431        let Ok(bincode) = self.validator.read().await.blockchain.contracts.get(contract_id) else {
432            return server_error(RpcError::ContractWasmNotFound, id, None)
433        };
434
435        let encoded = base64::encode(&bincode);
436        JsonResponse::new(encoded.to_string().into(), id).into()
437    }
438
439    // RPCAPI:
440    // Queries the blockchain database for a given contract state records.
441    // Returns the records value raw bytes as a `BTreeMap`.
442    //
443    // **Params:**
444    // * `array[0]`: base58-encoded contract ID string
445    // * `array[1]`: Contract tree name string
446    //
447    // **Returns:**
448    // * Records serialized `BTreeMap` encoded with base64
449    //
450    // --> {"jsonrpc": "2.0", "method": "blockchain.get_contract_state", "params": ["BZHK...", "tree"], "id": 1}
451    // <-- {"jsonrpc": "2.0", "result": "ABCD...", "id": 1}
452    pub async fn blockchain_get_contract_state(&self, id: u16, params: JsonValue) -> JsonResult {
453        let Some(params) = params.get::<Vec<JsonValue>>() else {
454            return JsonError::new(InvalidParams, None, id).into()
455        };
456        if params.len() != 2 || !params[0].is_string() || !params[1].is_string() {
457            return JsonError::new(InvalidParams, None, id).into()
458        }
459
460        let contract_id = params[0].get::<String>().unwrap();
461        let contract_id = match ContractId::from_str(contract_id) {
462            Ok(v) => v,
463            Err(e) => {
464                error!(target: "darkfid::rpc::blockchain_get_contract_state", "Error decoding string to ContractId: {e}");
465                return JsonError::new(InvalidParams, None, id).into()
466            }
467        };
468
469        let tree_name = params[1].get::<String>().unwrap();
470
471        let validator = self.validator.read().await;
472        match validator.blockchain.contracts.get_state_tree_records(
473            &validator.blockchain.sled_db,
474            &contract_id,
475            tree_name,
476        ) {
477            Ok(records) => JsonResponse::new(
478                JsonValue::String(base64::encode(&serialize_async(&records).await)),
479                id,
480            )
481            .into(),
482            Err(e) => {
483                error!(target: "darkfid::rpc::blockchain_get_contract_state", "Failed fetching contract state records: {e}");
484                server_error(RpcError::ContractStateNotFound, id, None)
485            }
486        }
487    }
488
489    // RPCAPI:
490    // Queries the blockchain database for a given contract state key raw bytes.
491    // Returns the record value raw bytes.
492    //
493    // **Params:**
494    // * `array[0]`: base58-encoded contract ID string
495    // * `array[1]`: Contract tree name string
496    // * `array[2]`: Key raw bytes, encoded with base64
497    //
498    // **Returns:**
499    // * Record value raw bytes encoded with base64
500    //
501    // --> {"jsonrpc": "2.0", "method": "blockchain.get_contract_state_key", "params": ["BZHK...", "tree", "ABCD..."], "id": 1}
502    // <-- {"jsonrpc": "2.0", "result": "ABCD...", "id": 1}
503    pub async fn blockchain_get_contract_state_key(
504        &self,
505        id: u16,
506        params: JsonValue,
507    ) -> JsonResult {
508        let Some(params) = params.get::<Vec<JsonValue>>() else {
509            return JsonError::new(InvalidParams, None, id).into()
510        };
511        if params.len() != 3 ||
512            !params[0].is_string() ||
513            !params[1].is_string() ||
514            !params[2].is_string()
515        {
516            return JsonError::new(InvalidParams, None, id).into()
517        }
518
519        let contract_id = params[0].get::<String>().unwrap();
520        let contract_id = match ContractId::from_str(contract_id) {
521            Ok(v) => v,
522            Err(e) => {
523                error!(target: "darkfid::rpc::blockchain_get_contract_state_key", "Error decoding string to ContractId: {e}");
524                return JsonError::new(InvalidParams, None, id).into()
525            }
526        };
527
528        let tree_name = params[1].get::<String>().unwrap();
529
530        let key_enc = params[2].get::<String>().unwrap().trim();
531        let Some(key) = base64::decode(key_enc) else {
532            error!(target: "darkfid::rpc::blockchain_get_contract_state_key", "Failed decoding base64 key");
533            return server_error(RpcError::ParseError, id, None)
534        };
535
536        let validator = self.validator.read().await;
537        match validator.blockchain.contracts.get_state_tree_value(
538            &validator.blockchain.sled_db,
539            &contract_id,
540            tree_name,
541            &key,
542        ) {
543            Ok(value) => JsonResponse::new(JsonValue::String(base64::encode(&value)), id).into(),
544            Err(e) => {
545                error!(target: "darkfid::rpc::blockchain_get_contract_state_key", "Failed fetching contract state key value: {e}");
546                server_error(RpcError::ContractStateKeyNotFound, id, None)
547            }
548        }
549    }
550}