Skip to content

Commit

Permalink
Make max supported version dependent on network
Browse files Browse the repository at this point in the history
Implement upgrade test
Add block tests
Add accounts tree tests and fix block number
Test creation of inherent
  • Loading branch information
paberr committed Nov 18, 2024
1 parent 477cfbd commit eb68def
Show file tree
Hide file tree
Showing 16 changed files with 703 additions and 45 deletions.
5 changes: 0 additions & 5 deletions blockchain/src/blockchain/inherents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,11 +326,6 @@ impl Blockchain {
let mut inherents = vec![Inherent::FinalizeEpoch];
// Add version upgrade inherent if needed.
if version != self.state.current_version() {
assert_eq!(
version,
self.state.current_version(),
"Version should only be upgraded in steps of one."
);
inherents.push(Inherent::VersionUpgrade {
new_version: version,
})
Expand Down
116 changes: 116 additions & 0 deletions blockchain/tests/inherents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -699,3 +699,119 @@ async fn create_fork_proof() {
// Verify that the fork proof was generated
assert!(fork_rx.next().await.is_some());
}

#[test]
fn it_can_create_version_upgrade_inherents() {
let time = Arc::new(OffsetTime::new());
let env = MdbxDatabase::new_volatile(Default::default()).unwrap();
let blockchain = Arc::new(
Blockchain::new(
env,
BlockchainConfig::default(),
NetworkId::UnitAlbatross,
time,
)
.unwrap(),
);

let block_number = Policy::election_block_after(Policy::genesis_block_number());

let staking_contract = blockchain.get_staking_contract();
let active_validators = staking_contract.active_validators.clone();
let next_batch_initial_punished_set = staking_contract
.punished_slots
.next_batch_initial_punished_set(block_number, &active_validators);

let mut macro_header = MacroHeader {
network: NetworkId::UnitAlbatross,
version: 2,
block_number,
round: 0,
timestamp: blockchain.state.election_head.header.timestamp + 20000,
parent_hash: Blake2bHash::default(),
parent_election_hash: Blake2bHash::default(),
interlink: None,
seed: VrfSeed::default(),
extra_data: vec![],
state_root: Blake2bHash::default(),
body_root: Blake2sHash::default(),
diff_root: Blake2bHash::default(),
history_root: Blake2bHash::default(),
validators: None,
next_batch_initial_punished_set,
..Default::default()
};

let reward_transactions =
blockchain.create_reward_transactions(&macro_header, &staking_contract);

let body = MacroBody {
transactions: reward_transactions,
};

let macro_block = MacroBlock {
header: macro_header.clone(),
body: Some(body.clone()),
justification: None,
};

// Simple case. Expect 1x FinalizeBatch, 1x FinalizeEpoch, 1x Reward to validator, 2x Version Upgrade
let inherents = blockchain.create_macro_block_inherents(&macro_block);
assert_eq!(inherents.len(), 4);

let mut got_reward = false;
let mut got_finalize_batch = false;
let mut got_finalize_epoch = false;
let mut got_version_upgrade = false;
for inherent in &inherents {
match inherent {
Inherent::Reward { value, .. } => {
got_reward = true;
}
Inherent::FinalizeBatch => {
got_finalize_batch = true;
}
Inherent::FinalizeEpoch => {
got_finalize_epoch = true;
}
Inherent::VersionUpgrade { new_version } => {
got_version_upgrade = true;
assert_eq!(*new_version, 2);
}
_ => panic!(),
}
}
assert!(got_reward && got_finalize_batch && got_finalize_epoch && got_version_upgrade);

// Downgrading version will remove version upgrade.
macro_header.version = 1;

let macro_block = MacroBlock {
header: macro_header.clone(),
body: Some(body),
justification: None,
};

// Simple case. Expect 1x FinalizeBatch, 1x FinalizeEpoch, 1x Reward to validator, 2x Version Upgrade
let inherents = blockchain.create_macro_block_inherents(&macro_block);
assert_eq!(inherents.len(), 3);

let mut got_reward = false;
let mut got_finalize_batch = false;
let mut got_finalize_epoch = false;
for inherent in &inherents {
match inherent {
Inherent::Reward { value, .. } => {
got_reward = true;
}
Inherent::FinalizeBatch => {
got_finalize_batch = true;
}
Inherent::FinalizeEpoch => {
got_finalize_epoch = true;
}
_ => panic!(),
}
}
assert!(got_reward && got_finalize_batch && got_finalize_epoch);
}
4 changes: 3 additions & 1 deletion blockchain/tests/push.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,9 @@ fn it_validates_network() {
fn it_validates_version() {
expect_push_micro_block(
BlockConfig {
version: Some(Policy::MAX_SUPPORTED_VERSION - 1),
version: Some(Policy::max_supported_version(NetworkId::UnitAlbatross) + 1),
// We exclude election blocks here, because they might try to perform an upgrade.
test_election: false,
..Default::default()
},
Err(InvalidBlock(BlockError::UnsupportedVersion)),
Expand Down
2 changes: 1 addition & 1 deletion light-blockchain/tests/push.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ fn it_works_with_valid_blocks() {
fn it_validates_version() {
expect_push_micro_block(
BlockConfig {
version: Some(Policy::MAX_SUPPORTED_VERSION - 1),
version: Some(Policy::max_supported_version(NetworkId::UnitAlbatross) + 1),
..Default::default()
},
Err(InvalidBlock(BlockError::UnsupportedVersion)),
Expand Down
2 changes: 1 addition & 1 deletion primitives/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ coin = ["hex", "nimiq-serde", "regex", "thiserror"]
key-nibbles = ["hex", "nimiq-keys", "nimiq-database-value", "nimiq-database-value-derive", "nimiq-serde"]
networks = ["thiserror"]
parallel = ["rayon", "ark-ec/parallel"]
policy = ["nimiq-keys", "nimiq-utils", "parking_lot"]
policy = ["nimiq-keys", "nimiq-utils", "parking_lot", "networks"]
serde-derive = ["nimiq-serde", "serde", "serde_bytes", "serde_repr"]
slots = ["nimiq-bls", "nimiq-keys", "nimiq-serde", "nimiq-utils", "policy"]
tendermint = ["networks", "nimiq-bls", "serde-derive"]
Expand Down
19 changes: 17 additions & 2 deletions primitives/account/src/account/staking_contract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ impl StakingContract {
slots_builder.build()
}

/// Returns the amount of active stake (i.e., total stake of active validators).
pub fn get_active_stake(&self) -> Coin {
self.active_validators.values().copied().sum()
}

/// Returns the total amount of coins that are supporting the upgrade and are active.
/// The support is determined by a function over the signal data.
/// IMPORTANT: This is a fairly expensive function, iterating over all validators.
Expand Down Expand Up @@ -235,7 +240,8 @@ impl StakingContract {
/// Deactivates validators that did not support the upgrade.
/// The support is determined by a function over the signal data.
/// IMPORTANT: This is a fairly expensive function, iterating over all validators.
pub fn deactivate_unsupporting_validators<F: Fn(Option<Blake2bHash>) -> bool>(
#[cfg(feature = "interaction-traits")]
pub(crate) fn deactivate_unsupporting_validators<F: Fn(Option<Blake2bHash>) -> bool>(
&mut self,
store: &mut StakingContractStoreWrite,
support_check: F,
Expand All @@ -253,7 +259,16 @@ impl StakingContract {

// Deactivate those validators.
for (validator_address, signer) in unsupporting_validators {
self.deactivate_validator(store, &validator_address, &signer, block_number, tx_logger)?;
// Since this function will deactivate the validator from the following election block,
// we will pass `block_number - 1` as the block number. This ensures the validator is
// inactive from this election block already.
self.deactivate_validator(
store,
&validator_address,
&signer,
block_number - 1,
tx_logger,
)?;
}

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion primitives/account/src/data_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ impl<'store, 'tree, 'txn, 'txni, 'env> DataStoreWrite<'store, 'tree, 'txn, 'txni
self.store
.tree
.iter_nodes(
&self.txn,
self.txn,
&(&self.store.prefix + start_key),
&(&self.store.prefix + end_key),
)
Expand Down
131 changes: 130 additions & 1 deletion primitives/account/tests/staking_contract/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use nimiq_primitives::{
account::AccountError,
coin::Coin,
policy::Policy,
slots_allocation::{JailedValidator, PenalizedSlot},
slots_allocation::{JailedValidator, PenalizedSlot, Validators, ValidatorsBuilder},
};
use nimiq_serde::{Deserialize, Serialize};
use nimiq_test_log::test;
Expand Down Expand Up @@ -3306,3 +3306,132 @@ fn commit_failed_delete_validator_does_not_work_if_jailed() {
Coin::from_u64_unchecked(Policy::VALIDATOR_DEPOSIT) - tx.fee
);
}

#[test]
fn version_upgrade_works() {
let env = MdbxDatabase::new_volatile(Default::default()).unwrap();
let accounts = Accounts::new(env.clone());
let data_store = accounts.data_store(&Policy::STAKING_CONTRACT_ADDRESS);
let block_state = BlockState::new(
Policy::blocks_per_epoch() + Policy::genesis_block_number(),
1000,
);
let mut db_txn = env.write_transaction();
let mut db_txn = (&mut db_txn).into();

// Start with one validator and add a second one.
let (validator_address1, _, mut staking_contract) =
make_sample_contract(data_store.write(&mut db_txn), None);
let mut data_store_write = data_store.write(&mut db_txn);
let mut store = StakingContractStoreWrite::new(&mut data_store_write);
let validator_address2 = Address::default();
// Create a second validator with support for version 2.
let mut support = [0; 32];
support[..2].copy_from_slice(&2u16.to_be_bytes());
let support = Blake2bHash(support);
staking_contract
.create_validator(
&mut store,
&validator_address2,
Ed25519PublicKey::default(),
Default::default(),
Address::default(),
Some(support),
Coin::from_u64_unchecked(Policy::VALIDATOR_DEPOSIT),
None,
None,
false,
&mut TransactionLog::empty(),
)
.unwrap();

// Check supporting stake.
let data_store_read = data_store.read(&db_txn);
assert_eq!(
staking_contract.get_active_stake(),
staking_contract.balance,
"The full stake is active"
);
assert_eq!(
staking_contract.get_supporting_stake(&data_store_read, |_| true),
staking_contract.balance,
"Always true support check returns full stake"
);
assert_eq!(
staking_contract
.get_supporting_stake(&data_store_read, |data| Policy::supports_upgrade(data, 2)),
Coin::from_u64_unchecked(Policy::VALIDATOR_DEPOSIT),
"Supporting stake for version 2 is one validator deposit"
);
assert_eq!(
staking_contract
.get_supporting_stake(&data_store_read, |data| Policy::supports_upgrade(data, 3)),
Coin::ZERO,
"Supporting stake for version 3 is zero"
);

// Check supporting slots.
let mut validators = ValidatorsBuilder::new();
for i in 0..Policy::SLOTS {
if i >= Policy::TWO_F_PLUS_ONE {
validators.push(
validator_address1.clone(),
BlsPublicKey::default(),
Ed25519PublicKey::default(),
);
} else {
validators.push(
validator_address2.clone(),
BlsPublicKey::default(),
Ed25519PublicKey::default(),
);
}
}
let validators = validators.build();
let supporting_slots =
staking_contract.get_supporting_slots(&data_store_read, &validators, |data| {
Policy::supports_upgrade(data, 2)
});
assert_eq!(
supporting_slots,
Policy::TWO_F_PLUS_ONE,
"Supporting slots are counted correctly"
);

// Check that the inherent correctly deactivates unsupporting validators.
let inherent = Inherent::VersionUpgrade { new_version: 2 };
let mut logs = vec![];
let mut inherent_logger = InherentLogger::new(&mut logs);
let receipt = staking_contract
.commit_inherent(
&inherent,
&block_state,
data_store.write(&mut db_txn),
&mut inherent_logger,
)
.expect("Failed to commit inherent");
assert_eq!(receipt, None);
assert_eq!(
logs,
vec![Log::DeactivateValidator {
validator_address: validator_address1.clone(),
inactive_from: block_state.number
}]
);

assert!(!staking_contract
.active_validators
.contains_key(&validator_address1));

// Cannot revert the inherent.
assert_eq!(
staking_contract.revert_inherent(
&inherent,
&block_state,
None,
data_store.write(&mut db_txn),
&mut InherentLogger::empty()
),
Err(AccountError::InvalidForTarget)
);
}
10 changes: 8 additions & 2 deletions primitives/block/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,13 +406,19 @@ impl Block {
return Err(BlockError::NetworkMismatch);
}

error!(
obtained_version = self.version(),
maximum_version = Policy::max_supported_version(network),
"Version check"
);

// Check that the version is supported.
// The blockchain should then make sure that the version is never decreased.
if self.version() > Policy::MAX_SUPPORTED_VERSION {
if self.version() > Policy::max_supported_version(network) {
warn!(
header = %self,
obtained_version = self.version(),
maximum_version = Policy::MAX_SUPPORTED_VERSION,
maximum_version = Policy::max_supported_version(network),
reason = "invalid version",
"Invalid block header"
);
Expand Down
4 changes: 2 additions & 2 deletions primitives/block/src/equivocation_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,7 @@ mod test {

let header1 = MicroHeader {
network: NetworkId::UnitAlbatross,
version: Policy::MAX_SUPPORTED_VERSION,
version: Policy::max_supported_version(NetworkId::UnitAlbatross),
block_number: Policy::genesis_block_number(),
timestamp: 0,
parent_hash: Blake2bHash::default(),
Expand Down Expand Up @@ -725,7 +725,7 @@ mod test {

let header1 = MacroHeader {
network: NetworkId::UnitAlbatross,
version: Policy::MAX_SUPPORTED_VERSION,
version: Policy::max_supported_version(NetworkId::UnitAlbatross),
block_number: Policy::genesis_block_number(),
round: 0,
timestamp: 0,
Expand Down
Loading

0 comments on commit eb68def

Please sign in to comment.