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