darkfi/blockchain/tx_store.rs
1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2026 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use std::collections::HashMap;
20
21use darkfi_sdk::tx::TransactionHash;
22use darkfi_serial::{deserialize, serialize};
23use sled_overlay::sled;
24
25use crate::{tx::Transaction, Error, Result};
26
27use super::{parse_record, SledDbOverlayPtr};
28
29pub const SLED_TX_TREE: &[u8] = b"_transactions";
30pub const SLED_TX_LOCATION_TREE: &[u8] = b"_transaction_location";
31pub const SLED_PENDING_TX_TREE: &[u8] = b"_pending_transactions";
32
33/// The `TxStore` is a structure representing all `sled` trees related
34/// to storing the blockchain's transactions information.
35#[derive(Clone)]
36pub struct TxStore {
37 /// Main `sled` tree, storing all the blockchain's transactions, where
38 /// the key is the transaction hash, and the value is the serialized
39 /// transaction.
40 pub main: sled::Tree,
41 /// The `sled` tree storing the location of the blockchain's transactions
42 /// locations, where the key is the transaction hash, and the value is a
43 /// serialized tuple containing the height and the vector index of the
44 /// block the transaction is included.
45 pub location: sled::Tree,
46 /// The `sled` tree storing all the node pending transactions, where
47 /// the key is the transaction hash, and the value is the serialized
48 /// transaction.
49 pub pending: sled::Tree,
50}
51
52impl TxStore {
53 /// Opens a new or existing `TxStore` on the given sled database.
54 pub fn new(db: &sled::Db) -> Result<Self> {
55 let main = db.open_tree(SLED_TX_TREE)?;
56 let location = db.open_tree(SLED_TX_LOCATION_TREE)?;
57 let pending = db.open_tree(SLED_PENDING_TX_TREE)?;
58 Ok(Self { main, location, pending })
59 }
60
61 /// Insert a slice of [`Transaction`] into the store's main tree.
62 pub fn insert(&self, transactions: &[Transaction]) -> Result<Vec<TransactionHash>> {
63 let (batch, ret) = self.insert_batch(transactions);
64 self.main.apply_batch(batch)?;
65 Ok(ret)
66 }
67
68 /// Insert a slice of [`TransactionHash`] into the store's location tree.
69 pub fn insert_location(&self, txs_hashes: &[TransactionHash], block_height: u32) -> Result<()> {
70 let batch = self.insert_batch_location(txs_hashes, block_height);
71 self.location.apply_batch(batch)?;
72 Ok(())
73 }
74
75 /// Insert a slice of [`Transaction`] into the store's pending txs tree.
76 pub fn insert_pending(&self, transactions: &[Transaction]) -> Result<Vec<TransactionHash>> {
77 let (batch, ret) = self.insert_batch_pending(transactions);
78 self.pending.apply_batch(batch)?;
79 Ok(ret)
80 }
81
82 /// Generate the sled batch corresponding to an insert to the main tree,
83 /// so caller can handle the write operation.
84 /// The transactions are hashed with BLAKE3 and this hash is used as
85 /// the key, while the value is the serialized [`Transaction`] itself.
86 /// On success, the function returns the transaction hashes in the same
87 /// order as the input transactions, along with the corresponding operation
88 /// batch.
89 pub fn insert_batch(
90 &self,
91 transactions: &[Transaction],
92 ) -> (sled::Batch, Vec<TransactionHash>) {
93 let mut ret = Vec::with_capacity(transactions.len());
94 let mut batch = sled::Batch::default();
95
96 for tx in transactions {
97 let tx_hash = tx.hash();
98 batch.insert(tx_hash.inner(), serialize(tx));
99 ret.push(tx_hash);
100 }
101
102 (batch, ret)
103 }
104
105 /// Generate the sled batch corresponding to an insert to the location tree,
106 /// so caller can handle the write operation.
107 /// The location tuple is built using the index of each transaction has in
108 /// the slice, along with the provided block height
109 pub fn insert_batch_location(
110 &self,
111 txs_hashes: &[TransactionHash],
112 block_height: u32,
113 ) -> sled::Batch {
114 let mut batch = sled::Batch::default();
115
116 for (index, tx_hash) in txs_hashes.iter().enumerate() {
117 let serialized = serialize(&(block_height, index as u16));
118 batch.insert(tx_hash.inner(), serialized);
119 }
120
121 batch
122 }
123
124 /// Generate the sled batch corresponding to an insert to the pending txs tree,
125 /// so caller can handle the write operation.
126 /// The transactions are hashed with BLAKE3 and this hash is used as
127 /// the key, while the value is the serialized [`Transaction`] itself.
128 /// On success, the function returns the transaction hashes in the same
129 /// order as the input transactions, along with the corresponding operation
130 /// batch.
131 pub fn insert_batch_pending(
132 &self,
133 transactions: &[Transaction],
134 ) -> (sled::Batch, Vec<TransactionHash>) {
135 let mut ret = Vec::with_capacity(transactions.len());
136 let mut batch = sled::Batch::default();
137
138 for tx in transactions {
139 let tx_hash = tx.hash();
140 batch.insert(tx_hash.inner(), serialize(tx));
141 ret.push(tx_hash);
142 }
143
144 (batch, ret)
145 }
146
147 /// Check if the store's main tree contains a given transaction hash.
148 pub fn contains(&self, tx_hash: &TransactionHash) -> Result<bool> {
149 Ok(self.main.contains_key(tx_hash.inner())?)
150 }
151
152 /// Check if the store's pending txs tree contains a given transaction hash.
153 pub fn contains_pending(&self, tx_hash: &TransactionHash) -> Result<bool> {
154 Ok(self.pending.contains_key(tx_hash.inner())?)
155 }
156
157 /// Fetch given tx hashes from the store's main tree.
158 /// The resulting vector contains `Option`, which is `Some` if the tx
159 /// was found in the txstore, and otherwise it is `None`, if it has not.
160 /// The second parameter is a boolean which tells the function to fail in
161 /// case at least one tx was not found.
162 pub fn get(
163 &self,
164 tx_hashes: &[TransactionHash],
165 strict: bool,
166 ) -> Result<Vec<Option<Transaction>>> {
167 let mut ret = Vec::with_capacity(tx_hashes.len());
168
169 for tx_hash in tx_hashes {
170 if let Some(found) = self.main.get(tx_hash.inner())? {
171 let tx = deserialize(&found)?;
172 ret.push(Some(tx));
173 continue
174 }
175 if strict {
176 return Err(Error::TransactionNotFound(tx_hash.as_string()))
177 }
178 ret.push(None);
179 }
180
181 Ok(ret)
182 }
183
184 /// Fetch given tx hashes locations from the store's location tree.
185 /// The resulting vector contains `Option`, which is `Some` if the tx
186 /// was found in the txstore, and otherwise it is `None`, if it has not.
187 /// The second parameter is a boolean which tells the function to fail in
188 /// case at least one tx was not found.
189 pub fn get_location(
190 &self,
191 tx_hashes: &[TransactionHash],
192 strict: bool,
193 ) -> Result<Vec<Option<(u32, u16)>>> {
194 let mut ret = Vec::with_capacity(tx_hashes.len());
195
196 for tx_hash in tx_hashes {
197 if let Some(found) = self.location.get(tx_hash.inner())? {
198 let location = deserialize(&found)?;
199 ret.push(Some(location));
200 continue
201 }
202 if strict {
203 return Err(Error::TransactionNotFound(tx_hash.as_string()))
204 }
205 ret.push(None);
206 }
207
208 Ok(ret)
209 }
210
211 /// Fetch given tx hashes from the store's pending txs tree.
212 /// The resulting vector contains `Option`, which is `Some` if the tx
213 /// was found in the pending tx store, and otherwise it is `None`, if it has not.
214 /// The second parameter is a boolean which tells the function to fail in
215 /// case at least one tx was not found.
216 pub fn get_pending(
217 &self,
218 tx_hashes: &[TransactionHash],
219 strict: bool,
220 ) -> Result<Vec<Option<Transaction>>> {
221 let mut ret = Vec::with_capacity(tx_hashes.len());
222
223 for tx_hash in tx_hashes {
224 if let Some(found) = self.pending.get(tx_hash.inner())? {
225 let tx = deserialize(&found)?;
226 ret.push(Some(tx));
227 continue
228 }
229 if strict {
230 return Err(Error::TransactionNotFound(tx_hash.as_string()))
231 }
232 ret.push(None);
233 }
234
235 Ok(ret)
236 }
237
238 /// Retrieve all transactions from the store's main tree in the form of
239 /// a tuple (`tx_hash`, `tx`).
240 /// Be careful as this will try to load everything in memory.
241 pub fn get_all(&self) -> Result<Vec<(TransactionHash, Transaction)>> {
242 let mut txs = vec![];
243
244 for tx in self.main.iter() {
245 txs.push(parse_record(tx.unwrap())?);
246 }
247
248 Ok(txs)
249 }
250
251 /// Retrieve all transactions locations from the store's location tree in
252 /// the form of a tuple (`tx_hash`, (`block_height`, `index`)).
253 /// Be careful as this will try to load everything in memory.
254 pub fn get_all_location(&self) -> Result<Vec<(TransactionHash, (u32, u16))>> {
255 let mut locations = vec![];
256
257 for location in self.location.iter() {
258 locations.push(parse_record(location.unwrap())?);
259 }
260
261 Ok(locations)
262 }
263
264 /// Retrieve all transactions from the store's pending txs tree in the
265 /// form of a HashMap with key the transaction hash and value the
266 /// transaction itself.
267 /// Be careful as this will try to load everything in memory.
268 pub fn get_all_pending(&self) -> Result<HashMap<TransactionHash, Transaction>> {
269 let mut txs = HashMap::new();
270
271 for tx in self.pending.iter() {
272 let (key, value) = parse_record(tx.unwrap())?;
273 txs.insert(key, value);
274 }
275
276 Ok(txs)
277 }
278
279 /// Fetch n transactions after given transaction hash.
280 pub fn get_after_pending(
281 &self,
282 tx_hash: &TransactionHash,
283 n: usize,
284 ) -> Result<Vec<Transaction>> {
285 let mut txs = vec![];
286 let mut key = tx_hash.inner().into();
287 let mut counter = 0;
288 while counter < n {
289 if let Some((tx_hash, tx)) = self.pending.get_gt(key)? {
290 key = tx_hash;
291 txs.push(deserialize(&tx)?);
292 counter += 1;
293 continue
294 }
295 break
296 }
297
298 Ok(txs)
299 }
300
301 /// Retrieve records count of the store's main tree.
302 pub fn len(&self) -> usize {
303 self.main.len()
304 }
305
306 /// Check if the store's main tree is empty.
307 pub fn is_empty(&self) -> bool {
308 self.main.is_empty()
309 }
310
311 /// Remove a slice of [`TransactionHash`] from the store's pending txs tree.
312 pub fn remove_pending(&self, txs_hashes: &[TransactionHash]) -> Result<()> {
313 let batch = self.remove_batch_pending(txs_hashes);
314 self.pending.apply_batch(batch)?;
315 Ok(())
316 }
317
318 /// Generate the sled batch corresponding to a remove from the store's pending
319 /// txs tree, so caller can handle the write operation.
320 pub fn remove_batch_pending(&self, txs_hashes: &[TransactionHash]) -> sled::Batch {
321 let mut batch = sled::Batch::default();
322
323 for tx_hash in txs_hashes {
324 batch.remove(tx_hash.inner());
325 }
326
327 batch
328 }
329
330 /// Generate the sled batch corresponding to a remove from the store's pending
331 /// txs order tree, so caller can handle the write operation.
332 pub fn remove_batch_pending_order(&self, indexes: &[u64]) -> sled::Batch {
333 let mut batch = sled::Batch::default();
334
335 for index in indexes {
336 batch.remove(&index.to_be_bytes());
337 }
338
339 batch
340 }
341}
342
343/// Overlay structure over a [`TxStore`] instance.
344pub struct TxStoreOverlay(SledDbOverlayPtr);
345
346impl TxStoreOverlay {
347 pub fn new(overlay: &SledDbOverlayPtr) -> Result<Self> {
348 overlay.lock().unwrap().open_tree(SLED_TX_TREE, true)?;
349 overlay.lock().unwrap().open_tree(SLED_TX_LOCATION_TREE, true)?;
350 Ok(Self(overlay.clone()))
351 }
352
353 /// Insert a slice of [`Transaction`] into the overlay's main tree.
354 /// The transactions are hashed with BLAKE3 and this hash is used as
355 /// the key, while the value is the serialized [`Transaction`] itself.
356 /// On success, the function returns the transaction hashes in the same
357 /// order as the input transactions.
358 pub fn insert(&self, transactions: &[Transaction]) -> Result<Vec<TransactionHash>> {
359 let mut ret = Vec::with_capacity(transactions.len());
360 let mut lock = self.0.lock().unwrap();
361
362 for tx in transactions {
363 let tx_hash = tx.hash();
364 lock.insert(SLED_TX_TREE, tx_hash.inner(), &serialize(tx))?;
365 ret.push(tx_hash);
366 }
367
368 Ok(ret)
369 }
370
371 /// Insert a slice of [`TransactionHash`] into the overlay's location tree.
372 /// The location tuple is built using the index of each transaction hash
373 /// in the slice, along with the provided block height
374 pub fn insert_location(&self, txs_hashes: &[TransactionHash], block_height: u32) -> Result<()> {
375 let mut lock = self.0.lock().unwrap();
376
377 for (index, tx_hash) in txs_hashes.iter().enumerate() {
378 let serialized = serialize(&(block_height, index as u16));
379 lock.insert(SLED_TX_LOCATION_TREE, tx_hash.inner(), &serialized)?;
380 }
381
382 Ok(())
383 }
384
385 /// Check if the overlay's main tree contains a given transaction hash.
386 pub fn contains(&self, tx_hash: &TransactionHash) -> Result<bool> {
387 Ok(self.0.lock().unwrap().contains_key(SLED_TX_TREE, tx_hash.inner())?)
388 }
389
390 /// Fetch given tx hashes from the overlay's main tree.
391 /// The resulting vector contains `Option`, which is `Some` if the tx
392 /// was found in the overlay, and otherwise it is `None`, if it has not.
393 /// The second parameter is a boolean which tells the function to fail in
394 /// case at least one tx was not found.
395 pub fn get(
396 &self,
397 tx_hashes: &[TransactionHash],
398 strict: bool,
399 ) -> Result<Vec<Option<Transaction>>> {
400 let mut ret = Vec::with_capacity(tx_hashes.len());
401 let lock = self.0.lock().unwrap();
402
403 for tx_hash in tx_hashes {
404 if let Some(found) = lock.get(SLED_TX_TREE, tx_hash.inner())? {
405 let tx = deserialize(&found)?;
406 ret.push(Some(tx));
407 continue
408 }
409 if strict {
410 return Err(Error::TransactionNotFound(tx_hash.as_string()))
411 }
412 ret.push(None);
413 }
414
415 Ok(ret)
416 }
417
418 /// Fetch given tx hash from the overlay's main tree. This function uses
419 /// raw bytes as input and doesn't deserialize the retrieved value.
420 /// The resulting vector contains `Option`, which is `Some` if the tx
421 /// was found in the overlay, and otherwise it is `None`, if it has not.
422 pub fn get_raw(&self, tx_hash: &[u8; 32]) -> Result<Option<Vec<u8>>> {
423 let lock = self.0.lock().unwrap();
424 if let Some(found) = lock.get(SLED_TX_TREE, tx_hash)? {
425 return Ok(Some(found.to_vec()))
426 }
427 Ok(None)
428 }
429
430 /// Fetch given tx hash location from the overlay's location tree.
431 /// This function uses raw bytes as input and doesn't deserialize the
432 /// retrieved value. The resulting vector contains `Option`, which is
433 /// `Some` if the location was found in the overlay, and otherwise it
434 /// is `None`, if it has not.
435 pub fn get_location_raw(&self, tx_hash: &[u8; 32]) -> Result<Option<Vec<u8>>> {
436 let lock = self.0.lock().unwrap();
437 if let Some(found) = lock.get(SLED_TX_LOCATION_TREE, tx_hash)? {
438 return Ok(Some(found.to_vec()))
439 }
440 Ok(None)
441 }
442}