use crate::utils::{get_median_time_past, BlocksGetter}; use crate::{BtcLightClient, BtcLightClientExt, Header, H256, U256}; use bitcoin::hashes::Hash; use btc_types::aux::AuxData; use btc_types::header::ExtendedHeader; use btc_types::network::{DogecoinConfig, Network, MAX_FUTURE_BLOCK_TIME_LOCAL}; use btc_types::utils::{target_from_bits, work_from_bits}; use near_sdk::{env, near, require}; //https://github.com/dogecoin/dogecoin/blob/2c513d0172e8bc86fe9a337693b26f2fdf68a013/src/auxpow.h#L24 const MERGED_MINING_HEADER: &str = "fabe6d6d"; #[near] impl BtcLightClient { pub fn get_config(&self) -> btc_types::network::DogecoinConfig { btc_types::network::get_dogecoin_config(self.network) } pub fn get_network(&self) -> (String, Network) { ("Dogecoin".to_owned(), self.network) } pub(crate) fn check_pow(&self, block_header: &Header, prev_block_header: &ExtendedHeader) { let expected_bits = get_next_work_required(&self.get_config(), block_header, prev_block_header, self); require!( expected_bits == block_header.bits, format!( "Error: Incorrect target. Expected bits: {:?}, Actual bits: {:?}", expected_bits, block_header.bits ) ); // Check timestamp against median time past of the previous 11 blocks require!( block_header.time > get_median_time_past(prev_block_header.clone(), self), "time-too-old: block's timestamp is too early" ); // Reject blocks whose timestamp is more than 2 hours ahead of local time let current_timestamp = u32::try_from(env::block_timestamp_ms() / 1000).unwrap(); require!( block_header.time <= current_timestamp + MAX_FUTURE_BLOCK_TIME_LOCAL, "time-too-new: block timestamp too far in the future" ); } pub(crate) fn check_aux(&mut self, block_header: &Header, aux_data: &AuxData) { // The Dogecoin block must have the AuxPoW flag set (bit 8) when AuxPoW data is present. // https://github.com/dogecoin/dogecoin/blob/master/src/auxpow.h const BLOCK_VERSION_AUXPOW: i32 = 0x100; require!( aux_data.chain_merkle_proof.len() <= 30, "Aux POW chain merkle branch too long" ); require!( block_header.version & BLOCK_VERSION_AUXPOW != 0, "Aux POW block does not have AuxPoW flag set in version" ); let chain_id = self.get_config().aux_chain_id; require!( chain_id == block_header.get_chain_id(), format!( "block does not have our chain ID (got {}, expected {chain_id})", block_header.get_chain_id() ) ); require!( chain_id != aux_data.parent_block.get_chain_id(), "Aux POW parent has our chain ID" ); let chain_root = merkle_tools::compute_root_from_merkle_proof( block_header.block_hash(), aux_data.chain_id, &aux_data.chain_merkle_proof, ); let coinbase_tx = aux_data.get_coinbase_tx(); let coinbase_tx_hash = coinbase_tx.compute_txid(); require!( merkle_tools::compute_root_from_merkle_proof( H256::from(coinbase_tx_hash.to_raw_hash().to_byte_array()), 0, &aux_data.merkle_proof, ) == aux_data.parent_block.merkle_root ); let script_sig = coinbase_tx .input .first() .unwrap() .script_sig .to_hex_string(); let pos_merged_mining_header = script_sig.find(MERGED_MINING_HEADER); let mut pos_chain_root = script_sig .find(&chain_root.to_string()) .expect("Aux POW missing chain merkle root in parent coinbase"); match pos_merged_mining_header { Some(pos_merged_mining_header) => { if script_sig[pos_merged_mining_header + MERGED_MINING_HEADER.len()..] .contains(MERGED_MINING_HEADER) { env::panic_str("Multiple merged mining headers in coinbase"); } require!( pos_merged_mining_header + MERGED_MINING_HEADER.len() == pos_chain_root, "Merged mining header is not just before chain merkle root" ); } None => { require!(pos_chain_root <= 40, "Aux POW chain merkle root must start in the first 20 bytes of the parent coinbase"); } } pos_chain_root += chain_root.to_string().len(); require!( script_sig.len() - pos_chain_root >= 16, "Aux POW missing chain merkle tree size and nonce in parent coinbase" ); let bytes = hex::decode(&script_sig[pos_chain_root..pos_chain_root + 8]).unwrap(); let n_size = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); require!( n_size == (1u32 << aux_data.chain_merkle_proof.len()), "Aux POW merkle branch size does not match parent coinbase" ); let bytes = hex::decode(&script_sig[pos_chain_root + 8..pos_chain_root + 16]).unwrap(); let n_nonce = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); let chain_id = block_header.get_chain_id(); let expected_index = Self::get_expected_index(n_nonce, chain_id, aux_data.chain_merkle_proof.len()); require!( u32::try_from(aux_data.chain_id).ok() == Some(expected_index), "Aux POW wrong index" ); let pow_hash = aux_data.parent_block.block_hash_pow(); require!( self.skip_pow_verification || U256::from_le_bytes(&pow_hash.0) <= target_from_bits(block_header.bits), format!("block should have correct pow") ); } fn get_expected_index(nonce: u32, chain_id: i32, merkle_height: usize) -> u32 { let mut rand = nonce; rand = rand.wrapping_mul(1_103_515_245).wrapping_add(12345); rand = rand.wrapping_add(u32::try_from(chain_id).unwrap()); rand = rand.wrapping_mul(1_103_515_245).wrapping_add(12345); rand.wrapping_rem(1u32 << merkle_height) } pub(crate) fn submit_block_header( &mut self, header: (Header, Option), skip_pow_verification: bool, ) { let (block_header, aux_data) = header; let prev_block_header = self.get_prev_header(&block_header); let current_block_hash = block_header.block_hash(); if !skip_pow_verification { self.check_target(&block_header, &prev_block_header); if let Some(ref aux_data) = aux_data { self.check_aux(&block_header, aux_data); } else { let pow_hash = block_header.block_hash_pow(); // Check if the block hash is less than or equal to the target require!( U256::from_le_bytes(&pow_hash.0) <= target_from_bits(block_header.bits), format!("block should have correct pow") ); } } let (current_block_computed_chain_work, overflow) = prev_block_header .chain_work .overflowing_add(work_from_bits(block_header.bits)); require!(!overflow, "Addition of U256 values overflowed"); let current_header = ExtendedHeader { block_header: block_header.clone().into_light(), block_hash: current_block_hash, chain_work: current_block_computed_chain_work, block_height: 1 + prev_block_header.block_height, }; self.submit_block_header_inner(current_header, &prev_block_header); } } // source https://github.com/dogecoin/dogecoin/blob/2c513d0172e8bc86fe9a337693b26f2fdf68a013/src/pow.cpp#L17 fn allow_min_difficulty_for_block( config: &DogecoinConfig, block_header: &Header, prev_block_header: &ExtendedHeader, ) -> bool { // check if the chain allows minimum difficulty blocks if !config.pow_allow_min_difficulty_blocks { return false; } // Dogecoin: Magic number at which reset protocol switches // check if we allow minimum difficulty at this block-height if prev_block_header.block_height < 157_500 { return false; } // Allow for a minimum block time if the elapsed time > 2*nTargetSpacing block_header.time > prev_block_header.block_header.time + config.pow_target_spacing * 2 } // source https://github.com/dogecoin/dogecoin/blob/2c513d0172e8bc86fe9a337693b26f2fdf68a013/src/pow.cpp#L17 fn get_next_work_required( config: &DogecoinConfig, block_header: &Header, prev_block_header: &ExtendedHeader, blocks_getter: &impl BlocksGetter, ) -> u32 { // Dogecoin: Special rules for minimum difficulty blocks with Digishield if allow_min_difficulty_for_block(config, block_header, prev_block_header) { // Special difficulty rule for testnet: // If the new block's timestamp is more than 2* nTargetSpacing minutes // then allow mining of a min-difficulty block. return config.proof_of_work_limit_bits; } // Only change once per difficulty adjustment interval let new_difficulty_protocol = prev_block_header.block_height >= 145_000; let difficulty_adjustment_interval = if new_difficulty_protocol { 1 } else { config.difficulty_adjustment_interval }; if (prev_block_header.block_height + 1) % difficulty_adjustment_interval != 0 { if config.pow_allow_min_difficulty_blocks { // Special difficulty rule for testnet: // If the new block's timestamp is more than 2* 10 minutes // then allow mining of a min-difficulty block. if block_header.time > prev_block_header.block_header.time + config.pow_target_spacing * 2 { return config.proof_of_work_limit_bits; } // Return the last non-special-min-difficulty-rules-block let mut current_block_header = prev_block_header.clone(); while current_block_header.block_header.bits == config.proof_of_work_limit_bits && current_block_header.block_height % config.difficulty_adjustment_interval != 0 { current_block_header = blocks_getter.get_prev_header(¤t_block_header.block_header); } return current_block_header.block_header.bits; } return prev_block_header.block_header.bits; } // Litecoin: This fixes an issue where a 51% attack can change difficulty at will. // Go back the full period unless it's the first retarget after genesis. Code courtesy of Art Forz let mut blocks_to_go_back = difficulty_adjustment_interval - 1; if prev_block_header.block_height + 1 != difficulty_adjustment_interval { blocks_to_go_back = difficulty_adjustment_interval; } // Go back by what we want to be 14 days worth of blocks let height_first = prev_block_header .block_height .checked_sub(blocks_to_go_back) .unwrap_or_else(|| env::panic_str("Height underflow when calculating first block height")); // TODO: check if it is correct to get block header by height from mainchain without looping to find the ancestor let first_block_time = blocks_getter .get_header_by_height(height_first) .block_header .time; calculate_next_work_required(config, prev_block_header, i64::from(first_block_time)) } // source https://github.com/dogecoin/dogecoin/blob/2c513d0172e8bc86fe9a337693b26f2fdf68a013/src/dogecoin.cpp#L41 fn calculate_next_work_required( config: &DogecoinConfig, prev_block_header: &ExtendedHeader, first_block_time: i64, ) -> u32 { let retarget_timespan = config.pow_target_timespan; let modulated_timespan = i64::from(prev_block_header.block_header.time) - first_block_time; let mut modulated_timespan = retarget_timespan + (modulated_timespan - retarget_timespan) / 8; let min_timespan = retarget_timespan - (retarget_timespan / 4); let max_timespan = retarget_timespan + (retarget_timespan / 2); if modulated_timespan < min_timespan { modulated_timespan = min_timespan; } else if modulated_timespan > max_timespan { modulated_timespan = max_timespan; } let new_target = target_from_bits(prev_block_header.block_header.bits); let (mut new_target, new_target_overflow) = new_target.overflowing_mul(>::try_into(modulated_timespan).unwrap()); require!(!new_target_overflow, "new target overflow"); new_target = new_target / U256::from(>::try_into(retarget_timespan).unwrap()); if new_target > config.pow_limit { new_target = config.pow_limit; } new_target.target_to_bits() }