Skip to main content

linera_sdk/test/
block.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! A builder of [`Block`]s which are then signed to become [`Certificate`]s.
5//!
6//! Helps with the construction of blocks, adding operations and
7
8use linera_base::{
9    abi::ContractAbi,
10    data_types::{Amount, ApplicationPermissions, Blob, Epoch, Timestamp},
11    identifiers::{Account, AccountOwner, ApplicationId, ChainId},
12    ownership::TimeoutConfig,
13};
14use linera_chain::{
15    data_types::{
16        BundleExecutionPolicy, IncomingBundle, MessageAction, ProposedBlock, Transaction, Vote,
17    },
18    justification::JustificationChain,
19    test::VoteTestExt,
20    types::{ConfirmedBlock, ConfirmedBlockCertificate},
21};
22use linera_core::{data_types::ChainInfoQuery, worker::WorkerError};
23use linera_execution::{system::SystemOperation, Operation, ResourceTracker};
24
25use super::TestValidator;
26
27/// A helper type to build a block proposal using the builder pattern, and then signing them into
28/// [`ConfirmedBlockCertificate`]s using a [`TestValidator`].
29pub struct BlockBuilder {
30    block: ProposedBlock,
31    validator: TestValidator,
32}
33
34impl BlockBuilder {
35    /// Creates a new [`BlockBuilder`], initializing the block so that it belongs to a microchain.
36    ///
37    /// Initializes the block so that it belongs to the microchain identified by `chain_id` and
38    /// owned by `owner`. It becomes the block after the specified `previous_block`, or the genesis
39    /// block if [`None`] is specified.
40    ///
41    /// The block's timestamp defaults to the maximum of the parent block's timestamp and the
42    /// validator's current clock time, ensuring it satisfies the validity rule that a block's
43    /// timestamp must not be earlier than its parent's. Use [`with_timestamp`](Self::with_timestamp)
44    /// to override.
45    ///
46    /// # Notes
47    ///
48    /// This is an internal method, because the [`BlockBuilder`] instance should be built by an
49    /// [`ActiveChain`]. External users should only be able to add operations and messages to the
50    /// block.
51    pub(crate) fn new(
52        chain_id: ChainId,
53        owner: AccountOwner,
54        epoch: Epoch,
55        previous_block: Option<&ConfirmedBlockCertificate>,
56        validator: TestValidator,
57    ) -> Self {
58        let previous_block_hash = previous_block.map(|certificate| certificate.hash());
59        let height = previous_block
60            .map(|certificate| {
61                certificate
62                    .inner()
63                    .height()
64                    .try_add_one()
65                    .expect("Block height limit reached")
66            })
67            .unwrap_or_default();
68        let parent_timestamp = previous_block
69            .map(|certificate| certificate.inner().timestamp())
70            .unwrap_or_default();
71        let timestamp = parent_timestamp.max(validator.clock().current_time());
72
73        BlockBuilder {
74            block: ProposedBlock {
75                epoch,
76                chain_id,
77                transactions: vec![],
78                previous_block_hash,
79                height,
80                authenticated_owner: Some(owner),
81                timestamp,
82            },
83            validator,
84        }
85    }
86
87    /// Configures the timestamp of this block.
88    ///
89    /// The timestamp must be at least as large as the parent block's timestamp (which is used as
90    /// the default). It must also be at least as large as the timestamp of any incoming message
91    /// bundle added via [`with_messages_from`](Self::with_messages_from) or
92    /// [`with_messages_from_by_action`](Self::with_messages_from_by_action).
93    pub fn with_timestamp(&mut self, timestamp: Timestamp) -> &mut Self {
94        self.block.timestamp = timestamp;
95        self
96    }
97
98    /// Adds a native token transfer to this block.
99    pub fn with_native_token_transfer(
100        &mut self,
101        sender: AccountOwner,
102        recipient: Account,
103        amount: Amount,
104    ) -> &mut Self {
105        self.with_system_operation(SystemOperation::Transfer {
106            owner: sender,
107            recipient,
108            amount,
109        })
110    }
111
112    /// Adds a [`SystemOperation`] to this block.
113    pub(crate) fn with_system_operation(&mut self, operation: SystemOperation) -> &mut Self {
114        self.block
115            .transactions
116            .push(Transaction::ExecuteOperation(operation.into()));
117        self
118    }
119
120    /// Adds an operation to change this chain's ownership.
121    pub fn with_owner_change(
122        &mut self,
123        super_owners: Vec<AccountOwner>,
124        owners: Vec<(AccountOwner, u64)>,
125        first_leader: Option<AccountOwner>,
126        multi_leader_rounds: u32,
127        open_multi_leader_rounds: bool,
128        timeout_config: TimeoutConfig,
129    ) -> &mut Self {
130        self.with_system_operation(SystemOperation::ChangeOwnership {
131            super_owners,
132            owners,
133            first_leader,
134            multi_leader_rounds,
135            open_multi_leader_rounds,
136            timeout_config,
137        })
138    }
139
140    /// Adds an application permissions change to this block.
141    pub fn with_change_application_permissions(
142        &mut self,
143        permissions: ApplicationPermissions,
144    ) -> &mut Self {
145        self.with_system_operation(SystemOperation::ChangeApplicationPermissions(permissions))
146    }
147
148    /// Adds a user `operation` to this block.
149    ///
150    /// The operation is serialized using the application ABI and added to the block, marked to be
151    /// executed by `application`.
152    #[expect(clippy::needless_pass_by_value)]
153    pub fn with_operation<Abi>(
154        &mut self,
155        application_id: ApplicationId<Abi>,
156        operation: Abi::Operation,
157    ) -> &mut Self
158    where
159        Abi: ContractAbi,
160    {
161        let operation = <Abi as ContractAbi>::serialize_operation(&operation)
162            .expect("Failed to serialize `Operation` in BlockBuilder");
163        self.with_raw_operation(application_id.forget_abi(), operation)
164    }
165
166    /// Adds an already serialized user `operation` to this block.
167    pub fn with_raw_operation(
168        &mut self,
169        application_id: ApplicationId,
170        operation: impl Into<Vec<u8>>,
171    ) -> &mut Self {
172        self.block
173            .transactions
174            .push(Transaction::ExecuteOperation(Operation::User {
175                application_id,
176                bytes: operation.into(),
177            }));
178        self
179    }
180
181    /// Receives incoming message bundles by specifying them directly.
182    ///
183    /// Automatically advances the block's timestamp to be at least as large as the latest
184    /// bundle's timestamp, since blocks are not allowed to have a timestamp older than any of
185    /// their incoming bundles. Use [`with_timestamp`](Self::with_timestamp) afterwards to set a
186    /// later timestamp if needed.
187    ///
188    /// This is an internal method that bypasses the check to see if the messages are already
189    /// present in the inboxes of the microchain that owns this block.
190    pub(crate) fn with_incoming_bundles(
191        &mut self,
192        bundles: impl IntoIterator<Item = IncomingBundle>,
193    ) -> &mut Self {
194        for bundle in bundles {
195            self.block.timestamp = self.block.timestamp.max(bundle.bundle.timestamp);
196            self.block
197                .transactions
198                .push(Transaction::ReceiveMessages(bundle));
199        }
200        self
201    }
202
203    /// Receives all direct messages that were sent to this chain by the given certificate.
204    ///
205    /// The block's timestamp is automatically advanced to be at least as large as the
206    /// certificate's block timestamp.
207    pub fn with_messages_from(&mut self, certificate: &ConfirmedBlockCertificate) -> &mut Self {
208        self.with_messages_from_by_action(certificate, MessageAction::Accept)
209    }
210
211    /// Receives all messages that were sent to this chain by the given certificate.
212    ///
213    /// The block's timestamp is automatically advanced to be at least as large as the
214    /// certificate's block timestamp.
215    pub fn with_messages_from_by_action(
216        &mut self,
217        certificate: &ConfirmedBlockCertificate,
218        action: MessageAction,
219    ) -> &mut Self {
220        let origin = certificate.inner().chain_id();
221        let bundles =
222            certificate
223                .message_bundles_for(self.block.chain_id)
224                .map(|(_epoch, bundle)| IncomingBundle {
225                    origin,
226                    bundle,
227                    action,
228                });
229        self.with_incoming_bundles(bundles)
230    }
231
232    /// Tries to sign the prepared block with the [`TestValidator`]'s keys and return the
233    /// resulting [`Certificate`] and the [`ResourceTracker`] with execution costs.
234    /// Returns an error if block execution fails.
235    pub(crate) async fn try_sign(
236        self,
237        blobs: &[Blob],
238    ) -> Result<(ConfirmedBlockCertificate, ResourceTracker), WorkerError> {
239        let published_blobs = self
240            .block
241            .published_blob_ids()
242            .into_iter()
243            .map(|blob_id| {
244                blobs
245                    .iter()
246                    .find(|blob| blob.id() == blob_id)
247                    .expect("missing published blob")
248                    .clone()
249            })
250            .collect();
251        let (_, block, _, resource_tracker, _) = self
252            .validator
253            .worker()
254            .stage_block_execution(
255                self.block,
256                None,
257                published_blobs,
258                BundleExecutionPolicy::committed(),
259            )
260            .await?;
261
262        let value = ConfirmedBlock::new(block);
263        // Confirm in the chain's first round so the votes can carry the first-round attestation
264        // and the certificate needs no justification chain. The chain's current ownership (the
265        // parent's, since this block isn't committed yet) is the one that governs this block's
266        // rounds.
267        let info = self
268            .validator
269            .worker()
270            .handle_chain_info_query(ChainInfoQuery::new(value.chain_id()))
271            .await
272            .expect("Failed to query chain ownership")
273            .info;
274        let round = info.manager.ownership.first_round();
275        let public_key = self.validator.key_pair().public();
276        // A first-round confirmation attests that no lower round exists, so it commits to no
277        // justifying quorum.
278        let quorum =
279            Vote::new_with_first_round(value, round, true, None, self.validator.key_pair())
280                .into_certificate(public_key);
281        let certificate =
282            ConfirmedBlockCertificate::from_parts(quorum, JustificationChain::default());
283
284        Ok((certificate, resource_tracker))
285    }
286}