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