Jelajahi Sumber

blockchain: Document contractstore.

parazyd 3 tahun lalu
induk
melakukan
b6be34e41d

+ 1 - 0
example/smart-contract/Cargo.lock

@@ -915,6 +915,7 @@ dependencies = [
 name = "darkfi-sdk"
 name = "darkfi-sdk"
 version = "0.3.0"
 version = "0.3.0"
 dependencies = [
 dependencies = [
+ "blake3",
  "bs58",
  "bs58",
  "darkfi-serial",
  "darkfi-serial",
  "halo2_gadgets",
  "halo2_gadgets",

+ 86 - 35
src/blockchain/contractstore.rs

@@ -18,48 +18,62 @@
 
 
 use darkfi_sdk::crypto::ContractId;
 use darkfi_sdk::crypto::ContractId;
 use darkfi_serial::{deserialize, serialize};
 use darkfi_serial::{deserialize, serialize};
+use log::debug;
 
 
 use crate::{
 use crate::{
     Error::{ContractAlreadyInitialized, ContractNotFound, ContractStateNotFound},
     Error::{ContractAlreadyInitialized, ContractNotFound, ContractStateNotFound},
     Result,
     Result,
 };
 };
 
 
+/// The `ContractStore` is a `sled` tree that stores pointers to contracts'
+/// databases. See the rustdoc for the impl functions for more info.
 #[derive(Clone)]
 #[derive(Clone)]
 pub struct ContractStore(sled::Tree);
 pub struct ContractStore(sled::Tree);
 
 
 const SLED_CONTRACTS_TREE: &[u8] = b"_contracts";
 const SLED_CONTRACTS_TREE: &[u8] = b"_contracts";
 
 
-// =================
-// TODO: Drop tree
-// =================
+// Logger targets
+const TGT_INIT: &str = "blockchain::contractstore::init";
+const TGT_LKUP: &str = "blockchain::contractstore::lookup";
+const TGT_DROP: &str = "blockchain::contractstore::remove";
 
 
 impl ContractStore {
 impl ContractStore {
+    /// Opens or creates a `ContractStore`. This main tree holds the links
+    /// of contracts' states,
+    /// The layout looks like this:
+    /// ```plaintext
+    ///  tree: "_contracts"
+    ///   key: ContractId
+    /// value: Vec<blake3(ContractId || tree_name)>
+    /// ```
+    /// These values get mutated with `init()` and `remove()`.
     pub fn new(db: &sled::Db) -> Result<Self> {
     pub fn new(db: &sled::Db) -> Result<Self> {
         let tree = db.open_tree(SLED_CONTRACTS_TREE)?;
         let tree = db.open_tree(SLED_CONTRACTS_TREE)?;
         Ok(Self(tree))
         Ok(Self(tree))
     }
     }
 
 
-    /// Database layout:
-    /// ```plaintext
-    /// Tree: _contracts
-    /// key:   ContractId
-    /// value: blake3(ContractId || tree_name)
-    /// ```
-    ///
-    /// `value` when init-ed represents a Contract's state tree:
-    /// ```plaintext
-    /// Tree: blake3(ContractId || tree_name)
-    /// key: &[u8]
-    /// value: &[u8]
-    /// ```
+    /// Try to initialize a new contract state. Contracts can create a number
+    /// of trees, separated by `tree_name`, which they can then use from the
+    /// smart contract API. `init()` will look into the main `ContractStore`
+    /// tree to check if the smart contract was already deployed, and if so
+    /// it will fetch a vector of these states that were initialized. If the
+    /// state was already found, this function will return an error, because
+    /// in this case the handle should be fetched using `lookup()`.
+    /// If the tree was not initialized previously, it will be appended to
+    /// the main `ContractStore` tree and a `sled::Tree` handle will be returned.
     pub fn init(
     pub fn init(
         &self,
         &self,
         db: &sled::Db,
         db: &sled::Db,
         contract_id: &ContractId,
         contract_id: &ContractId,
         tree_name: &str,
         tree_name: &str,
     ) -> Result<sled::Tree> {
     ) -> Result<sled::Tree> {
+        debug!(target: TGT_INIT, "Initializing state tree for {}:{}", contract_id, tree_name);
+
         let contract_id_bytes = serialize(contract_id);
         let contract_id_bytes = serialize(contract_id);
+        let ptr = contract_id.hash_state_id(tree_name);
 
 
+        // See if there are existing state trees. If not, just start with an
+        // empty vector.
         let mut state_pointers: Vec<[u8; 32]> = if self.0.contains_key(&contract_id_bytes)? {
         let mut state_pointers: Vec<[u8; 32]> = if self.0.contains_key(&contract_id_bytes)? {
             let bytes = self.0.get(&contract_id_bytes)?.unwrap();
             let bytes = self.0.get(&contract_id_bytes)?.unwrap();
             deserialize(&bytes)?
             deserialize(&bytes)?
@@ -67,33 +81,42 @@ impl ContractStore {
             vec![]
             vec![]
         };
         };
 
 
-        let mut hasher = blake3::Hasher::new();
-        hasher.update(&contract_id_bytes);
-        hasher.update(&tree_name.as_bytes());
-        let ptr = hasher.finalize();
-        let ptr = ptr.as_bytes();
-
         // If the db was never initialized, it should not be in here.
         // If the db was never initialized, it should not be in here.
-        if state_pointers.contains(ptr) {
+        if state_pointers.contains(&ptr) {
             return Err(ContractAlreadyInitialized)
             return Err(ContractAlreadyInitialized)
         }
         }
 
 
         // Now we add it so it's marked as initialized
         // Now we add it so it's marked as initialized
-        state_pointers.push(*ptr);
-        self.0.insert(&contract_id_bytes, serialize(&state_pointers))?;
+        state_pointers.push(ptr);
+
+        // We do this as a batch so in case of not being able to open the tree
+        // we don't write that it's initialized.
+        let mut batch = sled::Batch::default();
+        batch.insert(contract_id_bytes, serialize(&state_pointers));
 
 
         // We open the tree and return its handle
         // We open the tree and return its handle
         let tree = db.open_tree(ptr)?;
         let tree = db.open_tree(ptr)?;
+
+        // On success, apply the batch
+        self.0.apply_batch(batch)?;
+
         Ok(tree)
         Ok(tree)
     }
     }
 
 
+    /// Do a lookup of an existing contract state. In order to succeed, the
+    /// state must have been previously initialized with `init()`. If the
+    /// state has been found, a handle to it will be returned. Otherwise, we
+    /// return an error.
     pub fn lookup(
     pub fn lookup(
         &self,
         &self,
         db: &sled::Db,
         db: &sled::Db,
         contract_id: &ContractId,
         contract_id: &ContractId,
         tree_name: &str,
         tree_name: &str,
     ) -> Result<sled::Tree> {
     ) -> Result<sled::Tree> {
+        debug!(target: TGT_LKUP, "Looking up state tree for {}:{}", contract_id, tree_name);
+
         let contract_id_bytes = serialize(contract_id);
         let contract_id_bytes = serialize(contract_id);
+        let ptr = contract_id.hash_state_id(tree_name);
 
 
         // A guard to make sure we went through init()
         // A guard to make sure we went through init()
         if !self.0.contains_key(&contract_id_bytes)? {
         if !self.0.contains_key(&contract_id_bytes)? {
@@ -101,18 +124,11 @@ impl ContractStore {
         }
         }
 
 
         let state_pointers = self.0.get(&contract_id_bytes)?.unwrap();
         let state_pointers = self.0.get(&contract_id_bytes)?.unwrap();
-
         let state_pointers: Vec<[u8; 32]> = deserialize(&state_pointers)?;
         let state_pointers: Vec<[u8; 32]> = deserialize(&state_pointers)?;
 
 
-        let mut hasher = blake3::Hasher::new();
-        hasher.update(&contract_id_bytes);
-        hasher.update(&tree_name.as_bytes());
-        let ptr = hasher.finalize();
-        let ptr = ptr.as_bytes();
-
-        // We assume the tree has been created already, so it should be listed in this array.
-        // If not, that's an error.
-        if !state_pointers.contains(ptr) {
+        // We assume the tree has been created already, so it should be listed
+        // in this array. If not, that's an error.
+        if !state_pointers.contains(&ptr) {
             return Err(ContractStateNotFound)
             return Err(ContractStateNotFound)
         }
         }
 
 
@@ -120,4 +136,39 @@ impl ContractStore {
         let tree = db.open_tree(ptr)?;
         let tree = db.open_tree(ptr)?;
         Ok(tree)
         Ok(tree)
     }
     }
+
+    /// Attempt to remove an existing contract state. In order to succeed, the
+    /// state must have been previously initialized with `init()`. If the state
+    /// has been found, its contents in the tree will be cleared, and the pointer
+    /// will be removed from the main `ContractStore`. If anything is not found
+    /// as initialized, an error is returned.
+    pub fn remove(&self, db: &sled::Db, contract_id: &ContractId, tree_name: &str) -> Result<()> {
+        debug!(target: TGT_DROP, "Removing state tree for {}:{}", contract_id, tree_name);
+
+        let contract_id_bytes = serialize(contract_id);
+        let ptr = contract_id.hash_state_id(tree_name);
+
+        // A guard to make sure we went through init()
+        if !self.0.contains_key(&contract_id_bytes)? {
+            return Err(ContractNotFound(contract_id.to_string()))
+        }
+
+        let state_pointers = self.0.get(&contract_id_bytes)?.unwrap();
+        let mut state_pointers: Vec<[u8; 32]> = deserialize(&state_pointers)?;
+
+        // We assume the tree has been created already, so it should be listed
+        // in this array. If not, that's an error.
+        if !state_pointers.contains(&ptr) {
+            return Err(ContractStateNotFound)
+        }
+
+        // We open the tree and clear it. This is unfortunately not atomic.
+        let tree = db.open_tree(ptr)?;
+        tree.clear()?;
+
+        state_pointers.retain(|x| *x != ptr);
+        self.0.insert(contract_id_bytes, serialize(&state_pointers))?;
+
+        Ok(())
+    }
 }
 }

+ 3 - 2
src/sdk/Cargo.toml

@@ -23,9 +23,10 @@ thiserror = "1.0.37"
 bs58 = "0.4.0"
 bs58 = "0.4.0"
 
 
 # Cryptography
 # Cryptography
-pasta_curves = "0.4.0"
-incrementalmerkletree = "0.3.0"
+blake3 = "1.3.1"
 halo2_gadgets = "0.2.0"
 halo2_gadgets = "0.2.0"
+incrementalmerkletree = "0.3.0"
+pasta_curves = "0.4.0"
 
 
 # Misc
 # Misc
 lazy_static = "1.4.0"
 lazy_static = "1.4.0"

+ 11 - 1
src/sdk/src/crypto/contract_id.rs

@@ -16,7 +16,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
  */
 
 
-use darkfi_serial::{SerialDecodable, SerialEncodable};
+use darkfi_serial::{serialize, SerialDecodable, SerialEncodable};
 use pasta_curves::{group::ff::PrimeField, pallas};
 use pasta_curves::{group::ff::PrimeField, pallas};
 
 
 #[derive(Copy, Clone, Debug, Eq, PartialEq, SerialEncodable, SerialDecodable)]
 #[derive(Copy, Clone, Debug, Eq, PartialEq, SerialEncodable, SerialDecodable)]
@@ -35,6 +35,16 @@ impl ContractId {
         // FIXME: Handle Option
         // FIXME: Handle Option
         Self(pallas::Base::from_repr(x).unwrap())
         Self(pallas::Base::from_repr(x).unwrap())
     }
     }
+
+    /// `blake3(self || tree_name)` is used in datbases to have a
+    /// fixed-size name for a contract's state db.
+    pub fn hash_state_id(&self, tree_name: &str) -> [u8; 32] {
+        let mut hasher = blake3::Hasher::new();
+        hasher.update(&serialize(self));
+        hasher.update(&tree_name.as_bytes());
+        let id = hasher.finalize();
+        *id.as_bytes()
+    }
 }
 }
 
 
 impl std::fmt::Display for ContractId {
 impl std::fmt::Display for ContractId {