Skip to content

Commit

Permalink
fix: stop MTP attack (#3357)
Browse files Browse the repository at this point in the history
Description
---
This changes the timestamp of a new block template to be always greater than the median time past (MTP) of the past blocks as per consensus. 

Motivation and Context
---
It is possible for an attack to forward the median time to be greater than now, but less than the future time limit(FTL). This means that all valid blocks with the current timestamp will be rejected. This will ensure that ll newly created blocks always have a timestamp greater than the MTP. Due to consensus we also know that MTP will always be less than FTL. This ensures that all new block timestamps are `MTP < Timestamp < FTL`

For a description see: [Graft network](graft-project/GraftNetwork#118 (comment))
and [MTP Attack (Jagernan)](zawy12/difficulty-algorithms#30)


How Has This Been Tested?
---

All current unit tests pass.
  • Loading branch information
SWvheerden committed Sep 16, 2021
1 parent 7fa0572 commit a82638a
Show file tree
Hide file tree
Showing 15 changed files with 86 additions and 56 deletions.
12 changes: 9 additions & 3 deletions applications/tari_mining_node/src/difficulty.rs
Expand Up @@ -75,8 +75,14 @@ impl BlockHeaderSha3 {
.chain(self.header.total_script_offset.as_bytes())
}

pub fn set_timestamp(&mut self, timestamp: u64) {
self.timestamp = timestamp;
/// This function will update the timestamp of the header, but only if the new timestamp is greater than the current
/// one.
pub fn set_forward_timestamp(&mut self, timestamp: u64) {
// if the timestamp has been advanced by the base_node due to the median time we should not reverse it but we
// should only change the timestamp if we move it forward.
if timestamp > self.timestamp {
self.timestamp = timestamp;
}
}

pub fn random_nonce(&mut self) {
Expand Down Expand Up @@ -171,7 +177,7 @@ pub mod test {
);
timestamp = timestamp.increase(1);
core_header.timestamp = timestamp;
hasher.set_timestamp(timestamp.as_u64());
hasher.set_forward_timestamp(timestamp.as_u64());
}
}
}
2 changes: 1 addition & 1 deletion applications/tari_mining_node/src/miner.rs
Expand Up @@ -209,7 +209,7 @@ pub fn mining_task(
info!("Mining thread {} disconnected", miner);
return;
}
hasher.set_timestamp(timestamp().seconds as u64);
hasher.set_forward_timestamp(timestamp().seconds as u64);
}
hasher.inc_nonce();
}
Expand Down
Expand Up @@ -392,7 +392,7 @@ where T: BlockchainBackend + 'static
Ok(NodeCommsResponse::NewBlockTemplate(block_template))
},
NodeCommsRequest::GetNewBlock(block_template) => {
let block = self.blockchain_db.prepare_block_merkle_roots(block_template).await?;
let block = self.blockchain_db.prepare_new_block(block_template).await?;
Ok(NodeCommsResponse::NewBlock {
success: true,
error: None,
Expand Down
2 changes: 1 addition & 1 deletion base_layer/core/src/chain_storage/async_db.rs
Expand Up @@ -154,7 +154,7 @@ impl<B: BlockchainBackend + 'static> AsyncBlockchainDb<B> {
make_async_fn!(fetch_kernels_by_mmr_position(start: u64, end: u64) -> Vec<TransactionKernel>, "fetch_kernels_by_mmr_position");

//---------------------------------- MMR --------------------------------------------//
make_async_fn!(prepare_block_merkle_roots(template: NewBlockTemplate) -> Block, "prepare_block_merkle_roots");
make_async_fn!(prepare_new_block(template: NewBlockTemplate) -> Block, "prepare_new_block");

make_async_fn!(fetch_mmr_size(tree: MmrTree) -> u64, "fetch_mmr_size");

Expand Down
30 changes: 27 additions & 3 deletions base_layer/core/src/chain_storage/blockchain_database.rs
Expand Up @@ -49,7 +49,14 @@ use crate::{
proof_of_work::{monero_rx::MoneroPowData, PowAlgorithm, TargetDifficultyWindow},
tari_utilities::epoch_time::EpochTime,
transactions::transaction::TransactionKernel,
validation::{DifficultyCalculator, HeaderValidation, OrphanValidation, PostOrphanBodyValidation, ValidationError},
validation::{
helpers::calc_median_timestamp,
DifficultyCalculator,
HeaderValidation,
OrphanValidation,
PostOrphanBodyValidation,
ValidationError,
},
};
use croaring::Bitmap;
use log::*;
Expand Down Expand Up @@ -640,10 +647,27 @@ where B: BlockchainBackend
Ok(targets)
}

pub fn prepare_block_merkle_roots(&self, template: NewBlockTemplate) -> Result<Block, ChainStorageError> {
pub fn prepare_new_block(&self, template: NewBlockTemplate) -> Result<Block, ChainStorageError> {
let NewBlockTemplate { header, mut body, .. } = template;
body.sort();
let header = BlockHeader::from(header);
let mut header = BlockHeader::from(header);
// If someone advanced the median timestamp such that the local time is less than the median timestamp, we need
// to increase the timestamp to be greater than the median timestamp
let height = header.height - 1;
let min_height = header.height.saturating_sub(
self.consensus_manager
.consensus_constants(header.height)
.get_median_timestamp_count() as u64,
);
let db = self.db_read_access()?;
let timestamps = fetch_headers(&*db, min_height, height)?
.iter()
.map(|h| h.timestamp)
.collect::<Vec<_>>();
let median_timestamp = calc_median_timestamp(&timestamps);
if median_timestamp > header.timestamp {
header.timestamp = median_timestamp.increase(1);
}
let block = Block { header, body };
let (mut block, roots) = self.calculate_mmr_roots(block)?;
block.header.kernel_mr = roots.kernel_mr;
Expand Down
2 changes: 1 addition & 1 deletion base_layer/core/tests/async_db.rs
Expand Up @@ -149,7 +149,7 @@ fn async_add_new_block() {
)
.0;

let new_block = db.prepare_block_merkle_roots(new_block).unwrap();
let new_block = db.prepare_new_block(new_block).unwrap();

test_async(|rt| {
let db = AsyncBlockchainDb::new(db);
Expand Down
4 changes: 2 additions & 2 deletions base_layer/core/tests/base_node_rpc.rs
Expand Up @@ -182,7 +182,7 @@ async fn test_base_node_wallet_rpc() {
// Now submit a block with Tx1 in it so that Tx2 is no longer an orphan
let block1 = base_node
.blockchain_db
.prepare_block_merkle_roots(chain_block(&block0.block(), vec![tx1.clone()], &consensus_manager))
.prepare_new_block(chain_block(&block0.block(), vec![tx1.clone()], &consensus_manager))
.unwrap();

base_node
Expand Down Expand Up @@ -229,7 +229,7 @@ async fn test_base_node_wallet_rpc() {
// Now we will Mine block 2 so that we can see 1 confirmation on tx1
let mut block2 = base_node
.blockchain_db
.prepare_block_merkle_roots(chain_block(&block1, vec![], &consensus_manager))
.prepare_new_block(chain_block(&block1, vec![], &consensus_manager))
.unwrap();

block2.header.output_mmr_size += 1;
Expand Down
58 changes: 29 additions & 29 deletions base_layer/core/tests/block_validation.rs
Expand Up @@ -140,20 +140,20 @@ fn test_monero_blocks() {
);
let block_0 = db.fetch_block(0).unwrap().try_into_chain_block().unwrap();
let (block_1_t, _) = chain_block_with_new_coinbase(&block_0, vec![], &cm, &factories);
let mut block_1 = db.prepare_block_merkle_roots(block_1_t).unwrap();
let mut block_1 = db.prepare_new_block(block_1_t).unwrap();

// Now we have block 1, lets add monero data to it
add_monero_data(&mut block_1, seed1);
let cb_1 = db.add_block(Arc::new(block_1)).unwrap().assert_added();
// Now lets add a second faulty block using the same seed hash
let (block_2_t, _) = chain_block_with_new_coinbase(&cb_1, vec![], &cm, &factories);
let mut block_2 = db.prepare_block_merkle_roots(block_2_t).unwrap();
let mut block_2 = db.prepare_new_block(block_2_t).unwrap();

add_monero_data(&mut block_2, seed1);
let cb_2 = db.add_block(Arc::new(block_2)).unwrap().assert_added();
// Now lets add a third faulty block using the same seed hash. This should fail.
let (block_3_t, _) = chain_block_with_new_coinbase(&cb_2, vec![], &cm, &factories);
let mut block_3 = db.prepare_block_merkle_roots(block_3_t).unwrap();
let mut block_3 = db.prepare_new_block(block_3_t).unwrap();
let mut block_3_broken = block_3.clone();
add_monero_data(&mut block_3_broken, seed1);
match db.add_block(Arc::new(block_3_broken)) {
Expand Down Expand Up @@ -304,27 +304,27 @@ OutputFeatures::default()),
OutputFeatures::default()),
);
let (template, _) = chain_block_with_new_coinbase(&genesis, vec![tx01.clone(), tx02.clone()], &rules, &factories);
let new_block = db.prepare_block_merkle_roots(template).unwrap();
let new_block = db.prepare_new_block(template).unwrap();
// this block should be okay
assert!(orphan_validator.validate(&new_block).is_ok());

// lets break the block weight
let (template, _) =
chain_block_with_new_coinbase(&genesis, vec![tx01.clone(), tx02.clone(), tx03], &rules, &factories);
let new_block = db.prepare_block_merkle_roots(template).unwrap();
let new_block = db.prepare_new_block(template).unwrap();
assert!(orphan_validator.validate(&new_block).is_err());

// lets break the sorting
let (mut template, _) =
chain_block_with_new_coinbase(&genesis, vec![tx01.clone(), tx02.clone()], &rules, &factories);
let outputs = vec![template.body.outputs()[1].clone(), template.body.outputs()[2].clone()];
template.body = AggregateBody::new(template.body.inputs().clone(), outputs, template.body.kernels().clone());
let new_block = db.prepare_block_merkle_roots(template).unwrap();
let new_block = db.prepare_new_block(template).unwrap();
assert!(orphan_validator.validate(&new_block).is_err());

// lets break spend rules
let (template, _) = chain_block_with_new_coinbase(&genesis, vec![tx01.clone(), tx04.clone()], &rules, &factories);
let new_block = db.prepare_block_merkle_roots(template).unwrap();
let new_block = db.prepare_new_block(template).unwrap();
assert!(orphan_validator.validate(&new_block).is_err());

// let break coinbase value
Expand All @@ -340,7 +340,7 @@ OutputFeatures::default()),
coinbase_kernel,
&rules,
);
let new_block = db.prepare_block_merkle_roots(template).unwrap();
let new_block = db.prepare_new_block(template).unwrap();
assert!(orphan_validator.validate(&new_block).is_err());

// let break coinbase lock height
Expand All @@ -356,14 +356,14 @@ OutputFeatures::default()),
coinbase_kernel,
&rules,
);
let new_block = db.prepare_block_merkle_roots(template).unwrap();
let new_block = db.prepare_new_block(template).unwrap();
assert!(orphan_validator.validate(&new_block).is_err());

// lets break accounting
let (mut template, _) = chain_block_with_new_coinbase(&genesis, vec![tx01, tx02], &rules, &factories);
let outputs = vec![template.body.outputs()[1].clone(), tx04.body.outputs()[1].clone()];
template.body = AggregateBody::new(template.body.inputs().clone(), outputs, template.body.kernels().clone());
let new_block = db.prepare_block_merkle_roots(template).unwrap();
let new_block = db.prepare_new_block(template).unwrap();
assert!(orphan_validator.validate(&new_block).is_err());
}

Expand Down Expand Up @@ -416,7 +416,7 @@ OutputFeatures::default()),
OutputFeatures::default()),
);
let (template, _) = chain_block_with_new_coinbase(&genesis, vec![tx01, tx02], &rules, &factories);
let mut new_block = db.prepare_block_merkle_roots(template.clone()).unwrap();
let mut new_block = db.prepare_new_block(template.clone()).unwrap();
new_block.header.nonce = OsRng.next_u64();

