Skip to main content

drive_abci/rpc/
core.rs

1use crate::rpc::prefetch::CorePrefetcher;
2use dpp::dashcore::consensus::encode::deserialize_partial;
3use dpp::dashcore::ephemerealdata::chain_lock::ChainLock;
4use dpp::dashcore::transaction::special_transaction::coinbase::CoinbasePayload;
5use dpp::dashcore::transaction::special_transaction::TransactionPayload;
6use dpp::dashcore::{Block, BlockHash, QuorumHash, Transaction, Txid};
7use dpp::dashcore::{Header, InstantLock};
8use dpp::dashcore_rpc::dashcore_rpc_json::{
9    AssetUnlockStatusResult, ExtendedQuorumDetails, ExtendedQuorumListResult, GetChainTipsResult,
10    MasternodeListDiff, MnSyncStatus, QuorumInfoResult, QuorumType, SoftforkInfo,
11};
12use dpp::dashcore_rpc::json::GetRawTransactionResult;
13use dpp::dashcore_rpc::jsonrpc::error::{Error as JsonRpcError, RpcError};
14use dpp::dashcore_rpc::jsonrpc::simple_http::Error as HttpError;
15use dpp::dashcore_rpc::{Auth, Client, Error, RpcApi};
16use dpp::prelude::TimestampMillis;
17use serde_json::Value;
18use std::collections::HashMap;
19use std::time::Duration;
20
21/// Information returned by QuorumListExtended
22pub type QuorumListExtendedInfo = HashMap<QuorumHash, ExtendedQuorumDetails>;
23
24/// The special transaction type of a coinbase (`TRANSACTION_COINBASE` in Dash Core).
25const COINBASE_TRANSACTION_TYPE: u16 = 5;
26
27/// Reads Core's credit pool balance after a block, in duffs, from the block's serialized
28/// coinbase transaction; `0` when it carries no payload or its payload predates version 3,
29/// before the credit pool existed, which is how Core's own unlock limit reads such a block.
30///
31/// The transaction decoder types a payload only when the payload decoder reads exactly the
32/// bytes its length prefix frames, and keeps any other payload raw. A raw coinbase payload is
33/// read with `deserialize_partial`, so bytes after the fields the payload decoder knows are
34/// left unread rather than refused: a field a later Core version appends to the payload, as
35/// version 4 appended `merkleRootAssetUnlocks` after the balance, does not stop the balance
36/// from being read. The payload is the coinbase's last field and Core frames it exactly, so
37/// reading it raw reads the same bytes as reading it in place. Core has only ever appended
38/// fields to the payload, and its consensus rules (`CheckCbTx`) refuse versions it does not
39/// know, so the balance stays where version 3 put it unless a Core release moves it, which
40/// Platform would have to follow anyway.
41pub(crate) fn credit_pool_balance_from_coinbase(coinbase: &[u8]) -> Result<u64, String> {
42    let (transaction, _) = deserialize_partial::<Transaction>(coinbase)
43        .map_err(|e| format!("coinbase cannot be decoded: {e}"))?;
44    match transaction.special_transaction_payload {
45        None => Ok(0),
46        Some(TransactionPayload::CoinbasePayloadType(payload)) => {
47            Ok(payload.asset_locked_amount.unwrap_or_default())
48        }
49        Some(TransactionPayload::RawPayloadType(COINBASE_TRANSACTION_TYPE, bytes)) => {
50            let (payload, _) = deserialize_partial::<CoinbasePayload>(&bytes)
51                .map_err(|e| format!("coinbase payload cannot be decoded: {e}"))?;
52            Ok(payload.asset_locked_amount.unwrap_or_default())
53        }
54        Some(payload) => Err(format!(
55            "coinbase carries a {:?} payload",
56            payload.get_type()
57        )),
58    }
59}
60
61/// Reads the version of a block's coinbase payload from the block's serialized coinbase
62/// transaction; `None` when it carries no payload. The version is the payload's first field,
63/// so a raw payload is read the same way as in [`credit_pool_balance_from_coinbase`].
64pub(crate) fn coinbase_payload_version_from_coinbase(
65    coinbase: &[u8],
66) -> Result<Option<u16>, String> {
67    let (transaction, _) = deserialize_partial::<Transaction>(coinbase)
68        .map_err(|e| format!("coinbase cannot be decoded: {e}"))?;
69    match transaction.special_transaction_payload {
70        None => Ok(None),
71        Some(TransactionPayload::CoinbasePayloadType(payload)) => Ok(Some(payload.version)),
72        Some(TransactionPayload::RawPayloadType(COINBASE_TRANSACTION_TYPE, bytes)) => {
73            let (payload, _) = deserialize_partial::<CoinbasePayload>(&bytes)
74                .map_err(|e| format!("coinbase payload cannot be decoded: {e}"))?;
75            Ok(Some(payload.version))
76        }
77        Some(payload) => Err(format!(
78            "coinbase carries a {:?} payload",
79            payload.get_type()
80        )),
81    }
82}
83
84/// Core height must be of type u32 (Platform heights are u64)
85pub type CoreHeight = u32;
86/// Core RPC interface
87#[cfg_attr(any(feature = "mocks", test), mockall::automock)]
88pub trait CoreRPCLike {
89    /// Get block hash by height
90    fn get_block_hash(&self, height: CoreHeight) -> Result<BlockHash, Error>;
91
92    /// Get block hash by height
93    fn get_block_header(&self, block_hash: &BlockHash) -> Result<Header, Error>;
94
95    /// Get block time of a chain locked core height
96    fn get_block_time_from_height(&self, height: CoreHeight) -> Result<TimestampMillis, Error>;
97
98    /// Get the best chain lock
99    fn get_best_chain_lock(&self) -> Result<ChainLock, Error>;
100
101    /// Submit a chain lock
102    fn submit_chain_lock(&self, chain_lock: &ChainLock) -> Result<u32, Error>;
103
104    /// Get transaction
105    fn get_transaction(&self, tx_id: &Txid) -> Result<Transaction, Error>;
106
107    /// Get asset unlock statuses
108    fn get_asset_unlock_statuses(
109        &self,
110        indices: &[u64],
111        core_chain_locked_height: u32,
112    ) -> Result<Vec<AssetUnlockStatusResult>, Error>;
113
114    /// Get transaction
115    fn get_transaction_extended_info(&self, tx_id: &Txid)
116        -> Result<GetRawTransactionResult, Error>;
117
118    /// Get optional transaction extended info
119    /// Returns None if transaction doesn't exists
120    fn get_optional_transaction_extended_info(
121        &self,
122        transaction_id: &Txid,
123    ) -> Result<Option<GetRawTransactionResult>, Error> {
124        match self.get_transaction_extended_info(transaction_id) {
125            Ok(transaction_info) => Ok(Some(transaction_info)),
126            // Return None if transaction with specified tx id is not present
127            Err(Error::JsonRpc(dpp::dashcore_rpc::jsonrpc::error::Error::Rpc(
128                dpp::dashcore_rpc::jsonrpc::error::RpcError {
129                    code: CORE_RPC_INVALID_ADDRESS_OR_KEY,
130                    ..
131                },
132            ))) => Ok(None),
133            Err(e) => Err(e),
134        }
135    }
136
137    /// Get the state of the Core deployment `name` at the chain tip, `None` when Core does not
138    /// report it
139    fn get_fork_info(&self, name: &str) -> Result<Option<SoftforkInfo>, Error>;
140
141    /// Get block by hash
142    fn get_block(&self, block_hash: &BlockHash) -> Result<Block, Error>;
143
144    /// Get block by hash in JSON format
145    fn get_block_json(&self, block_hash: &BlockHash) -> Result<Value, Error>;
146
147    /// Get chain tips
148    fn get_chain_tips(&self) -> Result<GetChainTipsResult, Error>;
149
150    /// Get list of quorums by type at a given height.
151    ///
152    /// See <https://dashcore.readme.io/v19.0.0/docs/core-api-ref-remote-procedure-calls-evo#quorum-listextended>
153    fn get_quorum_listextended(
154        &self,
155        height: Option<CoreHeight>,
156    ) -> Result<ExtendedQuorumListResult, Error>;
157
158    /// Get quorum information.
159    ///
160    /// See <https://dashcore.readme.io/v19.0.0/docs/core-api-ref-remote-procedure-calls-evo#quorum-info>
161    fn get_quorum_info(
162        &self,
163        quorum_type: QuorumType,
164        hash: &QuorumHash,
165        include_secret_key_share: Option<bool>,
166    ) -> Result<QuorumInfoResult, Error>;
167
168    /// Get the difference in masternode list, return masternodes as diff elements
169    fn get_protx_diff_with_masternodes(
170        &self,
171        base_block: Option<u32>,
172        block: u32,
173    ) -> Result<MasternodeListDiff, Error>;
174
175    // /// Get the detailed information about a deterministic masternode
176    // fn get_protx_info(&self, pro_tx_hash: &ProTxHash) -> Result<ProTxInfo, Error>;
177
178    /// Verify Instant Lock signature
179    /// If `max_height` is provided the chain lock will be verified
180    /// against quorums available at this height
181    fn verify_instant_lock(
182        &self,
183        instant_lock: &InstantLock,
184        max_height: Option<u32>,
185    ) -> Result<bool, Error>;
186
187    /// Verify a chain lock signature
188    fn verify_chain_lock(&self, chain_lock: &ChainLock) -> Result<bool, Error>;
189
190    /// Returns masternode sync status
191    fn masternode_sync_status(&self) -> Result<MnSyncStatus, Error>;
192
193    /// Sends raw transaction to the network
194    fn send_raw_transaction(&self, transaction: &[u8]) -> Result<Txid, Error>;
195
196    /// Get Core's credit pool balance after the block at `height`, in duffs, read from the
197    /// block's coinbase (only the coinbase is transferred). Only ask for a chain locked height:
198    /// the answer is then the same on every node.
199    fn get_credit_pool_balance(&self, height: CoreHeight) -> Result<u64, Error>;
200
201    /// Get the version of the coinbase payload of the block at `height`, `None` for a coinbase
202    /// without a payload (only the coinbase is transferred). Only ask for a chain locked height:
203    /// the answer is then the same on every node.
204    fn get_coinbase_payload_version(&self, height: CoreHeight) -> Result<Option<u16>, Error>;
205}
206
207#[derive(Debug)]
208/// Default implementation of Dash Core RPC using DashCoreRPC client
209pub struct DefaultCoreRPC {
210    inner: Client,
211    /// Speculative fetcher for the next core height, on its own connection.
212    /// `None` when a second connection could not be opened.
213    prefetcher: Option<CorePrefetcher>,
214}
215
216// TODO: Create errors for these error codes in dashcore_rpc
217
218/// TX is invalid due to consensus rules
219pub const CORE_RPC_TX_CONSENSUS_ERROR: i32 = -26;
220/// Tx already broadcasted and included in the chain
221pub const CORE_RPC_TX_ALREADY_IN_CHAIN: i32 = -27;
222/// Client still warming up
223pub const CORE_RPC_ERROR_IN_WARMUP: i32 = -28;
224/// Dash is not connected
225pub const CORE_RPC_CLIENT_NOT_CONNECTED: i32 = -9;
226/// Still downloading initial blocks
227pub const CORE_RPC_CLIENT_IN_INITIAL_DOWNLOAD: i32 = -10;
228/// Parse error
229pub const CORE_RPC_PARSE_ERROR: i32 = -32700;
230/// Invalid address or key
231pub const CORE_RPC_INVALID_ADDRESS_OR_KEY: i32 = -5;
232/// Invalid, missing or duplicate parameter
233pub const CORE_RPC_INVALID_PARAMETER: i32 = -8;
234/// Method not found
235pub const CORE_RPC_METHOD_NOT_FOUND: i32 = -32601;
236
237/// The HTTP status Core refuses a request with, such as a method missing from the RPC user's
238/// `rpcwhitelist`
239const HTTP_FORBIDDEN: u16 = 403;
240
241/// Whether Core refused the request with HTTP 403 and no JSON-RPC answer
242fn is_http_forbidden(error: &Error) -> bool {
243    let Error::JsonRpc(JsonRpcError::Transport(transport)) = error else {
244        return false;
245    };
246    matches!(
247        transport.downcast_ref::<HttpError>(),
248        Some(HttpError::HttpErrorCode(HTTP_FORBIDDEN))
249    )
250}
251
252macro_rules! retry {
253    ($action:expr) => {{
254        /// Maximum number of retry attempts
255        const MAX_RETRIES: u32 = 4;
256        /// // Multiplier for Fibonacci sequence
257        const FIB_MULTIPLIER: u64 = 1;
258
259        fn fibonacci(n: u32) -> u64 {
260            match n {
261                0 => 0,
262                1 => 1,
263                _ => fibonacci(n - 1) + fibonacci(n - 2),
264            }
265        }
266
267        let mut last_err = None;
268        let result = (0..MAX_RETRIES).find_map(|i| {
269            match $action {
270                Ok(result) => Some(Ok(result)),
271                Err(e) => {
272                    match e {
273                        // Core answers HTTP 403 to a request it will never allow, such as a
274                        // method missing from the RPC user's rpcwhitelist, so asking again
275                        // only delays the error
276                        _ if is_http_forbidden(&e) => Some(Err(e)),
277                        dpp::dashcore_rpc::Error::JsonRpc(
278                            // Retry on transport connection error
279                            dpp::dashcore_rpc::jsonrpc::error::Error::Transport(_)
280                            | dpp::dashcore_rpc::jsonrpc::error::Error::Rpc(
281                                // Retry on Core RPC "not ready" errors
282                                dpp::dashcore_rpc::jsonrpc::error::RpcError {
283                                    code:
284                                        CORE_RPC_ERROR_IN_WARMUP
285                                        | CORE_RPC_CLIENT_NOT_CONNECTED
286                                        | CORE_RPC_CLIENT_IN_INITIAL_DOWNLOAD,
287                                    ..
288                                },
289                            ),
290                        ) => {
291                            // Delay before next try
292                            last_err = Some(e);
293                            let delay = fibonacci(i + 2) * FIB_MULTIPLIER;
294                            std::thread::sleep(Duration::from_secs(delay));
295                            None
296                        }
297                        _ => Some(Err(e)),
298                    }
299                }
300            }
301        });
302
303        result.unwrap_or_else(|| Err(last_err.unwrap()))
304    }};
305}
306
307impl DefaultCoreRPC {
308    /// Create new instance
309    pub fn open(url: &str, username: String, password: String) -> Result<Self, Error> {
310        let prefetcher = CorePrefetcher::new(url, username.clone(), password.clone());
311        if prefetcher.is_none() {
312            tracing::warn!(
313                "could not open a second Core RPC connection; masternode and quorum updates will be fetched on the critical path"
314            );
315        }
316        Ok(DefaultCoreRPC {
317            inner: Client::new(url, Auth::UserPass(username, password))?,
318            prefetcher,
319        })
320    }
321
322    /// The serialized coinbase of the block at `height`, `None` for a coinbase without a payload.
323    fn coinbase_with_payload(&self, height: CoreHeight) -> Result<Option<Vec<u8>>, Error> {
324        let block_hash = self.get_block_hash(height)?;
325        // Only the coinbase, as raw hex: the special transactions of type 5, the first one,
326        // verbosity 1. An empty answer is a coinbase without a payload, before DIP3.
327        let args = [
328            Value::String(block_hash.to_string()),
329            Value::from(COINBASE_TRANSACTION_TYPE),
330            Value::from(1),
331            Value::from(0),
332            Value::from(1),
333        ];
334        let coinbases = retry!(self.inner.call::<Vec<String>>("getspecialtxes", &args))?;
335        let Some(coinbase) = coinbases.first() else {
336            return Ok(None);
337        };
338        hex::decode(coinbase).map(Some).map_err(|e| {
339            Error::UnexpectedStructure(format!("getspecialtxes answered invalid hex: {e}"))
340        })
341    }
342}
343
344impl CoreRPCLike for DefaultCoreRPC {
345    fn get_block_hash(&self, height: u32) -> Result<BlockHash, Error> {
346        retry!(self.inner.get_block_hash(height))
347    }
348
349    fn get_block_header(&self, block_hash: &BlockHash) -> Result<Header, Error> {
350        retry!(self.inner.get_block_header(block_hash))
351    }
352
353    fn get_block_time_from_height(&self, height: CoreHeight) -> Result<TimestampMillis, Error> {
354        let block_hash = self.get_block_hash(height)?;
355        let block_header = self.get_block_header(&block_hash)?;
356        let block_time = block_header.time as u64 * 1000;
357        Ok(block_time)
358    }
359
360    fn get_best_chain_lock(&self) -> Result<ChainLock, Error> {
361        retry!(self.inner.get_best_chain_lock())
362    }
363
364    fn submit_chain_lock(&self, chain_lock: &ChainLock) -> Result<u32, Error> {
365        retry!(self.inner.submit_chain_lock(chain_lock))
366    }
367
368    fn get_transaction(&self, tx_id: &Txid) -> Result<Transaction, Error> {
369        retry!(self.inner.get_raw_transaction(tx_id, None))
370    }
371
372    fn get_transaction_extended_info(
373        &self,
374        tx_id: &Txid,
375    ) -> Result<GetRawTransactionResult, Error> {
376        retry!(self.inner.get_raw_transaction_info(tx_id, None))
377    }
378
379    fn get_fork_info(&self, name: &str) -> Result<Option<SoftforkInfo>, Error> {
380        match retry!(self.inner.get_deployment_info(None)) {
381            Ok(deployment_info) => Ok(deployment_info.deployments.get(name).cloned()),
382            // Core before v24 has no getdeploymentinfo, and reports the same deployments as
383            // getblockchaininfo's softforks. Core v24 reports them there only with
384            // -deprecatedrpc=softforks.
385            Err(Error::JsonRpc(JsonRpcError::Rpc(RpcError {
386                code: CORE_RPC_METHOD_NOT_FOUND,
387                ..
388            }))) => retry!(self
389                .inner
390                .get_blockchain_info()
391                .map(|blockchain_info| blockchain_info.softforks.get(name).cloned())),
392            // Core refuses a method missing from the RPC user's rpcwhitelist with HTTP 403. Core
393            // before v24 still reports the deployment in softforks. Core v24 started without
394            // -deprecatedrpc=softforks reports none there, so when softforks has no entry for
395            // the fork, the refusal is returned rather than `None`.
396            Err(e) if is_http_forbidden(&e) => {
397                tracing::warn!(
398                    "Core refused getdeploymentinfo with HTTP 403; add it to the Core RPC user's rpcwhitelist. Reading the {name} deployment from getblockchaininfo softforks instead"
399                );
400                let blockchain_info = retry!(self.inner.get_blockchain_info())?;
401                match blockchain_info.softforks.get(name) {
402                    Some(fork_info) => Ok(Some(fork_info.clone())),
403                    None => Err(e),
404                }
405            }
406            Err(e) => Err(e),
407        }
408    }
409
410    fn get_block(&self, block_hash: &BlockHash) -> Result<Block, Error> {
411        retry!(self.inner.get_block(block_hash))
412    }
413
414    fn get_block_json(&self, block_hash: &BlockHash) -> Result<Value, Error> {
415        retry!(self.inner.get_block_json(block_hash))
416    }
417
418    fn get_chain_tips(&self) -> Result<GetChainTipsResult, Error> {
419        retry!(self.inner.get_chain_tips())
420    }
421
422    fn get_quorum_listextended(
423        &self,
424        height: Option<CoreHeight>,
425    ) -> Result<ExtendedQuorumListResult, Error> {
426        // Block sync walks core heights in order, so the next call is almost
427        // always for height + 1. Take the speculative answer when it is for the
428        // height we were asked about, and start the next guess either way. The
429        // prefetcher declines a guess past the chain lock, so at the tip this is
430        // a no-op until Core locks the next block.
431        let prefetched = height
432            .zip(self.prefetcher.as_ref())
433            .and_then(|(height, prefetcher)| prefetcher.take_quorum_list(height));
434
435        let result = match prefetched {
436            Some(list) => Ok(list),
437            None => retry!(self.inner.get_quorum_listextended_reversed(height)),
438        };
439
440        if let (Ok(_), Some(height), Some(prefetcher)) = (&result, height, self.prefetcher.as_ref())
441        {
442            prefetcher.start_quorum_list(height + 1);
443        }
444
445        result
446    }
447
448    fn get_quorum_info(
449        &self,
450        quorum_type: QuorumType,
451        hash: &QuorumHash,
452        include_secret_key_share: Option<bool>,
453    ) -> Result<QuorumInfoResult, Error> {
454        retry!(self
455            .inner
456            .get_quorum_info_reversed(quorum_type, hash, include_secret_key_share))
457    }
458
459    fn get_protx_diff_with_masternodes(
460        &self,
461        base_block: Option<u32>,
462        block: u32,
463    ) -> Result<MasternodeListDiff, Error> {
464        let base = base_block.unwrap_or(1);
465
466        // Same reasoning as get_quorum_listextended: the next diff a syncing
467        // node asks for is from this block to the one after it.
468        let prefetched = self
469            .prefetcher
470            .as_ref()
471            .and_then(|prefetcher| prefetcher.take_protx_diff(base, block));
472
473        let result = match prefetched {
474            Some(diff) => Ok(diff),
475            None => retry!(self.inner.get_protx_listdiff(base, block)),
476        };
477
478        if let (Ok(_), Some(prefetcher)) = (&result, self.prefetcher.as_ref()) {
479            prefetcher.start_protx_diff(block, block + 1);
480        }
481
482        result
483    }
484
485    /// Verify Instant Lock signature
486    /// If `max_height` is provided the chain lock will be verified
487    /// against quorums available at this height
488    fn verify_instant_lock(
489        &self,
490        instant_lock: &InstantLock,
491        max_height: Option<u32>,
492    ) -> Result<bool, Error> {
493        let request_id = instant_lock.request_id()?.to_string();
494        let transaction_id = instant_lock.txid.to_string();
495        let signature = hex::encode(instant_lock.signature);
496
497        retry!(self
498            .inner
499            .get_verifyislock(&request_id, &transaction_id, &signature, max_height))
500    }
501
502    /// Verify a chain lock signature
503    fn verify_chain_lock(&self, chain_lock: &ChainLock) -> Result<bool, Error> {
504        let block_hash = chain_lock.block_hash.to_string();
505        let signature = hex::encode(chain_lock.signature);
506
507        retry!(self.inner.get_verifychainlock(
508            block_hash.as_str(),
509            &signature,
510            Some(chain_lock.block_height)
511        ))
512    }
513
514    /// Returns masternode sync status
515    fn masternode_sync_status(&self) -> Result<MnSyncStatus, Error> {
516        retry!(self.inner.mnsync_status())
517    }
518
519    fn send_raw_transaction(&self, transaction: &[u8]) -> Result<Txid, Error> {
520        retry!(self.inner.send_raw_transaction(transaction))
521    }
522
523    fn get_asset_unlock_statuses(
524        &self,
525        indices: &[u64],
526        core_chain_locked_height: u32,
527    ) -> Result<Vec<AssetUnlockStatusResult>, Error> {
528        retry!(self
529            .inner
530            .get_asset_unlock_statuses(indices, Some(core_chain_locked_height)))
531    }
532
533    fn get_credit_pool_balance(&self, height: CoreHeight) -> Result<u64, Error> {
534        let Some(coinbase) = self.coinbase_with_payload(height)? else {
535            return Ok(0);
536        };
537        credit_pool_balance_from_coinbase(&coinbase).map_err(Error::UnexpectedStructure)
538    }
539
540    fn get_coinbase_payload_version(&self, height: CoreHeight) -> Result<Option<u16>, Error> {
541        let Some(coinbase) = self.coinbase_with_payload(height)? else {
542            return Ok(None);
543        };
544        coinbase_payload_version_from_coinbase(&coinbase).map_err(Error::UnexpectedStructure)
545    }
546}
547
548#[cfg(test)]
549mod tests {
550    use super::{coinbase_payload_version_from_coinbase, credit_pool_balance_from_coinbase};
551    use dpp::dashcore::bls_sig_utils::BLSSignature;
552    use dpp::dashcore::consensus::serialize;
553    use dpp::dashcore::hash_types::{MerkleRootMasternodeList, MerkleRootQuorums};
554    use dpp::dashcore::hashes::Hash;
555    use dpp::dashcore::transaction::special_transaction::coinbase::CoinbasePayload;
556    use dpp::dashcore::transaction::special_transaction::TransactionPayload;
557    use dpp::dashcore::{OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
558
559    const BALANCE_DUFFS: u64 = 3_700_000_000_000;
560
561    /// A coinbase as the pinned rust-dashcore encodes it.
562    fn coinbase(payload: Option<TransactionPayload>) -> Vec<u8> {
563        serialize(&Transaction {
564            version: 3,
565            lock_time: 0,
566            input: vec![TxIn {
567                previous_output: OutPoint::null(),
568                script_sig: ScriptBuf::from(vec![0x51, 0x51]),
569                sequence: u32::MAX,
570                witness: Default::default(),
571            }],
572            output: vec![TxOut {
573                value: 5_000_000,
574                script_pubkey: ScriptBuf::from(vec![0x76; 25]),
575            }],
576            special_transaction_payload: payload,
577        })
578    }
579
580    fn version_3_payload() -> TransactionPayload {
581        TransactionPayload::CoinbasePayloadType(CoinbasePayload {
582            version: 3,
583            height: 1_000,
584            merkle_root_masternode_list: MerkleRootMasternodeList::from_byte_array([1; 32]),
585            merkle_root_quorums: MerkleRootQuorums::from_byte_array([2; 32]),
586            best_cl_height: Some(30),
587            best_cl_signature: Some(BLSSignature::from([3; 96])),
588            asset_locked_amount: Some(BALANCE_DUFFS),
589            merkle_root_asset_unlocks: None,
590        })
591    }
592
593    #[test]
594    fn should_read_the_balance_of_a_version_3_coinbase() {
595        assert_eq!(
596            credit_pool_balance_from_coinbase(&coinbase(Some(version_3_payload()))),
597            Ok(BALANCE_DUFFS)
598        );
599    }
600
601    /// The coinbase of `version_3_payload` raised to `version`, with
602    /// `after_balance` after its version 3 fields.
603    fn raised_coinbase(version: u16, after_balance: &[u8]) -> Vec<u8> {
604        let version_3 = coinbase(Some(version_3_payload()));
605        // The payload is last: its 1-byte length (175), then the payload itself.
606        let payload_start = version_3.len() - 175;
607        assert_eq!(version_3[payload_start - 1], 175);
608        let mut grown = version_3[..payload_start - 1].to_vec();
609        grown.push(u8::try_from(175 + after_balance.len()).expect("1-byte length"));
610        grown.extend_from_slice(&version.to_le_bytes());
611        grown.extend_from_slice(&version_3[payload_start + 2..]);
612        grown.extend_from_slice(after_balance);
613        grown
614    }
615
616    /// Core v24 blocks carry a version 4 payload, which appends `merkleRootAssetUnlocks`
617    /// after the balance.
618    #[test]
619    fn should_read_the_balance_of_a_version_4_coinbase() {
620        assert_eq!(
621            credit_pool_balance_from_coinbase(&raised_coinbase(4, &[0xaa; 32])),
622            Ok(BALANCE_DUFFS)
623        );
624    }
625
626    /// Fields a later Core version appends after `merkleRootAssetUnlocks` are
627    /// left unread, so the balance is still read.
628    #[test]
629    fn should_read_the_balance_of_a_coinbase_with_fields_after_version_4() {
630        let mut after_balance = vec![0xaa; 32];
631        after_balance.extend_from_slice(&[0xbb; 16]);
632        assert_eq!(
633            credit_pool_balance_from_coinbase(&raised_coinbase(4, &after_balance)),
634            Ok(BALANCE_DUFFS)
635        );
636    }
637
638    /// A payload version after 4, with a field after `merkleRootAssetUnlocks`:
639    /// the payload decoder reads a version it does not know with the version 4
640    /// layout, so the balance is still read from where version 3 put it.
641    #[test]
642    fn should_read_the_balance_of_a_coinbase_of_a_later_version() {
643        let mut after_balance = vec![0xaa; 32];
644        after_balance.extend_from_slice(&[0xbb; 16]);
645        assert_eq!(
646            credit_pool_balance_from_coinbase(&raised_coinbase(5, &after_balance)),
647            Ok(BALANCE_DUFFS)
648        );
649    }
650
651    #[test]
652    fn should_read_no_balance_from_a_coinbase_before_the_credit_pool() {
653        let version_2 = TransactionPayload::CoinbasePayloadType(CoinbasePayload {
654            version: 2,
655            height: 1_000,
656            merkle_root_masternode_list: MerkleRootMasternodeList::from_byte_array([1; 32]),
657            merkle_root_quorums: MerkleRootQuorums::from_byte_array([2; 32]),
658            best_cl_height: None,
659            best_cl_signature: None,
660            asset_locked_amount: None,
661            merkle_root_asset_unlocks: None,
662        });
663        assert_eq!(
664            credit_pool_balance_from_coinbase(&coinbase(Some(version_2))),
665            Ok(0)
666        );
667        assert_eq!(credit_pool_balance_from_coinbase(&coinbase(None)), Ok(0));
668    }
669
670    #[test]
671    fn should_fail_on_a_truncated_coinbase() {
672        let full = coinbase(Some(version_3_payload()));
673        // Cut inside the payload, before the balance.
674        assert!(credit_pool_balance_from_coinbase(&full[..full.len() - 60]).is_err());
675        assert!(credit_pool_balance_from_coinbase(&full[..40]).is_err());
676    }
677
678    /// The payload version tells whether Core's rules for that block required the version 4
679    /// payload, which Core v24 does exactly at the blocks where DEPLOYMENT_V24 and V20 are active.
680    #[test]
681    fn should_read_the_payload_version_of_a_coinbase() {
682        assert_eq!(
683            coinbase_payload_version_from_coinbase(&coinbase(Some(version_3_payload()))),
684            Ok(Some(3))
685        );
686        assert_eq!(
687            coinbase_payload_version_from_coinbase(&raised_coinbase(4, &[0xaa; 32])),
688            Ok(Some(4))
689        );
690        let mut after_balance = vec![0xaa; 32];
691        after_balance.extend_from_slice(&[0xbb; 16]);
692        assert_eq!(
693            coinbase_payload_version_from_coinbase(&raised_coinbase(5, &after_balance)),
694            Ok(Some(5))
695        );
696        assert_eq!(
697            coinbase_payload_version_from_coinbase(&coinbase(None)),
698            Ok(None)
699        );
700        let full = coinbase(Some(version_3_payload()));
701        assert!(coinbase_payload_version_from_coinbase(&full[..40]).is_err());
702    }
703
704    /// Reading a Core deployment over a real JSON-RPC connection, from a mock Core that serves
705    /// answers recorded from Dash Core v24.0.0-rc.3 and v23.1.8.
706    mod fork_info {
707        use crate::rpc::core::{CoreRPCLike, DefaultCoreRPC};
708        use dpp::dashcore_rpc::dashcore_rpc_json::{SoftforkInfo, SoftforkType};
709        use dpp::dashcore_rpc::jsonrpc::error::{Error as JsonRpcError, RpcError};
710        use dpp::dashcore_rpc::jsonrpc::simple_http::Error as HttpError;
711        use dpp::dashcore_rpc::Error;
712        use serde_json::{json, Value};
713        use std::io::{BufRead, BufReader, Read, Write};
714        use std::net::{TcpListener, TcpStream};
715        use std::sync::{Arc, Mutex};
716        use std::thread;
717
718        /// The `getblockchaininfo` result of Dash Core v24.0.0-rc.3 on regtest at height 440: no
719        /// `softforks`, which Core v24 reports only with `-deprecatedrpc=softforks`.
720        const CORE_V24_GETBLOCKCHAININFO: &str = r#"{
721          "chain": "regtest",
722          "blocks": 440,
723          "headers": 440,
724          "bestblockhash": "74b4fb2caeeebbc7cd5dcc50b58532d983f0e5c87f3ac729c6d47f0ace76b97d",
725          "difficulty": 4.656542373906925e-10,
726          "time": 1791945745,
727          "mediantime": 1791945744,
728          "verificationprogress": 1,
729          "initialblockdownload": false,
730          "chainwork": "0000000000000000000000000000000000000000000000000000000000000372",
731          "size_on_disk": 98178,
732          "pruned": false,
733          "warnings": "Make sure to encrypt your wallet and delete all non-encrypted backups after you have verified that the wallet works!"
734        }"#;
735
736        /// The `getdeploymentinfo` result of the same Core v24 node.
737        const CORE_V24_GETDEPLOYMENTINFO: &str = r#"{
738          "hash": "74b4fb2caeeebbc7cd5dcc50b58532d983f0e5c87f3ac729c6d47f0ace76b97d",
739          "height": 440,
740          "deployments": {
741            "bip34": {"type": "buried", "active": true, "height": 1},
742            "bip66": {"type": "buried", "active": true, "height": 1},
743            "bip65": {"type": "buried", "active": true, "height": 1},
744            "bip147": {"type": "buried", "active": true, "height": 0},
745            "csv": {"type": "buried", "active": true, "height": 1},
746            "dip0001": {"type": "buried", "active": true, "height": 1},
747            "dip0003": {"type": "buried", "active": true, "height": 432},
748            "dip0008": {"type": "buried", "active": true, "height": 1},
749            "dip0020": {"type": "buried", "active": true, "height": 1},
750            "dip0024": {"type": "buried", "active": true, "height": 1},
751            "realloc": {"type": "buried", "active": true, "height": 1},
752            "v19": {"type": "buried", "active": true, "height": 1},
753            "v20": {"type": "buried", "active": true, "height": 432},
754            "mn_rr": {"type": "buried", "active": true, "height": 432},
755            "withdrawals": {"type": "buried", "active": false, "height": 600},
756            "v24": {"type": "bip9", "active": false, "bip9": {"start_time": 0, "timeout": 9223372036854775807, "ehf": true, "min_activation_height": 0, "status": "defined", "since": 0, "status_next": "defined"}},
757            "evo_shares": {"type": "bip9", "active": false, "bip9": {"start_time": 0, "timeout": 9223372036854775807, "ehf": true, "min_activation_height": 0, "status": "defined", "since": 0, "status_next": "defined"}},
758            "testdummy": {"type": "bip9", "height": 432, "active": true, "bip9": {"start_time": 0, "timeout": 9223372036854775807, "ehf": false, "min_activation_height": 0, "status": "active", "since": 432, "status_next": "active"}}
759          }
760        }"#;
761
762        /// The `getblockchaininfo` result of Dash Core v23.1.8 on regtest at height 440. Core v23
763        /// has no `getdeploymentinfo`.
764        const CORE_V23_GETBLOCKCHAININFO: &str = r#"{
765          "chain": "regtest",
766          "blocks": 440,
767          "headers": 440,
768          "bestblockhash": "609b799530c36b8ad9d40b6c4dc182e9d64679bfe6c9252ed60822bbd8c8e748",
769          "difficulty": 4.656542373906925e-10,
770          "time": 1791945759,
771          "mediantime": 1791945758,
772          "verificationprogress": 1,
773          "initialblockdownload": false,
774          "chainwork": "0000000000000000000000000000000000000000000000000000000000000372",
775          "size_on_disk": 98178,
776          "pruned": false,
777          "softforks": {
778            "bip34": {"type": "buried", "active": true, "height": 1},
779            "bip66": {"type": "buried", "active": true, "height": 1},
780            "bip65": {"type": "buried", "active": true, "height": 1},
781            "bip147": {"type": "buried", "active": true, "height": 0},
782            "csv": {"type": "buried", "active": true, "height": 1},
783            "dip0001": {"type": "buried", "active": true, "height": 1},
784            "dip0003": {"type": "buried", "active": true, "height": 432},
785            "dip0008": {"type": "buried", "active": true, "height": 1},
786            "dip0020": {"type": "buried", "active": true, "height": 1},
787            "dip0024": {"type": "buried", "active": true, "height": 1},
788            "realloc": {"type": "buried", "active": true, "height": 1},
789            "v19": {"type": "buried", "active": true, "height": 1},
790            "v20": {"type": "buried", "active": true, "height": 432},
791            "mn_rr": {"type": "buried", "active": true, "height": 432},
792            "withdrawals": {"type": "buried", "active": false, "height": 600},
793            "v24": {"type": "bip9", "bip9": {"status": "defined", "start_time": 0, "timeout": 9223372036854775807, "min_activation_height": 0, "ehf": true, "since": 0}, "active": false},
794            "testdummy": {"type": "bip9", "bip9": {"status": "active", "start_time": 0, "timeout": 9223372036854775807, "min_activation_height": 0, "ehf": false, "since": 432}, "height": 432, "active": true}
795          },
796          "warnings": "Make sure to encrypt your wallet and delete all non-encrypted backups after you have verified that the wallet works!"
797        }"#;
798
799        /// How the mock Core answers a method it knows.
800        #[derive(Clone, Copy)]
801        enum Answer {
802            /// HTTP 200 with this JSON-RPC `result`.
803            Result(&'static str),
804            /// This HTTP status with this JSON-RPC `error`.
805            Error(u16, &'static str),
806            /// This HTTP status and no body, as Core refuses a method missing from the RPC
807            /// user's `rpcwhitelist` with 403.
808            Refused(u16),
809        }
810
811        /// A Core JSON-RPC endpoint on a local port. A method it is not given an answer for gets
812        /// what Core answers a method it does not have: HTTP 404 with error -32601.
813        struct MockCore {
814            url: String,
815            asked: Arc<Mutex<Vec<String>>>,
816        }
817
818        impl MockCore {
819            /// Answers a method with its first entry in `answers`, and with the method's next entry,
820            /// if it has one, from the next request on.
821            fn start(answers: Vec<(&'static str, Answer)>) -> Self {
822                let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock core");
823                let url = format!(
824                    "http://{}",
825                    listener.local_addr().expect("mock core address")
826                );
827                let asked = Arc::new(Mutex::new(Vec::new()));
828                let recorder = Arc::clone(&asked);
829                let answers = Arc::new(Mutex::new(answers));
830                thread::spawn(move || {
831                    for stream in listener.incoming() {
832                        let Ok(stream) = stream else { return };
833                        let answers = Arc::clone(&answers);
834                        let recorder = Arc::clone(&recorder);
835                        thread::spawn(move || serve(stream, &answers, &recorder));
836                    }
837                });
838                MockCore { url, asked }
839            }
840
841            fn rpc(&self) -> DefaultCoreRPC {
842                DefaultCoreRPC::open(&self.url, "user".to_string(), "password".to_string())
843                    .expect("open Core RPC")
844            }
845
846            /// The methods asked so far, in order.
847            fn asked(&self) -> Vec<String> {
848                self.asked.lock().expect("asked lock").clone()
849            }
850        }
851
852        /// The answer to `method`, moving on to the method's next entry if it has one.
853        fn next_answer(
854            answers: &Mutex<Vec<(&'static str, Answer)>>,
855            method: &str,
856        ) -> Option<Answer> {
857            let mut answers = answers.lock().expect("answers lock");
858            let mut entries = answers
859                .iter()
860                .enumerate()
861                .filter(|(_, (known, _))| *known == method)
862                .map(|(index, _)| index);
863            match (entries.next(), entries.next()) {
864                (Some(first), Some(_)) => Some(answers.remove(first).1),
865                (Some(first), None) => Some(answers[first].1),
866                (None, _) => None,
867            }
868        }
869
870        /// Answers the requests of one connection until the client closes it.
871        fn serve(
872            stream: TcpStream,
873            answers: &Mutex<Vec<(&'static str, Answer)>>,
874            asked: &Mutex<Vec<String>>,
875        ) {
876            let mut writer = stream.try_clone().expect("clone mock core stream");
877            let mut reader = BufReader::new(stream);
878            loop {
879                let mut line = String::new();
880                if reader.read_line(&mut line).unwrap_or(0) == 0 {
881                    return;
882                }
883                let mut content_length = 0;
884                loop {
885                    line.clear();
886                    if reader.read_line(&mut line).unwrap_or(0) == 0 {
887                        return;
888                    }
889                    if line == "\r\n" {
890                        break;
891                    }
892                    if let Some(length) = line.to_ascii_lowercase().strip_prefix("content-length:")
893                    {
894                        content_length = length.trim().parse().expect("request content length");
895                    }
896                }
897                let mut body = vec![0; content_length];
898                reader.read_exact(&mut body).expect("read request body");
899                let request: Value = serde_json::from_slice(&body).expect("request is JSON");
900                let method = request["method"]
901                    .as_str()
902                    .expect("request method")
903                    .to_string();
904                asked.lock().expect("asked lock").push(method.clone());
905
906                let json =
907                    |text: &str| serde_json::from_str::<Value>(text).expect("answer is JSON");
908                let (status, response) = match next_answer(answers, &method) {
909                    Some(Answer::Result(result)) => (
910                        200,
911                        json!({ "result": json(result), "error": null, "id": request["id"] })
912                            .to_string(),
913                    ),
914                    Some(Answer::Error(status, error)) => (
915                        status,
916                        json!({ "result": null, "error": json(error), "id": request["id"] })
917                            .to_string(),
918                    ),
919                    Some(Answer::Refused(status)) => (status, String::new()),
920                    None => (
921                        404,
922                        json!({
923                            "result": null,
924                            "error": { "code": -32601, "message": "Method not found" },
925                            "id": request["id"],
926                        })
927                        .to_string(),
928                    ),
929                };
930                write!(
931                    writer,
932                    "HTTP/1.1 {status} X\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{response}",
933                    response.len()
934                )
935                .expect("write response");
936            }
937        }
938
939        fn active_mn_rr() -> SoftforkInfo {
940            SoftforkInfo {
941                softfork_type: SoftforkType::Buried,
942                active: true,
943                height: Some(432),
944                bip9: None,
945            }
946        }
947
948        #[test]
949        fn should_read_the_fork_from_getdeploymentinfo_on_core_v24() {
950            let core = MockCore::start(vec![
951                (
952                    "getblockchaininfo",
953                    Answer::Result(CORE_V24_GETBLOCKCHAININFO),
954                ),
955                (
956                    "getdeploymentinfo",
957                    Answer::Result(CORE_V24_GETDEPLOYMENTINFO),
958                ),
959            ]);
960
961            let fork = core.rpc().get_fork_info("mn_rr");
962
963            assert_eq!(fork.expect("Core v24 reports mn_rr"), Some(active_mn_rr()));
964            assert_eq!(core.asked(), ["getdeploymentinfo"]);
965        }
966
967        #[test]
968        fn should_read_the_fork_from_softforks_on_core_v23_which_has_no_getdeploymentinfo() {
969            let core = MockCore::start(vec![(
970                "getblockchaininfo",
971                Answer::Result(CORE_V23_GETBLOCKCHAININFO),
972            )]);
973
974            let fork = core.rpc().get_fork_info("mn_rr");
975
976            assert_eq!(fork.expect("Core v23 reports mn_rr"), Some(active_mn_rr()));
977            assert_eq!(core.asked(), ["getdeploymentinfo", "getblockchaininfo"]);
978        }
979
980        #[test]
981        fn should_not_read_softforks_when_getdeploymentinfo_fails_for_another_reason() {
982            let core = MockCore::start(vec![
983                (
984                    "getblockchaininfo",
985                    Answer::Result(CORE_V23_GETBLOCKCHAININFO),
986                ),
987                (
988                    "getdeploymentinfo",
989                    Answer::Error(500, r#"{"code": -1, "message": "unexpected failure"}"#),
990                ),
991            ]);
992
993            let fork = core.rpc().get_fork_info("mn_rr");
994
995            assert!(
996                matches!(
997                    fork,
998                    Err(Error::JsonRpc(JsonRpcError::Rpc(RpcError { code: -1, .. })))
999                ),
1000                "expected the getdeploymentinfo error, got {fork:?}"
1001            );
1002            assert_eq!(core.asked(), ["getdeploymentinfo"]);
1003        }
1004
1005        #[test]
1006        fn should_read_the_fork_from_softforks_when_core_v23_refuses_getdeploymentinfo() {
1007            let core = MockCore::start(vec![
1008                (
1009                    "getblockchaininfo",
1010                    Answer::Result(CORE_V23_GETBLOCKCHAININFO),
1011                ),
1012                ("getdeploymentinfo", Answer::Refused(403)),
1013            ]);
1014
1015            let fork = core.rpc().get_fork_info("mn_rr");
1016
1017            assert_eq!(fork.expect("Core v23 reports mn_rr"), Some(active_mn_rr()));
1018            // A refusal is Core's answer, so getdeploymentinfo is not asked again.
1019            assert_eq!(core.asked(), ["getdeploymentinfo", "getblockchaininfo"]);
1020        }
1021
1022        #[test]
1023        fn should_fail_with_the_refusal_when_core_v24_refuses_getdeploymentinfo() {
1024            let core = MockCore::start(vec![
1025                (
1026                    "getblockchaininfo",
1027                    Answer::Result(CORE_V24_GETBLOCKCHAININFO),
1028                ),
1029                ("getdeploymentinfo", Answer::Refused(403)),
1030            ]);
1031
1032            let fork = core.rpc().get_fork_info("mn_rr");
1033
1034            // Core v24 reports no softforks, so their absence must not read as a fork that is
1035            // not known yet.
1036            assert!(
1037                matches!(
1038                    &fork,
1039                    Err(Error::JsonRpc(JsonRpcError::Transport(transport)))
1040                        if matches!(
1041                            transport.downcast_ref::<HttpError>(),
1042                            Some(HttpError::HttpErrorCode(403))
1043                        )
1044                ),
1045                "expected the getdeploymentinfo refusal, got {fork:?}"
1046            );
1047            assert_eq!(core.asked(), ["getdeploymentinfo", "getblockchaininfo"]);
1048        }
1049
1050        #[test]
1051        fn should_ask_getdeploymentinfo_again_while_core_warms_up() {
1052            let core = MockCore::start(vec![
1053                (
1054                    "getdeploymentinfo",
1055                    Answer::Error(500, r#"{"code": -28, "message": "Loading block index..."}"#),
1056                ),
1057                (
1058                    "getdeploymentinfo",
1059                    Answer::Result(CORE_V24_GETDEPLOYMENTINFO),
1060                ),
1061            ]);
1062
1063            let fork = core.rpc().get_fork_info("mn_rr");
1064
1065            assert_eq!(fork.expect("Core v24 reports mn_rr"), Some(active_mn_rr()));
1066            assert_eq!(core.asked(), ["getdeploymentinfo", "getdeploymentinfo"]);
1067        }
1068
1069        #[test]
1070        fn should_report_no_fork_when_core_v24_does_not_know_it() {
1071            let core = MockCore::start(vec![(
1072                "getdeploymentinfo",
1073                Answer::Result(CORE_V24_GETDEPLOYMENTINFO),
1074            )]);
1075
1076            let fork = core.rpc().get_fork_info("unknown_fork");
1077
1078            assert_eq!(fork.expect("Core v24 answers"), None);
1079            assert_eq!(core.asked(), ["getdeploymentinfo"]);
1080        }
1081    }
1082}