Skip to main content

fuel_core_shared_sequencer/
service.rs

1//! Defines the logic how to interact with the shared sequencer.
2
3use crate::{
4    Client,
5    Config,
6    http_api::AccountMetadata,
7    ports::{
8        BlocksProvider,
9        Signer,
10    },
11};
12use async_trait::async_trait;
13use core::time::Duration;
14use fuel_core_services::{
15    EmptyShared,
16    RunnableService,
17    RunnableTask,
18    ServiceRunner,
19    StateWatcher,
20    TaskNextAction,
21    stream::BoxStream,
22};
23use fuel_core_types::services::{
24    block_importer::SharedImportResult,
25    shared_sequencer::{
26        SSBlob,
27        SSBlobs,
28    },
29};
30use futures::StreamExt;
31use std::sync::Arc;
32
33/// Non-initialized shared sequencer task.
34pub struct NonInitializedTask<S> {
35    config: Config,
36    signer: Arc<S>,
37    blocks_events: BoxStream<SharedImportResult>,
38}
39
40/// Initialized shared sequencer task.
41pub struct Task<S> {
42    /// The client that communicates with shared sequencer.
43    shared_sequencer_client: Option<Client>,
44    config: Config,
45    signer: Arc<S>,
46    account_metadata: Option<AccountMetadata>,
47    prev_order: Option<u64>,
48    blobs: Arc<tokio::sync::Mutex<SSBlobs>>,
49    interval: tokio::time::Interval,
50}
51
52impl<S> NonInitializedTask<S> {
53    /// Create a new shared sequencer task.
54    fn new(
55        config: Config,
56        blocks_events: BoxStream<SharedImportResult>,
57        signer: Arc<S>,
58    ) -> anyhow::Result<Self> {
59        if config.enabled && config.endpoints.is_none() {
60            return Err(anyhow::anyhow!(
61                "Shared sequencer is enabled but no endpoints are set"
62            ));
63        }
64
65        Ok(Self {
66            config,
67            blocks_events,
68            signer,
69        })
70    }
71}
72
73#[async_trait]
74impl<S> RunnableService for NonInitializedTask<S>
75where
76    S: Signer + 'static,
77{
78    const NAME: &'static str = "SharedSequencer";
79
80    type SharedData = EmptyShared;
81    type Task = Task<S>;
82    type TaskParams = ();
83
84    fn shared_data(&self) -> Self::SharedData {
85        EmptyShared
86    }
87
88    async fn into_task(
89        mut self,
90        state: &StateWatcher,
91        _: Self::TaskParams,
92    ) -> anyhow::Result<Self::Task> {
93        let shared_sequencer_client = match &self.config.endpoints {
94            Some(endpoints) => {
95                let mut state = state.clone();
96                let init = Client::new(
97                    endpoints.clone(),
98                    self.config.topic,
99                    self.config.http_request_timeout,
100                    self.config.http_connect_timeout,
101                );
102
103                let ss = tokio::select! {
104                    biased;
105                    _ = state.wait_stopping_or_stopped() => {
106                        return Err(anyhow::anyhow!(
107                            "Shutdown requested during SharedSequencer initialization"
108                        ));
109                    }
110                    res = init => res?,
111                };
112
113                if self.signer.is_available() {
114                    let cosmos_public_address =
115                        ss.sender_account_id(self.signer.as_ref())?;
116
117                    tracing::info!(
118                        "Shared sequencer uses account ID: {}",
119                        cosmos_public_address
120                    );
121                }
122
123                Some(ss)
124            }
125            _ => None,
126        };
127
128        let blobs = Arc::new(tokio::sync::Mutex::new(SSBlobs::new()));
129
130        if self.config.enabled {
131            let mut block_events = self.blocks_events;
132
133            tokio::task::spawn({
134                let blobs = blobs.clone();
135                async move {
136                    while let Some(block) = block_events.next().await {
137                        let blob = SSBlob {
138                            block_height: *block.sealed_block.entity.header().height(),
139                            block_id: block.sealed_block.entity.id(),
140                        };
141                        blobs.lock().await.push(blob);
142                    }
143                }
144            });
145        }
146
147        Ok(Task {
148            interval: tokio::time::interval(self.config.block_posting_frequency),
149            shared_sequencer_client,
150            config: self.config,
151            signer: self.signer,
152            account_metadata: None,
153            prev_order: None,
154            blobs,
155        })
156    }
157}
158
159impl<S> Task<S>
160where
161    S: Signer,
162{
163    /// Fetch latest account metadata if it's not set
164    async fn ensure_account_metadata(&mut self) -> anyhow::Result<()> {
165        if self.account_metadata.is_some() {
166            return Ok(());
167        }
168        let ss = self
169            .shared_sequencer_client
170            .as_ref()
171            .expect("Shared sequencer client is not set");
172        self.account_metadata = Some(ss.get_account_meta(self.signer.as_ref()).await?);
173        Ok(())
174    }
175
176    /// Fetch previous order in the topic if it's not set
177    async fn ensure_prev_order(&mut self) -> anyhow::Result<()> {
178        if self.prev_order.is_some() {
179            return Ok(());
180        }
181        let ss = self
182            .shared_sequencer_client
183            .as_ref()
184            .expect("Shared sequencer client is not set");
185        self.prev_order = ss.get_topic().await?.map(|f| f.order);
186        Ok(())
187    }
188}
189
190impl<S> RunnableTask for Task<S>
191where
192    S: Signer + 'static,
193{
194    async fn run(&mut self, watcher: &mut StateWatcher) -> TaskNextAction {
195        if !self.config.enabled {
196            let _ = watcher.while_started().await;
197            return TaskNextAction::Stop
198        }
199
200        tokio::select! {
201            biased;
202            _ = watcher.while_started() => return TaskNextAction::Stop,
203            res = self.ensure_account_metadata() => {
204                if let Err(err) = res {
205                    // We don't want to spam the RPC endpoint with a lot of queries,
206                    // so wait for one second before sending the next one — but still
207                    // bail out immediately on shutdown.
208                    tokio::select! {
209                        biased;
210                        _ = watcher.while_started() => return TaskNextAction::Stop,
211                        _ = tokio::time::sleep(Duration::from_secs(1)) => {}
212                    }
213                    return TaskNextAction::ErrorContinue(err)
214                }
215            }
216        }
217
218        tokio::select! {
219            biased;
220            _ = watcher.while_started() => return TaskNextAction::Stop,
221            res = self.ensure_prev_order() => {
222                if let Err(err) = res {
223                    return TaskNextAction::ErrorContinue(err)
224                }
225            }
226        }
227
228        tokio::select! {
229            biased;
230            _ = watcher.while_started() => {
231                TaskNextAction::Stop
232            },
233            _ = self.interval.tick() => {
234                let blobs = {
235                    let mut lock = self.blobs.lock().await;
236                    core::mem::take(&mut *lock)
237                };
238                if blobs.is_empty() {
239                    tokio::select! {
240                        biased;
241                        _ = watcher.while_started() => return TaskNextAction::Stop,
242                        _ = tokio::time::sleep(Duration::from_secs(1)) => {}
243                    }
244                    return TaskNextAction::Continue;
245                };
246
247                let mut account = self.account_metadata.take().expect("Account metadata is not set");
248                let next_order = self.prev_order.map(|prev| prev.wrapping_add(1)).unwrap_or(0);
249                let ss =  self.shared_sequencer_client
250                    .as_ref().expect("Shared sequencer client is not set");
251                let blobs_bytes = postcard::to_allocvec(&blobs).expect("Failed to serialize SSBlob");
252
253                let send = ss.send(self.signer.as_ref(), account, next_order, blobs_bytes);
254                tokio::select! {
255                    biased;
256                    _ = watcher.while_started() => return TaskNextAction::Stop,
257                    res = send => {
258                        if let Err(err) = res {
259                            return TaskNextAction::ErrorContinue(err);
260                        }
261                    }
262                }
263
264                tracing::info!("Posted block to shared sequencer {blobs:?}");
265                account.sequence = account.sequence.saturating_add(1);
266                self.prev_order = Some(next_order);
267                self.account_metadata = Some(account);
268                TaskNextAction::Continue
269            },
270        }
271    }
272
273    async fn shutdown(self) -> anyhow::Result<()> {
274        // Nothing to shut down because we don't have any temporary state that should be dumped,
275        // and we don't spawn any sub-tasks that we need to finish or await.
276        Ok(())
277    }
278}
279
280/// Creates an instance of runnable shared sequencer service.
281pub fn new_service<B, S>(
282    block_provider: B,
283    config: Config,
284    signer: Arc<S>,
285) -> anyhow::Result<ServiceRunner<NonInitializedTask<S>>>
286where
287    B: BlocksProvider,
288    S: Signer,
289{
290    let blocks_events = block_provider.subscribe();
291    Ok(ServiceRunner::new(NonInitializedTask::new(
292        config,
293        blocks_events,
294        signer,
295    )?))
296}