use assert_matches::assert_matches;
use exonum::{
helpers::Height,
runtime::{InstanceId, InstanceStatus, SnapshotExt, SUPERVISOR_INSTANCE_ID},
};
use exonum_explorer::BlockchainExplorer;
use exonum_merkledb::{BinaryValue, ObjectHash};
use exonum_rust_runtime::{RustRuntime, ServiceFactory};
use exonum_testkit::{Spec, TestKitBuilder};
use pretty_assertions::assert_eq;
pub use crate::{
hooks_service::{
AfterCommitInterface, AfterCommitService, AfterCommitServiceV2, SERVICE_ID, SERVICE_NAME,
},
supervisor::{StartMigration, Supervisor, SupervisorInterface},
};
mod hooks_service;
mod supervisor;
const SUPERVISOR_ID: InstanceId = SUPERVISOR_INSTANCE_ID;
#[tokio::test]
async fn test_after_commit() {
let service = AfterCommitService::new();
let mut testkit = TestKitBuilder::validator()
.with(Spec::new(service.clone()).with_default_instance())
.build();
for i in 1..5 {
let block = testkit.create_block();
if i > 1 {
let arguments = &block[0].message().payload().arguments;
let height_from_tx = u64::from_bytes(arguments.into()).unwrap();
assert_eq!(height_from_tx, i - 1);
}
assert_eq!(service.counter() as u64, i);
let blockchain = testkit.blockchain();
let keypair = blockchain.service_keypair();
let tx = keypair.after_commit(SERVICE_ID, i);
assert!(testkit.is_tx_in_pool(&tx.object_hash()));
}
let snapshot = testkit.snapshot();
let expected_block_sizes = BlockchainExplorer::new(snapshot.as_ref())
.blocks(Height(1)..)
.all(|block| block.len() == if block.height() == Height(1) { 0 } else { 1 });
assert!(expected_block_sizes);
}
#[tokio::test]
async fn test_after_commit_with_auditor() {
let service = AfterCommitService::new();
let mut testkit = TestKitBuilder::auditor()
.with_validators(2)
.with(Spec::new(service.clone()).with_default_instance())
.build();
for i in 1..5 {
let block = testkit.create_block();
assert!(block.is_empty());
assert_eq!(service.counter() as u64, i);
let blockchain = testkit.blockchain();
let keypair = blockchain.service_keypair();
let tx = keypair.after_commit(SERVICE_ID, i);
assert!(!testkit.is_tx_in_pool(&tx.object_hash()));
}
service.switch_to_generic_broadcast();
for i in 0..5 {
let block = testkit.create_block();
let expected_block_len = if i == 0 { 0 } else { 1 };
assert_eq!(block.len(), expected_block_len);
}
}
#[tokio::test]
async fn after_commit_not_called_after_service_stop() {
let service = AfterCommitService::new();
let mut testkit = TestKitBuilder::validator()
.with(Spec::new(Supervisor).with_default_instance())
.with(Spec::new(service.clone()).with_default_instance())
.build();
service.switch_to_generic_broadcast();
let keys = testkit.us().service_keypair();
let tx = keys.stop_service(SUPERVISOR_ID, SERVICE_ID);
let block = testkit.create_block_with_transaction(tx);
block[0].status().expect("Service should stop");
for _ in 0..5 {
let block = testkit.create_block();
assert!(block.is_empty());
}
let tx = keys.resume_service(SUPERVISOR_ID, SERVICE_ID);
let block = testkit.create_block_with_transaction(tx);
block[0].status().expect("Service should resume");
for _ in 0..5 {
let block = testkit.create_block();
assert_eq!(block.len(), 1);
}
}
#[tokio::test]
async fn after_commit_during_service_freeze() {
let service = AfterCommitService::new();
let mut testkit = TestKitBuilder::validator()
.with(Spec::new(Supervisor).with_default_instance())
.with(Spec::new(service.clone()).with_default_instance())
.build();
let keys = testkit.us().service_keypair();
let tx = keys.freeze_service(SUPERVISOR_ID, SERVICE_ID);
let block = testkit.create_block_with_transaction(tx);
block[0].status().expect("Service should freeze");
for _ in 0..5 {
let block = testkit.create_block();
assert!(block.is_empty());
}
service.switch_to_generic_broadcast();
for _ in 0..5 {
let block = testkit.create_block();
assert!(block.is_empty());
}
}
#[tokio::test]
async fn after_commit_during_migration() {
let service = AfterCommitService::new();
let mut testkit = TestKitBuilder::validator()
.with(Spec::new(Supervisor).with_default_instance())
.with(Spec::new(service.clone()).with_default_instance())
.with(Spec::migrating(AfterCommitServiceV2))
.build();
let keys = testkit.us().service_keypair();
let tx = keys.stop_service(SUPERVISOR_ID, SERVICE_ID);
let block = testkit.create_block_with_transaction(tx);
block[0].status().expect("Service should stop");
let tx = keys.start_migration(
SUPERVISOR_ID,
StartMigration {
instance_id: SERVICE_ID,
new_artifact: AfterCommitServiceV2.artifact_id(),
migration_len: 10,
},
);
let block = testkit.create_block_with_transaction(tx);
block[0].status().expect("Service should start migrating");
for _ in 0..5 {
let block = testkit.create_block();
assert!(block.is_empty());
let snapshot = testkit.snapshot();
let service_state = snapshot.for_dispatcher().get_instance(SERVICE_ID).unwrap();
assert_matches!(service_state.status, Some(InstanceStatus::Migrating(_)));
}
service.switch_to_generic_broadcast();
for _ in 0..5 {
let block = testkit.create_block();
assert!(block.is_empty());
let snapshot = testkit.snapshot();
let service_state = snapshot.for_dispatcher().get_instance(SERVICE_ID).unwrap();
assert_matches!(service_state.status, Some(InstanceStatus::Migrating(_)));
}
testkit.create_block();
testkit.create_block();
let snapshot = testkit.snapshot();
let service_state = snapshot.for_dispatcher().get_instance(SERVICE_ID).unwrap();
assert_matches!(service_state.status, Some(InstanceStatus::Stopped));
}
#[tokio::test]
async fn incorrect_txs_are_not_included_into_blocks() {
let service = AfterCommitService::new();
let mut testkit = TestKitBuilder::validator()
.with(Spec::new(Supervisor).with_default_instance())
.with(Spec::new(service).with_default_instance())
.build();
let keys = testkit.us().service_keypair();
for _ in 0..5 {
let block = testkit.create_block_with_tx_hashes(&[]);
assert!(block.is_empty());
let new_tx = keys.after_commit(SERVICE_ID, testkit.height().0);
assert!(testkit.is_tx_in_pool(&new_tx.object_hash()));
}
let tx = keys.freeze_service(SUPERVISOR_ID, SERVICE_ID);
let block = testkit.create_block_with_transaction(tx);
block[0].status().expect("Service should freeze");
let block = testkit.create_block();
assert!(block.is_empty());
let tx = keys.resume_service(SUPERVISOR_ID, SERVICE_ID);
let block = testkit.create_block_with_transaction(tx);
block[0].status().expect("Service should resume");
let block = testkit.create_block();
assert_eq!(block.len(), 6); }
#[tokio::test]
async fn restart_testkit() {
let mut testkit = TestKitBuilder::validator()
.with_validators(3)
.with(Spec::new(AfterCommitService::new()).with_default_instance())
.build();
testkit.create_blocks_until(Height(5));
let stopped = testkit.stop();
assert_eq!(stopped.height(), Height(5));
assert_eq!(stopped.network().validators().len(), 3);
let service = AfterCommitService::new();
let rust_runtime = RustRuntime::builder().with_factory(service.clone());
let mut testkit = stopped.resume(rust_runtime);
for _ in 0..3 {
testkit.create_block();
}
assert_eq!(service.counter(), 3);
assert_eq!(testkit.height(), Height(8));
assert_eq!(testkit.network().validators().len(), 3);
let transactions_are_committed = (1..=8)
.map(|i| {
testkit
.blockchain()
.service_keypair()
.after_commit(SERVICE_ID, i)
.object_hash()
})
.all(|hash| {
let snapshot = testkit.snapshot();
BlockchainExplorer::new(snapshot.as_ref())
.transaction_without_proof(&hash)
.is_some()
});
assert!(transactions_are_committed);
}
#[test]
fn tx_pool_is_retained_on_restart() {
let mut testkit = TestKitBuilder::validator()
.with(Spec::new(AfterCommitService::new()).with_default_instance())
.build();
let tx_hashes: Vec<_> = (100..105)
.map(|i| {
let tx = testkit
.blockchain()
.service_keypair()
.after_commit(SERVICE_ID, i);
let tx_hash = tx.object_hash();
testkit.add_tx(tx);
assert!(testkit.is_tx_in_pool(&tx_hash));
tx_hash
})
.collect();
let stopped = testkit.stop();
let rust_runtime = RustRuntime::builder().with_factory(AfterCommitService::new());
let testkit = stopped.resume(rust_runtime);
assert!(tx_hashes
.iter()
.all(|tx_hash| testkit.is_tx_in_pool(tx_hash)));
}