explorerd/store/
contract_metadata.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 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::sync::{Arc, Mutex, MutexGuard};
20
21use log::debug;
22use sled_overlay::{sled, SledDbOverlay};
23
24use darkfi::{blockchain::SledDbOverlayPtr, Error, Result};
25use darkfi_sdk::crypto::ContractId;
26use darkfi_serial::{async_trait, deserialize, serialize, SerialDecodable, SerialEncodable};
27
28/// Contract metadata tree name.
29pub const SLED_CONTRACT_METADATA_TREE: &[u8] = b"_contact_metadata";
30
31/// Contract source code tree name.
32pub const SLED_CONTRACT_SOURCE_CODE_TREE: &[u8] = b"_contact_source_code";
33
34/// Represents contract metadata containing additional contract information that is not stored on-chain.
35#[derive(Debug, Clone, Eq, PartialEq, SerialEncodable, SerialDecodable)]
36pub struct ContractMetaData {
37    pub name: String,
38    pub description: String,
39}
40
41impl ContractMetaData {
42    pub fn new(name: String, description: String) -> Self {
43        Self { name, description }
44    }
45}
46
47/// Represents a source file containing its file path as a string and its content as a vector of bytes.
48#[derive(Debug, Clone)]
49pub struct ContractSourceFile {
50    pub path: String,
51    pub content: String,
52}
53
54impl ContractSourceFile {
55    /// Creates a `ContractSourceFile` instance.
56    pub fn new(path: String, content: String) -> Self {
57        Self { path, content }
58    }
59}
60
61pub struct ContractMetaStore {
62    /// Pointer to the underlying sled database used by the store and its associated overlay.
63    pub sled_db: sled::Db,
64
65    /// Primary sled tree for storing contract metadata, utilizing [`ContractId::to_string`] as keys
66    /// and serialized [`ContractMetaData`] as values.
67    pub main: sled::Tree,
68
69    /// Sled tree for storing contract source code, utilizing source file paths as keys pre-appended with a contract id
70    /// and serialized contract source code [`ContractSourceFile`] content as values.
71    pub source_code: sled::Tree,
72}
73
74impl ContractMetaStore {
75    /// Creates a `ContractMetaStore` instance.
76    pub fn new(db: &sled::Db) -> Result<Self> {
77        let main = db.open_tree(SLED_CONTRACT_METADATA_TREE)?;
78        let source_code = db.open_tree(SLED_CONTRACT_SOURCE_CODE_TREE)?;
79
80        Ok(Self { sled_db: db.clone(), main, source_code })
81    }
82
83    /// Retrieves associated contract metadata for a given [`ContractId`],
84    /// returning an `Option` of [`ContractMetaData`] upon success.
85    pub fn get(&self, contract_id: &ContractId) -> Result<Option<ContractMetaData>> {
86        let opt = self.main.get(contract_id.to_string().as_bytes())?;
87        opt.map(|bytes| deserialize(&bytes).map_err(Error::from)).transpose()
88    }
89
90    /// Provides the number of stored [`ContractMetaData`].
91    pub fn len(&self) -> usize {
92        self.main.len()
93    }
94
95    /// Checks if there is contract metadata stored.
96    pub fn is_empty(&self) -> bool {
97        self.main.is_empty()
98    }
99
100    /// Retrieves all the source file paths associated for provided [`ContractId`].
101    ///
102    /// This function uses provided [`ContractId`] as a prefix to filter relevant paths
103    /// stored in the underlying sled tree, ensuring only files belonging to
104    /// the given contract ID are included. Returns a `Vec` of [`String`]
105    /// representing source code paths.
106    pub fn get_source_paths(&self, contract_id: &ContractId) -> Result<Vec<String>> {
107        let prefix = format!("{contract_id}/");
108
109        // Get all the source paths for provided `ContractId`
110        let mut entries = self
111            .source_code
112            .scan_prefix(&prefix)
113            .filter_map(|item| {
114                let (key, _) = item.ok()?;
115                let key_str = String::from_utf8(key.to_vec()).ok()?;
116                key_str.strip_prefix(&prefix).map(|path| path.to_string())
117            })
118            .collect::<Vec<String>>();
119
120        // Sort the entries to ensure a consistent order
121        entries.sort();
122
123        Ok(entries)
124    }
125
126    /// Retrieves a source content as a [`String`] given a [`ContractId`] and path.
127    pub fn get_source_content(
128        &self,
129        contract_id: &ContractId,
130        source_path: &str,
131    ) -> Result<Option<String>> {
132        let key = format!("{contract_id}/{source_path}");
133        match self.source_code.get(key.as_bytes())? {
134            Some(ivec) => Ok(Some(String::from_utf8(ivec.to_vec()).map_err(|e| {
135                Error::Custom(format!(
136                    "[get_source_content] Failed to retrieve source content: {e:?}"
137                ))
138            })?)),
139            None => Ok(None),
140        }
141    }
142
143    /// Adds contract source code [`ContractId`] and `Vec` of [`ContractSourceFile`]s to the store,
144    /// deleting existing source associated with the provided contract id before doing so.
145    ///
146    /// Delegates operation to [`ContractMetadataStoreOverlay::insert_source`],
147    /// whose documentation provides more details.
148    pub fn insert_source(
149        &self,
150        contract_id: &ContractId,
151        source: &[ContractSourceFile],
152    ) -> Result<()> {
153        let existing_source_paths = self.get_source_paths(contract_id)?;
154        let overlay = ContractMetadataStoreOverlay::new(self.sled_db.clone())?;
155        overlay.insert_source(contract_id, source, Some(&existing_source_paths))?;
156        Ok(())
157    }
158
159    /// Adds contract metadata using provided [`ContractId`] and [`ContractMetaData`] pairs to the store.
160    ///
161    /// Delegates operation to [`ContractMetadataStoreOverlay::insert_metadata`], whose documentation
162    /// provides more details.
163    pub fn insert_metadata(
164        &self,
165        contract_ids: &[ContractId],
166        metadata: &[ContractMetaData],
167    ) -> Result<()> {
168        let overlay = ContractMetadataStoreOverlay::new(self.sled_db.clone())?;
169        overlay.insert_metadata(contract_ids, metadata)?;
170        Ok(())
171    }
172}
173
174/// The `ContractMetadataStoreOverlay` provides write operations for managing contract metadata in
175/// underlying sled database. It supports inserting new [`ContractMetaData`] and contract source code
176/// [`ContractSourceFile`] content and deleting existing source code.
177struct ContractMetadataStoreOverlay {
178    /// Pointer to the overlay used for accessing and performing database write operations on the store.
179    overlay: SledDbOverlayPtr,
180}
181
182impl ContractMetadataStoreOverlay {
183    /// Instantiate a [`ContractMetadataStoreOverlay`] over the provided [`sled::Db`] instance.
184    pub fn new(db: sled::Db) -> Result<Self> {
185        // Create overlay pointer
186        let overlay = Arc::new(Mutex::new(SledDbOverlay::new(&db, vec![])));
187        Ok(Self { overlay: overlay.clone() })
188    }
189
190    /// Inserts [`ContractSourceFile`]s associated with provided [`ContractId`] into the store's
191    /// [`SLED_CONTRACT_SOURCE_CODE_TREE`], committing the changes upon success.
192    ///
193    /// This function locks the overlay, then inserts the provided source files into the store while
194    /// handling serialization and potential errors. The provided contract ID is used to create a key
195    /// for each source file by prepending the contract ID to each source code path. On success, the
196    /// contract source code is persisted and made available for use.
197    ///
198    /// If optional `source_paths_to_delete` is provided, the function first deletes the existing
199    /// source code associated with these paths before inserting the provided source code.
200    pub fn insert_source(
201        &self,
202        contract_id: &ContractId,
203        source: &[ContractSourceFile],
204        source_paths_to_delete: Option<&[String]>,
205    ) -> Result<()> {
206        // Obtain lock
207        let mut lock = self.lock(SLED_CONTRACT_SOURCE_CODE_TREE)?;
208
209        // Delete existing source when existing paths are provided
210        if let Some(paths_to_delete) = source_paths_to_delete {
211            self.delete_source(contract_id, paths_to_delete, &mut lock)?;
212        };
213
214        // Insert each source code file
215        for source_file in source.iter() {
216            // Create key by pre-pending contract id to the source code path
217            let key = format!("{contract_id}/{}", source_file.path);
218            // Insert the source code
219            lock.insert(
220                SLED_CONTRACT_SOURCE_CODE_TREE,
221                key.as_bytes(),
222                source_file.content.as_bytes(),
223            )?;
224            debug!(target: "explorerd::contract_meta_store::insert_source", "Inserted contract source for path {key}");
225        }
226
227        // Commit the changes
228        lock.apply()?;
229
230        Ok(())
231    }
232
233    /// Deletes source code associated with provided [`ContractId`] from the store's [`SLED_CONTRACT_SOURCE_CODE_TREE`],
234    /// committing the changes upon success.
235    ///
236    /// This auxiliary function locks the overlay, then removes the code associated with the provided
237    /// contract ID from the store, handling serialization and potential errors. The contract ID is
238    /// prepended to each source code path to create the keys for deletion. On success, the contract
239    /// source code is permanently deleted.
240    fn delete_source(
241        &self,
242        contract_id: &ContractId,
243        source_paths: &[String],
244        lock: &mut MutexGuard<SledDbOverlay>,
245    ) -> Result<()> {
246        // Delete each source file associated with provided paths
247        for path in source_paths.iter() {
248            // Create key by pre-pending contract id to the source code path
249            let key = format!("{contract_id}/{path}");
250            // Delete the source code
251            lock.remove(SLED_CONTRACT_SOURCE_CODE_TREE, key.as_bytes())?;
252            debug!(target: "explorerd::contract_meta_store::delete_source", "Deleted contract source for path {key}");
253        }
254
255        Ok(())
256    }
257
258    /// Inserts [`ContractId`] and [`ContractMetaData`] pairs into the store's [`SLED_CONTRACT_METADATA_TREE`],
259    /// committing the changes upon success.
260    ///
261    /// This function locks the overlay, verifies that the contract_ids and metadata arrays have matching lengths,
262    /// then inserts them into the store while handling serialization and potential errors. On success,
263    /// contract metadata is persisted and available for use.
264    pub fn insert_metadata(
265        &self,
266        contract_ids: &[ContractId],
267        metadata: &[ContractMetaData],
268    ) -> Result<()> {
269        let mut lock = self.lock(SLED_CONTRACT_METADATA_TREE)?;
270
271        // Ensure lengths of contract_ids and metadata arrays match
272        if contract_ids.len() != metadata.len() {
273            return Err(Error::Custom(String::from(
274                "The lengths of contract_ids and metadata arrays must match",
275            )));
276        }
277
278        // Insert each contract id and metadata pair
279        for (contract_id, metadata) in contract_ids.iter().zip(metadata.iter()) {
280            // Serialize the gas data
281            let serialized_metadata = serialize(metadata);
282
283            // Insert serialized gas data
284            lock.insert(
285                SLED_CONTRACT_METADATA_TREE,
286                contract_id.to_string().as_bytes(),
287                &serialized_metadata,
288            )?;
289            debug!(target: "explorerd::contract_meta_store::insert_metadata",
290                "Inserted contract metadata for contract_id {contract_id}: {metadata:?}");
291        }
292
293        // Commit the changes
294        lock.apply()?;
295
296        Ok(())
297    }
298
299    /// Acquires a lock on the database, opening a specified tree for write operations, returning a
300    /// [`MutexGuard<SledDbOverlay>`] representing the locked state.
301    pub fn lock(&self, tree_name: &[u8]) -> Result<MutexGuard<SledDbOverlay>> {
302        // Lock the database, open tree, and return lock
303        let mut lock = self.overlay.lock().unwrap();
304        lock.open_tree(tree_name, true)?;
305        Ok(lock)
306    }
307}
308
309#[cfg(test)]
310///  This test module verifies the correct insertion and retrieval of contract metadata and source code.
311mod tests {
312    use super::*;
313    use crate::test_utils::init_logger;
314    use darkfi_sdk::crypto::MONEY_CONTRACT_ID;
315    use sled_overlay::sled::Config;
316
317    // Test source paths data
318    const TEST_SOURCE_PATHS: &[&str] = &["test/source1.rs", "test/source2.rs"];
319
320    // Test source code data
321    const TEST_SOURCE_CONTENT: &[&str] =
322        &["fn main() { println!(\"Hello, world!\"); }", "fn add(a: i32, b: i32) -> i32 { a + b }"];
323
324    /// Tests the storing of contract source code by setting up the store, retrieving loaded source paths
325    /// and verifying that the retrieved paths match against expected results.
326    #[test]
327    fn test_add_contract_source() -> Result<()> {
328        // Setup test, returning initialized contract metadata store
329        let store = setup()?;
330
331        // Load source code tests data
332        let contract_id = load_source_code(&store)?;
333
334        // Initialize expected source paths
335        let expected_source_paths: Vec<String> =
336            TEST_SOURCE_PATHS.iter().map(|s| s.to_string()).collect();
337
338        // Retrieve actual loaded source files
339        let actual_source_paths = store.get_source_paths(contract_id)?;
340
341        // Verify that loaded source code matches expected results
342        assert_eq!(expected_source_paths, actual_source_paths);
343
344        Ok(())
345    }
346
347    /// Validates the retrieval of a contract source file from the metadata store by setting up the store,
348    /// loading test source code data, and verifying that loaded source contents match against
349    /// expected content.
350    #[test]
351    fn test_get_contract_source() -> Result<()> {
352        // Setup test, returning initialized contract metadata store
353        let store = setup()?;
354
355        // Load source code tests data
356        let contract_id = load_source_code(&store)?;
357
358        // Iterate through test data
359        for (source_path, expected_content) in
360            TEST_SOURCE_PATHS.iter().zip(TEST_SOURCE_CONTENT.iter())
361        {
362            // Get the content of the source path from the store
363            let actual_source = store.get_source_content(contract_id, source_path)?;
364
365            // Verify that the source code content is the store
366            assert!(actual_source.is_some(), "No content found for path: {source_path}");
367
368            // Validate that the source content matches expected results
369            assert_eq!(
370                actual_source.unwrap(),
371                expected_content.to_string(),
372                "Actual source does not match the expected results for path: {source_path}"
373            );
374        }
375
376        Ok(())
377    }
378
379    /// Tests the addition of [`ContractMetaData`] to the store by setting up the store, inserting
380    /// metadata, and verifying the inserted data matches the expected results.
381    #[test]
382    fn test_add_metadata() -> Result<()> {
383        // Setup test, returning initialized contract metadata store
384        let store = setup()?;
385
386        // Unique identifier for contracts in tests
387        let contract_id: ContractId = *MONEY_CONTRACT_ID;
388
389        // Declare expected metadata used for test
390        let expected_metadata: ContractMetaData = ContractMetaData::new(
391            "Money Contract".to_string(),
392            "Money Contract Description".to_string(),
393        );
394
395        // Add metadata for the source code to the test
396        store.insert_metadata(&[contract_id], &[expected_metadata.clone()])?;
397
398        // Get the metadata content from the store
399        let actual_metadata = store.get(&contract_id)?;
400
401        // Verify that the metadata exists in the store
402        assert!(actual_metadata.is_some());
403
404        // Verify actual metadata matches expected results
405        assert_eq!(actual_metadata.unwrap(), expected_metadata.clone());
406
407        Ok(())
408    }
409
410    /// Sets up a test case for contract metadata store testing by initializing the logger
411    /// and returning an initialized [`ContractMetaStore`].
412    fn setup() -> Result<ContractMetaStore> {
413        // Initialize logger to show execution output
414        init_logger(simplelog::LevelFilter::Off, vec!["sled", "runtime", "net"]);
415
416        // Initialize an in-memory sled db instance
417        let db = Config::new().temporary(true).open()?;
418
419        // Initialize the contract store
420        ContractMetaStore::new(&db)
421    }
422
423    /// Loads [`TEST_SOURCE_PATHS`] and [`TEST_SOURCE_CONTENT`] into the provided
424    /// [`ContractMetaStore`] to test source code insertion and retrieval.
425    fn load_source_code(store: &ContractMetaStore) -> Result<&'static ContractId> {
426        // Define the contract ID for testing
427        let contract_id = &MONEY_CONTRACT_ID;
428
429        // Define sample source files for testing using the shared paths and content
430        let test_sources: Vec<ContractSourceFile> = TEST_SOURCE_PATHS
431            .iter()
432            .zip(TEST_SOURCE_CONTENT.iter())
433            .map(|(path, content)| ContractSourceFile::new(path.to_string(), content.to_string()))
434            .collect();
435
436        // Add test source code to the store
437        store.insert_source(contract_id, &test_sources)?;
438
439        Ok(contract_id)
440    }
441}