contract_metadata.rs 17 KB

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