use std::sync::Arc;
use futures::{
lock::{MappedMutexGuard, Mutex, MutexGuard},
FutureExt as _,
};
use linera_base::{
crypto::{AccountSecretKey, CryptoHash, ValidatorKeypair, ValidatorSecretKey},
data_types::{
Amount, ApplicationPermissions, Blob, BlobContent, ChainDescription, ChainOrigin, Epoch,
InitialChainConfig, NetworkDescription, Timestamp,
},
identifiers::{AccountOwner, ApplicationId, ChainId, ModuleId},
ownership::ChainOwnership,
};
use linera_core::{worker::WorkerState, ChainWorkerConfig};
use linera_execution::{
committee::Committee,
system::{AdminOperation, OpenChainConfig, SystemOperation},
ResourceControlPolicy, WasmRuntime,
};
use linera_storage::{DbStorage, Storage, TestClock};
use linera_views::memory::MemoryDatabase;
use serde::Serialize;
use super::ActiveChain;
use crate::ContractAbi;
pub struct TestValidator {
validator_secret: ValidatorSecretKey,
account_secret: AccountSecretKey,
committee: Arc<Mutex<(Epoch, Committee)>>,
storage: DbStorage<MemoryDatabase, TestClock>,
worker: WorkerState<DbStorage<MemoryDatabase, TestClock>>,
clock: TestClock,
admin_chain_id: ChainId,
chains: Arc<papaya::HashMap<ChainId, ActiveChain>>,
}
impl Clone for TestValidator {
fn clone(&self) -> Self {
TestValidator {
admin_chain_id: self.admin_chain_id,
validator_secret: self.validator_secret.copy(),
account_secret: self.account_secret.copy(),
committee: self.committee.clone(),
storage: self.storage.clone(),
worker: self.worker.clone(),
clock: self.clock.clone(),
chains: self.chains.clone(),
}
}
}
impl TestValidator {
pub async fn new() -> Self {
let validator_keypair = ValidatorKeypair::generate();
let account_secret = AccountSecretKey::generate();
let epoch = Epoch::ZERO;
let committee = Committee::make_simple(vec![(
validator_keypair.public_key,
account_secret.public(),
)]);
let wasm_runtime = Some(WasmRuntime::default());
let storage = DbStorage::<MemoryDatabase, _>::make_test_storage(wasm_runtime)
.now_or_never()
.expect("execution of DbStorage::new should not await anything");
let clock = storage.clock().clone();
let config = ChainWorkerConfig {
nickname: "Single validator node".to_string(),
key_pair: Some(Arc::new(validator_keypair.secret_key.copy())),
..ChainWorkerConfig::default()
};
let worker = WorkerState::new(storage.clone(), config, None);
let key_pair = AccountSecretKey::generate();
let new_chain_config = InitialChainConfig {
ownership: ChainOwnership::single(key_pair.public().into()),
min_active_epoch: epoch,
max_active_epoch: epoch,
epoch,
balance: Amount::from_tokens(1_000_000),
application_permissions: ApplicationPermissions::default(),
};
let origin = ChainOrigin::Root(0);
let description = ChainDescription::new(origin, new_chain_config, Timestamp::from(0));
let admin_chain_id = description.id();
let committee_blob = Blob::new_committee(
bcs::to_bytes(&committee).expect("serializing a committee should succeed"),
);
let network_description = NetworkDescription {
name: "Test network".to_string(),
genesis_config_hash: CryptoHash::test_hash("genesis config"),
genesis_timestamp: description.timestamp(),
genesis_committee_blob_hash: committee_blob.id().hash,
admin_chain_id,
};
storage
.write_network_description(&network_description)
.await
.unwrap();
storage
.write_blob(&committee_blob)
.await
.expect("writing a blob should succeed");
worker
.storage_client()
.create_chain(description.clone())
.await
.expect("Failed to create root admin chain");
let validator = TestValidator {
validator_secret: validator_keypair.secret_key,
account_secret,
committee: Arc::new(Mutex::new((epoch, committee))),
storage,
worker,
clock,
admin_chain_id,
chains: Arc::default(),
};
let chain = ActiveChain::new(key_pair, description.clone(), validator.clone());
validator.chains.pin().insert(description.id(), chain);
validator
}
pub async fn with_current_module<Abi, Parameters, InstantiationArgument>() -> (
TestValidator,
ModuleId<Abi, Parameters, InstantiationArgument>,
) {
let validator = TestValidator::new().await;
let publisher = Box::pin(validator.new_chain()).await;
let module_id = publisher.publish_current_module().await;
(validator, module_id)
}
pub async fn with_current_application<Abi, Parameters, InstantiationArgument>(
parameters: Parameters,
instantiation_argument: InstantiationArgument,
) -> (TestValidator, ApplicationId<Abi>, ActiveChain)
where
Abi: ContractAbi,
Parameters: Serialize,
InstantiationArgument: Serialize,
{
let (validator, module_id) =
TestValidator::with_current_module::<Abi, Parameters, InstantiationArgument>().await;
let mut creator = validator.new_chain().await;
let application_id = creator
.create_application(module_id, parameters, instantiation_argument, vec![])
.await;
(validator, application_id, creator)
}
pub(crate) fn storage(&self) -> &DbStorage<MemoryDatabase, TestClock> {
&self.storage
}
pub(crate) fn worker(&self) -> WorkerState<DbStorage<MemoryDatabase, TestClock>> {
self.worker.clone()
}
pub fn clock(&self) -> &TestClock {
&self.clock
}
pub fn key_pair(&self) -> &ValidatorSecretKey {
&self.validator_secret
}
pub fn admin_chain_id(&self) -> ChainId {
self.admin_chain_id
}
pub async fn committee(&self) -> MappedMutexGuard<'_, (Epoch, Committee), Committee> {
MutexGuard::map(self.committee.lock().await, |(_epoch, committee)| committee)
}
pub async fn change_resource_control_policy(
&mut self,
adjustment: impl FnOnce(&mut ResourceControlPolicy),
) {
let (epoch, committee) = {
let (ref mut epoch, ref mut committee) = &mut *self.committee.lock().await;
epoch
.try_add_assign_one()
.expect("Reached the limit of epochs");
adjustment(committee.policy_mut());
(*epoch, committee.clone())
};
let admin_chain = self.get_chain(&self.admin_chain_id);
let committee_blob = Blob::new(BlobContent::new_committee(
bcs::to_bytes(&committee).unwrap(),
));
let blob_hash = committee_blob.id().hash;
self.storage
.write_blob(&committee_blob)
.await
.expect("Should write committee blob");
admin_chain
.add_block(|block| {
block.with_system_operation(SystemOperation::Admin(
AdminOperation::CreateCommittee { epoch, blob_hash },
));
})
.await;
let pinned = self.chains.pin();
for chain in pinned.values() {
if chain.id() != self.admin_chain_id {
chain
.add_block(|block| {
block.with_system_operation(SystemOperation::ProcessNewEpoch(epoch));
})
.await;
}
}
}
pub async fn new_chain_with_keypair(&self, key_pair: AccountSecretKey) -> ActiveChain {
let description = self
.request_new_chain_from_admin_chain(key_pair.public().into())
.await;
let chain = ActiveChain::new(key_pair, description.clone(), self.clone());
chain.handle_received_messages().await;
self.chains.pin().insert(description.id(), chain.clone());
chain
}
pub async fn new_chain(&self) -> ActiveChain {
let key_pair = AccountSecretKey::generate();
self.new_chain_with_keypair(key_pair).await
}
pub fn add_chain(&self, chain: ActiveChain) {
self.chains.pin().insert(chain.id(), chain);
}
async fn request_new_chain_from_admin_chain(&self, owner: AccountOwner) -> ChainDescription {
let admin_chain_id = self.admin_chain_id;
let pinned = self.chains.pin();
let admin_chain = pinned
.get(&admin_chain_id)
.expect("Admin chain should be created when the `TestValidator` is constructed");
let open_chain_config = OpenChainConfig {
ownership: ChainOwnership::single(owner),
balance: Amount::from_tokens(10),
application_permissions: ApplicationPermissions::default(),
};
let chain_state = self
.worker
.chain_state_view(admin_chain_id)
.await
.expect("Failed to read admin chain state");
let committees = chain_state
.execution_state
.system
.committees
.get()
.await
.expect("Failed to read committees");
let epoch = *chain_state.execution_state.system.epoch.get();
let min_active_epoch = committees.keys().min().copied().unwrap_or(Epoch::ZERO);
let max_active_epoch = committees.keys().max().copied().unwrap_or(Epoch::ZERO);
drop(chain_state);
let new_chain_config =
open_chain_config.init_chain_config(epoch, min_active_epoch, max_active_epoch);
let (certificate, _) = admin_chain
.add_block(|block| {
block.with_system_operation(SystemOperation::OpenChain(open_chain_config));
})
.await;
let block = certificate.inner().block();
let origin = ChainOrigin::Child {
parent: block.header.chain_id,
block_height: block.header.height,
chain_index: 0,
};
ChainDescription::new(origin, new_chain_config, Timestamp::from(0))
}
pub fn get_chain(&self, chain_id: &ChainId) -> ActiveChain {
self.chains
.pin()
.get(chain_id)
.expect("Chain not found")
.clone()
}
}