From f6ce4fba3b72a30b31cf02ad69b798ea9e634ad5 Mon Sep 17 00:00:00 2001 From: Olga Kunyavskaya Date: Thu, 18 Jun 2026 21:06:10 +0100 Subject: [PATCH 1/2] trunck tips --- .gitignore | 3 +- contract/src/lib.rs | 146 +++++++++++++++++++++++++++++++++++++ relayer/src/main.rs | 12 ++- relayer/src/near_client.rs | 3 +- 4 files changed, 158 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index f0d1c005..0119d211 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ *.log tmp/ target/ -res \ No newline at end of file +res +.env \ No newline at end of file diff --git a/contract/src/lib.rs b/contract/src/lib.rs index 12d780eb..d17346de 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -414,9 +414,54 @@ impl BtcLightClient { .unwrap_or_else(|| env::panic_str(ERR_KEY_NOT_EXIST)); } } + + #[pause] + #[trusted_relayer] + pub fn truncate_tip(&mut self, num_blocks: u64) { + self.truncate_tip_inner(num_blocks); + } } impl BtcLightClient { + fn truncate_tip_inner(&mut self, num_blocks: u64) { + let initial_height = self + .headers_pool + .get(&self.mainchain_initial_blockhash) + .unwrap_or_else(|| env::panic_str(ERR_KEY_NOT_EXIST)) + .block_height; + + let tip_height = self + .headers_pool + .get(&self.mainchain_tip_blockhash) + .unwrap_or_else(|| env::panic_str(ERR_KEY_NOT_EXIST)) + .block_height; + + let to_remove = std::cmp::min(num_blocks, tip_height - initial_height); + if to_remove == 0 { + return; + } + + let new_tip_height = tip_height - to_remove; + + for height in (new_tip_height + 1)..=tip_height { + let blockhash = self + .mainchain_height_to_header + .get(&height) + .unwrap_or_else(|| env::panic_str(ERR_KEY_NOT_EXIST)); + self.remove_block_header(&blockhash); + self.mainchain_height_to_header.remove(&height); + } + + self.mainchain_tip_blockhash = self + .mainchain_height_to_header + .get(&new_tip_height) + .unwrap_or_else(|| env::panic_str(ERR_KEY_NOT_EXIST)); + + env::log_str(&format!( + "Truncated {to_remove} block(s) from tip; new tip height {new_tip_height}" + )); + } + fn init_genesis( &mut self, block_hash: &H256, @@ -1001,6 +1046,40 @@ mod tests { } } + // Builds a linear chain: genesis followed by `count` headers, each linking to the + // previous block's hash. `blocks[i]` sits at height `i`. + fn linear_chain_blocks(count: u32) -> Vec
{ + let genesis = genesis_block_header(); + let mut prev = genesis.block_hash().to_string(); + let mut blocks = vec![genesis]; + for i in 0u32..count { + let header: Header = serde_json::from_value(serde_json::json!({ + "version": 1, + "prev_block_hash": prev, + "merkle_root": "0000000000000000000000000000000000000000000000000000000000000000", + "time": 1_231_006_506u32 + i, + "bits": 0x207f_ffffu32, + "nonce": i, + })) + .unwrap(); + prev = header.block_hash().to_string(); + blocks.push(header); + } + blocks + } + + fn linear_init_args(count: u32) -> InitArgs { + let blocks = linear_chain_blocks(count); + InitArgs { + network: Network::Mainnet, + genesis_block_hash: blocks[0].block_hash(), + genesis_block_height: 0, + skip_pow_verification: true, + gc_threshold: 1000, + submit_blocks: blocks, + } + } + #[test] #[should_panic(expected = "block should have correct pow")] fn test_pow_validator_works_correctly_for_wrong_block() { @@ -1131,6 +1210,73 @@ mod tests { ); } + #[test] + fn test_truncate_tip() { + let blocks = linear_chain_blocks(11); + let mut contract = BtcLightClient::init(linear_init_args(11)); + + // Linear chain genesis(0)..block(11). + assert_eq!(contract.get_last_block_height(), 11); + assert_eq!( + contract.get_last_block_header().block_hash, + blocks[11].block_hash() + ); + + // Roll the tip back by 5 blocks: 11 -> 6. + contract.truncate_tip_inner(5); + + assert_eq!(contract.get_last_block_height(), 6); + assert_eq!( + contract.get_last_block_header().block_hash, + blocks[6].block_hash() + ); + + // Removed blocks are gone from both indexes and the pool. + assert!(contract.get_block_hash_by_height(7).is_none()); + assert!(contract + .get_height_by_block_hash(blocks[7].block_hash()) + .is_none()); + // Kept blocks remain reachable. + assert_eq!( + contract.get_block_hash_by_height(6).unwrap(), + blocks[6].block_hash() + ); + + // The heavier chain can now be submitted forward as a normal tip extension. + let next: Header = serde_json::from_value(serde_json::json!({ + "version": 1, + "prev_block_hash": blocks[6].block_hash().to_string(), + "merkle_root": "0000000000000000000000000000000000000000000000000000000000000000", + "time": 1_300_000_000u32, + "nonce": 42u32, + "bits": 0x207f_ffffu32, + })) + .unwrap(); + contract.submit_block_header(next.clone(), contract.skip_pow_verification); + + assert_eq!(contract.get_last_block_height(), 7); + assert_eq!( + contract.get_last_block_header().block_hash, + next.block_hash() + ); + } + + #[test] + fn test_truncate_tip_never_removes_initial_block() { + let blocks = linear_chain_blocks(11); + let mut contract = BtcLightClient::init(linear_init_args(11)); + + // Request to remove far more blocks than exist; must stop at the initial block. + contract.truncate_tip_inner(1000); + + assert_eq!(contract.get_last_block_height(), 0); + assert_eq!( + contract.get_last_block_header().block_hash, + blocks[0].block_hash() + ); + assert!(contract.get_block_hash_by_height(1).is_none()); + } + #[test] fn test_submitting_existing_fork_block_header_and_promote_fork() { let mut contract = BtcLightClient::init(get_default_init_args_with_skip_pow()); diff --git a/relayer/src/main.rs b/relayer/src/main.rs index 2afd97fa..747efe92 100644 --- a/relayer/src/main.rs +++ b/relayer/src/main.rs @@ -243,8 +243,10 @@ impl Synchronizer { 'main_loop ); + info!("first_block_height_to_submit={:?}, latest_height={:?}", first_block_height_to_submit, latest_height); + let start_height = - first_block_height_to_submit.load(std::sync::atomic::Ordering::Relaxed); + std::cmp::max(4997950, first_block_height_to_submit.load(std::sync::atomic::Ordering::Relaxed)); let end_height = latest_height.min(start_height.saturating_add(current_fetch_size)); let blocks_to_submit = self.fetch_blocks_to_submit(start_height, end_height).await; @@ -298,6 +300,9 @@ impl Synchronizer { ) -> Result> { let last_block_header = self.near_client.get_last_block_header().await?; let last_block_height = last_block_header.block_height; + + info!("{:?}", self.get_bitcoin_block_hash_by_height(last_block_height)); + if self.get_bitcoin_block_hash_by_height(last_block_height)? == last_block_header.block_hash.to_string() { @@ -310,9 +315,10 @@ impl Synchronizer { let last_block_hashes_count = last_block_hashes_in_relay_contract.len(); - let mut height: u64 = last_block_height - 1; + let mut height: u64 = last_block_height - 1840; - for i in 0..last_block_hashes_count { + for i in 1739..last_block_hashes_count { + info!("h={:?}, block hash: (from btc rpc={:?}, from near contract={:?})", height, self.get_bitcoin_block_hash_by_height(height), last_block_hashes_in_relay_contract[last_block_hashes_count - i - 1]); if last_block_hashes_in_relay_contract[last_block_hashes_count - i - 1] == self.get_bitcoin_block_hash_by_height(height)? { diff --git a/relayer/src/near_client.rs b/relayer/src/near_client.rs index e895b657..93e79339 100644 --- a/relayer/src/near_client.rs +++ b/relayer/src/near_client.rs @@ -400,7 +400,6 @@ impl NearClient { .await?; let block_hashes = from_slice::>(&result)?; - info!("{block_hashes:#?}"); Ok(block_hashes) } @@ -565,7 +564,7 @@ impl NearClient { actions: vec![Action::FunctionCall(Box::new(FunctionCallAction { method_name: method_name.to_string(), args, - gas: 300_000_000_000_000, // 300 TeraGas + gas: 1_000_000_000_000_000, // 1 PetaGas deposit, }))], }; From 788431a1b02cefa1fc65334182291b1a97b71cb1 Mon Sep 17 00:00:00 2001 From: Olga Kunyavskaya Date: Mon, 22 Jun 2026 12:27:28 +0100 Subject: [PATCH 2/2] truncate tip relayer command --- relayer/src/main.rs | 21 ++++++++++++++++---- relayer/src/near_client.rs | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/relayer/src/main.rs b/relayer/src/main.rs index 747efe92..70f4a274 100644 --- a/relayer/src/main.rs +++ b/relayer/src/main.rs @@ -245,8 +245,7 @@ impl Synchronizer { info!("first_block_height_to_submit={:?}, latest_height={:?}", first_block_height_to_submit, latest_height); - let start_height = - std::cmp::max(4997950, first_block_height_to_submit.load(std::sync::atomic::Ordering::Relaxed)); + let start_height = first_block_height_to_submit.load(std::sync::atomic::Ordering::Relaxed); let end_height = latest_height.min(start_height.saturating_add(current_fetch_size)); let blocks_to_submit = self.fetch_blocks_to_submit(start_height, end_height).await; @@ -315,9 +314,9 @@ impl Synchronizer { let last_block_hashes_count = last_block_hashes_in_relay_contract.len(); - let mut height: u64 = last_block_height - 1840; + let mut height: u64 = last_block_height - 1; - for i in 1739..last_block_hashes_count { + for i in 0..last_block_hashes_count { info!("h={:?}, block hash: (from btc rpc={:?}, from near contract={:?})", height, self.get_bitcoin_block_hash_by_height(height), last_block_hashes_in_relay_contract[last_block_hashes_count - i - 1]); if last_block_hashes_in_relay_contract[last_block_hashes_count - i - 1] == self.get_bitcoin_block_hash_by_height(height)? @@ -404,6 +403,10 @@ struct CliArgs { /// Initialize contract #[clap(long)] init_contract: bool, + /// Roll the main chain tip back by the given number of blocks, then exit. Used to resolve a + /// reorg too deep to process on-chain (e.g. `--truncate-tip 100`). + #[clap(long)] + truncate_tip: Option, } #[tokio::main] @@ -417,6 +420,16 @@ async fn main() -> Result<(), Box> { let bitcoin_client = Arc::new(BitcoinClient::new(&config)); let near_client = NearClient::new(&config.near); + if let Some(num_blocks) = args.truncate_tip { + info!("truncate_tip: rolling main chain tip back by {num_blocks} blocks"); + near_client + .truncate_tip(num_blocks) + .await + .expect("Failed to truncate tip"); + info!("truncate_tip finished"); + return Ok(()); + } + if args.init_contract { let init_config = config.init.clone().expect("Init Config not found"); init_contract(&bitcoin_client, &near_client, init_config).await; diff --git a/relayer/src/near_client.rs b/relayer/src/near_client.rs index 93e79339..58486fd5 100644 --- a/relayer/src/near_client.rs +++ b/relayer/src/near_client.rs @@ -27,6 +27,7 @@ use tokio::time; use crate::config::NearConfig; const SUBMIT_BLOCKS: &str = "submit_blocks"; +const TRUNCATE_TIP: &str = "truncate_tip"; const GET_LAST_BLOCK_HEADER: &str = "get_last_block_header"; #[allow(dead_code)] const VERIFY_TRANSACTION_INCLUSION: &str = "verify_transaction_inclusion"; @@ -178,6 +179,44 @@ impl NearClient { .and_then(|result| result) } + /// Calls the `truncate_tip` method on the contract, rolling the main chain tip back by up + /// to `num_blocks` of the newest blocks. Used to resolve a reorg too deep to process + /// on-chain: truncate down to the divergence point, then submit the heavier chain forward. + /// + /// # Errors + /// * Connection issue, transaction signing failure, or the transaction failing on-chain. + pub async fn truncate_tip( + &self, + num_blocks: u64, + ) -> Result> { + let tx_hash = self + .submit_tx( + self.sign_tx( + TRUNCATE_TIP, + json!({ "num_blocks": num_blocks }).to_string().into_bytes(), + 0, + None, + ) + .await?, + ) + .await?; + + self.get_tx_status(tx_hash) + .await + .map_err(std::convert::Into::into) + .map(|response| { + if let Some(final_execution_outcome) = response.final_execution_outcome.clone() { + if let near_primitives::views::FinalExecutionStatus::Failure(err) = + final_execution_outcome.into_outcome().status + { + Err(format!("Transaction failed with error: {err:?}"))?; + } + } + Ok(response) + }) + .and_then(|result| result) + } + /// Signs one or more transactions to call the `submit_blocks` method on the smart contract. /// /// This method splits the provided block headers into batches of `batch_size` and