Эх сурвалжийг харах

runtime/import/db: rework database host function gas metering

Charge MIN_GAS at the start of each call, before the ACL check.

Apply per-byte and per-operation pricing:

* db_get/db_contains_key: READ_GAS_PER_BYTE for key and return value

* db_set: WRITE_GAS_PER_BYTE for bytes written, plus STATE_GROWTH_GAS
 for new keys (checked via contains_key before insert)

* db_init: TREE_GAS for sled tree creation

* zkas_db_set: COMPILE_GAS_PER_ROW * 2^k for VerifyingKey compilation,
  WRITE_GAS_PER_BYTE for storage, STATE_GROWTH_GAS for new circuits

Tx-local variants are priced per raw byte without the on-chain
multipliers. db_del remains MIN_GAS-only.
darkfi 1 сар өмнө
parent
commit
44b631f5c8

+ 13 - 7
src/runtime/import/db/db_contains_key.rs

@@ -20,9 +20,12 @@ use darkfi_serial::Decodable;
 use tracing::error;
 use wasmer::{FunctionEnvMut, WasmPtr};
 
-use crate::runtime::{
-    import::{acl::acl_allow, util::wasm_mem_read},
-    vm_runtime::{ContractSection, Env},
+use crate::{
+    runtime::{
+        import::{acl::acl_allow, util::wasm_mem_read},
+        vm_runtime::{ContractSection, Env},
+    },
+    validator::fees::{MIN_GAS, READ_GAS_PER_BYTE},
 };
 
 /// Check if an on-chain database contains a given key.
@@ -74,6 +77,9 @@ fn db_contains_key_internal(
     let (env, mut store) = ctx.data_and_store_mut();
     let cid = env.contract_id;
 
+    // Subtract base gas before the ACL check.
+    env.subtract_gas(&mut store, MIN_GAS);
+
     // Enforce function ACL
     if let Err(e) =
         acl_allow(env, &[ContractSection::Deploy, ContractSection::Metadata, ContractSection::Exec])
@@ -85,10 +91,6 @@ fn db_contains_key_internal(
         return darkfi_sdk::error::CALLER_ACCESS_DENIED
     }
 
-    // Subtract used gas.
-    // Reading is free.
-    env.subtract_gas(&mut store, 1);
-
     // Get the WASM memory reader
     let mut buf_reader = match wasm_mem_read(env, &store, ptr, ptr_len) {
         Ok(v) => v,
@@ -136,6 +138,10 @@ fn db_contains_key_internal(
         return darkfi_sdk::error::DB_CONTAINS_KEY_FAILED
     }
 
+    // Charge per byte of the lookup key.
+    let key_gas = if local { key.len() as u64 } else { key.len() as u64 * READ_GAS_PER_BYTE };
+    env.subtract_gas(&mut store, key_gas);
+
     // Fetch requested db handles
     let db_handles = if local { env.local_db_handles.borrow() } else { env.db_handles.borrow() };
 

+ 9 - 7
src/runtime/import/db/db_del.rs

@@ -21,9 +21,12 @@ use darkfi_serial::Decodable;
 use tracing::error;
 use wasmer::{FunctionEnvMut, WasmPtr};
 
-use crate::runtime::{
-    import::{acl::acl_allow, util::wasm_mem_read},
-    vm_runtime::{ContractSection, Env},
+use crate::{
+    runtime::{
+        import::{acl::acl_allow, util::wasm_mem_read},
+        vm_runtime::{ContractSection, Env},
+    },
+    validator::fees::MIN_GAS,
 };
 
 /// Remove a key from the on-chain database.
@@ -63,6 +66,9 @@ fn db_del_internal(
     let (env, mut store) = ctx.data_and_store_mut();
     let cid = env.contract_id;
 
+    // Subtract base gas before the ACL check.
+    env.subtract_gas(&mut store, MIN_GAS);
+
     // Enforce function ACL
     if let Err(e) = acl_allow(env, &[ContractSection::Deploy, ContractSection::Update]) {
         error!(
@@ -72,10 +78,6 @@ fn db_del_internal(
         return darkfi_sdk::error::CALLER_ACCESS_DENIED
     }
 
-    // Subtract used gas.
-    // We make deletion free.
-    env.subtract_gas(&mut store, 1);
-
     // Get the WASM memory reader
     let mut buf_reader = match wasm_mem_read(env, &store, ptr, ptr_len) {
         Ok(v) => v,

+ 17 - 8
src/runtime/import/db/db_get.rs

@@ -20,9 +20,12 @@ use darkfi_serial::Decodable;
 use tracing::{debug, error};
 use wasmer::{FunctionEnvMut, WasmPtr};
 
-use crate::runtime::{
-    import::{acl::acl_allow, util::wasm_mem_read},
-    vm_runtime::{ContractSection, Env},
+use crate::{
+    runtime::{
+        import::{acl::acl_allow, util::wasm_mem_read},
+        vm_runtime::{ContractSection, Env},
+    },
+    validator::fees::{MIN_GAS, READ_GAS_PER_BYTE},
 };
 
 /// Reads a value by key from the on-chain key-value store.
@@ -67,6 +70,9 @@ pub(crate) fn db_get_internal(
     let (env, mut store) = ctx.data_and_store_mut();
     let cid = env.contract_id;
 
+    // Subtract base gas before the ACL check.
+    env.subtract_gas(&mut store, MIN_GAS);
+
     // Enforce function ACL
     if let Err(e) =
         acl_allow(env, &[ContractSection::Deploy, ContractSection::Metadata, ContractSection::Exec])
@@ -78,9 +84,6 @@ pub(crate) fn db_get_internal(
         return darkfi_sdk::error::CALLER_ACCESS_DENIED
     }
 
-    // Subtract used gas.
-    env.subtract_gas(&mut store, 1);
-
     // Get the WASM memory reader
     let mut buf_reader = match wasm_mem_read(env, &store, ptr, ptr_len) {
         Ok(v) => v,
@@ -128,6 +131,10 @@ pub(crate) fn db_get_internal(
         return darkfi_sdk::error::DB_GET_FAILED
     }
 
+    // Charge per byte of the lookup key.
+    let key_gas = if local { key.len() as u64 } else { key.len() as u64 * READ_GAS_PER_BYTE };
+    env.subtract_gas(&mut store, key_gas);
+
     // Fetch requested db handles
     let db_handles = if local { env.local_db_handles.borrow() } else { env.db_handles.borrow() };
 
@@ -193,8 +200,10 @@ pub(crate) fn db_get_internal(
         return darkfi_sdk::error::DATA_TOO_LARGE
     }
 
-    // Subtract used gas. Here we count the length of the data read from db.
-    env.subtract_gas(&mut store, return_data.len() as u64);
+    // Charge per byte of the returned value.
+    let value_gas =
+        if local { return_data.len() as u64 } else { return_data.len() as u64 * READ_GAS_PER_BYTE };
+    env.subtract_gas(&mut store, value_gas);
 
     // Copy the data (Vec<u8>) to the VM by pushing it to the objects Vector.
     let mut objects = env.objects.borrow_mut();

+ 9 - 7
src/runtime/import/db/db_init.rs

@@ -23,9 +23,12 @@ use darkfi_serial::Decodable;
 use tracing::error;
 use wasmer::{FunctionEnvMut, WasmPtr};
 
-use crate::runtime::{
-    import::{acl::acl_allow, util::wasm_mem_read},
-    vm_runtime::{ContractSection, Env},
+use crate::{
+    runtime::{
+        import::{acl::acl_allow, util::wasm_mem_read},
+        vm_runtime::{ContractSection, Env},
+    },
+    validator::fees::{MIN_GAS, TREE_GAS},
 };
 
 use super::DbHandle;
@@ -46,6 +49,9 @@ pub(crate) fn db_init(mut ctx: FunctionEnvMut<Env>, ptr: WasmPtr<u8>, ptr_len: u
     let (env, mut store) = ctx.data_and_store_mut();
     let cid = env.contract_id;
 
+    // Subtract base gas and tree creation fee before the ACL check.
+    env.subtract_gas(&mut store, MIN_GAS.saturating_add(TREE_GAS));
+
     // Enforce function ACL
     if let Err(e) = acl_allow(env, &[ContractSection::Deploy]) {
         error!(
@@ -55,10 +61,6 @@ pub(crate) fn db_init(mut ctx: FunctionEnvMut<Env>, ptr: WasmPtr<u8>, ptr_len: u
         return darkfi_sdk::error::CALLER_ACCESS_DENIED
     }
 
-    // Subtract used gas.
-    // TODO: There should probably be an additional fee to open a new sled tree.
-    env.subtract_gas(&mut store, 1);
-
     // Get the wasm memory reader
     let mut buf_reader = match wasm_mem_read(env, &store, ptr, ptr_len) {
         Ok(v) => v,

+ 14 - 7
src/runtime/import/db/db_lookup.rs

@@ -23,9 +23,12 @@ use darkfi_serial::Decodable;
 use tracing::error;
 use wasmer::{FunctionEnvMut, WasmPtr};
 
-use crate::runtime::{
-    import::{acl::acl_allow, util::wasm_mem_read},
-    vm_runtime::{ContractSection, Env},
+use crate::{
+    runtime::{
+        import::{acl::acl_allow, util::wasm_mem_read},
+        vm_runtime::{ContractSection, Env},
+    },
+    validator::fees::{MIN_GAS, READ_GAS_PER_BYTE},
 };
 
 use super::DbHandle;
@@ -85,6 +88,9 @@ fn db_lookup_internal(
     let (env, mut store) = ctx.data_and_store_mut();
     let cid = env.contract_id;
 
+    // Subtract base gas before the ACL check.
+    env.subtract_gas(&mut store, MIN_GAS);
+
     // Enforce function ACL
     if let Err(e) = acl_allow(
         env,
@@ -102,10 +108,6 @@ fn db_lookup_internal(
         return darkfi_sdk::error::CALLER_ACCESS_DENIED
     }
 
-    // Subtract used gas.
-    // Opening an existing db should be free (i.e. 1 gas unit).
-    env.subtract_gas(&mut store, 1);
-
     // Get the wasm memory reader
     let mut buf_reader = match wasm_mem_read(env, &store, ptr, ptr_len) {
         Ok(v) => v,
@@ -151,6 +153,11 @@ fn db_lookup_internal(
         return darkfi_sdk::error::DB_LOOKUP_FAILED
     }
 
+    // Charge per byte of the db name.
+    let name_gas =
+        if local { db_name.len() as u64 } else { db_name.len() as u64 * READ_GAS_PER_BYTE };
+    env.subtract_gas(&mut store, name_gas);
+
     // We won't allow reading from the special zkas db or monotree db
     if [SMART_CONTRACT_ZKAS_DB_NAME, SMART_CONTRACT_MONOTREE_DB_NAME].contains(&db_name.as_str()) {
         error!(

+ 58 - 29
src/runtime/import/db/db_set.rs

@@ -21,9 +21,12 @@ use darkfi_serial::Decodable;
 use tracing::error;
 use wasmer::{FunctionEnvMut, WasmPtr};
 
-use crate::runtime::{
-    import::{acl::acl_allow, util::wasm_mem_read},
-    vm_runtime::{ContractSection, Env},
+use crate::{
+    runtime::{
+        import::{acl::acl_allow, util::wasm_mem_read},
+        vm_runtime::{ContractSection, Env},
+    },
+    validator::fees::{MIN_GAS, STATE_GROWTH_GAS, WRITE_GAS_PER_BYTE},
 };
 
 /// Set a value in the on-chain database for the given DbHandle.
@@ -69,6 +72,9 @@ fn db_set_internal(
     let (env, mut store) = ctx.data_and_store_mut();
     let cid = env.contract_id;
 
+    // Subtract base gas before the ACL check.
+    env.subtract_gas(&mut store, MIN_GAS);
+
     // Enforce function ACL
     if let Err(e) = acl_allow(env, &[ContractSection::Deploy, ContractSection::Update]) {
         error!(
@@ -78,11 +84,6 @@ fn db_set_internal(
         return darkfi_sdk::error::CALLER_ACCESS_DENIED
     }
 
-    // Subtract used gas. Here we count the bytes written into the database.
-    // TODO: We might want to count only the difference in size if we're replacing
-    // data and the new data is larger.
-    env.subtract_gas(&mut store, ptr_len as u64);
-
     // Get the wasm memory reader
     let mut buf_reader = match wasm_mem_read(env, &store, ptr, ptr_len) {
         Ok(v) => v,
@@ -151,28 +152,56 @@ fn db_set_internal(
         return darkfi_sdk::error::DB_SET_FAILED
     }
 
-    // Fetch requested db handles
-    let db_handles = if local { env.local_db_handles.borrow() } else { env.db_handles.borrow() };
+    // Fetch requested db handles and validate the tree handle.
+    let tree_handle = {
+        let db_handles =
+            if local { env.local_db_handles.borrow() } else { env.db_handles.borrow() };
 
-    // Check DbHandle index is within bounds
-    if db_handles.len() <= db_handle_index {
-        error!(
-            target: "runtime::db::{lt}",
-            "[WASM] [{cid}] {lt}(): Requested DbHandle that is out of bounds",
-        );
-        return darkfi_sdk::error::DB_SET_FAILED
-    }
+        // Check DbHandle index is within bounds
+        if db_handles.len() <= db_handle_index {
+            error!(
+                target: "runtime::db::{lt}",
+                "[WASM] [{cid}] {lt}(): Requested DbHandle that is out of bounds",
+            );
+            return darkfi_sdk::error::DB_SET_FAILED
+        }
 
-    // Retrive DbHandle using the index
-    let db_handle = &db_handles[db_handle_index];
+        // Retrive DbHandle using the index
+        let db_handle = &db_handles[db_handle_index];
 
-    // Validate that the DbHandle matches the contract ID
-    if db_handle.contract_id != env.contract_id {
-        error!(
-            target: "runtime::db::{lt}",
-            "[WASM] [{cid}] {lt}(): Unauthorized to write to DbHandle",
-        );
-        return darkfi_sdk::error::CALLER_ACCESS_DENIED
+        // Validate that the DbHandle matches the contract ID
+        if db_handle.contract_id != env.contract_id {
+            error!(
+                target: "runtime::db::{lt}",
+                "[WASM] [{cid}] {lt}(): Unauthorized to write to DbHandle",
+            );
+            return darkfi_sdk::error::CALLER_ACCESS_DENIED
+        }
+
+        db_handle.tree
+    };
+
+    // Charge for bytes written. New on-chain keys also incur STATE_GROWTH_GAS.
+    let bytes_written = (key.len() + value.len()) as u64;
+    if local {
+        env.subtract_gas(&mut store, bytes_written);
+    } else {
+        // Check whether the key already exists in on-chain storage.
+        let is_new_key = !env
+            .blockchain
+            .lock()
+            .unwrap()
+            .overlay
+            .lock()
+            .unwrap()
+            .contains_key(&tree_handle, &key)
+            .unwrap_or(false);
+
+        let mut storage_gas = bytes_written.saturating_mul(WRITE_GAS_PER_BYTE);
+        if is_new_key {
+            storage_gas = storage_gas.saturating_add(STATE_GROWTH_GAS);
+        }
+        env.subtract_gas(&mut store, storage_gas);
     }
 
     // Insert key-value pair into the database corresponding to this contract
@@ -180,7 +209,7 @@ fn db_set_internal(
         // Safe to unwrap here.
         let mut db = env.tx_local.lock();
         let db_cid = db.get_mut(&cid).unwrap();
-        let Some(tree) = db_cid.get_mut(&db_handle.tree) else {
+        let Some(tree) = db_cid.get_mut(&tree_handle) else {
             error!(
                 target: "runtime::db::{lt}",
                 "[WASM] [{cid}] {lt}(): Could not insert to tx-local tree",
@@ -196,7 +225,7 @@ fn db_set_internal(
         .overlay
         .lock()
         .unwrap()
-        .insert(&db_handle.tree, &key, &value)
+        .insert(&tree_handle, &key, &value)
         .is_err()
     {
         error!(

+ 49 - 39
src/runtime/import/db/zkas_db_set.rs

@@ -26,6 +26,7 @@ use crate::{
         import::{acl::acl_allow, util::wasm_mem_read},
         vm_runtime::{ContractSection, Env},
     },
+    validator::fees::{COMPILE_GAS_PER_ROW, MIN_GAS, STATE_GROWTH_GAS, WRITE_GAS_PER_BYTE},
     zk::{empty_witnesses, VerifyingKey, ZkCircuit},
     zkas::ZkBinary,
 };
@@ -41,6 +42,9 @@ pub(crate) fn zkas_db_set(mut ctx: FunctionEnvMut<Env>, ptr: WasmPtr<u8>, ptr_le
     let (env, mut store) = ctx.data_and_store_mut();
     let cid = env.contract_id;
 
+    // Subtract base gas before the ACL check.
+    env.subtract_gas(&mut store, MIN_GAS);
+
     // Enforce function ACL
     if let Err(e) = acl_allow(env, &[ContractSection::Deploy]) {
         error!(
@@ -95,52 +99,48 @@ pub(crate) fn zkas_db_set(mut ctx: FunctionEnvMut<Env>, ptr: WasmPtr<u8>, ptr_le
         }
     };
 
-    // Subtract used gas. We count 100 gas per opcode, witness, and literal.
-    // This is likely bad.
-    // TODO: This should be better-priced.
-    let gas_cost =
-        (zkbin.literals.len() + zkbin.witnesses.len() + zkbin.opcodes.len()) as u64 * 100;
-    env.subtract_gas(&mut store, gas_cost);
-
     // Because of `Runtime::Deploy`, we should be sure that the zkas db is index zero.
-    let db_handles = env.db_handles.borrow();
-    let db_handle = &db_handles[0];
-    // Redundant check
-    if db_handle.contract_id != cid {
-        error!(
-            target: "runtime::db::zkas_db_set",
-            "[WASM] [{cid}] zkas_db_set(): Internal error, zkas db at index 0 incorrect"
-        );
-        return darkfi_sdk::error::DB_SET_FAILED
-    }
+    let tree_handle = {
+        let db_handles = env.db_handles.borrow();
+        let db_handle = &db_handles[0];
+        // Redundant check
+        if db_handle.contract_id != cid {
+            error!(
+                target: "runtime::db::zkas_db_set",
+                "[WASM] [{cid}] zkas_db_set(): Internal error, zkas db at index 0 incorrect"
+            );
+            return darkfi_sdk::error::DB_SET_FAILED
+        }
+        db_handle.tree
+    };
 
     // Check if there is existing bincode and compare it. Return DB_SUCCESS if
-    // they're the same. The assumption should be that VerifyingKey was generated
-    // already so we can skip things after this guard.
-    match env
+    // they're the same.
+    let is_new_key = match env
         .blockchain
         .lock()
         .unwrap()
         .overlay
         .lock()
         .unwrap()
-        .get(&db_handle.tree, &serialize(&zkbin.namespace))
+        .get(&tree_handle, &serialize(&zkbin.namespace))
     {
-        Ok(v) => {
-            if let Some(bytes) = v {
-                // We allow a panic here because this db should never be corrupted in this way.
-                let (existing_zkbin, _): (Vec<u8>, Vec<u8>) =
-                    deserialize(&bytes).expect("deserialize tuple");
-
-                if existing_zkbin == zkbin_bytes {
-                    debug!(
-                        target: "runtime::db::zkas_db_set",
-                        "[WASM] [{cid}] zkas_db_set(): Existing zkas bincode is the same. Skipping."
-                    );
-                    return wasm::entrypoint::SUCCESS
-                }
+        Ok(Some(bytes)) => {
+            // We allow a panic here because this db should never be corrupted in this way.
+            let (existing_zkbin, _): (Vec<u8>, Vec<u8>) =
+                deserialize(&bytes).expect("deserialize tuple");
+
+            if existing_zkbin == zkbin_bytes {
+                debug!(
+                    target: "runtime::db::zkas_db_set",
+                    "[WASM] [{cid}] zkas_db_set(): Existing zkas bincode is the same. Skipping."
+                );
+                return wasm::entrypoint::SUCCESS
             }
+            // Existing key, will overwrite.
+            false
         }
+        Ok(None) => true,
         Err(e) => {
             error!(
                 target: "runtime::db::zkas_db_set",
@@ -150,6 +150,11 @@ pub(crate) fn zkas_db_set(mut ctx: FunctionEnvMut<Env>, ptr: WasmPtr<u8>, ptr_le
         }
     };
 
+    // Charge the per-row compile cost.
+    let compile_gas =
+        COMPILE_GAS_PER_ROW.saturating_mul(1u64.checked_shl(zkbin.k).unwrap_or(u64::MAX));
+    env.subtract_gas(&mut store, compile_gas);
+
     // We didn't find any existing bincode, so let's create a new VerifyingKey and write it all.
     info!(
         target: "runtime::db::zkas_db_set",
@@ -183,6 +188,15 @@ pub(crate) fn zkas_db_set(mut ctx: FunctionEnvMut<Env>, ptr: WasmPtr<u8>, ptr_le
     // Insert the key-value pair into the database.
     let key = serialize(&zkbin.namespace);
     let value = serialize(&(zkbin_bytes, vk_buf));
+
+    // Charge for bytes written. New keys also incur STATE_GROWTH_GAS.
+    let bytes_written = (key.len() + value.len()) as u64;
+    let mut storage_gas = bytes_written.saturating_mul(WRITE_GAS_PER_BYTE);
+    if is_new_key {
+        storage_gas = storage_gas.saturating_add(STATE_GROWTH_GAS);
+    }
+    env.subtract_gas(&mut store, storage_gas);
+
     if env
         .blockchain
         .lock()
@@ -190,7 +204,7 @@ pub(crate) fn zkas_db_set(mut ctx: FunctionEnvMut<Env>, ptr: WasmPtr<u8>, ptr_le
         .overlay
         .lock()
         .unwrap()
-        .insert(&db_handle.tree, &key, &value)
+        .insert(&tree_handle, &key, &value)
         .is_err()
     {
         error!(
@@ -199,10 +213,6 @@ pub(crate) fn zkas_db_set(mut ctx: FunctionEnvMut<Env>, ptr: WasmPtr<u8>, ptr_le
         );
         return darkfi_sdk::error::DB_SET_FAILED
     }
-    drop(db_handles);
-
-    // Subtract used gas. Here we count the bytes written into the db.
-    env.subtract_gas(&mut store, (key.len() + value.len()) as u64);
 
     wasm::entrypoint::SUCCESS
 }