Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
*.log
tmp/
target/
res
res
.env
146 changes: 146 additions & 0 deletions contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Header> {
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() {
Expand Down Expand Up @@ -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());
Expand Down
23 changes: 21 additions & 2 deletions relayer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,9 @@ impl Synchronizer {
'main_loop
);

let start_height =
first_block_height_to_submit.load(std::sync::atomic::Ordering::Relaxed);
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);
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;
Expand Down Expand Up @@ -298,6 +299,9 @@ impl Synchronizer {
) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
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()
{
Expand All @@ -313,6 +317,7 @@ impl Synchronizer {
let mut height: u64 = last_block_height - 1;

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)?
{
Expand Down Expand Up @@ -398,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<u64>,
}

#[tokio::main]
Expand All @@ -411,6 +420,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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;
Expand Down
42 changes: 40 additions & 2 deletions relayer/src/near_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<RpcTransactionResponse, Box<dyn std::error::Error + Send + Sync>> {
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
Expand Down Expand Up @@ -400,7 +439,6 @@ impl NearClient {
.await?;

let block_hashes = from_slice::<Vec<String>>(&result)?;
info!("{block_hashes:#?}");
Ok(block_hashes)
}

Expand Down Expand Up @@ -565,7 +603,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,
}))],
};
Expand Down
Loading