Skip to main content

linera_chain/test/
mod.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Test utilities
5
6mod http_server;
7
8use std::collections::BTreeMap;
9
10use linera_base::{
11    crypto::{CryptoHash, Signer, ValidatorPublicKey},
12    data_types::{Amount, Blob, BlockHeight, Epoch, Event, OracleResponse, Round, Timestamp},
13    identifiers::{Account, AccountOwner, ChainId},
14};
15use linera_execution::{Message, MessageKind, Operation, OutgoingMessage, SystemOperation};
16
17pub use self::http_server::HttpServer;
18use crate::{
19    block::{Block, ConfirmedBlock},
20    data_types::{
21        BlockExecutionOutcome, BlockProposal, IncomingBundle, OperationResult, PostedMessage,
22        ProposedBlock, Transaction, Vote,
23    },
24    types::{CertificateValue, GenericCertificate},
25};
26
27/// Creates a new child of the given block, with the same timestamp.
28pub fn make_child_block(parent: &ConfirmedBlock) -> ProposedBlock {
29    let parent_header = &parent.block().header;
30    ProposedBlock {
31        epoch: parent_header.epoch,
32        chain_id: parent_header.chain_id,
33        transactions: vec![],
34        previous_block_hash: Some(parent.hash()),
35        height: parent_header.height.try_add_one().unwrap(),
36        authenticated_owner: parent_header.authenticated_owner,
37        timestamp: parent_header.timestamp,
38    }
39}
40
41/// Creates a block at height 0 for a new chain.
42pub fn make_first_block(chain_id: ChainId) -> ProposedBlock {
43    ProposedBlock {
44        epoch: Epoch::ZERO,
45        chain_id,
46        transactions: vec![],
47        previous_block_hash: None,
48        height: BlockHeight::ZERO,
49        authenticated_owner: None,
50        timestamp: Timestamp::default(),
51    }
52}
53
54/// Builds a [`Block`] for tests with a header that stays consistent with its body (the
55/// header is computed via [`Block::new`]), so it round-trips through serialization and
56/// storage. Tests start from an empty body and add messages, events, and so on.
57pub struct BlockBuilder {
58    block: ProposedBlock,
59    outcome: BlockExecutionOutcome,
60}
61
62impl BlockBuilder {
63    /// Starts building a block at the given chain and height with an empty body.
64    pub fn new(chain_id: ChainId, height: BlockHeight) -> Self {
65        BlockBuilder {
66            block: ProposedBlock {
67                epoch: Epoch::ZERO,
68                chain_id,
69                transactions: vec![],
70                previous_block_hash: None,
71                height,
72                authenticated_owner: None,
73                timestamp: Timestamp::default(),
74            },
75            outcome: BlockExecutionOutcome {
76                state_hash: CryptoHash::default(),
77                messages: vec![],
78                previous_message_blocks: BTreeMap::new(),
79                previous_event_blocks: BTreeMap::new(),
80                oracle_responses: vec![],
81                events: vec![],
82                blobs: vec![],
83                operation_results: vec![],
84            },
85        }
86    }
87
88    /// Sets the execution state hash recorded in the header.
89    pub fn with_state_hash(mut self, state_hash: CryptoHash) -> Self {
90        self.outcome.state_hash = state_hash;
91        self
92    }
93
94    /// Appends a transaction to the block's inputs.
95    pub fn with_transaction(mut self, transaction: Transaction) -> Self {
96        self.block.transactions.push(transaction);
97        self
98    }
99
100    /// Appends one transaction's outgoing messages to the body.
101    pub fn with_messages(mut self, messages: Vec<OutgoingMessage>) -> Self {
102        self.outcome.messages.push(messages);
103        self
104    }
105
106    /// Appends one transaction's events to the body.
107    pub fn with_events(mut self, events: Vec<Event>) -> Self {
108        self.outcome.events.push(events);
109        self
110    }
111
112    /// Appends one transaction's oracle responses to the body.
113    pub fn with_oracle_responses(mut self, oracle_responses: Vec<OracleResponse>) -> Self {
114        self.outcome.oracle_responses.push(oracle_responses);
115        self
116    }
117
118    /// Appends one transaction's created blobs to the body.
119    pub fn with_blobs(mut self, blobs: Vec<Blob>) -> Self {
120        self.outcome.blobs.push(blobs);
121        self
122    }
123
124    /// Appends an operation result to the body.
125    pub fn with_operation_result(mut self, operation_result: OperationResult) -> Self {
126        self.outcome.operation_results.push(operation_result);
127        self
128    }
129
130    /// Builds the block, computing a header that is consistent with the body.
131    pub fn build(self) -> Block {
132        self.outcome.with(self.block)
133    }
134}
135
136/// A helper trait to simplify constructing blocks for tests.
137#[allow(async_fn_in_trait)]
138pub trait BlockTestExt: Sized {
139    /// Returns the block with the given authenticated owner.
140    fn with_authenticated_owner(self, authenticated_owner: Option<AccountOwner>) -> Self;
141
142    /// Returns the block with the given operation appended at the end.
143    fn with_operation(self, operation: impl Into<Operation>) -> Self;
144
145    /// Returns the block with a transfer operation appended at the end.
146    fn with_transfer(self, owner: AccountOwner, recipient: Account, amount: Amount) -> Self;
147
148    /// Returns the block with a simple transfer operation appended at the end.
149    fn with_simple_transfer(self, chain_id: ChainId, amount: Amount) -> Self;
150
151    /// Returns the block with the given message appended at the end.
152    fn with_incoming_bundle(self, incoming_bundle: IncomingBundle) -> Self;
153
154    /// Returns the block with the given messages appended at the end.
155    fn with_incoming_bundles(
156        self,
157        incoming_bundles: impl IntoIterator<Item = IncomingBundle>,
158    ) -> Self;
159
160    /// Returns the block with the specified timestamp.
161    fn with_timestamp(self, timestamp: impl Into<Timestamp>) -> Self;
162
163    /// Returns the block with the specified epoch.
164    fn with_epoch(self, epoch: impl Into<Epoch>) -> Self;
165
166    /// Returns the block with the burn operation (transfer to a special address) appended at the end.
167    fn with_burn(self, amount: Amount) -> Self;
168
169    /// Returns a block proposal in the first round in a default ownership configuration
170    /// (`Round::MultiLeader(0)`) without any hashed certificate values or validated block.
171    async fn into_first_proposal<S: Signer + ?Sized>(
172        self,
173        owner: AccountOwner,
174        signer: &S,
175    ) -> Result<BlockProposal, S::Error> {
176        self.into_proposal_with_round(owner, signer, Round::MultiLeader(0))
177            .await
178    }
179
180    /// Returns a block proposal without any hashed certificate values or validated block.
181    async fn into_proposal_with_round<S: Signer + ?Sized>(
182        self,
183        owner: AccountOwner,
184        signer: &S,
185        round: Round,
186    ) -> Result<BlockProposal, S::Error>;
187}
188
189impl BlockTestExt for ProposedBlock {
190    fn with_authenticated_owner(mut self, authenticated_owner: Option<AccountOwner>) -> Self {
191        self.authenticated_owner = authenticated_owner;
192        self
193    }
194
195    fn with_operation(mut self, operation: impl Into<Operation>) -> Self {
196        self.transactions
197            .push(Transaction::ExecuteOperation(operation.into()));
198        self
199    }
200
201    fn with_transfer(self, owner: AccountOwner, recipient: Account, amount: Amount) -> Self {
202        self.with_operation(SystemOperation::Transfer {
203            owner,
204            recipient,
205            amount,
206        })
207    }
208
209    fn with_simple_transfer(self, chain_id: ChainId, amount: Amount) -> Self {
210        self.with_transfer(AccountOwner::CHAIN, Account::chain(chain_id), amount)
211    }
212
213    fn with_burn(self, amount: Amount) -> Self {
214        let recipient = Account::burn_address(self.chain_id);
215        self.with_operation(SystemOperation::Transfer {
216            owner: AccountOwner::CHAIN,
217            recipient,
218            amount,
219        })
220    }
221
222    fn with_incoming_bundle(mut self, incoming_bundle: IncomingBundle) -> Self {
223        self.transactions
224            .push(Transaction::ReceiveMessages(incoming_bundle));
225        self
226    }
227
228    fn with_incoming_bundles(
229        mut self,
230        incoming_bundles: impl IntoIterator<Item = IncomingBundle>,
231    ) -> Self {
232        self.transactions.extend(
233            incoming_bundles
234                .into_iter()
235                .map(Transaction::ReceiveMessages),
236        );
237        self
238    }
239
240    fn with_timestamp(mut self, timestamp: impl Into<Timestamp>) -> Self {
241        self.timestamp = timestamp.into();
242        self
243    }
244
245    fn with_epoch(mut self, epoch: impl Into<Epoch>) -> Self {
246        self.epoch = epoch.into();
247        self
248    }
249
250    async fn into_proposal_with_round<S: Signer + ?Sized>(
251        self,
252        owner: AccountOwner,
253        signer: &S,
254        round: Round,
255    ) -> Result<BlockProposal, S::Error> {
256        BlockProposal::new_initial(owner, round, self, signer).await
257    }
258}
259
260/// Helper trait to simplify creating certificates from votes in tests.
261pub trait VoteTestExt<T: CertificateValue>: Sized {
262    /// Returns a certificate for a committee consisting only of this validator.
263    fn into_certificate(self, public_key: ValidatorPublicKey) -> GenericCertificate<T>;
264}
265
266impl<T: CertificateValue> VoteTestExt<T> for Vote<T> {
267    fn into_certificate(self, public_key: ValidatorPublicKey) -> GenericCertificate<T> {
268        // Preserve the vote's own signed payload — unlocking round, first-round attestation and
269        // justification commitment — so this works for any vote, not just the default payload.
270        GenericCertificate::new_with_payload(
271            self.value,
272            self.round,
273            self.unlocking_round,
274            self.first_round,
275            self.justification_commitment,
276            vec![(public_key, self.signature)],
277        )
278    }
279}
280
281/// Helper trait to simplify constructing messages for tests.
282pub trait MessageTestExt: Sized {
283    /// Wraps the message into a [`PostedMessage`] with the given kind.
284    fn to_posted(self, kind: MessageKind) -> PostedMessage;
285}
286
287impl<T: Into<Message>> MessageTestExt for T {
288    fn to_posted(self, kind: MessageKind) -> PostedMessage {
289        PostedMessage {
290            authenticated_owner: None,
291            grant: Amount::ZERO,
292            refund_grant_to: None,
293            kind,
294            message: self.into(),
295        }
296    }
297}