Просмотр исходного кода

runtime: Cleanup and add payload support to init()

parazyd 3 лет назад
Родитель
Сommit
84bf94d856

+ 7 - 8
example/smart-contract/src/lib.rs

@@ -1,15 +1,13 @@
 use darkfi_sdk::{
-    crypto::Nullifier,
-    db::{db_init, db_lookup, db_get, db_begin_tx, db_set, db_end_tx},
+    db::{db_begin_tx, db_end_tx, db_get, db_init, db_lookup, db_set},
     entrypoint,
-    error::{ContractError, ContractResult},
+    error::ContractResult,
     initialize, msg,
-    pasta::pallas,
-    state::{nullifier_exists, set_update},
+    state::set_update,
     tx::Transaction,
     update_state,
 };
-use darkfi_serial::{deserialize, serialize, SerialDecodable, SerialEncodable, ReadExt, Decodable};
+use darkfi_serial::{deserialize, serialize, SerialDecodable, SerialEncodable};
 
 /// Available functions for this contract.
 /// We identify them with the first byte passed in through the payload.
@@ -48,7 +46,7 @@ pub struct FooUpdate {
 }
 
 initialize!(init_contract);
-fn init_contract() -> ContractResult {
+fn init_contract(_ix: &[u8]) -> ContractResult {
     msg!("wakeup wagies!");
     db_init("wagies")?;
 
@@ -73,7 +71,8 @@ fn process_instruction(ix: &[u8]) -> ContractResult {
             let tx_data = &ix[1..];
             // ...
             let (func_call_index, tx): (u32, Transaction) = deserialize(tx_data)?;
-            let call_data: FooCallData = deserialize(&tx.func_calls[func_call_index as usize].call_data)?;
+            let call_data: FooCallData =
+                deserialize(&tx.func_calls[func_call_index as usize].call_data)?;
             msg!("call_data {{ a: {}, b: {} }}", call_data.a, call_data.b);
             // ...
             let update = FooUpdate { name: "john_doe".to_string(), age: 110 };

+ 21 - 25
example/smart-contract/tests/runtime.rs

@@ -16,15 +16,14 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use darkfi::{
-    crypto::contract_id::ContractId,
-    runtime::{util::serialize_payload, vm_runtime::Runtime},
-    Result,
+use darkfi::{crypto::contract_id::ContractId, runtime::vm_runtime::Runtime, Result};
+use darkfi_sdk::{
+    pasta::pallas,
+    tx::{FuncCall, Transaction},
 };
-use darkfi_sdk::{crypto::nullifier::Nullifier, pasta::pallas, tx::{Transaction, FuncCall}};
 use darkfi_serial::{serialize, Encodable, WriteExt};
 
-use smart_contract::FooCallData;
+use smart_contract::{FooCallData, Function};
 
 #[test]
 fn run_contract() -> Result<()> {
@@ -52,43 +51,40 @@ fn run_contract() -> Result<()> {
     let contract_id = ContractId::new(pallas::Base::from(1));
     let mut runtime = Runtime::new(&wasm_bytes, contract_id)?;
 
-    runtime.deploy()?;
+    // Deploy function to initialize the smart contract state.
+    // Here we pass an empty payload, but it's possible to feed in arbitrary data.
+    runtime.deploy(&[])?;
 
     // =============================================
     // Build some kind of payload to show an example
     // =============================================
     let tx = Transaction {
-        func_calls: vec![
-            FuncCall {
-                contract_id: pallas::Base::from(110),
-                func_id: pallas::Base::from(4),
-                call_data: serialize(&FooCallData { a: 777, b: 666 }),
-                proofs: Vec::new()
-            }
-        ],
-        signatures: Vec::new()
+        func_calls: vec![FuncCall {
+            contract_id: pallas::Base::from(110),
+            func_id: pallas::Base::from(4),
+            call_data: serialize(&FooCallData { a: 777, b: 666 }),
+            proofs: Vec::new(),
+        }],
+        signatures: Vec::new(),
     };
     let func_call_index: u32 = 0;
 
     let mut payload = Vec::new();
-    // Prepend the func id = 0x00
     // Selects which path executes in the contract.
-    payload.write_u8(0x00);
+    payload.write_u8(Function::Foo as u8)?;
     // Write the actual payload data
-    payload.write_u32(func_call_index);
+    payload.write_u32(func_call_index)?;
     tx.encode(&mut payload)?;
 
     // ============================================================
     // Serialize the payload into the runtime format and execute it
     // ============================================================
-    runtime.exec(&serialize_payload(&payload))?;
+    runtime.exec(&payload)?;
 
+    // =====================================================
+    // If exec was successful, try to apply the state change
+    // =====================================================
     runtime.apply()?;
-    //Ok(())
-
-    //runtime.exec(&serialize_payload(&payload))?;
-
-    //runtime.apply()?;
 
     Ok(())
 }

+ 3 - 10
src/runtime/import/chain_state.rs

@@ -16,17 +16,10 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use darkfi_sdk::crypto::{MerkleNode, Nullifier};
-use log::{debug, error};
-use wasmer::{AsStoreRef, FunctionEnvMut, WasmPtr};
+use log::debug;
+use wasmer::{FunctionEnvMut, WasmPtr};
 
-use crate::{
-    node::state::ProgramState,
-    runtime::{
-        memory::MemoryManipulation,
-        vm_runtime::{ContractSection, Env},
-    },
-};
+use crate::runtime::vm_runtime::{ContractSection, Env};
 
 pub(crate) fn set_update(mut ctx: FunctionEnvMut<Env>, ptr: WasmPtr<u8>, len: u32) -> i32 {
     let env = ctx.data();

+ 3 - 10
src/runtime/import/db.rs

@@ -1,14 +1,7 @@
-use darkfi_sdk::crypto::{MerkleNode, Nullifier};
-use log::{debug, error};
-use wasmer::{AsStoreRef, FunctionEnvMut, WasmPtr};
+use log::error;
+use wasmer::{FunctionEnvMut, WasmPtr};
 
-use crate::{
-    node::state::ProgramState,
-    runtime::{
-        memory::MemoryManipulation,
-        vm_runtime::{ContractSection, Env},
-    },
-};
+use crate::runtime::vm_runtime::{ContractSection, Env};
 
 /// Only deploy() can call this. Creates a new database instance for this contract.
 ///

+ 3 - 3
src/runtime/import/util.rs

@@ -1,7 +1,7 @@
-use log::{error, warn};
-use wasmer::{AsStoreRef, FunctionEnvMut, WasmPtr};
+use log::error;
+use wasmer::{FunctionEnvMut, WasmPtr};
 
-use crate::runtime::{memory::MemoryManipulation, vm_runtime::Env};
+use crate::runtime::vm_runtime::Env;
 
 /// Host function for logging strings.
 /// This is injected into the runtime with wasmer's `imports!` macro.

+ 1 - 1
src/runtime/memory.rs

@@ -18,7 +18,7 @@
 
 use wasmer::{MemoryView, WasmPtr};
 
-use crate::{Error, Result};
+use crate::Result;
 
 pub trait MemoryManipulation {
     fn write_slice(&self, value_slice: &[u8], mem_offset: u32) -> Result<()>;

+ 0 - 3
src/runtime/mod.rs

@@ -22,8 +22,5 @@ pub mod vm_runtime;
 /// VM memory access (read/write)
 pub(crate) mod memory;
 
-/// Utility functions
-pub mod util;
-
 /// Imported host functions
 pub(crate) mod import;

+ 0 - 30
src/runtime/util.rs

@@ -1,30 +0,0 @@
-/* This file is part of DarkFi (https://dark.fi)
- *
- * Copyright (C) 2020-2022 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/>.
- */
-
-/// Serialize contract payload to format accepted by the runtime entrypoint.
-/// We keep the same payload as a slice of bytes, and prepend it with a
-/// little-endian u64 to tell the payload's length.
-pub fn serialize_payload(payload: &[u8]) -> Vec<u8> {
-    let mut out = vec![];
-
-    let len = payload.len() as u64;
-    out.extend_from_slice(&len.to_le_bytes());
-    out.extend_from_slice(payload);
-
-    out
-}

+ 67 - 30
src/runtime/vm_runtime.rs

@@ -37,7 +37,6 @@ use super::{
     import,
     //chain_state::{is_valid_merkle, nullifier_exists, set_update},
     memory::MemoryManipulation,
-    util::serialize_payload,
 };
 use crate::{crypto::contract_id::ContractId, Error, Result};
 
@@ -213,30 +212,46 @@ impl Runtime {
         Ok(Self { instance, store, ctx })
     }
 
-    pub fn deploy(&mut self) -> Result<()> {
+    /// This function runs when a smart contract is initially deployed, or re-deployed.
+    /// The runtime will look for an [`INITIALIZE`] symbol in the wasm code, and execute
+    /// it if found. Optionally, it is possible to pass in a payload for any kind of special
+    /// instructions the developer wants to manage in the initialize function.
+    /// This process is supposed to set up the sled db trees for storing the smart contract
+    /// state, and it can create, delete, modify, read, and write to databases it's allowed to.
+    /// The permissions for this are handled by the `ContractId` in the sled db API so we
+    /// assume that the contract is only able to do write operations on its own sled trees.
+    pub fn deploy(&mut self, payload: &[u8]) -> Result<()> {
         let mut env_mut = self.ctx.as_mut(&mut self.store);
         env_mut.contract_section = ContractSection::Deploy;
 
-        debug!(target: "wasm_runtime::run", "Getting initialize function");
+        // Serialize the payload for the format the wasm runtime is expecting.
+        let payload = Self::serialize_payload(payload);
+
+        // Allocate enough memory for the payload and copy it into the memory.
+        let pages_required = payload.len() / WASM_PAGE_SIZE + 1;
+        self.set_memory_page_size(pages_required as u32)?;
+        self.copy_to_memory(&payload)?;
+
+        debug!(target: "wasm_runtime::deploy", "Getting initialize function");
         let entrypoint = self.instance.exports.get_function(INITIALIZE)?;
 
-        debug!(target: "wasm_runtime::run", "Executing wasm");
-        let ret = match entrypoint.call(&mut self.store, &[]) {
+        debug!(target: "wasm_runtime::deploy", "Executing wasm");
+        let ret = match entrypoint.call(&mut self.store, &[Value::I32(0 as i32)]) {
             Ok(retvals) => {
                 self.print_logs();
-                debug!(target: "wasm_runtime::run", "{}", self.gas_info());
+                debug!(target: "wasm_runtime::deploy", "{}", self.gas_info());
                 retvals
             }
             Err(e) => {
                 self.print_logs();
-                debug!(target: "wasm_runtime::run", "{}", self.gas_info());
+                debug!(target: "wasm_runtime::deploy", "{}", self.gas_info());
                 // WasmerRuntimeError panics are handled here. Return from run() immediately.
                 return Err(e.into())
             }
         };
 
-        debug!(target: "wasm_runtime::run", "wasm executed successfully");
-        debug!(target: "wasm_runtime::run", "Contract returned: {:?}", ret[0]);
+        debug!(target: "wasm_runtime::deploy", "wasm executed successfully");
+        debug!(target: "wasm_runtime::deploy", "Contract returned: {:?}", ret[0]);
 
         let retval = match ret[0] {
             Value::I64(v) => v as u64,
@@ -251,37 +266,43 @@ impl Runtime {
         }
     }
 
-    /// Run the hardcoded `ENTRYPOINT` function with the given payload as input.
+    /// This funcion runs when someone wants to execute a smart contract.
+    /// The runtime will look for an [`ENTRYPOINT`] symbol in the wasm code, and
+    /// execute it if found. A payload is also passed as an instruction that can
+    /// be used inside the vm by the runtime.
     pub fn exec(&mut self, payload: &[u8]) -> Result<()> {
         let mut env_mut = self.ctx.as_mut(&mut self.store);
         env_mut.contract_section = ContractSection::Exec;
 
+        // Serialize the payload for the format the wasm runtime is expecting.
+        let payload = Self::serialize_payload(payload);
+
+        // Allocate enough memory for the payload and copy it into the memory.
         let pages_required = payload.len() / WASM_PAGE_SIZE + 1;
         self.set_memory_page_size(pages_required as u32)?;
+        self.copy_to_memory(&payload)?;
 
-        self.copy_to_memory(payload)?;
-
-        debug!(target: "wasm_runtime::run", "Getting entrypoint function");
+        debug!(target: "wasm_runtime::exec", "Getting entrypoint function");
         let entrypoint = self.instance.exports.get_function(ENTRYPOINT)?;
 
-        debug!(target: "wasm_runtime::run", "Executing wasm");
+        debug!(target: "wasm_runtime::exec", "Executing wasm");
         // We pass 0 to entrypoint() which is the location of the payload data in the memory
         let ret = match entrypoint.call(&mut self.store, &[Value::I32(0 as i32)]) {
             Ok(retvals) => {
                 self.print_logs();
-                debug!(target: "wasm_runtime::run", "{}", self.gas_info());
+                debug!(target: "wasm_runtime::exec", "{}", self.gas_info());
                 retvals
             }
             Err(e) => {
                 self.print_logs();
-                debug!(target: "wasm_runtime::run", "{}", self.gas_info());
+                debug!(target: "wasm_runtime::exec", "{}", self.gas_info());
                 // WasmerRuntimeError panics are handled here. Return from run() immediately.
                 return Err(e.into())
             }
         };
 
-        debug!(target: "wasm_runtime::run", "wasm executed successfully");
-        debug!(target: "wasm_runtime::run", "Contract returned: {:?}", ret[0]);
+        debug!(target: "wasm_runtime::exec", "wasm executed successfully");
+        debug!(target: "wasm_runtime::exec", "Contract returned: {:?}", ret[0]);
 
         let retval = match ret[0] {
             Value::I64(v) => v as u64,
@@ -294,43 +315,48 @@ impl Runtime {
         }
     }
 
+    /// This function runs after successful execution of [`exec`] and tries to
+    /// apply the state change to the sled databases.
+    /// The runtime will lok for an [`UPDATE`] symbol in the wasm code, and execute
+    /// it if found. The function does not take an arbitrary payload, but just takes
+    /// a state update from `env` and passes it into the wasm runtime.
     pub fn apply(&mut self) -> Result<()> {
         let mut env_mut = self.ctx.as_mut(&mut self.store);
         env_mut.contract_section = ContractSection::Update;
 
+        // Take the update data from env, and serialize it for the format the wasm
+        // runtime is expecting.
         let update_data = env_mut.contract_update.take().unwrap();
-        // FIXME: Less realloc
-        let mut payload = vec![update_data.0];
+        let mut payload = Vec::with_capacity(1 + update_data.1.len());
+        payload.extend_from_slice(&[update_data.0]);
         payload.extend_from_slice(&update_data.1);
-        let payload = serialize_payload(&payload);
+        let payload = Self::serialize_payload(&payload);
 
-        // TODO: Test if this works when state update is larger than the initial payload
-        // The question is if we need to allocate more memory or if it's ok to just
-        // overwrite from zero (and even if overwrite - is there enough space?)
+        // Allocate enough memory for the payload and copy it into the memory.
         let pages_required = payload.len() / WASM_PAGE_SIZE + 1;
         self.set_memory_page_size(pages_required as u32)?;
         self.copy_to_memory(&payload)?;
 
-        debug!(target: "wasm_runtime::run", "Getting initialize function");
+        debug!(target: "wasm_runtime::apply", "Getting update function");
         let entrypoint = self.instance.exports.get_function(UPDATE)?;
 
-        debug!(target: "wasm_runtime::run", "Executing wasm");
+        debug!(target: "wasm_runtime::apply", "Executing wasm");
         let ret = match entrypoint.call(&mut self.store, &[Value::I32(0 as i32)]) {
             Ok(retvals) => {
                 self.print_logs();
-                debug!(target: "wasm_runtime::run", "{}", self.gas_info());
+                debug!(target: "wasm_runtime::apply", "{}", self.gas_info());
                 retvals
             }
             Err(e) => {
                 self.print_logs();
-                debug!(target: "wasm_runtime::run", "{}", self.gas_info());
+                debug!(target: "wasm_runtime::apply", "{}", self.gas_info());
                 // WasmerRuntimeError panics are handled here. Return from run() immediately.
                 return Err(e.into())
             }
         };
 
-        debug!(target: "wasm_runtime::run", "wasm executed successfully");
-        debug!(target: "wasm_runtime::run", "Contract returned: {:?}", ret[0]);
+        debug!(target: "wasm_runtime::apply", "wasm executed successfully");
+        debug!(target: "wasm_runtime::apply", "Contract returned: {:?}", ret[0]);
 
         let retval = match ret[0] {
             Value::I64(v) => v as u64,
@@ -389,4 +415,15 @@ impl Runtime {
         let memory_view = env.memory_view(&self.store);
         memory_view.write_slice(payload, 0)
     }
+
+    /// Serialize contract payload to the format accepted by the runtime functions.
+    /// We keep the same payload as a slice of bytes, and prepend it with a
+    /// little-endian u64 to tell the payload's length.
+    fn serialize_payload(payload: &[u8]) -> Vec<u8> {
+        let payload_len = payload.len();
+        let mut out = Vec::with_capacity(8 + payload_len);
+        out.extend_from_slice(&(payload_len as u64).to_le_bytes());
+        out.extend_from_slice(payload);
+        out
+    }
 }

+ 1 - 4
src/sdk/src/db.rs

@@ -1,7 +1,4 @@
-use super::{
-    crypto::{MerkleNode, Nullifier},
-    error::{ContractError, GenericResult},
-};
+use super::error::GenericResult;
 
 type DbHandle = u32;
 type TxHandle = u32;

+ 4 - 2
src/sdk/src/entrypoint.rs

@@ -26,8 +26,10 @@ macro_rules! initialize {
     ($process_init:ident) => {
         /// # Safety
         #[no_mangle]
-        pub unsafe extern "C" fn __initialize() -> u64 {
-            match $process_init() {
+        pub unsafe extern "C" fn __initialize(input: *mut u8) -> u64 {
+            let instruction_data = $crate::entrypoint::deserialize(input);
+
+            match $process_init(&instruction_data) {
                 Ok(()) => $crate::entrypoint::SUCCESS,
                 Err(e) => e.into(),
             }

+ 1 - 1
src/sdk/src/state.rs

@@ -18,7 +18,7 @@
 
 use super::{
     crypto::{MerkleNode, Nullifier},
-    error::{ContractError, ContractResult},
+    error::ContractError,
 };
 
 pub fn set_update(update_data: &[u8]) -> Result<(), ContractError> {

+ 1 - 4
src/sdk/src/tx.rs

@@ -1,8 +1,5 @@
 use darkfi_serial::{SerialDecodable, SerialEncodable};
-use pasta_curves::{
-    group::ff::{Field, PrimeField},
-    pallas,
-};
+use pasta_curves::pallas;
 
 type ContractId = pallas::Base;
 type FuncId = pallas::Base;