fedimint_server_bitcoin_rpc/
hybrid.rs1#[cfg(test)]
2mod tests;
3
4use std::sync::OnceLock;
5use std::sync::atomic::{AtomicBool, Ordering};
6
7use anyhow::{Result, anyhow};
8use bitcoin::{BlockHash, Transaction, Txid};
9use fedimint_core::envs::BitcoinRpcConfig;
10use fedimint_core::util::{FmtCompact as _, SafeUrl};
11use fedimint_core::{ChainId, Feerate};
12use fedimint_logging::LOG_SERVER;
13use fedimint_server_core::bitcoin_rpc::{DynServerBitcoinRpc, IServerBitcoinRpc};
14use tracing::{info, warn};
15
16use crate::bitcoind::BitcoindClient;
17use crate::esplora::EsploraClient;
18
19#[derive(Debug)]
28pub struct BitcoindClientWithFallback {
29 bitcoind_client: DynServerBitcoinRpc,
31 esplora_client: DynServerBitcoinRpc,
33 chain_id: OnceLock<ChainId>,
35 bitcoind_ibd_complete: AtomicBool,
37}
38
39impl BitcoindClientWithFallback {
40 pub async fn new(
43 username: String,
44 password: String,
45 bitcoind_url: &SafeUrl,
46 esplora_url: &SafeUrl,
47 ) -> Result<Self> {
48 info!(
49 target: LOG_SERVER,
50 %bitcoind_url,
51 %esplora_url,
52 "Initializing bitcoin bitcoind backend with trusted esplora fallback"
53 );
54 Ok(Self::from_clients(
55 BitcoindClient::new(username, password, bitcoind_url)?.into_dyn(),
56 EsploraClient::new(esplora_url)?.into_dyn(),
57 ))
58 }
59
60 fn from_clients(
62 bitcoind_client: DynServerBitcoinRpc,
63 esplora_client: DynServerBitcoinRpc,
64 ) -> Self {
65 Self {
66 bitcoind_client,
67 esplora_client,
68 chain_id: OnceLock::new(),
69 bitcoind_ibd_complete: AtomicBool::new(false),
70 }
71 }
72
73 async fn fallback_block_count(&self, primary: anyhow::Error) -> Result<u64> {
74 warn!(
75 target: LOG_SERVER,
76 error = %primary.fmt_compact(),
77 "Bitcoind block count unavailable; falling back to Esplora"
78 );
79 match self.esplora_client.get_block_count().await {
80 Ok(count) => Ok(count),
81 Err(_) => {
82 warn!(
83 target: LOG_SERVER,
84 "Esplora block-count fallback also failed; returning the bitcoind error"
85 );
86 Err(primary)
87 }
88 }
89 }
90}
91
92macro_rules! read_rpc {
94 ($self:ident, $method:ident $(, $arg:expr)*) => {{
95 let primary = $self.bitcoind_client.$method($($arg),*).await;
96 match primary {
97 Ok(value) => Ok(value),
98 Err(primary) => {
99 warn!(
100 target: LOG_SERVER,
101 method = stringify!($method),
102 error = %primary.fmt_compact(),
103 "Bitcoind read failed; trying Esplora"
104 );
105 match $self.esplora_client.$method($($arg),*).await {
106 Ok(value) => Ok(value),
107 Err(_) => {
108 warn!(
109 target: LOG_SERVER,
110 method = stringify!($method),
111 "Esplora read fallback also failed; returning the bitcoind error"
112 );
113 Err(primary)
114 }
115 }
116 }
117 }
118 }};
119}
120
121#[async_trait::async_trait]
122impl IServerBitcoinRpc for BitcoindClientWithFallback {
123 fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
124 self.bitcoind_client.get_bitcoin_rpc_config()
125 }
126
127 fn get_url(&self) -> SafeUrl {
128 self.bitcoind_client.get_url()
129 }
130
131 async fn get_block_count(&self) -> Result<u64> {
132 if self.bitcoind_ibd_complete.load(Ordering::Relaxed) {
133 return match self.bitcoind_client.get_block_count().await {
134 Ok(count) => Ok(count),
135 Err(primary) => self.fallback_block_count(primary).await,
136 };
137 }
138
139 match self
140 .bitcoind_client
141 .get_block_count_and_initial_block_download()
142 .await
143 {
144 Ok((_count, true)) => {
145 self.fallback_block_count(anyhow!("Bitcoind is in initial block download"))
146 .await
147 }
148 Ok((count, false)) => {
149 self.bitcoind_ibd_complete.store(true, Ordering::Relaxed);
150 Ok(count)
151 }
152 Err(primary) => self.fallback_block_count(primary).await,
153 }
154 }
155
156 async fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
157 read_rpc!(self, get_block_hash, height)
158 }
159
160 async fn get_block(&self, block_hash: &BlockHash) -> Result<bitcoin::Block> {
161 read_rpc!(self, get_block, block_hash)
162 }
163
164 async fn get_feerate(&self) -> Result<Option<Feerate>> {
165 match self.bitcoind_client.get_feerate().await {
166 Ok(Some(feerate)) => Ok(Some(feerate)),
167 Ok(None) => {
168 warn!(
169 target: LOG_SERVER,
170 "Bitcoind fee estimate unavailable; trying Esplora"
171 );
172 match self.esplora_client.get_feerate().await {
173 Ok(feerate) => Ok(feerate),
174 Err(_) => {
175 warn!(
176 target: LOG_SERVER,
177 "Esplora fee-estimate fallback failed; retaining the unavailable bitcoind estimate"
178 );
179 Ok(None)
180 }
181 }
182 }
183 Err(primary) => {
184 warn!(
185 target: LOG_SERVER,
186 error = %primary.fmt_compact(),
187 "Bitcoind fee-estimate request failed; trying Esplora"
188 );
189 match self.esplora_client.get_feerate().await {
190 Ok(feerate) => Ok(feerate),
191 Err(_) => {
192 warn!(
193 target: LOG_SERVER,
194 "Esplora fee-estimate fallback also failed; returning the bitcoind error"
195 );
196 Err(primary)
197 }
198 }
199 }
200 }
201 }
202
203 async fn submit_transaction(&self, transaction: Transaction) -> Result<()> {
204 match self
205 .bitcoind_client
206 .submit_transaction(transaction.clone())
207 .await
208 {
209 Ok(()) => Ok(()),
210 Err(primary) => {
211 warn!(target: LOG_SERVER, error = %primary.fmt_compact(), "Bitcoind broadcast failed; trying Esplora");
212 match self.esplora_client.submit_transaction(transaction).await {
213 Ok(()) => Ok(()),
214 Err(_) => {
215 warn!(
216 target: LOG_SERVER,
217 "Esplora broadcast fallback also failed; returning the bitcoind error"
218 );
219 Err(primary)
220 }
221 }
222 }
223 }
224 }
225
226 async fn get_sync_progress(&self) -> Result<Option<f64>> {
227 Ok(None)
228 }
229
230 async fn get_mempool_txids(&self) -> Result<Option<Vec<Txid>>> {
235 self.bitcoind_client.get_mempool_txids().await
236 }
237
238 async fn get_mempool_tx(&self, txid: &Txid) -> Result<Option<Transaction>> {
239 self.bitcoind_client.get_mempool_tx(txid).await
240 }
241
242 async fn get_chain_id(&self) -> Result<ChainId> {
243 if let Some(chain_id) = self.chain_id.get() {
244 return Ok(*chain_id);
245 }
246
247 let chain_id = match self.bitcoind_client.get_chain_id().await {
248 Ok(chain_id) => chain_id,
249 Err(primary) => {
250 warn!(
251 target: LOG_SERVER,
252 error = %primary.fmt_compact(),
253 "Bitcoind chain identity unavailable; trying Esplora"
254 );
255 self.esplora_client.get_chain_id().await?
256 }
257 };
258 let _ = self.chain_id.set(chain_id);
259 Ok(*self
260 .chain_id
261 .get()
262 .expect("chain identity was just initialized"))
263 }
264}