Skip to content

Commit

Permalink
Merge pull request #44 from EthanYuan/fix-gettxproof-api
Browse files Browse the repository at this point in the history
Fix: SPV Service gettxproof API Unavailable During Reorg
  • Loading branch information
EthanYuan authored Jan 9, 2025
2 parents 23e3475 + d8101de commit e9aa85a
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 11 deletions.
26 changes: 18 additions & 8 deletions src/cli/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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()?;
Expand All @@ -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();
}
}
}
Expand Down
71 changes: 68 additions & 3 deletions src/components/api_service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions src/components/ckb_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SpvClientCell> {
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 {
Expand Down
29 changes: 29 additions & 0 deletions src/utilities/type_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}

0 comments on commit e9aa85a

Please sign in to comment.