find_header_with_achieved_difficulty(&mut new_block.header, 10.into());
Expand All @@ -443,7 +443,7 @@ OutputFeatures::default()),
.is_ok());

// lets break the chain sequence
let mut new_block = db.prepare_block_merkle_roots(template.clone()).unwrap();
let mut new_block = db.prepare_new_block(template.clone()).unwrap();
new_block.header.nonce = OsRng.next_u64();
new_block.header.height = 3;
find_header_with_achieved_difficulty(&mut new_block.header, 10.into());
Expand All @@ -468,7 +468,7 @@ OutputFeatures::default()),
.is_err());

// lets have unknown inputs;
let mut new_block = db.prepare_block_merkle_roots(template.clone()).unwrap();
let mut new_block = db.prepare_new_block(template.clone()).unwrap();
let test_params1 = TestParams::new();
let test_params2 = TestParams::new();
// We dont need proper utxo's with signatures as the post_orphan validator does not check accounting balance +
Expand Down Expand Up @@ -508,7 +508,7 @@ OutputFeatures::default()),
.is_err());

// lets check duplicate txos
let mut new_block = db.prepare_block_merkle_roots(template.clone()).unwrap();
let mut new_block = db.prepare_new_block(template.clone()).unwrap();
// We dont need proper utxo's with signatures as the post_orphan validator does not check accounting balance +
// signatures.
let inputs = vec![new_block.body.inputs()[0].clone(), new_block.body.inputs()[0].clone()];
Expand Down Expand Up @@ -539,7 +539,7 @@ OutputFeatures::default()),
.is_err());

