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
21pub type QuorumListExtendedInfo = HashMap<QuorumHash, ExtendedQuorumDetails>;
23
24const COINBASE_TRANSACTION_TYPE: u16 = 5;
26
27pub(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
61pub(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
84pub type CoreHeight = u32;
86#[cfg_attr(any(feature = "mocks", test), mockall::automock)]
88pub trait CoreRPCLike {
89 fn get_block_hash(&self, height: CoreHeight) -> Result<BlockHash, Error>;
91
92 fn get_block_header(&self, block_hash: &BlockHash) -> Result<Header, Error>;
94
95 fn get_block_time_from_height(&self, height: CoreHeight) -> Result<TimestampMillis, Error>;
97
98 fn get_best_chain_lock(&self) -> Result<ChainLock, Error>;
100
101 fn submit_chain_lock(&self, chain_lock: &ChainLock) -> Result<u32, Error>;
103
104 fn get_transaction(&self, tx_id: &Txid) -> Result<Transaction, Error>;
106
107 fn get_asset_unlock_statuses(
109 &self,
110 indices: &[u64],
111 core_chain_locked_height: u32,
112 ) -> Result<Vec<AssetUnlockStatusResult>, Error>;
113
114 fn get_transaction_extended_info(&self, tx_id: &Txid)
116 -> Result<GetRawTransactionResult, Error>;
117
118 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 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 fn get_fork_info(&self, name: &str) -> Result<Option<SoftforkInfo>, Error>;
140
141 fn get_block(&self, block_hash: &BlockHash) -> Result<Block, Error>;
143
144 fn get_block_json(&self, block_hash: &BlockHash) -> Result<Value, Error>;
146
147 fn get_chain_tips(&self) -> Result<GetChainTipsResult, Error>;
149
150 fn get_quorum_listextended(
154 &self,
155 height: Option<CoreHeight>,
156 ) -> Result<ExtendedQuorumListResult, Error>;
157
158 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 fn get_protx_diff_with_masternodes(
170 &self,
171 base_block: Option<u32>,
172 block: u32,
173 ) -> Result<MasternodeListDiff, Error>;
174
175 fn verify_instant_lock(
182 &self,
183 instant_lock: &InstantLock,
184 max_height: Option<u32>,
185 ) -> Result<bool, Error>;
186
187 fn verify_chain_lock(&self, chain_lock: &ChainLock) -> Result<bool, Error>;
189
190 fn masternode_sync_status(&self) -> Result<MnSyncStatus, Error>;
192
193 fn send_raw_transaction(&self, transaction: &[u8]) -> Result<Txid, Error>;
195
196 fn get_credit_pool_balance(&self, height: CoreHeight) -> Result<u64, Error>;
200
201 fn get_coinbase_payload_version(&self, height: CoreHeight) -> Result<Option<u16>, Error>;
205}
206
207#[derive(Debug)]
208pub struct DefaultCoreRPC {
210 inner: Client,
211 prefetcher: Option<CorePrefetcher>,
214}
215
216pub const CORE_RPC_TX_CONSENSUS_ERROR: i32 = -26;
220pub const CORE_RPC_TX_ALREADY_IN_CHAIN: i32 = -27;
222pub const CORE_RPC_ERROR_IN_WARMUP: i32 = -28;
224pub const CORE_RPC_CLIENT_NOT_CONNECTED: i32 = -9;
226pub const CORE_RPC_CLIENT_IN_INITIAL_DOWNLOAD: i32 = -10;
228pub const CORE_RPC_PARSE_ERROR: i32 = -32700;
230pub const CORE_RPC_INVALID_ADDRESS_OR_KEY: i32 = -5;
232pub const CORE_RPC_INVALID_PARAMETER: i32 = -8;
234pub const CORE_RPC_METHOD_NOT_FOUND: i32 = -32601;
236
237const HTTP_FORBIDDEN: u16 = 403;
240
241fn 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 const MAX_RETRIES: u32 = 4;
256 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 _ if is_http_forbidden(&e) => Some(Err(e)),
277 dpp::dashcore_rpc::Error::JsonRpc(
278 dpp::dashcore_rpc::jsonrpc::error::Error::Transport(_)
280 | dpp::dashcore_rpc::jsonrpc::error::Error::Rpc(
281 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 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 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 fn coinbase_with_payload(&self, height: CoreHeight) -> Result<Option<Vec<u8>>, Error> {
324 let block_hash = self.get_block_hash(height)?;
325 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 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 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 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 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 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 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 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 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 fn raised_coinbase(version: u16, after_balance: &[u8]) -> Vec<u8> {
604 let version_3 = coinbase(Some(version_3_payload()));
605 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 #[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 #[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 #[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 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 #[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 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 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 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 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 #[derive(Clone, Copy)]
801 enum Answer {
802 Result(&'static str),
804 Error(u16, &'static str),
806 Refused(u16),
809 }
810
811 struct MockCore {
814 url: String,
815 asked: Arc<Mutex<Vec<String>>>,
816 }
817
818 impl MockCore {
819 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 fn asked(&self) -> Vec<String> {
848 self.asked.lock().expect("asked lock").clone()
849 }
850 }
851
852 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 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 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 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}