diff --git a/src/cli/serve.rs b/src/cli/serve.rs index c8091d4..def2798 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -191,9 +191,13 @@ impl Args { )?; let tx_hash = - self.update_spv_cells(&spv_service, input, spv_client, spv_update)?; + self.update_spv_cells(&spv_service, input, spv_client, spv_update); - prev_tx_hash = Some(tx_hash); + if let Err(e) = &tx_hash { + log::warn!("Failed to update SPV instance: {:?}", e); + } + + prev_tx_hash = tx_hash.ok(); } SpvOperation::Reorg(input) => { log::info!("Try to reorg SPV instance"); @@ -208,10 +212,13 @@ impl Args { let (spv_client, spv_update) = storage.generate_spv_client_and_spv_update(spv_tip_height, limit, flags)?; - let tx_hash = - self.reorg_spv_cells(&spv_service, input, spv_client, spv_update)?; + let tx_hash = self.reorg_spv_cells(&spv_service, input, spv_client, spv_update); + + if let Err(e) = &tx_hash { + log::warn!("Failed to reorg SPV instance: {:?}", e); + } - prev_tx_hash = Some(tx_hash); + prev_tx_hash = tx_hash.ok(); } SpvOperation::Reset(input) => { let flags = input.info.get_flags()?; @@ -236,10 +243,13 @@ impl Args { flags, )?; - let tx_hash = - self.reorg_spv_cells(&spv_service, input, spv_client, spv_update)?; + let tx_hash = self.reorg_spv_cells(&spv_service, input, spv_client, spv_update); + + if let Err(e) = &tx_hash { + log::warn!("Failed to reset SPV instance: {:?}", e); + } - prev_tx_hash = Some(tx_hash); + prev_tx_hash = tx_hash.ok(); } } } diff --git a/src/components/api_service/mod.rs b/src/components/api_service/mod.rs index 77680ea..e74b0b8 100644 --- a/src/components/api_service/mod.rs +++ b/src/components/api_service/mod.rs @@ -292,7 +292,11 @@ impl SpvRpc for SpvRpcImpl { spv_instance }; - let spv_client_cell = spv_instance + // First Strategy: find the best SPV client not greater than the storage tip height. + // The spv client found has the longest lifetime and + // is most likely to cover the height of the block where the bitcoin tx is located. + // The downside is that it can be affected by reorg. + let mut spv_client_cell = spv_instance .find_best_spv_client_not_greater_than_height(stg_tip_height) .map_err(|err| { let message = format!( @@ -339,8 +343,69 @@ impl SpvRpc for SpvRpcImpl { log::warn!("[onchain] header#{spv_best_height}; mmr-root {spv_header_root}"); let stg_header_root = packed_stg_header_root.unpack(); log::warn!("[storage] header#{spv_best_height}; mmr-root {stg_header_root}"); - let desc = "the SPV instance on chain is unknown, reorg is required"; - return Err(ApiErrorCode::OnchainReorgRequired.with_desc(desc)); + let desc = "Strategy 1 failed to find a valid SPV client due to reorg, switching to strategy 2 for further lookup"; + log::warn!("{desc}"); + + // Second Strategy: Find the Nth (20% of total) spv cell before the tip spv cell. + // The cell is far enough away from the tip to be less affected by the reorg, + // and has a relatively long survival period. + // But it may not be able to cover the height of the block where the newer bitcoin tx is located + let count = spv_instance.clients.len() / 5; + spv_client_cell = spv_instance + .find_spv_client_before_tip(count) + .map_err(|err| { + let message = + format!("failed to get the {count}th SPV client before the tip client"); + log::error!("{message} since {err}"); + RpcError { + code: RpcErrorCode::InternalError, + message, + data: None, + } + })?; + + log::debug!( + ">>> the best SPV client is {} found in the {} blocks before tip", + spv_client_cell.client, + count + ); + + let spv_header_root = &spv_client_cell.client.headers_mmr_root; + + let spv_best_height = spv_header_root.max_height; + if spv_best_height < target_height + confirmations { + let desc = format!( + "target transaction is in header#{target_height} \ + and it requires {confirmations} confirmations, \ + but the best SPV header is header#{spv_best_height}", + ); + return Err(ApiErrorCode::OnchainTxUnconfirmed.with_desc(desc)); + } + + let packed_stg_header_root = spv + .storage + .generate_headers_root(spv_best_height) + .map_err(|err| { + let message = + format!("failed to generate headers MMR root for height {spv_best_height}"); + log::error!("{message} since {err}"); + RpcError { + code: RpcErrorCode::InternalError, + message, + data: None, + } + })?; + + let packed_spv_header_root = spv_header_root.pack(); + + if packed_stg_header_root.as_slice() != packed_spv_header_root.as_slice() { + log::warn!("[onchain] header#{spv_best_height}; mmr-root {spv_header_root}"); + let stg_header_root = packed_stg_header_root.unpack(); + log::warn!("[storage] header#{spv_best_height}; mmr-root {stg_header_root}"); + let desc = "the SPV instance on chain is unknown, reorg is required"; + log::warn!("{desc}"); + return Err(ApiErrorCode::OnchainReorgRequired.with_desc(desc)); + } } let header_proof = spv diff --git a/src/components/ckb_client.rs b/src/components/ckb_client.rs index d5d2f62..6874f10 100644 --- a/src/components/ckb_client.rs +++ b/src/components/ckb_client.rs @@ -214,6 +214,22 @@ impl SpvInstance { let msg = format!("all SPV clients have better heights than server has (height: {height})"); Err(Error::other(msg)) } + + pub(crate) fn find_spv_client_before_tip(&self, count: usize) -> Result { + let SpvInstance { ref info, clients } = self; + let mut info = info.to_owned(); + for _ in 0..count { + info.info.tip_client_id = info.prev_tip_client_id() + } + let cell = clients.get(&info.info.tip_client_id).ok_or_else(|| { + let msg = format!( + "the SPV client (id={}) is not found", + info.info.tip_client_id + ); + Error::other(msg) + })?; + Ok(cell.clone()) + } } impl fmt::Display for SpvInstance { diff --git a/src/utilities/type_id.rs b/src/utilities/type_id.rs index 6c014f9..b73acf8 100644 --- a/src/utilities/type_id.rs +++ b/src/utilities/type_id.rs @@ -9,3 +9,32 @@ pub fn calculate_type_id(input: packed::CellInput, outputs_count: usize) -> [u8; blake2b.finalize(&mut ret); ret } + +mod tests { + use super::*; + use ckb_types::packed::CellInput; + use ckb_types::{h256, H256}; + + #[test] + fn test_calculate_type_id() { + let previous_output = packed::OutPoint::new_builder() + .tx_hash( + h256!("0x806600be4ae8330e8ce3893f208d169684e0b998acf9549c07b9fd5357eb157e").pack(), + ) + .index(1u32.pack()) + .build(); + + let input = CellInput::new_builder() + .previous_output(previous_output) + .since(0u64.pack()) + .build(); + let outputs_count = 33; + let type_id = calculate_type_id(input.clone(), outputs_count); + + // Expected value calculated manually or from a trusted source + let expected_type_id: H256 = + h256!("0xac4cd5342895c4861631310f2fd731b8f8c71682577e384b78ca3ee3361ec3a1"); + + assert_eq!(type_id, expected_type_id.as_bytes()); + } +}