Skip to main content

linera_execution/
transaction_tracker.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    collections::{BTreeMap, BTreeSet},
6    future::Future,
7    mem, vec,
8};
9
10use custom_debug_derive::Debug;
11use linera_base::{
12    data_types::{Blob, BlobContent, Event, OracleResponse, StreamUpdate, Timestamp},
13    ensure,
14    identifiers::{ApplicationId, BlobId, ChainId, StreamId},
15};
16
17use crate::{ExecutionError, OutgoingMessage};
18
19type AppStreamUpdates = BTreeMap<(ChainId, StreamId), (u32, u32)>;
20
21/// Tracks oracle responses and execution outcomes of an ongoing transaction execution, as well
22/// as replayed oracle responses.
23#[derive(Debug, Default)]
24pub struct TransactionTracker {
25    #[debug(skip_if = Option::is_none)]
26    replaying_oracle_responses: Option<vec::IntoIter<OracleResponse>>,
27    #[debug(skip_if = Vec::is_empty)]
28    oracle_responses: Vec<OracleResponse>,
29    #[debug(skip_if = Vec::is_empty)]
30    outgoing_messages: Vec<OutgoingMessage>,
31    /// The current local time.
32    local_time: Timestamp,
33    /// The index of the current transaction in the block.
34    transaction_index: u32,
35    next_application_index: u32,
36    next_chain_index: u32,
37    /// Events recorded by contracts' `emit` calls.
38    events: Vec<Event>,
39    /// Blobs created by contracts.
40    ///
41    /// As of right now, blobs created by the contracts are one of the following types:
42    /// - [`Data`]
43    /// - [`ContractBytecode`]
44    /// - [`ServiceBytecode`]
45    /// - [`EvmBytecode`]
46    /// - [`ApplicationDescription`]
47    /// - [`ChainDescription`]
48    blobs: BTreeMap<BlobId, BlobContent>,
49    /// The blobs created in the previous transactions.
50    previously_created_blobs: BTreeMap<BlobId, BlobContent>,
51    /// Operation result.
52    operation_result: Option<Vec<u8>>,
53    /// Streams that have been updated but not yet processed during this transaction.
54    streams_to_process: BTreeMap<ApplicationId, AppStreamUpdates>,
55    /// Published blobs this transaction refers to by [`BlobId`].
56    blobs_published: BTreeSet<BlobId>,
57    /// Blob IDs created or published by free apps (fees waived).
58    free_blob_ids: BTreeSet<BlobId>,
59}
60
61/// The [`TransactionTracker`] contents after a transaction has finished.
62#[derive(Debug, Default)]
63pub struct TransactionOutcome {
64    /// The recorded oracle responses.
65    #[debug(skip_if = Vec::is_empty)]
66    pub oracle_responses: Vec<OracleResponse>,
67    /// The messages to be sent to other chains.
68    #[debug(skip_if = Vec::is_empty)]
69    pub outgoing_messages: Vec<OutgoingMessage>,
70    /// The index to be assigned to the next created application.
71    pub next_application_index: u32,
72    /// The index to be assigned to the next created chain.
73    pub next_chain_index: u32,
74    /// Events recorded by contracts' `emit` calls.
75    pub events: Vec<Event>,
76    /// Blobs created by contracts.
77    pub blobs: Vec<Blob>,
78    /// Operation result.
79    pub operation_result: Vec<u8>,
80    /// Blobs published by this transaction.
81    pub blobs_published: BTreeSet<BlobId>,
82    /// Blob IDs created or published by free apps (fees waived).
83    pub free_blob_ids: BTreeSet<BlobId>,
84}
85
86impl TransactionTracker {
87    /// Creates a new [`TransactionTracker`].
88    pub fn new(
89        local_time: Timestamp,
90        transaction_index: u32,
91        next_application_index: u32,
92        next_chain_index: u32,
93        oracle_responses: Option<Vec<OracleResponse>>,
94        blobs: &[Vec<Blob>],
95    ) -> Self {
96        let mut previously_created_blobs = BTreeMap::new();
97        for tx_blobs in blobs {
98            for blob in tx_blobs {
99                previously_created_blobs.insert(blob.id(), blob.content().clone());
100            }
101        }
102        TransactionTracker {
103            local_time,
104            transaction_index,
105            next_application_index,
106            next_chain_index,
107            replaying_oracle_responses: oracle_responses.map(Vec::into_iter),
108            previously_created_blobs,
109            ..Self::default()
110        }
111    }
112
113    /// Sets the blobs known to the tracker and returns the updated tracker.
114    pub fn with_blobs(mut self, blobs: BTreeMap<BlobId, BlobContent>) -> Self {
115        self.blobs = blobs;
116        self
117    }
118
119    /// Returns the local time recorded by the tracker.
120    pub fn local_time(&self) -> Timestamp {
121        self.local_time
122    }
123
124    /// Sets the local time recorded by the tracker.
125    pub fn set_local_time(&mut self, local_time: Timestamp) {
126        self.local_time = local_time;
127    }
128
129    /// Returns the index of the current transaction in the block.
130    pub fn transaction_index(&self) -> u32 {
131        self.transaction_index
132    }
133
134    /// Returns the index to be assigned to the next created application and increments the counter.
135    pub fn next_application_index(&mut self) -> u32 {
136        let index = self.next_application_index;
137        self.next_application_index += 1;
138        index
139    }
140
141    /// Returns the index to be assigned to the next created chain and increments the counter.
142    pub fn next_chain_index(&mut self) -> u32 {
143        let index = self.next_chain_index;
144        self.next_chain_index += 1;
145        index
146    }
147
148    /// Records an outgoing message.
149    pub fn add_outgoing_message(&mut self, message: OutgoingMessage) {
150        self.outgoing_messages.push(message);
151    }
152
153    /// Records multiple outgoing messages.
154    pub fn add_outgoing_messages(&mut self, messages: impl IntoIterator<Item = OutgoingMessage>) {
155        for message in messages {
156            self.add_outgoing_message(message);
157        }
158    }
159
160    /// Records an event emitted on the given stream.
161    pub fn add_event(&mut self, stream_id: StreamId, index: u32, value: Vec<u8>) {
162        self.events.push(Event {
163            stream_id,
164            index,
165            value,
166        });
167    }
168
169    /// Returns the content of the blob with the given ID, if known to the tracker.
170    pub fn get_blob_content(&self, blob_id: &BlobId) -> Option<&BlobContent> {
171        if let Some(content) = self.blobs.get(blob_id) {
172            return Some(content);
173        }
174        self.previously_created_blobs.get(blob_id)
175    }
176
177    /// Records a blob created by this transaction.
178    pub fn add_created_blob(&mut self, blob: Blob) {
179        self.blobs.insert(blob.id(), blob.into_content());
180    }
181
182    /// Records a blob published by this transaction.
183    pub fn add_published_blob(&mut self, blob_id: BlobId) {
184        self.blobs_published.insert(blob_id);
185    }
186
187    /// Marks a blob as created/published by a free app, so its fees will be waived.
188    pub fn mark_blob_free(&mut self, blob_id: BlobId) {
189        self.free_blob_ids.insert(blob_id);
190    }
191
192    /// Returns the blobs created by this transaction.
193    pub fn created_blobs(&self) -> &BTreeMap<BlobId, BlobContent> {
194        &self.blobs
195    }
196
197    /// Records the result of the operation.
198    pub fn add_operation_result(&mut self, result: Option<Vec<u8>>) {
199        self.operation_result = result
200    }
201
202    /// In replay mode, returns the next recorded oracle response. Otherwise executes `f` and
203    /// records and returns the result. `f` is the implementation of the actual oracle and is
204    /// only called in validation mode, so it does not have to be fully deterministic.
205    pub async fn oracle<F, G>(&mut self, f: F) -> Result<&OracleResponse, ExecutionError>
206    where
207        F: FnOnce() -> G,
208        G: Future<Output = Result<OracleResponse, ExecutionError>>,
209    {
210        let response = match self.next_replayed_oracle_response()? {
211            Some(response) => response,
212            None => f().await?,
213        };
214        self.oracle_responses.push(response);
215        Ok(self.oracle_responses.last().unwrap())
216    }
217
218    /// Records that the given range of events on a stream must be processed by the application.
219    pub fn add_stream_to_process(
220        &mut self,
221        application_id: ApplicationId,
222        chain_id: ChainId,
223        stream_id: StreamId,
224        previous_index: u32,
225        next_index: u32,
226    ) {
227        if next_index == previous_index {
228            return; // No new events in the stream.
229        }
230        self.streams_to_process
231            .entry(application_id)
232            .or_default()
233            .entry((chain_id, stream_id))
234            .and_modify(|(pi, ni)| {
235                *pi = (*pi).min(previous_index);
236                *ni = (*ni).max(next_index);
237            })
238            .or_insert_with(|| (previous_index, next_index));
239    }
240
241    /// Removes a stream from the set of streams to be processed by the application.
242    pub fn remove_stream_to_process(
243        &mut self,
244        application_id: ApplicationId,
245        chain_id: ChainId,
246        stream_id: StreamId,
247    ) {
248        let Some(streams) = self.streams_to_process.get_mut(&application_id) else {
249            return;
250        };
251        if streams.remove(&(chain_id, stream_id)).is_some() && streams.is_empty() {
252            self.streams_to_process.remove(&application_id);
253        }
254    }
255
256    /// Takes the streams to be processed, grouped by application.
257    pub fn take_streams_to_process(&mut self) -> BTreeMap<ApplicationId, Vec<StreamUpdate>> {
258        mem::take(&mut self.streams_to_process)
259            .into_iter()
260            .map(|(app_id, streams)| {
261                let updates = streams
262                    .into_iter()
263                    .map(
264                        |((chain_id, stream_id), (previous_index, next_index))| StreamUpdate {
265                            chain_id,
266                            stream_id,
267                            previous_index,
268                            next_index,
269                        },
270                    )
271                    .collect();
272                (app_id, updates)
273            })
274            .collect()
275    }
276
277    /// Adds the oracle response to the record.
278    /// If replaying, it also checks that it matches the next replayed one and returns `true`.
279    pub fn replay_oracle_response(
280        &mut self,
281        oracle_response: OracleResponse,
282    ) -> Result<bool, ExecutionError> {
283        let replaying = if let Some(recorded_response) = self.next_replayed_oracle_response()? {
284            ensure!(
285                recorded_response == oracle_response,
286                ExecutionError::OracleResponseMismatch
287            );
288            true
289        } else {
290            false
291        };
292        self.oracle_responses.push(oracle_response);
293        Ok(replaying)
294    }
295
296    /// If in replay mode, returns the next oracle response, or an error if it is missing.
297    ///
298    /// If not in replay mode, `None` is returned, and the caller must execute the actual oracle
299    /// to obtain the value.
300    ///
301    /// In both cases, the value (returned or obtained from the oracle) must be recorded using
302    /// `add_oracle_response`.
303    fn next_replayed_oracle_response(&mut self) -> Result<Option<OracleResponse>, ExecutionError> {
304        let Some(responses) = &mut self.replaying_oracle_responses else {
305            return Ok(None); // Not in replay mode.
306        };
307        let response = responses
308            .next()
309            .ok_or_else(|| ExecutionError::MissingOracleResponse)?;
310        Ok(Some(response))
311    }
312
313    /// Consumes the tracker and returns the resulting [`TransactionOutcome`].
314    pub fn into_outcome(self) -> Result<TransactionOutcome, ExecutionError> {
315        let TransactionTracker {
316            replaying_oracle_responses,
317            oracle_responses,
318            outgoing_messages,
319            local_time: _,
320            transaction_index: _,
321            next_application_index,
322            next_chain_index,
323            events,
324            blobs,
325            previously_created_blobs: _,
326            operation_result,
327            streams_to_process,
328            blobs_published,
329            free_blob_ids,
330        } = self;
331        ensure!(
332            streams_to_process.is_empty(),
333            ExecutionError::UnprocessedStreams
334        );
335        if let Some(mut responses) = replaying_oracle_responses {
336            ensure!(
337                responses.next().is_none(),
338                ExecutionError::UnexpectedOracleResponse
339            );
340        }
341        let blobs = blobs
342            .into_iter()
343            .map(|(blob_id, content)| Blob::new_with_hash_unchecked(blob_id, content))
344            .collect::<Vec<_>>();
345        Ok(TransactionOutcome {
346            outgoing_messages,
347            oracle_responses,
348            next_application_index,
349            next_chain_index,
350            events,
351            blobs,
352            operation_result: operation_result.unwrap_or_default(),
353            blobs_published,
354            free_blob_ids,
355        })
356    }
357}
358
359#[cfg(with_testing)]
360impl TransactionTracker {
361    /// Creates a new [`TransactionTracker`] for testing, with default values and the given
362    /// oracle responses.
363    pub fn new_replaying(oracle_responses: Vec<OracleResponse>) -> Self {
364        TransactionTracker::new(Timestamp::from(0), 0, 0, 0, Some(oracle_responses), &[])
365    }
366
367    /// Creates a new [`TransactionTracker`] for testing, with default values and oracle responses
368    /// for the given blobs.
369    pub fn new_replaying_blobs<T>(blob_ids: T) -> Self
370    where
371        T: IntoIterator,
372        T::Item: std::borrow::Borrow<BlobId>,
373    {
374        use std::borrow::Borrow;
375
376        let oracle_responses = blob_ids
377            .into_iter()
378            .map(|blob_id| OracleResponse::Blob(*blob_id.borrow()))
379            .collect();
380        TransactionTracker::new_replaying(oracle_responses)
381    }
382}