linera_sdk/test/
validator.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! A minimal validator implementation suited for tests.
5//!
6//! The [`TestValidator`] is a minimal validator with a single shard. Micro-chains can be added to
7//! it, and blocks can be added to each microchain individually.
8
9use std::sync::Arc;
10
11use futures::{
12    lock::{MappedMutexGuard, Mutex, MutexGuard},
13    FutureExt as _,
14};
15use linera_base::{
16    crypto::{AccountSecretKey, CryptoHash, ValidatorKeypair, ValidatorSecretKey},
17    data_types::{
18        Amount, ApplicationPermissions, Blob, BlobContent, ChainDescription, ChainOrigin, Epoch,
19        InitialChainConfig, NetworkDescription, Timestamp,
20    },
21    identifiers::{AccountOwner, ApplicationId, ChainId, ModuleId},
22    ownership::ChainOwnership,
23};
24use linera_core::{worker::WorkerState, ChainWorkerConfig};
25use linera_execution::{
26    committee::Committee,
27    system::{AdminOperation, OpenChainConfig, SystemOperation},
28    ResourceControlPolicy, WasmRuntime,
29};
30use linera_storage::{DbStorage, Storage, TestClock};
31use linera_views::memory::MemoryDatabase;
32use serde::Serialize;
33
34use super::ActiveChain;
35use crate::ContractAbi;
36
37/// A minimal validator implementation suited for tests.
38///
39/// ```rust
40/// # use linera_sdk::test::*;
41/// # use linera_base::{data_types::Amount, identifiers::ChainId};
42/// # tokio_test::block_on(async {
43/// let validator = TestValidator::new().await;
44/// assert_eq!(
45///     validator.new_chain().await.chain_balance().await,
46///     Amount::from_tokens(10)
47/// );
48/// # });
49/// ```
50pub struct TestValidator {
51    validator_secret: ValidatorSecretKey,
52    account_secret: AccountSecretKey,
53    committee: Arc<Mutex<(Epoch, Committee)>>,
54    storage: DbStorage<MemoryDatabase, TestClock>,
55    worker: WorkerState<DbStorage<MemoryDatabase, TestClock>>,
56    clock: TestClock,
57    admin_chain_id: ChainId,
58    chains: Arc<papaya::HashMap<ChainId, ActiveChain>>,
59}
60
61impl Clone for TestValidator {
62    fn clone(&self) -> Self {
63        TestValidator {
64            admin_chain_id: self.admin_chain_id,
65            validator_secret: self.validator_secret.copy(),
66            account_secret: self.account_secret.copy(),
67            committee: self.committee.clone(),
68            storage: self.storage.clone(),
69            worker: self.worker.clone(),
70            clock: self.clock.clone(),
71            chains: self.chains.clone(),
72        }
73    }
74}
75
76impl TestValidator {
77    /// Creates a new [`TestValidator`].
78    pub async fn new() -> Self {
79        let validator_keypair = ValidatorKeypair::generate();
80        let account_secret = AccountSecretKey::generate();
81        let epoch = Epoch::ZERO;
82        let committee = Committee::make_simple(vec![(
83            validator_keypair.public_key,
84            account_secret.public(),
85        )]);
86        let wasm_runtime = Some(WasmRuntime::default());
87        let storage = DbStorage::<MemoryDatabase, _>::make_test_storage(wasm_runtime)
88            .now_or_never()
89            .expect("execution of DbStorage::new should not await anything");
90        let clock = storage.clock().clone();
91        let config = ChainWorkerConfig {
92            nickname: "Single validator node".to_string(),
93            key_pair: Some(Arc::new(validator_keypair.secret_key.copy())),
94            ..ChainWorkerConfig::default()
95        };
96        let worker = WorkerState::new(storage.clone(), config, None);
97
98        // Create an admin chain.
99        let key_pair = AccountSecretKey::generate();
100
101        let new_chain_config = InitialChainConfig {
102            ownership: ChainOwnership::single(key_pair.public().into()),
103            min_active_epoch: epoch,
104            max_active_epoch: epoch,
105            epoch,
106            balance: Amount::from_tokens(1_000_000),
107            application_permissions: ApplicationPermissions::default(),
108        };
109
110        let origin = ChainOrigin::Root(0);
111        let description = ChainDescription::new(origin, new_chain_config, Timestamp::from(0));
112        let admin_chain_id = description.id();
113
114        let committee_blob = Blob::new_committee(
115            bcs::to_bytes(&committee).expect("serializing a committee should succeed"),
116        );
117
118        let network_description = NetworkDescription {
119            name: "Test network".to_string(),
120            genesis_config_hash: CryptoHash::test_hash("genesis config"),
121            genesis_timestamp: description.timestamp(),
122            genesis_committee_blob_hash: committee_blob.id().hash,
123            admin_chain_id,
124        };
125        storage
126            .write_network_description(&network_description)
127            .await
128            .unwrap();
129        storage
130            .write_blob(&committee_blob)
131            .await
132            .expect("writing a blob should succeed");
133        worker
134            .storage_client()
135            .create_chain(description.clone())
136            .await
137            .expect("Failed to create root admin chain");
138
139        let validator = TestValidator {
140            validator_secret: validator_keypair.secret_key,
141            account_secret,
142            committee: Arc::new(Mutex::new((epoch, committee))),
143            storage,
144            worker,
145            clock,
146            admin_chain_id,
147            chains: Arc::default(),
148        };
149
150        let chain = ActiveChain::new(key_pair, description.clone(), validator.clone());
151
152        validator.chains.pin().insert(description.id(), chain);
153
154        validator
155    }
156
157    /// Creates a new [`TestValidator`] with a single microchain with the bytecode of the crate
158    /// calling this method published on it.
159    ///
160    /// Returns the new [`TestValidator`] and the [`ModuleId`] of the published module.
161    pub async fn with_current_module<Abi, Parameters, InstantiationArgument>() -> (
162        TestValidator,
163        ModuleId<Abi, Parameters, InstantiationArgument>,
164    ) {
165        let validator = TestValidator::new().await;
166        let publisher = Box::pin(validator.new_chain()).await;
167
168        let module_id = Box::pin(publisher.publish_current_module()).await;
169
170        (validator, module_id)
171    }
172
173    /// Creates a new [`TestValidator`] with the application of the crate calling this method
174    /// created on a chain.
175    ///
176    /// The bytecode is first published on one microchain, then the application is created on
177    /// another microchain.
178    ///
179    /// Returns the new [`TestValidator`], the [`ApplicationId`] of the created application, and
180    /// the chain on which it was created.
181    pub async fn with_current_application<Abi, Parameters, InstantiationArgument>(
182        parameters: Parameters,
183        instantiation_argument: InstantiationArgument,
184    ) -> (TestValidator, ApplicationId<Abi>, ActiveChain)
185    where
186        Abi: ContractAbi,
187        Parameters: Serialize,
188        InstantiationArgument: Serialize,
189    {
190        let (validator, module_id) = Box::pin(TestValidator::with_current_module::<
191            Abi,
192            Parameters,
193            InstantiationArgument,
194        >())
195        .await;
196
197        let mut creator = Box::pin(validator.new_chain()).await;
198
199        let application_id = creator
200            .create_application(module_id, parameters, instantiation_argument, vec![])
201            .await;
202
203        (validator, application_id, creator)
204    }
205
206    /// Returns this validator's storage.
207    pub(crate) fn storage(&self) -> &DbStorage<MemoryDatabase, TestClock> {
208        &self.storage
209    }
210
211    /// Returns the locked [`WorkerState`] of this validator.
212    pub(crate) fn worker(&self) -> WorkerState<DbStorage<MemoryDatabase, TestClock>> {
213        self.worker.clone()
214    }
215
216    /// Returns the [`TestClock`] of this validator.
217    pub fn clock(&self) -> &TestClock {
218        &self.clock
219    }
220
221    /// Returns the keys this test validator uses for signing certificates.
222    pub fn key_pair(&self) -> &ValidatorSecretKey {
223        &self.validator_secret
224    }
225
226    /// Returns the ID of the admin chain.
227    pub fn admin_chain_id(&self) -> ChainId {
228        self.admin_chain_id
229    }
230
231    /// Returns the latest committee that this test validator is part of.
232    ///
233    /// The committee contains only this validator.
234    pub async fn committee(&self) -> MappedMutexGuard<'_, (Epoch, Committee), Committee> {
235        MutexGuard::map(self.committee.lock().await, |(_epoch, committee)| committee)
236    }
237
238    /// Updates the admin chain, creating a new epoch with an updated
239    /// [`ResourceControlPolicy`].
240    pub async fn change_resource_control_policy(
241        &mut self,
242        adjustment: impl FnOnce(&mut ResourceControlPolicy),
243    ) {
244        let (epoch, committee) = {
245            let (ref mut epoch, ref mut committee) = &mut *self.committee.lock().await;
246
247            epoch
248                .try_add_assign_one()
249                .expect("Reached the limit of epochs");
250
251            adjustment(committee.policy_mut());
252
253            (*epoch, committee.clone())
254        };
255
256        let admin_chain = self.get_chain(&self.admin_chain_id);
257
258        let committee_blob = Blob::new(BlobContent::new_committee(
259            bcs::to_bytes(&committee).unwrap(),
260        ));
261        let blob_hash = committee_blob.id().hash;
262        self.storage
263            .write_blob(&committee_blob)
264            .await
265            .expect("Should write committee blob");
266
267        Box::pin(admin_chain.add_block(|block| {
268            block.with_system_operation(SystemOperation::Admin(AdminOperation::CreateCommittee {
269                epoch,
270                blob_hash,
271            }));
272        }))
273        .await;
274
275        let pinned = self.chains.pin();
276        for chain in pinned.values() {
277            if chain.id() != self.admin_chain_id {
278                Box::pin(chain.add_block(|block| {
279                    block.with_system_operation(SystemOperation::ProcessNewEpoch(epoch));
280                }))
281                .await;
282            }
283        }
284    }
285
286    /// Creates a new microchain and returns the [`ActiveChain`] that can be used to add blocks to
287    /// it with the given key pair.
288    pub async fn new_chain_with_keypair(&self, key_pair: AccountSecretKey) -> ActiveChain {
289        let description =
290            Box::pin(self.request_new_chain_from_admin_chain(key_pair.public().into())).await;
291        let chain = ActiveChain::new(key_pair, description.clone(), self.clone());
292
293        Box::pin(chain.handle_received_messages()).await;
294
295        self.chains.pin().insert(description.id(), chain.clone());
296
297        chain
298    }
299
300    /// Creates a new microchain and returns the [`ActiveChain`] that can be used to add blocks to
301    /// it.
302    pub async fn new_chain(&self) -> ActiveChain {
303        let key_pair = AccountSecretKey::generate();
304        Box::pin(self.new_chain_with_keypair(key_pair)).await
305    }
306
307    /// Adds an existing [`ActiveChain`].
308    pub fn add_chain(&self, chain: ActiveChain) {
309        self.chains.pin().insert(chain.id(), chain);
310    }
311
312    /// Adds a block to the admin chain to create a new chain.
313    ///
314    /// Returns the [`ChainDescription`] of the new chain.
315    async fn request_new_chain_from_admin_chain(&self, owner: AccountOwner) -> ChainDescription {
316        let admin_chain_id = self.admin_chain_id;
317        let pinned = self.chains.pin();
318        let admin_chain = pinned
319            .get(&admin_chain_id)
320            .expect("Admin chain should be created when the `TestValidator` is constructed");
321
322        let open_chain_config = OpenChainConfig {
323            ownership: ChainOwnership::single(owner),
324            balance: Amount::from_tokens(10),
325            application_permissions: ApplicationPermissions::default(),
326        };
327
328        // Query the admin chain's committees to get the correct min/max active epochs,
329        // matching what the execution does in `SystemExecutionStateView::open_chain`.
330        let chain_state = Box::pin(self.worker.chain_state_view(admin_chain_id))
331            .await
332            .expect("Failed to read admin chain state");
333        let committees = chain_state.execution_state.system.committees.get();
334        let epoch = *chain_state.execution_state.system.epoch.get();
335        let min_active_epoch = committees.keys().min().copied().unwrap_or(Epoch::ZERO);
336        let max_active_epoch = committees.keys().max().copied().unwrap_or(Epoch::ZERO);
337        drop(chain_state);
338
339        let new_chain_config =
340            open_chain_config.init_chain_config(epoch, min_active_epoch, max_active_epoch);
341
342        let (certificate, _) = Box::pin(admin_chain.add_block(|block| {
343            block.with_system_operation(SystemOperation::OpenChain(open_chain_config));
344        }))
345        .await;
346        let block = certificate.inner().block();
347
348        let origin = ChainOrigin::Child {
349            parent: block.header.chain_id,
350            block_height: block.header.height,
351            chain_index: 0,
352        };
353
354        ChainDescription::new(origin, new_chain_config, Timestamp::from(0))
355    }
356
357    /// Returns the [`ActiveChain`] reference to the microchain identified by `chain_id`.
358    pub fn get_chain(&self, chain_id: &ChainId) -> ActiveChain {
359        self.chains
360            .pin()
361            .get(chain_id)
362            .expect("Chain not found")
363            .clone()
364    }
365}