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 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        );
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.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 = validator.new_chain().await;
167
168        let module_id = 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) =
191            TestValidator::with_current_module::<Abi, Parameters, InstantiationArgument>().await;
192
193        let mut creator = validator.new_chain().await;
194
195        let application_id = creator
196            .create_application(module_id, parameters, instantiation_argument, vec![])
197            .await;
198
199        (validator, application_id, creator)
200    }
201
202    /// Returns this validator's storage.
203    pub(crate) fn storage(&self) -> &DbStorage<MemoryStore, TestClock> {
204        &self.storage
205    }
206
207    /// Returns the locked [`WorkerState`] of this validator.
208    pub(crate) fn worker(&self) -> WorkerState<DbStorage<MemoryStore, TestClock>> {
209        self.worker.clone()
210    }
211
212    /// Returns the [`TestClock`] of this validator.
213    pub fn clock(&self) -> &TestClock {
214        &self.clock
215    }
216
217    /// Returns the keys this test validator uses for signing certificates.
218    pub fn key_pair(&self) -> &ValidatorSecretKey {
219        &self.validator_secret
220    }
221
222    /// Returns the ID of the admin chain.
223    pub fn admin_chain_id(&self) -> ChainId {
224        self.admin_chain_id
225    }
226
227    /// Returns the latest committee that this test validator is part of.
228    ///
229    /// The committee contains only this validator.
230    pub async fn committee(&self) -> MappedMutexGuard<'_, (Epoch, Committee), Committee> {
231        MutexGuard::map(self.committee.lock().await, |(_epoch, committee)| committee)
232    }
233
234    /// Updates the admin chain, creating a new epoch with an updated
235    /// [`ResourceControlPolicy`].
236    pub async fn change_resource_control_policy(
237        &mut self,
238        adjustment: impl FnOnce(&mut ResourceControlPolicy),
239    ) {
240        let (epoch, committee) = {
241            let (ref mut epoch, ref mut committee) = &mut *self.committee.lock().await;
242
243            epoch
244                .try_add_assign_one()
245                .expect("Reached the limit of epochs");
246
247            adjustment(committee.policy_mut());
248
249            (*epoch, committee.clone())
250        };
251
252        let admin_chain = self.get_chain(&self.admin_chain_id);
253
254        let committee_blob = Blob::new(BlobContent::new_committee(
255            bcs::to_bytes(&committee).unwrap(),
256        ));
257        let blob_hash = committee_blob.id().hash;
258        self.storage
259            .write_blob(&committee_blob)
260            .await
261            .expect("Should write committee blob");
262
263        admin_chain
264            .add_block(|block| {
265                block.with_system_operation(SystemOperation::Admin(
266                    AdminOperation::CreateCommittee { epoch, blob_hash },
267                ));
268            })
269            .await;
270
271        for entry in self.chains.iter() {
272            let chain = entry.value();
273
274            if chain.id() != self.admin_chain_id {
275                chain
276                    .add_block(|block| {
277                        block.with_system_operation(SystemOperation::ProcessNewEpoch(epoch));
278                    })
279                    .await;
280            }
281        }
282    }
283
284    /// Creates a new microchain and returns the [`ActiveChain`] that can be used to add blocks to
285    /// it with the given key pair.
286    pub async fn new_chain_with_keypair(&self, key_pair: AccountSecretKey) -> ActiveChain {
287        let description = self
288            .request_new_chain_from_admin_chain(key_pair.public().into())
289            .await;
290        let chain = ActiveChain::new(key_pair, description.clone(), self.clone());
291
292        chain.handle_received_messages().await;
293
294        self.chains.insert(description.id(), chain.clone());
295
296        chain
297    }
298
299    /// Creates a new microchain and returns the [`ActiveChain`] that can be used to add blocks to
300    /// it.
301    pub async fn new_chain(&self) -> ActiveChain {
302        let key_pair = AccountSecretKey::generate();
303        self.new_chain_with_keypair(key_pair).await
304    }
305
306    /// Adds an existing [`ActiveChain`].
307    pub fn add_chain(&self, chain: ActiveChain) {
308        self.chains.insert(chain.id(), chain);
309    }
310
311    /// Adds a block to the admin chain to create a new chain.
312    ///
313    /// Returns the [`ChainDescription`] of the new chain.
314    async fn request_new_chain_from_admin_chain(&self, owner: AccountOwner) -> ChainDescription {
315        let admin_id = self.admin_chain_id;
316        let admin_chain = self
317            .chains
318            .get(&admin_id)
319            .expect("Admin chain should be created when the `TestValidator` is constructed");
320
321        let (epoch, _) = self.committee.lock().await.clone();
322
323        let open_chain_config = OpenChainConfig {
324            ownership: ChainOwnership::single(owner),
325            balance: Amount::from_tokens(10),
326            application_permissions: ApplicationPermissions::default(),
327        };
328        let new_chain_config = open_chain_config.init_chain_config(epoch, epoch, epoch);
329
330        let certificate = admin_chain
331            .add_block(|block| {
332                block.with_system_operation(SystemOperation::OpenChain(open_chain_config));
333            })
334            .await;
335        let block = certificate.inner().block();
336
337        let origin = ChainOrigin::Child {
338            parent: block.header.chain_id,
339            block_height: block.header.height,
340            chain_index: 0,
341        };
342
343        ChainDescription::new(origin, new_chain_config, Timestamp::from(0))
344    }
345
346    /// Returns the [`ActiveChain`] reference to the microchain identified by `chain_id`.
347    pub fn get_chain(&self, chain_id: &ChainId) -> ActiveChain {
348        self.chains.get(chain_id).expect("Chain not found").clone()
349    }
350}