Skip to content

Commit d23fff5

Browse files
committed
Artificial checkpoint only for 1st batch
1 parent 92bebbd commit d23fff5

2 files changed

Lines changed: 278 additions & 40 deletions

File tree

blockchain/src/blockchain/history_sync.rs

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -199,54 +199,46 @@ impl Blockchain {
199199
let mut block_state = vec![];
200200
let mut block_transactions = vec![];
201201
let mut block_inherents = vec![];
202-
203-
let mut prev_batch = 0;
204202
let mut prev_block = 0;
203+
let mut prev_batch = this
204+
.chain_store
205+
.get_block(&this.state.head_hash, false, Some(&txn))
206+
.map_or(0, |head| Policy::batch_at(head.block_number()));
205207

206208
for hist_tx in history.iter().skip(first_new_hist_tx) {
207209
if hist_tx.block_number > prev_block {
208-
// If a macro block does not have any history items, we need to add it here so that
209-
// we always commit FinalizeBatch/FinalizeEpoch inherents.
210-
// FIXME We're missing the block timestamp to do this correctly.
211-
// Also, this works only if a single macro block is missing between history items.
212-
let batch_number = Policy::batch_at(hist_tx.block_number);
213-
if batch_number > prev_batch
214-
&& block_state.last().is_some_and(|block_state: &BlockState| {
215-
!Policy::is_macro_block_at(block_state.number)
216-
})
217-
{
210+
let new_batch = Policy::batch_at(hist_tx.block_number);
211+
// If we are crossing a batch boundary we must artificially put the macro block, so that we have the
212+
// finalize batch inherent generated as well. However, since checkpoint blocks have reward txs, the only macro block
213+
// we may have a gap for is the first checkpoint block due to the batch delay on reward payouts.
214+
if prev_batch < new_batch {
215+
if prev_batch + 1 < new_batch && (prev_batch != 0 || new_batch != 2) {
216+
error!(
217+
new_batch,
218+
"We cannot skip over macro blocks after batch 1 due to reward payout txs."
219+
);
220+
#[cfg(feature = "metrics")]
221+
this.metrics.note_invalid_block();
222+
return Err(PushError::InvalidBlock(BlockError::InvalidHistoryRoot));
223+
}
224+
// If we are missing the very first macro block we add it artificially here and this also
225+
// means we lost its timestamp.
218226
debug!(
219227
history_item_block_number = hist_tx.block_number,
220228
prev_block,
221-
history_item_batch = batch_number,
229+
history_item_batch = new_batch,
222230
prev_batch,
223231
last_block = ?block_state.last(),
224-
"Inserting macro block"
232+
"Adding the first checkpoint block manually since there weren't any txs on it."
225233
);
226-
if batch_number != prev_batch + 1 {
227-
warn!(
228-
%block,
229-
reason = "missing batch in history",
230-
history_item_block_number = hist_tx.block_number,
231-
history_item_batch = batch_number,
232-
prev_batch,
233-
"Rejecting block",
234-
);
235-
txn.abort();
236-
#[cfg(feature = "metrics")]
237-
this.metrics.note_invalid_block();
238-
return Err(PushError::InvalidBlock(BlockError::InvalidHistoryRoot));
239-
}
240-
241234
block_state.push(BlockState {
242-
number: Policy::macro_block_of(prev_batch).unwrap(),
243-
time: 0, // FIXME
235+
number: Policy::macro_block_before(hist_tx.block_number),
236+
time: 0,
244237
protocol_version: this.state.current_version(), // Cannot change, protocol version upgrades only on election blocks.
245238
});
246239
block_transactions.push(vec![]);
247240
block_inherents.push(vec![]);
248241
}
249-
250242
block_state.push(BlockState {
251243
number: hist_tx.block_number,
252244
time: hist_tx.block_time,
@@ -255,8 +247,8 @@ impl Blockchain {
255247
block_transactions.push(vec![]);
256248
block_inherents.push(vec![]);
257249

258-
prev_batch = batch_number;
259250
prev_block = hist_tx.block_number;
251+
prev_batch = new_batch;
260252
}
261253

262254
match &hist_tx.data {

blockchain/tests/history_sync.rs

Lines changed: 253 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
use std::sync::Arc;
1+
use std::{collections::BTreeSet, sync::Arc};
22

3+
use nimiq_account::Account;
34
use nimiq_block::BlockError;
45
use nimiq_blockchain::{interface::HistoryInterface, BlockProducer, Blockchain, BlockchainConfig};
56
use nimiq_blockchain_interface::{AbstractBlockchain, BlockchainEvent, PushError, PushResult};
6-
use nimiq_database::mdbx::MdbxDatabase;
7+
use nimiq_database::{mdbx::MdbxDatabase, traits::WriteTransaction};
78
use nimiq_genesis::NetworkId;
8-
use nimiq_primitives::policy::Policy;
9+
use nimiq_primitives::{key_nibbles::KeyNibbles, policy::Policy};
910
use nimiq_serde::{Deserialize, Serialize};
1011
use nimiq_test_log::test;
1112
use nimiq_test_utils::{
@@ -288,12 +289,10 @@ fn history_sync_emits_protocol_upgrade_event_when_version_changes() {
288289
));
289290

290291
let producer = BlockProducer::new(signing_key(), voting_key());
291-
produce_macro_blocks_with_txns(
292+
produce_macro_blocks(
292293
&producer,
293294
&blockchain1,
294295
(Policy::batches_per_epoch() - 1) as usize,
295-
1,
296-
0,
297296
);
298297
let upgrade_version = signal_next_protocol_version_via_tx(&producer, &blockchain1);
299298
fill_micro_blocks_with_txns(&producer, &blockchain1, 1, 0);
@@ -308,7 +307,7 @@ fn history_sync_emits_protocol_upgrade_event_when_version_changes() {
308307
let upgrade_history = blockchain1
309308
.read()
310309
.history_store
311-
.get_epoch_transactions(1, None);
310+
.get_epoch_transactions(upgrade_block.epoch_number(), None);
312311

313312
let time2 = Arc::new(OffsetTime::new());
314313
let env2 = MdbxDatabase::new_volatile(Default::default()).unwrap();
@@ -341,6 +340,253 @@ fn history_sync_emits_protocol_upgrade_event_when_version_changes() {
341340
);
342341
}
343342

343+
#[test]
344+
fn history_sync_replays_empty_first_batch_when_it_changes_state() {
345+
fn seed_current_batch_punished_slot(blockchain: &Arc<RwLock<Blockchain>>) {
346+
let blockchain = blockchain.write();
347+
let mut staking_contract = blockchain.get_staking_contract();
348+
let validator_address = staking_contract
349+
.active_validators
350+
.keys()
351+
.next()
352+
.expect("missing active validator")
353+
.clone();
354+
355+
staking_contract
356+
.punished_slots
357+
.current_batch_punished_slots
358+
.clear();
359+
staking_contract
360+
.punished_slots
361+
.previous_batch_punished_slots
362+
.clear();
363+
364+
let mut punished_slots = BTreeSet::new();
365+
punished_slots.insert(0);
366+
staking_contract
367+
.punished_slots
368+
.current_batch_punished_slots
369+
.insert(validator_address, punished_slots);
370+
371+
let mut txn = blockchain.write_transaction();
372+
blockchain
373+
.state
374+
.accounts
375+
.tree
376+
.put(
377+
&mut (&mut txn).into(),
378+
&KeyNibbles::from(&Policy::STAKING_CONTRACT_ADDRESS),
379+
Account::Staking(staking_contract),
380+
)
381+
.unwrap();
382+
txn.commit();
383+
}
384+
385+
let genesis_block_number = Policy::genesis_block_number();
386+
let num_macro_blocks = 2;
387+
388+
// Create a blockchain to produce the macro blocks.
389+
let time = Arc::new(OffsetTime::new());
390+
let env = MdbxDatabase::new_volatile(Default::default()).unwrap();
391+
let blockchain = Arc::new(RwLock::new(
392+
Blockchain::new(
393+
env,
394+
BlockchainConfig::default(),
395+
NetworkId::UnitAlbatross,
396+
time,
397+
)
398+
.unwrap(),
399+
));
400+
seed_current_batch_punished_slot(&blockchain);
401+
402+
// Create a second blockchain to push blocks to.
403+
let time = Arc::new(OffsetTime::new());
404+
let env2 = MdbxDatabase::new_volatile(Default::default()).unwrap();
405+
let blockchain2 = Arc::new(RwLock::new(
406+
Blockchain::new(
407+
env2,
408+
BlockchainConfig::default(),
409+
NetworkId::UnitAlbatross,
410+
time,
411+
)
412+
.unwrap(),
413+
));
414+
seed_current_batch_punished_slot(&blockchain2);
415+
416+
// Produce the blocks on blockchain1.
417+
let producer = BlockProducer::new(signing_key(), voting_key());
418+
produce_macro_blocks(&producer, &blockchain, num_macro_blocks);
419+
420+
// Get the first checkpoint and corresponding history tree transactions.
421+
let blockchain_rg = blockchain.read();
422+
let election_txs_1 = blockchain_rg.history_store.get_epoch_transactions(1, None);
423+
424+
let checkpoint_block_1_2 = blockchain_rg
425+
.chain_store
426+
.get_block_at(
427+
Policy::blocks_per_batch() * 2 + genesis_block_number,
428+
true,
429+
None,
430+
)
431+
.unwrap();
432+
let mut checkpoint_txs_1_2 = vec![];
433+
434+
for hist_tx in &election_txs_1 {
435+
if hist_tx.block_number > Policy::blocks_per_batch() * 2 + genesis_block_number {
436+
break;
437+
}
438+
checkpoint_txs_1_2.push(hist_tx.clone());
439+
}
440+
441+
// The first checkpoint of the first epoch has no history items of its own. Replaying it still
442+
// matters here because its FinalizeBatch clears the seeded punished slots before the next
443+
// checkpoint reward is applied.
444+
assert_eq!(
445+
Blockchain::push_history_sync(
446+
blockchain2.upgradable_read(),
447+
checkpoint_block_1_2,
448+
&checkpoint_txs_1_2
449+
),
450+
Ok(PushResult::Extended)
451+
);
452+
}
453+
454+
#[test]
455+
fn history_sync_replays_missing_first_checkpoint_before_second_batch_history() {
456+
let genesis_block_number = Policy::genesis_block_number();
457+
458+
let time = Arc::new(OffsetTime::new());
459+
let env = MdbxDatabase::new_volatile(Default::default()).unwrap();
460+
let blockchain1 = Arc::new(RwLock::new(
461+
Blockchain::new(
462+
env,
463+
BlockchainConfig::default(),
464+
NetworkId::UnitAlbatross,
465+
time,
466+
)
467+
.unwrap(),
468+
));
469+
470+
let producer = BlockProducer::new(signing_key(), voting_key());
471+
472+
// Leave the first batch empty so the first checkpoint is missing from history, then add
473+
// transactions in the second batch before producing the target checkpoint.
474+
produce_macro_blocks(&producer, &blockchain1, 1);
475+
fill_micro_blocks_with_txns(&producer, &blockchain1, 1, 0);
476+
produce_macro_blocks(&producer, &blockchain1, 1);
477+
478+
let blockchain_rg = blockchain1.read();
479+
let checkpoint_block_1_2 = blockchain_rg
480+
.chain_store
481+
.get_block_at(
482+
Policy::blocks_per_batch() * 2 + genesis_block_number,
483+
true,
484+
None,
485+
)
486+
.unwrap();
487+
let checkpoint_txs_1_2 = blockchain_rg.history_store.get_epoch_transactions(1, None);
488+
489+
assert!(
490+
checkpoint_txs_1_2
491+
.iter()
492+
.all(|hist_tx| hist_tx.block_number > Policy::blocks_per_batch() + genesis_block_number),
493+
"test setup must keep the first batch empty",
494+
);
495+
assert!(
496+
checkpoint_txs_1_2
497+
.iter()
498+
.any(|hist_tx| Policy::batch_at(hist_tx.block_number) == 2),
499+
"test setup must retain second-batch history",
500+
);
501+
502+
let time = Arc::new(OffsetTime::new());
503+
let env2 = MdbxDatabase::new_volatile(Default::default()).unwrap();
504+
let blockchain2 = Arc::new(RwLock::new(
505+
Blockchain::new(
506+
env2,
507+
BlockchainConfig::default(),
508+
NetworkId::UnitAlbatross,
509+
time,
510+
)
511+
.unwrap(),
512+
));
513+
514+
assert_eq!(
515+
Blockchain::push_history_sync(
516+
blockchain2.upgradable_read(),
517+
checkpoint_block_1_2,
518+
&checkpoint_txs_1_2
519+
),
520+
Ok(PushResult::Extended)
521+
);
522+
assert_eq!(blockchain_rg.head(), blockchain2.read().head());
523+
}
524+
525+
#[test]
526+
fn history_sync_rejects_missing_non_first_batch_history() {
527+
let genesis_block_number = Policy::genesis_block_number();
528+
let num_macro_blocks = 3;
529+
530+
let time = Arc::new(OffsetTime::new());
531+
let env = MdbxDatabase::new_volatile(Default::default()).unwrap();
532+
let blockchain = Arc::new(RwLock::new(
533+
Blockchain::new(
534+
env,
535+
BlockchainConfig::default(),
536+
NetworkId::UnitAlbatross,
537+
time,
538+
)
539+
.unwrap(),
540+
));
541+
542+
let producer = BlockProducer::new(signing_key(), voting_key());
543+
produce_macro_blocks_with_txns(&producer, &blockchain, num_macro_blocks, 1, 0);
544+
545+
let blockchain_rg = blockchain.read();
546+
let election_txs_1 = blockchain_rg.history_store.get_epoch_transactions(1, None);
547+
let checkpoint_block_1_3 = blockchain_rg
548+
.chain_store
549+
.get_block_at(
550+
Policy::blocks_per_batch() * 3 + genesis_block_number,
551+
true,
552+
None,
553+
)
554+
.unwrap();
555+
556+
let checkpoint_txs_1_3: Vec<_> = election_txs_1
557+
.into_iter()
558+
.filter(|hist_tx| {
559+
hist_tx.block_number > Policy::blocks_per_batch() * 2 + genesis_block_number
560+
})
561+
.collect();
562+
563+
assert!(
564+
!checkpoint_txs_1_3.is_empty(),
565+
"test setup must retain history after the skipped batch",
566+
);
567+
568+
let time = Arc::new(OffsetTime::new());
569+
let env2 = MdbxDatabase::new_volatile(Default::default()).unwrap();
570+
let blockchain2 = Arc::new(RwLock::new(
571+
Blockchain::new(
572+
env2,
573+
BlockchainConfig::default(),
574+
NetworkId::UnitAlbatross,
575+
time,
576+
)
577+
.unwrap(),
578+
));
579+
580+
assert_eq!(
581+
Blockchain::push_history_sync(
582+
blockchain2.upgradable_read(),
583+
checkpoint_block_1_3,
584+
&checkpoint_txs_1_3
585+
),
586+
Err(PushError::InvalidBlock(BlockError::InvalidHistoryRoot))
587+
);
588+
}
589+
344590
// Tests if the history sync works when micro blocks have already been pushed in the blockchain.
345591
// This basically tests if we can go from the history sync to the normal follow mode and back.
346592
#[test]

0 commit comments

Comments
 (0)