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