// check mmr roots
let mut new_block = db.prepare_block_merkle_roots(template).unwrap();
let mut new_block = db.prepare_new_block(template).unwrap();
new_block.header.output_mr = Vec::new();
new_block.header.nonce = OsRng.next_u64();

Expand Down Expand Up @@ -614,7 +614,7 @@ OutputFeatures::default()),
OutputFeatures::default()),
);
let (template, _) = chain_block_with_new_coinbase(&genesis, vec![tx01, tx02], &rules, &factories);
let mut new_block = db.prepare_block_merkle_roots(template.clone()).unwrap();
let mut new_block = db.prepare_new_block(template.clone()).unwrap();
new_block.header.nonce = OsRng.next_u64();

find_header_with_achieved_difficulty(&mut new_block.header, 20.into());
Expand All @@ -628,7 +628,7 @@ OutputFeatures::default()),
.is_ok());

// Lets break ftl rules
let mut new_block = db.prepare_block_merkle_roots(template.clone()).unwrap();
let mut new_block = db.prepare_new_block(template.clone()).unwrap();
new_block.header.nonce = OsRng.next_u64();
// we take the max ftl time and give 10 seconds for mining then check it, it should still be more than the ftl
new_block.header.timestamp = rules.consensus_constants(0).ftl().increase(10);
Expand All @@ -642,7 +642,7 @@ OutputFeatures::default()),
.is_err());

