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}