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