Skip to main content

miden_node_ntx_builder/
lib.rs

1use std::num::NonZeroUsize;
2use std::path::PathBuf;
3use std::sync::Arc;
4use std::time::Duration;
5
6use actor::AccountActorContext;
7use anyhow::Context;
8use builder::MempoolEventStream;
9use chain_state::ChainState;
10use clients::{BlockProducerClient, StoreClient, ValidatorClient};
11use coordinator::Coordinator;
12use db::Db;
13use futures::TryStreamExt;
14use miden_node_utils::ErrorReport;
15use miden_node_utils::lru_cache::LruCache;
16use miden_remote_prover_client::RemoteTransactionProver;
17use tokio::sync::{RwLock, mpsc};
18use url::Url;
19
20pub(crate) type NoteError = Arc<dyn ErrorReport + Send + Sync>;
21
22mod actor;
23mod builder;
24mod chain_state;
25mod clients;
26mod coordinator;
27pub(crate) mod db;
28pub(crate) mod inflight_note;
29pub mod server;
30
31#[cfg(test)]
32pub(crate) mod test_utils;
33
34pub use builder::NetworkTransactionBuilder;
35
36// CONSTANTS
37// =================================================================================================
38
39const COMPONENT: &str = "miden-ntx-builder";
40
41/// Default maximum number of network notes a network transaction is allowed to consume.
42const DEFAULT_MAX_NOTES_PER_TX: NonZeroUsize = NonZeroUsize::new(20).expect("literal is non-zero");
43const _: () = assert!(DEFAULT_MAX_NOTES_PER_TX.get() <= miden_tx::MAX_NUM_CHECKER_NOTES);
44
45/// Default maximum number of network transactions which should be in progress concurrently.
46///
47/// This only counts transactions which are being computed locally and does not include
48/// uncommitted transactions in the mempool.
49const DEFAULT_MAX_CONCURRENT_TXS: usize = 4;
50
51/// Default maximum number of blocks to keep in the chain MMR.
52const DEFAULT_MAX_BLOCK_COUNT: usize = 4;
53
54/// Default channel capacity for account loading from the store.
55const DEFAULT_ACCOUNT_CHANNEL_CAPACITY: usize = 1_000;
56
57/// Default maximum number of attempts to execute a failing note before dropping it.
58const DEFAULT_MAX_NOTE_ATTEMPTS: usize = 30;
59
60/// Default script cache size.
61const DEFAULT_SCRIPT_CACHE_SIZE: NonZeroUsize =
62    NonZeroUsize::new(1_000).expect("literal is non-zero");
63
64/// Default duration after which an idle network account actor will deactivate.
65const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
66
67/// Default maximum number of crashes an account actor is allowed before being deactivated.
68const DEFAULT_MAX_ACCOUNT_CRASHES: usize = 10;
69
70/// Default maximum number of VM execution cycles allowed for a network transaction.
71///
72/// This limits the computational cost of network transactions. The protocol maximum is
73/// `1 << 29` but network transactions should be much cheaper.
74const DEFAULT_MAX_TX_CYCLES: u32 = 1 << 19;
75
76// CONFIGURATION
77// =================================================================================================
78
79/// Configuration for the Network Transaction Builder.
80///
81/// This struct contains all the settings needed to create and run a `NetworkTransactionBuilder`.
82#[derive(Debug, Clone)]
83pub struct NtxBuilderConfig {
84    /// Address of the store gRPC server (ntx-builder API).
85    pub store_url: Url,
86
87    /// Address of the block producer gRPC server.
88    pub block_producer_url: Url,
89
90    /// Address of the validator gRPC server.
91    pub validator_url: Url,
92
93    /// Address of the remote transaction prover. If `None`, transactions will be proven locally.
94    pub tx_prover_url: Option<Url>,
95
96    /// Size of the LRU cache for note scripts. Scripts are fetched from the store and cached
97    /// to avoid repeated gRPC calls.
98    pub script_cache_size: NonZeroUsize,
99
100    /// Maximum number of network transactions which should be in progress concurrently across
101    /// all account actors.
102    pub max_concurrent_txs: usize,
103
104    /// Maximum number of network notes a single transaction is allowed to consume.
105    pub max_notes_per_tx: NonZeroUsize,
106
107    /// Maximum number of attempts to execute a failing note before dropping it.
108    /// Notes use exponential backoff between attempts.
109    pub max_note_attempts: usize,
110
111    /// Maximum number of blocks to keep in the chain MMR. Older blocks are pruned.
112    pub max_block_count: usize,
113
114    /// Channel capacity for loading accounts from the store during startup.
115    pub account_channel_capacity: usize,
116
117    /// Duration after which an idle network account will deactivate.
118    ///
119    /// An account is considered idle once it has no viable notes to consume.
120    /// A deactivated account will reactivate if targeted with new notes.
121    pub idle_timeout: Duration,
122
123    /// Maximum number of crashes before an account deactivated.
124    ///
125    /// Once this limit is reached, no new transactions will be created for this account.
126    pub max_account_crashes: usize,
127
128    /// Maximum number of VM execution cycles allowed for a single network transaction.
129    ///
130    /// Network transactions that exceed this limit will fail with an execution error.
131    /// Defaults to 2^18 cycles.
132    pub max_cycles: u32,
133
134    /// Path to the SQLite database file used for persistent state.
135    pub database_filepath: PathBuf,
136}
137
138impl NtxBuilderConfig {
139    pub fn new(
140        store_url: Url,
141        block_producer_url: Url,
142        validator_url: Url,
143        database_filepath: PathBuf,
144    ) -> Self {
145        Self {
146            store_url,
147            block_producer_url,
148            validator_url,
149            tx_prover_url: None,
150            script_cache_size: DEFAULT_SCRIPT_CACHE_SIZE,
151            max_concurrent_txs: DEFAULT_MAX_CONCURRENT_TXS,
152            max_notes_per_tx: DEFAULT_MAX_NOTES_PER_TX,
153            max_note_attempts: DEFAULT_MAX_NOTE_ATTEMPTS,
154            max_block_count: DEFAULT_MAX_BLOCK_COUNT,
155            account_channel_capacity: DEFAULT_ACCOUNT_CHANNEL_CAPACITY,
156            idle_timeout: DEFAULT_IDLE_TIMEOUT,
157            max_account_crashes: DEFAULT_MAX_ACCOUNT_CRASHES,
158            max_cycles: DEFAULT_MAX_TX_CYCLES,
159            database_filepath,
160        }
161    }
162
163    /// Sets the remote transaction prover URL.
164    ///
165    /// If not set, transactions will be proven locally.
166    #[must_use]
167    pub fn with_tx_prover_url(mut self, url: Option<Url>) -> Self {
168        self.tx_prover_url = url;
169        self
170    }
171
172    /// Sets the script cache size.
173    #[must_use]
174    pub fn with_script_cache_size(mut self, size: NonZeroUsize) -> Self {
175        self.script_cache_size = size;
176        self
177    }
178
179    /// Sets the maximum number of concurrent transactions.
180    #[must_use]
181    pub fn with_max_concurrent_txs(mut self, max: usize) -> Self {
182        self.max_concurrent_txs = max;
183        self
184    }
185
186    /// Sets the maximum number of notes per transaction.
187    ///
188    /// # Panics
189    ///
190    /// Panics if `max` exceeds `miden_tx::MAX_NUM_CHECKER_NOTES`.
191    #[must_use]
192    pub fn with_max_notes_per_tx(mut self, max: NonZeroUsize) -> Self {
193        assert!(
194            max.get() <= miden_tx::MAX_NUM_CHECKER_NOTES,
195            "max_notes_per_tx ({}) exceeds MAX_NUM_CHECKER_NOTES ({})",
196            max,
197            miden_tx::MAX_NUM_CHECKER_NOTES
198        );
199        self.max_notes_per_tx = max;
200        self
201    }
202
203    /// Sets the maximum number of note execution attempts.
204    #[must_use]
205    pub fn with_max_note_attempts(mut self, max: usize) -> Self {
206        self.max_note_attempts = max;
207        self
208    }
209
210    /// Sets the maximum number of blocks to keep in the chain MMR.
211    #[must_use]
212    pub fn with_max_block_count(mut self, max: usize) -> Self {
213        self.max_block_count = max;
214        self
215    }
216
217    /// Sets the account channel capacity for startup loading.
218    #[must_use]
219    pub fn with_account_channel_capacity(mut self, capacity: usize) -> Self {
220        self.account_channel_capacity = capacity;
221        self
222    }
223
224    /// Sets the idle timeout for actors.
225    ///
226    /// Actors that remain idle (no viable notes) for this duration will be deactivated.
227    #[must_use]
228    pub fn with_idle_timeout(mut self, timeout: Duration) -> Self {
229        self.idle_timeout = timeout;
230        self
231    }
232
233    /// Sets the maximum number of crashes before an account actor is deactivated.
234    #[must_use]
235    pub fn with_max_account_crashes(mut self, max: usize) -> Self {
236        self.max_account_crashes = max;
237        self
238    }
239
240    /// Sets the maximum number of VM execution cycles for network transactions.
241    #[must_use]
242    pub fn with_max_cycles(mut self, max: u32) -> Self {
243        self.max_cycles = max;
244        self
245    }
246
247    /// Builds and initializes the network transaction builder.
248    ///
249    /// This method connects to the store and block producer services, fetches the current
250    /// chain tip, and subscribes to mempool events.
251    ///
252    /// # Errors
253    ///
254    /// Returns an error if:
255    /// - The store connection fails
256    /// - The mempool subscription fails (after retries)
257    /// - The store contains no blocks (not bootstrapped)
258    pub async fn build(self) -> anyhow::Result<NetworkTransactionBuilder> {
259        // Set up the database (bootstrap + connection pool).
260        let db = Db::setup(self.database_filepath.clone()).await?;
261
262        // Purge inflight state from previous run.
263        db.purge_inflight().await.context("failed to purge inflight state")?;
264
265        let script_cache = LruCache::new(self.script_cache_size);
266        let coordinator =
267            Coordinator::new(self.max_concurrent_txs, self.max_account_crashes, db.clone());
268
269        let store = StoreClient::new(self.store_url.clone());
270        let block_producer = BlockProducerClient::new(self.block_producer_url.clone());
271        let validator = ValidatorClient::new(self.validator_url.clone());
272        let prover = self.tx_prover_url.clone().map(RemoteTransactionProver::new);
273
274        // Subscribe to mempool first to ensure we don't miss any events. The subscription
275        // replays all inflight transactions, so the subscriber's state is fully reconstructed.
276        let subscription = block_producer
277            .subscribe_to_mempool_with_retry()
278            .await
279            .map_err(|err| anyhow::anyhow!(err))
280            .context("failed to subscribe to mempool events")?;
281        let mempool_events: MempoolEventStream = Box::pin(subscription.into_stream());
282
283        let (chain_tip_header, chain_mmr) = store
284            .get_latest_blockchain_data_with_retry()
285            .await?
286            .context("store should contain a latest block")?;
287
288        // Store the chain tip in the DB.
289        db.upsert_chain_state(chain_tip_header.block_num(), chain_tip_header.clone())
290            .await
291            .context("failed to upsert chain state")?;
292
293        let chain_state = Arc::new(RwLock::new(ChainState::new(chain_tip_header, chain_mmr)));
294
295        let (request_tx, actor_request_rx) = mpsc::channel(1);
296
297        let actor_context = AccountActorContext {
298            block_producer: block_producer.clone(),
299            validator,
300            prover,
301            chain_state: chain_state.clone(),
302            store: store.clone(),
303            script_cache,
304            max_notes_per_tx: self.max_notes_per_tx,
305            max_note_attempts: self.max_note_attempts,
306            idle_timeout: self.idle_timeout,
307            db: db.clone(),
308            request_tx,
309            max_cycles: self.max_cycles,
310        };
311
312        Ok(NetworkTransactionBuilder::new(
313            self,
314            coordinator,
315            store,
316            db,
317            chain_state,
318            actor_context,
319            mempool_events,
320            actor_request_rx,
321        ))
322    }
323}