darkfid/
rpc_blockchain.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::str::FromStr;
20
21use darkfi_sdk::{crypto::ContractId, tx::TransactionHash};
22use darkfi_serial::{deserialize_async, serialize_async};
23use log::{debug, error};
24use tinyjson::JsonValue;
25
26use darkfi::{
27    blockchain::contract_store::SMART_CONTRACT_ZKAS_DB_NAME,
28    rpc::jsonrpc::{
29        ErrorCode::{InternalError, InvalidParams, ParseError},
30        JsonError, JsonResponse, JsonResult,
31    },
32    util::encoding::base64,
33};
34
35use crate::{server_error, DarkfiNode, RpcError};
36
37impl DarkfiNode {
38    // RPCAPI:
39    // Queries the blockchain database for a block in the given height.
40    // Returns a readable block upon success.
41    //
42    // **Params:**
43    // * `array[0]`: `u64` Block height (as string)
44    //
45    // **Returns:**
46    // * [`BlockInfo`](https://darkrenaissance.github.io/darkfi/dev/darkfi/blockchain/block_store/struct.BlockInfo.html)
47    //   struct serialized into base64.
48    //
49    // --> {"jsonrpc": "2.0", "method": "blockchain.get_block", "params": ["0"], "id": 1}
50    // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
51    pub async fn blockchain_get_block(&self, id: u16, params: JsonValue) -> JsonResult {
52        let params = params.get::<Vec<JsonValue>>().unwrap();
53        if params.len() != 1 || !params[0].is_string() {
54            return JsonError::new(InvalidParams, None, id).into()
55        }
56
57        let block_height = match params[0].get::<String>().unwrap().parse::<u32>() {
58            Ok(v) => v,
59            Err(_) => return JsonError::new(ParseError, None, id).into(),
60        };
61
62        let blocks = match self.validator.blockchain.get_blocks_by_heights(&[block_height]) {
63            Ok(v) => v,
64            Err(e) => {
65                error!(target: "darkfid::rpc::blockchain_get_block", "Failed fetching block by height: {}", e);
66                return JsonError::new(InternalError, None, id).into()
67            }
68        };
69
70        if blocks.is_empty() {
71            return server_error(RpcError::UnknownBlockHeight, id, None)
72        }
73
74        let block = base64::encode(&serialize_async(&blocks[0]).await);
75        JsonResponse::new(JsonValue::String(block), id).into()
76    }
77
78    // RPCAPI:
79    // Queries the blockchain database for a given transaction.
80    // Returns a serialized `Transaction` object.
81    //
82    // **Params:**
83    // * `array[0]`: Hex-encoded transaction hash string
84    //
85    // **Returns:**
86    // * Serialized [`Transaction`](https://darkrenaissance.github.io/darkfi/dev/darkfi/tx/struct.Transaction.html)
87    //   object encoded with base64
88    //
89    // --> {"jsonrpc": "2.0", "method": "blockchain.get_tx", "params": ["TxHash"], "id": 1}
90    // <-- {"jsonrpc": "2.0", "result": "ABCD...", "id": 1}
91    pub async fn blockchain_get_tx(&self, id: u16, params: JsonValue) -> JsonResult {
92        let params = params.get::<Vec<JsonValue>>().unwrap();
93        if params.len() != 1 || !params[0].is_string() {
94            return JsonError::new(InvalidParams, None, id).into()
95        }
96
97        let tx_hash = params[0].get::<String>().unwrap();
98        let tx_hash = match TransactionHash::from_str(tx_hash) {
99            Ok(v) => v,
100            Err(_) => return JsonError::new(ParseError, None, id).into(),
101        };
102
103        let txs = match self.validator.blockchain.transactions.get(&[tx_hash], true) {
104            Ok(txs) => txs,
105            Err(e) => {
106                error!(target: "darkfid::rpc::blockchain_get_tx", "Failed fetching tx by hash: {}", e);
107                return JsonError::new(InternalError, None, id).into()
108            }
109        };
110        // This would be an logic error somewhere
111        assert_eq!(txs.len(), 1);
112        // and strict was used during .get()
113        let tx = txs[0].as_ref().unwrap();
114
115        let tx_enc = base64::encode(&serialize_async(tx).await);
116        JsonResponse::new(JsonValue::String(tx_enc), id).into()
117    }
118
119    // RPCAPI:
120    // Queries the blockchain database to find the last confirmed block.
121    //
122    // **Params:**
123    // * `None`
124    //
125    // **Returns:**
126    // * `f64`   : Height of the last confirmed block
127    // * `String`: Header hash of the last confirmed block
128    //
129    // --> {"jsonrpc": "2.0", "method": "blockchain.last_confirmed_block", "params": [], "id": 1}
130    // <-- {"jsonrpc": "2.0", "result": [1234, "HeaderHash"], "id": 1}
131    pub async fn blockchain_last_confirmed_block(&self, id: u16, params: JsonValue) -> JsonResult {
132        let params = params.get::<Vec<JsonValue>>().unwrap();
133        if !params.is_empty() {
134            return JsonError::new(InvalidParams, None, id).into()
135        }
136
137        let Ok((height, hash)) = self.validator.blockchain.last() else {
138            return JsonError::new(InternalError, None, id).into()
139        };
140
141        JsonResponse::new(
142            JsonValue::Array(vec![
143                JsonValue::Number(height as f64),
144                JsonValue::String(hash.to_string()),
145            ]),
146            id,
147        )
148        .into()
149    }
150
151    // RPCAPI:
152    // Queries the validator to find the current best fork next block height.
153    //
154    // **Params:**
155    // * `None`
156    //
157    // **Returns:**
158    // * `f64`: Current best fork next block height
159    //
160    // --> {"jsonrpc": "2.0", "method": "blockchain.best_fork_next_block_height", "params": [], "id": 1}
161    // <-- {"jsonrpc": "2.0", "result": 1234, "id": 1}
162    pub async fn blockchain_best_fork_next_block_height(
163        &self,
164        id: u16,
165        params: JsonValue,
166    ) -> JsonResult {
167        let params = params.get::<Vec<JsonValue>>().unwrap();
168        if !params.is_empty() {
169            return JsonError::new(InvalidParams, None, id).into()
170        }
171
172        let Ok(next_block_height) = self.validator.best_fork_next_block_height().await else {
173            return JsonError::new(InternalError, None, id).into()
174        };
175
176        JsonResponse::new(JsonValue::Number(next_block_height as f64), id).into()
177    }
178
179    // RPCAPI:
180    // Queries the validator to get the currently configured block target time.
181    //
182    // **Params:**
183    // * `None`
184    //
185    // **Returns:**
186    // * `f64`: Current block target time
187    //
188    // --> {"jsonrpc": "2.0", "method": "blockchain.block_target", "params": [], "id": 1}
189    // <-- {"jsonrpc": "2.0", "result": 1234, "id": 1}
190    pub async fn blockchain_block_target(&self, id: u16, params: JsonValue) -> JsonResult {
191        let params = params.get::<Vec<JsonValue>>().unwrap();
192        if !params.is_empty() {
193            return JsonError::new(InvalidParams, None, id).into()
194        }
195
196        let block_target = self.validator.consensus.module.read().await.target;
197
198        JsonResponse::new(JsonValue::Number(block_target as f64), id).into()
199    }
200
201    // RPCAPI:
202    // Initializes a subscription to new incoming blocks.
203    // Once a subscription is established, `darkfid` will send JSON-RPC notifications of
204    // new incoming blocks to the subscriber.
205    //
206    // --> {"jsonrpc": "2.0", "method": "blockchain.subscribe_blocks", "params": [], "id": 1}
207    // <-- {"jsonrpc": "2.0", "method": "blockchain.subscribe_blocks", "params": [`blockinfo`]}
208    pub async fn blockchain_subscribe_blocks(&self, id: u16, params: JsonValue) -> JsonResult {
209        let params = params.get::<Vec<JsonValue>>().unwrap();
210        if !params.is_empty() {
211            return JsonError::new(InvalidParams, None, id).into()
212        }
213
214        self.subscribers.get("blocks").unwrap().clone().into()
215    }
216
217    // RPCAPI:
218    // Initializes a subscription to new incoming transactions.
219    // Once a subscription is established, `darkfid` will send JSON-RPC notifications of
220    // new incoming transactions to the subscriber.
221    //
222    // --> {"jsonrpc": "2.0", "method": "blockchain.subscribe_txs", "params": [], "id": 1}
223    // <-- {"jsonrpc": "2.0", "method": "blockchain.subscribe_txs", "params": [`tx_hash`]}
224    pub async fn blockchain_subscribe_txs(&self, id: u16, params: JsonValue) -> JsonResult {
225        let params = params.get::<Vec<JsonValue>>().unwrap();
226        if !params.is_empty() {
227            return JsonError::new(InvalidParams, None, id).into()
228        }
229
230        self.subscribers.get("txs").unwrap().clone().into()
231    }
232
233    // RPCAPI:
234    // Initializes a subscription to new incoming proposals. Once a subscription is established,
235    // `darkfid` will send JSON-RPC notifications of new incoming proposals to the subscriber.
236    //
237    // --> {"jsonrpc": "2.0", "method": "blockchain.subscribe_proposals", "params": [], "id": 1}
238    // <-- {"jsonrpc": "2.0", "method": "blockchain.subscribe_proposals", "params": [`blockinfo`]}
239    pub async fn blockchain_subscribe_proposals(&self, id: u16, params: JsonValue) -> JsonResult {
240        let params = params.get::<Vec<JsonValue>>().unwrap();
241        if !params.is_empty() {
242            return JsonError::new(InvalidParams, None, id).into()
243        }
244
245        self.subscribers.get("proposals").unwrap().clone().into()
246    }
247
248    // RPCAPI:
249    // Performs a lookup of zkas bincodes for a given contract ID and returns all of
250    // them, including their namespace.
251    //
252    // **Params:**
253    // * `array[0]`: base58-encoded contract ID string
254    //
255    // **Returns:**
256    // * `array[n]`: Pairs of: `zkas_namespace` string, serialized
257    //   [`ZkBinary`](https://darkrenaissance.github.io/darkfi/dev/darkfi/zkas/decoder/struct.ZkBinary.html)
258    //   object
259    //
260    // --> {"jsonrpc": "2.0", "method": "blockchain.lookup_zkas", "params": ["BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o"], "id": 1}
261    // <-- {"jsonrpc": "2.0", "result": [["Foo", "ABCD..."], ["Bar", "EFGH..."]], "id": 1}
262    pub async fn blockchain_lookup_zkas(&self, id: u16, params: JsonValue) -> JsonResult {
263        let params = params.get::<Vec<JsonValue>>().unwrap();
264        if params.len() != 1 || !params[0].is_string() {
265            return JsonError::new(InvalidParams, None, id).into()
266        }
267
268        let contract_id = params[0].get::<String>().unwrap();
269        let contract_id = match ContractId::from_str(contract_id) {
270            Ok(v) => v,
271            Err(e) => {
272                error!(target: "darkfid::rpc::blockchain_lookup_zkas", "Error decoding string to ContractId: {}", e);
273                return JsonError::new(InvalidParams, None, id).into()
274            }
275        };
276
277        let Ok(zkas_db) = self.validator.blockchain.contracts.lookup(
278            &self.validator.blockchain.sled_db,
279            &contract_id,
280            SMART_CONTRACT_ZKAS_DB_NAME,
281        ) else {
282            error!(
283                target: "darkfid::rpc::blockchain_lookup_zkas", "Did not find zkas db for ContractId: {}",
284                contract_id
285            );
286            return server_error(RpcError::ContractZkasDbNotFound, id, None)
287        };
288
289        let mut ret = vec![];
290
291        for i in zkas_db.iter() {
292            debug!(target: "darkfid::rpc::blockchain_lookup_zkas", "Iterating over zkas db");
293            let Ok((zkas_ns, zkas_bytes)) = i else {
294                error!(target: "darkfid::rpc::blockchain_lookup_zkas", "Internal sled error iterating db");
295                return JsonError::new(InternalError, None, id).into()
296            };
297
298            let Ok(zkas_ns) = deserialize_async(&zkas_ns).await else {
299                return JsonError::new(InternalError, None, id).into()
300            };
301
302            let (zkbin, _): (Vec<u8>, Vec<u8>) = match deserialize_async(&zkas_bytes).await {
303                Ok(pair) => pair,
304                Err(_) => return JsonError::new(InternalError, None, id).into(),
305            };
306
307            let zkas_bincode = base64::encode(&zkbin);
308            ret.push(JsonValue::Array(vec![
309                JsonValue::String(zkas_ns),
310                JsonValue::String(zkas_bincode),
311            ]));
312        }
313
314        JsonResponse::new(JsonValue::Array(ret), id).into()
315    }
316
317    // RPCAPI:
318    // Queries the blockchain database for a given contract state records.
319    // Returns the records value raw bytes as a `BTreeMap`.
320    //
321    // **Params:**
322    // * `array[0]`: base58-encoded contract ID string
323    // * `array[1]`: Contract tree name string
324    //
325    // **Returns:**
326    // * Records serialized `BTreeMap` encoded with base64
327    //
328    // --> {"jsonrpc": "2.0", "method": "blockchain.get_contract_state", "params": ["BZHK...", "tree"], "id": 1}
329    // <-- {"jsonrpc": "2.0", "result": "ABCD...", "id": 1}
330    pub async fn blockchain_get_contract_state(&self, id: u16, params: JsonValue) -> JsonResult {
331        let params = params.get::<Vec<JsonValue>>().unwrap();
332        if params.len() != 2 || !params[0].is_string() || !params[1].is_string() {
333            return JsonError::new(InvalidParams, None, id).into()
334        }
335
336        let contract_id = params[0].get::<String>().unwrap();
337        let contract_id = match ContractId::from_str(contract_id) {
338            Ok(v) => v,
339            Err(e) => {
340                error!(target: "darkfid::rpc::blockchain_get_contract_state", "Error decoding string to ContractId: {}", e);
341                return JsonError::new(InvalidParams, None, id).into()
342            }
343        };
344
345        let tree_name = params[1].get::<String>().unwrap();
346
347        match self.validator.blockchain.contracts.get_state_tree_records(
348            &self.validator.blockchain.sled_db,
349            &contract_id,
350            tree_name,
351        ) {
352            Ok(records) => JsonResponse::new(
353                JsonValue::String(base64::encode(&serialize_async(&records).await)),
354                id,
355            )
356            .into(),
357            Err(e) => {
358                error!(target: "darkfid::rpc::blockchain_get_contract_state", "Failed fetching contract state records: {}", e);
359                server_error(RpcError::ContractStateNotFound, id, None)
360            }
361        }
362    }
363
364    // RPCAPI:
365    // Queries the blockchain database for a given contract state key raw bytes.
366    // Returns the record value raw bytes.
367    //
368    // **Params:**
369    // * `array[0]`: base58-encoded contract ID string
370    // * `array[1]`: Contract tree name string
371    // * `array[2]`: Key raw bytes, encoded with base64
372    //
373    // **Returns:**
374    // * Record value raw bytes encoded with base64
375    //
376    // --> {"jsonrpc": "2.0", "method": "blockchain.get_contract_state_key", "params": ["BZHK...", "tree", "ABCD..."], "id": 1}
377    // <-- {"jsonrpc": "2.0", "result": "ABCD...", "id": 1}
378    pub async fn blockchain_get_contract_state_key(
379        &self,
380        id: u16,
381        params: JsonValue,
382    ) -> JsonResult {
383        let params = params.get::<Vec<JsonValue>>().unwrap();
384        if params.len() != 3 ||
385            !params[0].is_string() ||
386            !params[1].is_string() ||
387            !params[2].is_string()
388        {
389            return JsonError::new(InvalidParams, None, id).into()
390        }
391
392        let contract_id = params[0].get::<String>().unwrap();
393        let contract_id = match ContractId::from_str(contract_id) {
394            Ok(v) => v,
395            Err(e) => {
396                error!(target: "darkfid::rpc::blockchain_get_contract_state_key", "Error decoding string to ContractId: {}", e);
397                return JsonError::new(InvalidParams, None, id).into()
398            }
399        };
400
401        let tree_name = params[1].get::<String>().unwrap();
402
403        let key_enc = params[2].get::<String>().unwrap().trim();
404        let Some(key) = base64::decode(key_enc) else {
405            error!(target: "darkfid::rpc::blockchain_get_contract_state_key", "Failed decoding base64 key");
406            return server_error(RpcError::ParseError, id, None)
407        };
408
409        match self.validator.blockchain.contracts.get_state_tree_value(
410            &self.validator.blockchain.sled_db,
411            &contract_id,
412            tree_name,
413            &key,
414        ) {
415            Ok(value) => JsonResponse::new(JsonValue::String(base64::encode(&value)), id).into(),
416            Err(e) => {
417                error!(target: "darkfid::rpc::blockchain_get_contract_state_key", "Failed fetching contract state key value: {}", e);
418                server_error(RpcError::ContractStateKeyNotFound, id, None)
419            }
420        }
421    }
422}