// lets break the median rules
let mut new_block = db.prepare_block_merkle_roots(template.clone()).unwrap();
let mut new_block = db.prepare_new_block(template.clone()).unwrap();
new_block.header.nonce = OsRng.next_u64();
// we take the max ftl time and give 10 seconds for mining then check it, it should still be more than the ftl
new_block.header.timestamp = genesis.header().timestamp.checked_sub(100.into()).unwrap();
Expand All @@ -656,7 +656,7 @@ OutputFeatures::default()),
.is_err());

// lets break difficulty
let mut new_block = db.prepare_block_merkle_roots(template).unwrap();
let mut new_block = db.prepare_new_block(template).unwrap();
new_block.header.nonce = OsRng.next_u64();
find_header_with_achieved_difficulty(&mut new_block.header, 10.into());
let mut result = header_validator
Expand Down Expand Up @@ -730,32 +730,32 @@ async fn test_block_sync_body_validator() {
txn_schema!(from: vec![outputs[3].clone()], to: vec![50_000 * uT], fee: 20*uT, lock: 2, features: OutputFeatures::default()),
);
let (template, _) = chain_block_with_new_coinbase(&genesis, vec![tx01.clone(), tx02.clone()], &rules, &factories);
let new_block = db.prepare_block_merkle_roots(template).unwrap();
let new_block = db.prepare_new_block(template).unwrap();
// this block should be okay
validator.validate_body(new_block).await.unwrap();

// lets break the block weight
let (template, _) =
chain_block_with_new_coinbase(&genesis, vec![tx01.clone(), tx02.clone(), tx03], &rules, &factories);
let new_block = db.prepare_block_merkle_roots(template).unwrap();
let new_block = db.prepare_new_block(template).unwrap();
validator.validate_body(new_block).await.unwrap_err();

