/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2024 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 .
*/
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use darkfi_sdk::{
crypto::{
pasta_prelude::PrimeField,
smt::{PoseidonFp, SparseMerkleTree, StorageAdapter, SMT_FP_DEPTH},
},
error::{ContractError, ContractResult},
pasta::pallas,
};
use log::{debug, error};
use num_bigint::BigUint;
use rusqlite::{
types::{ToSql, Value},
Connection,
};
use crate::error::{WalletDbError, WalletDbResult};
pub type WalletPtr = Arc;
/// Structure representing base wallet database operations.
pub struct WalletDb {
/// Connection to the SQLite database
pub conn: Mutex,
}
impl WalletDb {
/// Create a new wallet database handler. If `path` is `None`, create it in memory.
pub fn new(path: Option, password: Option<&str>) -> WalletDbResult {
let Ok(conn) = (match path.clone() {
Some(p) => Connection::open(p),
None => Connection::open_in_memory(),
}) else {
return Err(WalletDbError::ConnectionFailed);
};
if let Some(password) = password {
if let Err(e) = conn.pragma_update(None, "key", password) {
error!(target: "walletdb::new", "[WalletDb] Pragma update failed: {e}");
return Err(WalletDbError::PragmaUpdateError);
};
}
if let Err(e) = conn.pragma_update(None, "foreign_keys", "ON") {
error!(target: "walletdb::new", "[WalletDb] Pragma update failed: {e}");
return Err(WalletDbError::PragmaUpdateError);
};
debug!(target: "walletdb::new", "[WalletDb] Opened Sqlite connection at \"{path:?}\"");
Ok(Arc::new(Self { conn: Mutex::new(conn) }))
}
/// This function executes a given SQL query that contains multiple SQL statements,
/// that don't contain any parameters.
pub fn exec_batch_sql(&self, query: &str) -> WalletDbResult<()> {
debug!(target: "walletdb::exec_batch_sql", "[WalletDb] Executing batch SQL query:\n{query}");
let Ok(conn) = self.conn.lock() else { return Err(WalletDbError::FailedToAquireLock) };
if let Err(e) = conn.execute_batch(query) {
error!(target: "walletdb::exec_batch_sql", "[WalletDb] Query failed: {e}");
return Err(WalletDbError::QueryExecutionFailed)
};
Ok(())
}
/// This function executes a given SQL query, but isn't able to return anything.
/// Therefore it's best to use it for initializing a table or similar things.
pub fn exec_sql(&self, query: &str, params: &[&dyn ToSql]) -> WalletDbResult<()> {
debug!(target: "walletdb::exec_sql", "[WalletDb] Executing SQL query:\n{query}");
let Ok(conn) = self.conn.lock() else { return Err(WalletDbError::FailedToAquireLock) };
// If no params are provided, execute directly
if params.is_empty() {
if let Err(e) = conn.execute(query, ()) {
error!(target: "walletdb::exec_sql", "[WalletDb] Query failed: {e}");
return Err(WalletDbError::QueryExecutionFailed)
};
return Ok(())
}
// First we prepare the query
let Ok(mut stmt) = conn.prepare(query) else {
return Err(WalletDbError::QueryPreparationFailed)
};
// Execute the query using provided params
if let Err(e) = stmt.execute(params) {
error!(target: "walletdb::exec_sql", "[WalletDb] Query failed: {e}");
return Err(WalletDbError::QueryExecutionFailed)
};
// Finalize query and drop connection lock
if let Err(e) = stmt.finalize() {
error!(target: "walletdb::exec_sql", "[WalletDb] Query finalization failed: {e}");
return Err(WalletDbError::QueryFinalizationFailed)
};
drop(conn);
Ok(())
}
/// Generate a `SELECT` query for provided table from selected column names and
/// provided `WHERE` clauses. Named parameters are supported in the `WHERE` clauses,
/// assuming they follow the normal formatting ":{column_name}".
fn generate_select_query(
&self,
table: &str,
col_names: &[&str],
params: &[(&str, &dyn ToSql)],
) -> String {
let mut query = if col_names.is_empty() {
format!("SELECT * FROM {}", table)
} else {
format!("SELECT {} FROM {}", col_names.join(", "), table)
};
if params.is_empty() {
return query
}
let mut where_str = Vec::with_capacity(params.len());
for (k, _) in params {
let col = &k[1..];
where_str.push(format!("{col} = {k}"));
}
query.push_str(&format!(" WHERE {}", where_str.join(" AND ")));
query
}
/// Query provided table from selected column names and provided `WHERE` clauses,
/// for a single row.
pub fn query_single(
&self,
table: &str,
col_names: &[&str],
params: &[(&str, &dyn ToSql)],
) -> WalletDbResult> {
// Generate `SELECT` query
let query = self.generate_select_query(table, col_names, params);
debug!(target: "walletdb::query_single", "[WalletDb] Executing SQL query:\n{query}");
// First we prepare the query
let Ok(conn) = self.conn.lock() else { return Err(WalletDbError::FailedToAquireLock) };
let Ok(mut stmt) = conn.prepare(&query) else {
return Err(WalletDbError::QueryPreparationFailed)
};
// Execute the query using provided params
let Ok(mut rows) = stmt.query(params) else {
return Err(WalletDbError::QueryExecutionFailed)
};
// Check if row exists
let Ok(next) = rows.next() else { return Err(WalletDbError::QueryExecutionFailed) };
let row = match next {
Some(row_result) => row_result,
None => return Err(WalletDbError::RowNotFound),
};
// Grab returned values
let mut result = vec![];
if col_names.is_empty() {
let mut idx = 0;
loop {
let Ok(value) = row.get(idx) else { break };
result.push(value);
idx += 1;
}
} else {
for col in col_names {
let Ok(value) = row.get(*col) else {
return Err(WalletDbError::ParseColumnValueError)
};
result.push(value);
}
}
Ok(result)
}
/// Query provided table from selected column names and provided `WHERE` clauses,
/// for multiple rows.
pub fn query_multiple(
&self,
table: &str,
col_names: &[&str],
params: &[(&str, &dyn ToSql)],
) -> WalletDbResult>> {
// Generate `SELECT` query
let query = self.generate_select_query(table, col_names, params);
debug!(target: "walletdb::multiple", "[WalletDb] Executing SQL query:\n{query}");
// First we prepare the query
let Ok(conn) = self.conn.lock() else { return Err(WalletDbError::FailedToAquireLock) };
let Ok(mut stmt) = conn.prepare(&query) else {
return Err(WalletDbError::QueryPreparationFailed)
};
// Execute the query using provided converted params
let Ok(mut rows) = stmt.query(params) else {
return Err(WalletDbError::QueryExecutionFailed)
};
// Loop over returned rows and parse them
let mut result = vec![];
loop {
// Check if an error occured
let row = match rows.next() {
Ok(r) => r,
Err(_) => return Err(WalletDbError::QueryExecutionFailed),
};
// Check if no row was returned
let row = match row {
Some(r) => r,
None => break,
};
// Grab row returned values
let mut row_values = vec![];
if col_names.is_empty() {
let mut idx = 0;
loop {
let Ok(value) = row.get(idx) else { break };
row_values.push(value);
idx += 1;
}
} else {
for col in col_names {
let Ok(value) = row.get(*col) else {
return Err(WalletDbError::ParseColumnValueError)
};
row_values.push(value);
}
}
result.push(row_values);
}
Ok(result)
}
}
/// Custom implementation of rusqlite::named_params! to use `expr` instead of `literal` as `$param_name`,
/// and append the ":" named parameters prefix.
#[macro_export]
macro_rules! convert_named_params {
() => {
&[] as &[(&str, &dyn rusqlite::types::ToSql)]
};
($(($param_name:expr, $param_val:expr)),+ $(,)?) => {
&[$((format!(":{}", $param_name).as_str(), &$param_val as &dyn rusqlite::types::ToSql)),+] as &[(&str, &dyn rusqlite::types::ToSql)]
};
}
/// Wallet SMT definition
pub type WalletSmt<'a> = SparseMerkleTree<
'static,
SMT_FP_DEPTH,
{ SMT_FP_DEPTH + 1 },
pallas::Base,
PoseidonFp,
WalletStorage<'a>,
>;
/// An SMT adapter for wallet SQLite database storage.
pub struct WalletStorage<'a> {
wallet: &'a WalletPtr,
table: &'a str,
key_col: &'a str,
value_col: &'a str,
}
impl<'a> WalletStorage<'a> {
pub fn new(
wallet: &'a WalletPtr,
table: &'a str,
key_col: &'a str,
value_col: &'a str,
) -> Self {
Self { wallet, table, key_col, value_col }
}
}
impl<'a> StorageAdapter for WalletStorage<'a> {
type Value = pallas::Base;
fn put(&mut self, key: BigUint, value: pallas::Base) -> ContractResult {
let query = format!(
"INSERT OR REPLACE INTO {} ({}, {}) VALUES (?1, ?2);",
self.table, self.key_col, self.value_col
);
if let Err(e) =
self.wallet.exec_sql(&query, rusqlite::params![key.to_bytes_le(), value.to_repr()])
{
error!(target: "walletdb::StorageAdapter::put", "Inserting key {key:?}, value {value:?} into DB failed: {e:?}");
return Err(ContractError::SmtPutFailed)
}
Ok(())
}
fn get(&self, key: &BigUint) -> Option {
let row = match self.wallet.query_single(
self.table,
&[self.value_col],
convert_named_params! {(self.key_col, key.to_bytes_le())},
) {
Ok(r) => r,
Err(WalletDbError::RowNotFound) => return None,
Err(e) => {
error!(target: "walletdb::StorageAdapter::get", "Fetching key {key:?} from DB failed: {e:?}");
return None
}
};
let Value::Blob(ref value_bytes) = row[0] else {
error!(target: "walletdb::StorageAdapter::get", "Parsing key {key:?} value bytes");
return None
};
let mut repr = [0; 32];
repr.copy_from_slice(value_bytes);
pallas::Base::from_repr(repr).into()
}
fn del(&mut self, key: &BigUint) -> ContractResult {
let query = format!("DELETE FROM {} WHERE {} = ?1;", self.table, self.key_col);
if let Err(e) = self.wallet.exec_sql(&query, rusqlite::params![key.to_bytes_le()]) {
error!(target: "walletdb::StorageAdapter::del", "Removing key {key:?} from DB failed: {e:?}");
return Err(ContractError::SmtDelFailed)
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use darkfi::zk::halo2::Field;
use darkfi_sdk::{
crypto::smt::{gen_empty_nodes, util::FieldHasher, PoseidonFp, SparseMerkleTree},
pasta::pallas,
};
use rand::rngs::OsRng;
use rusqlite::types::Value;
use crate::walletdb::{WalletDb, WalletStorage};
#[test]
fn test_mem_wallet() {
let wallet = WalletDb::new(None, Some("foobar")).unwrap();
wallet.exec_sql("CREATE TABLE mista ( numba INTEGER );", &[]).unwrap();
wallet.exec_sql("INSERT INTO mista ( numba ) VALUES ( 42 );", &[]).unwrap();
let ret = wallet.query_single("mista", &["numba"], &[]).unwrap();
assert_eq!(ret.len(), 1);
let numba: i64 = if let Value::Integer(numba) = ret[0] { numba } else { -1 };
assert_eq!(numba, 42);
}
#[test]
fn test_query_single() {
let wallet = WalletDb::new(None, None).unwrap();
wallet
.exec_sql("CREATE TABLE mista ( why INTEGER, are TEXT, you INTEGER, gae BLOB );", &[])
.unwrap();
let why = 42;
let are = "are".to_string();
let you = 69;
let gae = vec![42u8; 32];
wallet
.exec_sql(
"INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);",
rusqlite::params![why, are, you, gae],
)
.unwrap();
let ret = wallet.query_single("mista", &["why", "are", "you", "gae"], &[]).unwrap();
assert_eq!(ret.len(), 4);
assert_eq!(ret[0], Value::Integer(why));
assert_eq!(ret[1], Value::Text(are.clone()));
assert_eq!(ret[2], Value::Integer(you));
assert_eq!(ret[3], Value::Blob(gae.clone()));
let ret = wallet
.query_single(
"mista",
&["gae"],
rusqlite::named_params! {":why": why, ":are": are, ":you": you},
)
.unwrap();
assert_eq!(ret.len(), 1);
assert_eq!(ret[0], Value::Blob(gae));
}
#[test]
fn test_query_multi() {
let wallet = WalletDb::new(None, None).unwrap();
wallet
.exec_sql("CREATE TABLE mista ( why INTEGER, are TEXT, you INTEGER, gae BLOB );", &[])
.unwrap();
let why = 42;
let are = "are".to_string();
let you = 69;
let gae = vec![42u8; 32];
wallet
.exec_sql(
"INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);",
rusqlite::params![why, are, you, gae],
)
.unwrap();
wallet
.exec_sql(
"INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);",
rusqlite::params![why, are, you, gae],
)
.unwrap();
let ret = wallet.query_multiple("mista", &[], &[]).unwrap();
assert_eq!(ret.len(), 2);
for row in ret {
assert_eq!(row.len(), 4);
assert_eq!(row[0], Value::Integer(why));
assert_eq!(row[1], Value::Text(are.clone()));
assert_eq!(row[2], Value::Integer(you));
assert_eq!(row[3], Value::Blob(gae.clone()));
}
let ret = wallet
.query_multiple(
"mista",
&["gae"],
convert_named_params! {("why", why), ("are", are), ("you", you)},
)
.unwrap();
assert_eq!(ret.len(), 2);
for row in ret {
assert_eq!(row.len(), 1);
assert_eq!(row[0], Value::Blob(gae.clone()));
}
}
#[test]
fn test_sqlite_smt() {
// Setup SQLite database
let table = &"smt";
let key_col = &"smt_key";
let value_col = &"smt_value";
let wallet = WalletDb::new(None, None).unwrap();
wallet.exec_sql(&format!("CREATE TABLE {table} ( {key_col} BLOB INTEGER PRIMARY KEY NOT NULL, {value_col} BLOB NOT NULL);"), &[]).unwrap();
// Setup SMT
const HEIGHT: usize = 3;
let hasher = PoseidonFp::new();
let empty_leaf = pallas::Base::ZERO;
let empty_nodes = gen_empty_nodes::<{ HEIGHT + 1 }, _, _>(&hasher, empty_leaf);
let store = WalletStorage::new(&wallet, table, key_col, value_col);
let mut smt = SparseMerkleTree::::new(
store,
hasher.clone(),
&empty_nodes,
);
let leaves = vec![
(pallas::Base::from(1), pallas::Base::random(&mut OsRng)),
(pallas::Base::from(2), pallas::Base::random(&mut OsRng)),
(pallas::Base::from(3), pallas::Base::random(&mut OsRng)),
];
smt.insert_batch(leaves.clone()).unwrap();
let hash1 = leaves[0].1;
let hash2 = leaves[1].1;
let hash3 = leaves[2].1;
let hash = |l, r| hasher.hash([l, r]);
let hash01 = hash(empty_nodes[3], hash1);
let hash23 = hash(hash2, hash3);
let hash0123 = hash(hash01, hash23);
let root = hash(hash0123, empty_nodes[1]);
assert_eq!(root, smt.root());
// Now try to construct a membership proof for leaf 3
let pos = leaves[2].0;
let path = smt.prove_membership(&pos);
assert_eq!(path.path[0], empty_nodes[1]);
assert_eq!(path.path[1], hash01);
assert_eq!(path.path[2], hash2);
assert_eq!(hash23, hash(path.path[2], hash3));
assert_eq!(hash0123, hash(path.path[1], hash(path.path[2], hash3)));
assert_eq!(root, hash(hash(path.path[1], hash(path.path[2], hash3)), path.path[0]));
assert!(path.verify(&root, &hash3, &pos));
}
}