explorerd/store/
contract_metadata.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
/* This file is part of DarkFi (https://dark.fi)
 *
 * Copyright (C) 2020-2025 Dyne.org foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

use std::sync::{Arc, Mutex, MutexGuard};

use log::debug;
use sled_overlay::{sled, SledDbOverlay};

use darkfi::{blockchain::SledDbOverlayPtr, Error, Result};
use darkfi_sdk::crypto::ContractId;
use darkfi_serial::{async_trait, deserialize, serialize, SerialDecodable, SerialEncodable};

/// Contract metadata tree name.
pub const SLED_CONTRACT_METADATA_TREE: &[u8] = b"_contact_metadata";

/// Contract source code tree name.
pub const SLED_CONTRACT_SOURCE_CODE_TREE: &[u8] = b"_contact_source_code";

/// Represents contract metadata containing additional contract information that is not stored on-chain.
#[derive(Debug, Clone, Eq, PartialEq, SerialEncodable, SerialDecodable)]
pub struct ContractMetaData {
    pub name: String,
    pub description: String,
}

impl ContractMetaData {
    pub fn new(name: String, description: String) -> Self {
        Self { name, description }
    }
}

/// Represents a source file containing its file path as a string and its content as a vector of bytes.
#[derive(Debug, Clone)]
pub struct ContractSourceFile {
    pub path: String,
    pub content: String,
}

impl ContractSourceFile {
    /// Creates a `ContractSourceFile` instance.
    pub fn new(path: String, content: String) -> Self {
        Self { path, content }
    }
}

pub struct ContractMetaStore {
    /// Pointer to the underlying sled database used by the store and its associated overlay.
    pub sled_db: sled::Db,

    /// Primary sled tree for storing contract metadata, utilizing [`ContractId::to_string`] as keys
    /// and serialized [`ContractMetaData`] as values.
    pub main: sled::Tree,

    /// Sled tree for storing contract source code, utilizing source file paths as keys pre-appended with a contract id
    /// and serialized contract source code [`ContractSourceFile`] content as values.
    pub source_code: sled::Tree,
}

impl ContractMetaStore {
    /// Creates a `ContractMetaStore` instance.
    pub fn new(db: &sled::Db) -> Result<Self> {
        let main = db.open_tree(SLED_CONTRACT_METADATA_TREE)?;
        let source_code = db.open_tree(SLED_CONTRACT_SOURCE_CODE_TREE)?;

        Ok(Self { sled_db: db.clone(), main, source_code })
    }

    /// Retrieves associated contract metadata for a given [`ContractId`],
    /// returning an `Option` of [`ContractMetaData`] upon success.
    pub fn get(&self, contract_id: &ContractId) -> Result<Option<ContractMetaData>> {
        let opt = self.main.get(contract_id.to_string().as_bytes())?;
        opt.map(|bytes| deserialize(&bytes).map_err(Error::from)).transpose()
    }

    /// Provides the number of stored [`ContractMetaData`].
    pub fn len(&self) -> usize {
        self.main.len()
    }

    /// Checks if there is contract metadata stored.
    pub fn is_empty(&self) -> bool {
        self.main.is_empty()
    }

    /// Retrieves all the source file paths associated for provided [`ContractId`].
    ///
    /// This function uses provided [`ContractId`] as a prefix to filter relevant paths
    /// stored in the underlying sled tree, ensuring only files belonging to
    /// the given contract ID are included. Returns a `Vec` of [`String`]
    /// representing source code paths.
    pub fn get_source_paths(&self, contract_id: &ContractId) -> Result<Vec<String>> {
        let prefix = format!("{}/", contract_id);

        // Get all the source paths for provided `ContractId`
        let mut entries = self
            .source_code
            .scan_prefix(&prefix)
            .filter_map(|item| {
                let (key, _) = item.ok()?;
                let key_str = String::from_utf8(key.to_vec()).ok()?;
                key_str.strip_prefix(&prefix).map(|path| path.to_string())
            })
            .collect::<Vec<String>>();

        // Sort the entries to ensure a consistent order
        entries.sort();

        Ok(entries)
    }

    /// Retrieves a source content as a [`String`] given a [`ContractId`] and path.
    pub fn get_source_content(
        &self,
        contract_id: &ContractId,
        source_path: &str,
    ) -> Result<Option<String>> {
        let key = format!("{}/{}", contract_id, source_path);
        match self.source_code.get(key.as_bytes())? {
            Some(ivec) => Ok(Some(String::from_utf8(ivec.to_vec()).map_err(|e| {
                Error::Custom(format!(
                    "[get_source_content] Failed to retrieve source content: {e:?}"
                ))
            })?)),
            None => Ok(None),
        }
    }

    /// Adds contract source code [`ContractId`] and `Vec` of [`ContractSourceFile`]s to the store,
    /// deleting existing source associated with the provided contract id before doing so.
    ///
    /// Delegates operation to [`ContractMetadataStoreOverlay::insert_source`],
    /// whose documentation provides more details.
    pub fn insert_source(
        &self,
        contract_id: &ContractId,
        source: &[ContractSourceFile],
    ) -> Result<()> {
        let existing_source_paths = self.get_source_paths(contract_id)?;
        let overlay = ContractMetadataStoreOverlay::new(self.sled_db.clone())?;
        overlay.insert_source(contract_id, source, Some(&existing_source_paths))?;
        Ok(())
    }

    /// Adds contract metadata using provided [`ContractId`] and [`ContractMetaData`] pairs to the store.
    ///
    /// Delegates operation to [`ContractMetadataStoreOverlay::insert_metadata`], whose documentation
    /// provides more details.
    pub fn insert_metadata(
        &self,
        contract_ids: &[ContractId],
        metadata: &[ContractMetaData],
    ) -> Result<()> {
        let overlay = ContractMetadataStoreOverlay::new(self.sled_db.clone())?;
        overlay.insert_metadata(contract_ids, metadata)?;
        Ok(())
    }
}

/// The `ContractMetadataStoreOverlay` provides write operations for managing contract metadata in
/// underlying sled database. It supports inserting new [`ContractMetaData`] and contract source code
/// [`ContractSourceFile`] content and deleting existing source code.
struct ContractMetadataStoreOverlay {
    /// Pointer to the overlay used for accessing and performing database write operations on the store.
    overlay: SledDbOverlayPtr,
}

impl ContractMetadataStoreOverlay {
    /// Instantiate a [`ContractMetadataStoreOverlay`] over the provided [`sled::Db`] instance.
    pub fn new(db: sled::Db) -> Result<Self> {
        // Create overlay pointer
        let overlay = Arc::new(Mutex::new(SledDbOverlay::new(&db, vec![])));
        Ok(Self { overlay: overlay.clone() })
    }

    /// Inserts [`ContractSourceFile`]s associated with provided [`ContractId`] into the store's
    /// [`SLED_CONTRACT_SOURCE_CODE_TREE`], committing the changes upon success.
    ///
    /// This function locks the overlay, then inserts the provided source files into the store while
    /// handling serialization and potential errors. The provided contract ID is used to create a key
    /// for each source file by prepending the contract ID to each source code path. On success, the
    /// contract source code is persisted and made available for use.
    ///
    /// If optional `source_paths_to_delete` is provided, the function first deletes the existing
    /// source code associated with these paths before inserting the provided source code.
    pub fn insert_source(
        &self,
        contract_id: &ContractId,
        source: &[ContractSourceFile],
        source_paths_to_delete: Option<&[String]>,
    ) -> Result<()> {
        // Obtain lock
        let mut lock = self.lock(SLED_CONTRACT_SOURCE_CODE_TREE)?;

        // Delete existing source when existing paths are provided
        if let Some(paths_to_delete) = source_paths_to_delete {
            self.delete_source(contract_id, paths_to_delete, &mut lock)?;
        };

        // Insert each source code file
        for source_file in source.iter() {
            // Create key by pre-pending contract id to the source code path
            let key = format!("{}/{}", contract_id, source_file.path);
            // Insert the source code
            lock.insert(
                SLED_CONTRACT_SOURCE_CODE_TREE,
                key.as_bytes(),
                source_file.content.as_bytes(),
            )?;
            debug!(target: "explorerd::contract_meta_store::insert_source", "Inserted contract source for path {}", key);
        }

        // Commit the changes
        lock.apply()?;

        Ok(())
    }

    /// Deletes source code associated with provided [`ContractId`] from the store's [`SLED_CONTRACT_SOURCE_CODE_TREE`],
    /// committing the changes upon success.
    ///
    /// This auxiliary function locks the overlay, then removes the code associated with the provided
    /// contract ID from the store, handling serialization and potential errors. The contract ID is
    /// prepended to each source code path to create the keys for deletion. On success, the contract
    /// source code is permanently deleted.
    fn delete_source(
        &self,
        contract_id: &ContractId,
        source_paths: &[String],
        lock: &mut MutexGuard<SledDbOverlay>,
    ) -> Result<()> {
        // Delete each source file associated with provided paths
        for path in source_paths.iter() {
            // Create key by pre-pending contract id to the source code path
            let key = format!("{}/{}", contract_id, path);
            // Delete the source code
            lock.remove(SLED_CONTRACT_SOURCE_CODE_TREE, key.as_bytes())?;
            debug!(target: "explorerd::contract_meta_store::delete_source", "Deleted contract source for path {}", key);
        }

        Ok(())
    }

    /// Inserts [`ContractId`] and [`ContractMetaData`] pairs into the store's [`SLED_CONTRACT_METADATA_TREE`],
    /// committing the changes upon success.
    ///
    /// This function locks the overlay, verifies that the contract_ids and metadata arrays have matching lengths,
    /// then inserts them into the store while handling serialization and potential errors. On success,
    /// contract metadata is persisted and available for use.
    pub fn insert_metadata(
        &self,
        contract_ids: &[ContractId],
        metadata: &[ContractMetaData],
    ) -> Result<()> {
        let mut lock = self.lock(SLED_CONTRACT_METADATA_TREE)?;

        // Ensure lengths of contract_ids and metadata arrays match
        if contract_ids.len() != metadata.len() {
            return Err(Error::Custom(String::from(
                "The lengths of contract_ids and metadata arrays must match",
            )));
        }

        // Insert each contract id and metadata pair
        for (contract_id, metadata) in contract_ids.iter().zip(metadata.iter()) {
            // Serialize the gas data
            let serialized_metadata = serialize(metadata);

            // Insert serialized gas data
            lock.insert(
                SLED_CONTRACT_METADATA_TREE,
                contract_id.to_string().as_bytes(),
                &serialized_metadata,
            )?;
            debug!(target: "explorerd::contract_meta_store::insert_metadata",
                "Inserted contract metadata for contract_id {}: {metadata:?}", contract_id.to_string());
        }

        // Commit the changes
        lock.apply()?;

        Ok(())
    }

    /// Acquires a lock on the database, opening a specified tree for write operations, returning a
    /// [`MutexGuard<SledDbOverlay>`] representing the locked state.
    pub fn lock(&self, tree_name: &[u8]) -> Result<MutexGuard<SledDbOverlay>> {
        // Lock the database, open tree, and return lock
        let mut lock = self.overlay.lock().unwrap();
        lock.open_tree(tree_name, true)?;
        Ok(lock)
    }
}

#[cfg(test)]
///  This test module verifies the correct insertion and retrieval of contract metadata and source code.
mod tests {
    use super::*;
    use crate::test_utils::init_logger;
    use darkfi_sdk::crypto::MONEY_CONTRACT_ID;
    use sled_overlay::sled::Config;

    // Test source paths data
    const TEST_SOURCE_PATHS: &[&str] = &["test/source1.rs", "test/source2.rs"];

    // Test source code data
    const TEST_SOURCE_CONTENT: &[&str] =
        &["fn main() { println!(\"Hello, world!\"); }", "fn add(a: i32, b: i32) -> i32 { a + b }"];

    /// Tests the storing of contract source code by setting up the store, retrieving loaded source paths
    /// and verifying that the retrieved paths match against expected results.
    #[test]
    fn test_add_contract_source() -> Result<()> {
        // Setup test, returning initialized contract metadata store
        let store = setup()?;

        // Load source code tests data
        let contract_id = load_source_code(&store)?;

        // Initialize expected source paths
        let expected_source_paths: Vec<String> =
            TEST_SOURCE_PATHS.iter().map(|s| s.to_string()).collect();

        // Retrieve actual loaded source files
        let actual_source_paths = store.get_source_paths(contract_id)?;

        // Verify that loaded source code matches expected results
        assert_eq!(expected_source_paths, actual_source_paths);

        Ok(())
    }

    /// Validates the retrieval of a contract source file from the metadata store by setting up the store,
    /// loading test source code data, and verifying that loaded source contents match against
    /// expected content.
    #[test]
    fn test_get_contract_source() -> Result<()> {
        // Setup test, returning initialized contract metadata store
        let store = setup()?;

        // Load source code tests data
        let contract_id = load_source_code(&store)?;

        // Iterate through test data
        for (source_path, expected_content) in
            TEST_SOURCE_PATHS.iter().zip(TEST_SOURCE_CONTENT.iter())
        {
            // Get the content of the source path from the store
            let actual_source = store.get_source_content(contract_id, source_path)?;

            // Verify that the source code content is the store
            assert!(actual_source.is_some(), "No content found for path: {}", source_path);

            // Validate that the source content matches expected results
            assert_eq!(
                actual_source.unwrap(),
                expected_content.to_string(),
                "Actual source does not match the expected results for path: {}",
                source_path
            );
        }

        Ok(())
    }

    /// Tests the addition of [`ContractMetaData`] to the store by setting up the store, inserting
    /// metadata, and verifying the inserted data matches the expected results.
    #[test]
    fn test_add_metadata() -> Result<()> {
        // Setup test, returning initialized contract metadata store
        let store = setup()?;

        // Unique identifier for contracts in tests
        let contract_id: ContractId = *MONEY_CONTRACT_ID;

        // Declare expected metadata used for test
        let expected_metadata: ContractMetaData = ContractMetaData::new(
            "Money Contract".to_string(),
            "Money Contract Description".to_string(),
        );

        // Add metadata for the source code to the test
        store.insert_metadata(&[contract_id], &[expected_metadata.clone()])?;

        // Get the metadata content from the store
        let actual_metadata = store.get(&contract_id)?;

        // Verify that the metadata exists in the store
        assert!(actual_metadata.is_some());

        // Verify actual metadata matches expected results
        assert_eq!(actual_metadata.unwrap(), expected_metadata.clone());

        Ok(())
    }

    /// Sets up a test case for contract metadata store testing by initializing the logger
    /// and returning an initialized [`ContractMetaStore`].
    fn setup() -> Result<ContractMetaStore> {
        // Initialize logger to show execution output
        init_logger(simplelog::LevelFilter::Off, vec!["sled", "runtime", "net"]);

        // Initialize an in-memory sled db instance
        let db = Config::new().temporary(true).open()?;

        // Initialize the contract store
        ContractMetaStore::new(&db)
    }

    /// Loads [`TEST_SOURCE_PATHS`] and [`TEST_SOURCE_CONTENT`] into the provided
    /// [`ContractMetaStore`] to test source code insertion and retrieval.
    fn load_source_code(store: &ContractMetaStore) -> Result<&'static ContractId> {
        // Define the contract ID for testing
        let contract_id = &MONEY_CONTRACT_ID;

        // Define sample source files for testing using the shared paths and content
        let test_sources: Vec<ContractSourceFile> = TEST_SOURCE_PATHS
            .iter()
            .zip(TEST_SOURCE_CONTENT.iter())
            .map(|(path, content)| ContractSourceFile::new(path.to_string(), content.to_string()))
            .collect();

        // Add test source code to the store
        store.insert_source(contract_id, &test_sources)?;

        Ok(contract_id)
    }
}