// lets break spend rules
let (template, _) = chain_block_with_new_coinbase(&genesis, vec![tx01.clone(), tx04.clone()], &rules, &factories);
let new_block = db.prepare_block_merkle_roots(template).unwrap();
let new_block = db.prepare_new_block(template).unwrap();
validator.validate_body(new_block).await.unwrap_err();

// lets break the sorting
let (mut template, _) =
chain_block_with_new_coinbase(&genesis, vec![tx01.clone(), tx02.clone()], &rules, &factories);
let output = vec![template.body.outputs()[1].clone(), template.body.outputs()[2].clone()];
template.body = AggregateBody::new(template.body.inputs().clone(), output, template.body.kernels().clone());
let new_block = db.prepare_block_merkle_roots(template).unwrap();
let new_block = db.prepare_new_block(template).unwrap();
validator.validate_body(new_block).await.unwrap_err();

// lets have unknown inputs;
let (template, _) = chain_block_with_new_coinbase(&genesis, vec![tx01.clone(), tx02.clone()], &rules, &factories);
let mut new_block = db.prepare_block_merkle_roots(template.clone()).unwrap();
let mut new_block = db.prepare_new_block(template.clone()).unwrap();
let test_params1 = TestParams::new();
let test_params2 = TestParams::new();
// We dont need proper utxo's with signatures as the post_orphan validator does not check accounting balance +
Expand All @@ -774,7 +774,7 @@ async fn test_block_sync_body_validator() {

// lets check duplicate txos
let (template, _) = chain_block_with_new_coinbase(&genesis, vec![tx01.clone(), tx02.clone()], &rules, &factories);
let mut new_block = db.prepare_block_merkle_roots(template.clone()).unwrap();
let mut new_block = db.prepare_new_block(template.clone()).unwrap();
// We dont need proper utxo's with signatures as the post_orphan validator does not check accounting balance +
// signatures.
let inputs = vec![new_block.body.inputs()[0].clone(), new_block.body.inputs()[0].clone()];
Expand All @@ -795,7 +795,7 @@ async fn test_block_sync_body_validator() {
coinbase_kernel,
&rules,
);
let new_block = db.prepare_block_merkle_roots(template).unwrap();
let new_block = db.prepare_new_block(template).unwrap();
validator.validate_body(new_block).await.unwrap_err();

// let break coinbase lock height
Expand All @@ -811,20 +811,20 @@ async fn test_block_sync_body_validator() {
coinbase_kernel,
&rules,
);
let new_block = db.prepare_block_merkle_roots(template).unwrap();
let new_block = db.prepare_new_block(template).unwrap();
validator.validate_body(new_block).await.unwrap();

// lets break accounting
let (mut template, _) =
chain_block_with_new_coinbase(&genesis, vec![tx01.clone(), tx02.clone()], &rules, &factories);
let outputs = vec![template.body.outputs()[1].clone(), tx04.body.outputs()[1].clone()];
template.body = AggregateBody::new(template.body.inputs().clone(), outputs, template.body.kernels().clone());
let new_block = db.prepare_block_merkle_roots(template).unwrap();
let new_block = db.prepare_new_block(template).unwrap();
validator.validate_body(new_block).await.unwrap_err();

// lets the mmr root
let (template, _) = chain_block_with_new_coinbase(&genesis, vec![tx01, tx02], &rules, &factories);
let mut new_block = db.prepare_block_merkle_roots(template).unwrap();
let mut new_block = db.prepare_new_block(template).unwrap();
new_block.header.output_mr = Vec::new();
validator.validate_body(new_block).await.unwrap_err();
}
2 changes: 1 addition & 1 deletion base_layer/core/tests/chain_storage_tests/chain_storage.rs
Expand Up @@ -969,7 +969,7 @@ fn handle_reorg_failure_recovery() {
orphan1_outputs.push(block_utxos);

let template = chain_block(&orphan1_blocks.last().unwrap().block(), txns, &consensus_manager);
let mut block = orphan1_store.prepare_block_merkle_roots(template).unwrap();
let mut block = orphan1_store.prepare_new_block(template).unwrap();
block.header.nonce = OsRng.next_u64();
block.header.height += 1;
find_header_with_achieved_difficulty(&mut block.header, Difficulty::from(2));
Expand Down

0 comments on commit a82638a

Please sign in to comment.