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