/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2026 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::collections::HashMap; use lazy_static::lazy_static; use rand::rngs::OsRng; use darkfi::{ tx::{ContractCallLeaf, Transaction, TransactionBuilder}, zk::{proof::ProvingKey, vm::ZkCircuit, vm_heap::empty_witnesses}, zkas::ZkBinary, Error, Result, }; use darkfi_deployooor_contract::{ client::{deploy_v1::DeployCallBuilder, lock_v1::LockCallBuilder}, model::LockParamsV1, DeployFunction, }; use darkfi_money_contract::MONEY_CONTRACT_ZKAS_FEE_NS_V1; use darkfi_sdk::{ crypto::{ ContractId, Keypair, PublicKey, SecretKey, DEPLOYOOOR_CONTRACT_ID, MONEY_CONTRACT_ID, }, deploy::DeployParamsV1, tx::TransactionHash, ContractCall, }; use darkfi_serial::{deserialize_async, serialize, serialize_async, AsyncEncodable}; use rusqlite::types::Value; use crate::{convert_named_params, error::WalletDbResult, rpc::ScanCache, Drk}; // Wallet SQL table constant names. These have to represent the `wallet.sql` // SQL schema. Table names are prefixed with the contract ID to avoid collisions. lazy_static! { pub static ref DEPLOY_AUTH_TABLE: String = format!("{}_deploy_auth", DEPLOYOOOR_CONTRACT_ID.to_string()); pub static ref DEPLOY_HISTORY_TABLE: String = format!("{}_deploy_history", DEPLOYOOOR_CONTRACT_ID.to_string()); } // DEPLOY_AUTH_TABLE pub const DEPLOY_AUTH_COL_CONTRACT_ID: &str = "contract_id"; pub const DEPLOY_AUTH_COL_SECRET_KEY: &str = "secret_key"; pub const DEPLOY_AUTH_COL_IS_LOCKED: &str = "is_locked"; pub const DEPLOY_AUTH_COL_LOCK_HEIGHT: &str = "lock_height"; // DEPLOY_HISTORY_TABLE pub const DEPLOY_HISTORY_COL_TX_HASH: &str = "tx_hash"; pub const DEPLOY_HISTORY_COL_CONTRACT: &str = "contract"; pub const DEPLOY_HISTORY_COL_TYPE: &str = "type"; pub const DEPLOY_HISTORY_COL_BLOCK_HEIGHT: &str = "block_height"; pub const DEPLOY_HISTORY_COL_WASM_BINCODE: &str = "wasm_bincode"; pub const DEPLOY_HISTORY_COL_DEPLOY_IX: &str = "deploy_ix"; impl Drk { /// Initialize wallet with tables for the Deployooor contract. pub fn initialize_deployooor(&self) -> WalletDbResult<()> { // Initialize Deployooor wallet schema let wallet_schema = include_str!("../deploy.sql"); self.wallet.exec_batch_sql(wallet_schema)?; Ok(()) } /// Generate a new deploy authority keypair and place it into the wallet pub async fn deploy_auth_keygen(&self, output: &mut Vec) -> WalletDbResult<()> { output.push(String::from("Generating a new keypair")); let secret_key = SecretKey::random(&mut OsRng); let contract_id = ContractId::derive_public(PublicKey::from_secret(secret_key)); let lock_height: Option = None; let query = format!( "INSERT INTO {} ({}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4);", *DEPLOY_AUTH_TABLE, DEPLOY_AUTH_COL_CONTRACT_ID, DEPLOY_AUTH_COL_SECRET_KEY, DEPLOY_AUTH_COL_IS_LOCKED, DEPLOY_AUTH_COL_LOCK_HEIGHT, ); self.wallet.exec_sql( &query, rusqlite::params![ serialize_async(&contract_id).await, serialize_async(&secret_key).await, 0, lock_height ], )?; output.push(String::from("Created new contract deploy authority")); output.push(format!("Contract ID: {contract_id}")); Ok(()) } /// Insert a deploy authority history record into the wallet. pub fn put_deploy_history_record( &self, tx_hash: &TransactionHash, contract: &ContractId, tx_type: &str, block_height: &u32, wasm_bincode: &Option>, deploy_ix: &Option>, ) -> WalletDbResult<()> { let query = format!( "INSERT INTO {} ({}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6);", *DEPLOY_HISTORY_TABLE, DEPLOY_HISTORY_COL_TX_HASH, DEPLOY_HISTORY_COL_CONTRACT, DEPLOY_HISTORY_COL_TYPE, DEPLOY_HISTORY_COL_BLOCK_HEIGHT, DEPLOY_HISTORY_COL_WASM_BINCODE, DEPLOY_HISTORY_COL_DEPLOY_IX, ); self.wallet.exec_sql( &query, rusqlite::params![ tx_hash.to_string(), serialize(contract), tx_type, block_height, serialize(wasm_bincode), serialize(deploy_ix), ], )?; Ok(()) } /// Reset all contract deploy authorities locked status in the wallet. pub fn reset_deploy_authorities(&self, output: &mut Vec) -> WalletDbResult<()> { output.push(String::from("Resetting deploy authorities locked status")); let query = format!( "UPDATE {} SET {} = 0, {} = NULL;", *DEPLOY_AUTH_TABLE, DEPLOY_AUTH_COL_IS_LOCKED, DEPLOY_AUTH_COL_LOCK_HEIGHT ); self.wallet.exec_sql(&query, &[])?; output.push(String::from("Successfully reset deploy authorities locked status")); Ok(()) } /// Remove deploy authorities locked status in the wallet that /// where locked after provided height. pub fn unlock_deploy_authorities_after( &self, height: &u32, output: &mut Vec, ) -> WalletDbResult<()> { output.push(format!("Resetting deploy authorities locked status after: {height}")); let query = format!( "UPDATE {} SET {} = 0, {} = NULL WHERE {} > ?1;", *DEPLOY_AUTH_TABLE, DEPLOY_AUTH_COL_IS_LOCKED, DEPLOY_AUTH_COL_LOCK_HEIGHT, DEPLOY_AUTH_COL_LOCK_HEIGHT ); self.wallet.exec_sql(&query, rusqlite::params![Some(*height)])?; output.push(String::from("Successfully reset deploy authorities locked status")); Ok(()) } /// Reset all contracts history records in the wallet. pub fn reset_deploy_history(&self, output: &mut Vec) -> WalletDbResult<()> { output.push(String::from("Resetting deployment history")); let query = format!("DELETE FROM {};", *DEPLOY_HISTORY_TABLE); self.wallet.exec_sql(&query, &[])?; output.push(String::from("Successfully deployment history")); Ok(()) } /// Remove the contracts history records in the wallet that were /// created after provided height. pub fn remove_deploy_history_after( &self, height: &u32, output: &mut Vec, ) -> WalletDbResult<()> { output.push(format!("Removing deployment history records after: {height}")); let query = format!( "DELETE FROM {} WHERE {} > ?1;", *DEPLOY_HISTORY_TABLE, DEPLOY_HISTORY_COL_BLOCK_HEIGHT ); self.wallet.exec_sql(&query, rusqlite::params![height])?; output.push(String::from("Successfully removed deployment history records")); Ok(()) } /// List contract deploy authorities from the wallet pub async fn list_deploy_auth( &self, ) -> Result)>> { let rows = match self.wallet.query_multiple(&DEPLOY_AUTH_TABLE, &[], &[]) { Ok(r) => r, Err(e) => { return Err(Error::DatabaseError(format!( "[list_deploy_auth] Deploy auth retrieval failed: {e}", ))) } }; let mut ret = Vec::with_capacity(rows.len()); for row in rows { let Value::Blob(ref contract_id_bytes) = row[0] else { return Err(Error::ParseFailed( "[list_deploy_auth] Failed to parse contract id bytes", )) }; let contract_id: ContractId = deserialize_async(contract_id_bytes).await?; let Value::Blob(ref secret_key_bytes) = row[1] else { return Err(Error::ParseFailed( "[list_deploy_auth] Failed to parse secret key bytes", )) }; let secret_key: SecretKey = deserialize_async(secret_key_bytes).await?; let Value::Integer(locked) = row[2] else { return Err(Error::ParseFailed("[list_deploy_auth] Failed to parse \"is_locked\"")) }; let lock_height = match row[3] { Value::Integer(lock_height) => { let Ok(lock_height) = u32::try_from(lock_height) else { return Err(Error::ParseFailed( "[list_deploy_auth] Lock height parsing failed", )) }; Some(lock_height) } Value::Null => None, _ => { return Err(Error::ParseFailed("[list_deploy_auth] Lock height parsing failed")) } }; ret.push((contract_id, secret_key, locked != 0, lock_height)) } Ok(ret) } /// Retrieve a deploy authority keypair and status for provided /// contract id. async fn get_deploy_auth(&self, contract_id: &ContractId) -> Result<(Keypair, bool)> { // Find the deploy authority keypair let row = match self.wallet.query_single( &DEPLOY_AUTH_TABLE, &[DEPLOY_AUTH_COL_SECRET_KEY, DEPLOY_AUTH_COL_IS_LOCKED], convert_named_params! {(DEPLOY_AUTH_COL_CONTRACT_ID, serialize_async(contract_id).await)}, ) { Ok(v) => v, Err(e) => { return Err(Error::DatabaseError(format!( "[get_deploy_auth] Failed to retrieve deploy authority keypair: {e}" ))) } }; let Value::Blob(ref secret_key_bytes) = row[0] else { return Err(Error::ParseFailed("[get_deploy_auth] Failed to parse secret key bytes")) }; let secret_key: SecretKey = deserialize_async(secret_key_bytes).await?; let keypair = Keypair::new(secret_key); let Value::Integer(locked) = row[1] else { return Err(Error::ParseFailed("[get_deploy_auth] Failed to parse \"is_locked\"")) }; Ok((keypair, locked != 0)) } /// Retrieve contract deploy authorities keys map from the wallet. pub async fn get_deploy_auths_keys_map(&self) -> Result> { let rows = match self.wallet.query_multiple( &DEPLOY_AUTH_TABLE, &[DEPLOY_AUTH_COL_SECRET_KEY], &[], ) { Ok(r) => r, Err(e) => { return Err(Error::DatabaseError(format!( "[get_deploy_auths_keys_map] Failed to retrieve deploy authorities secret keys: {e}", ))) } }; let mut ret = HashMap::new(); for row in rows { let Value::Blob(ref secret_key_bytes) = row[0] else { return Err(Error::ParseFailed( "[get_deploy_auths_keys_map] Failed to parse secret key bytes", )) }; let secret_key: SecretKey = deserialize_async(secret_key_bytes).await?; ret.insert(PublicKey::from_secret(secret_key).to_bytes(), secret_key); } Ok(ret) } /// Retrieve all deploy history records basic information, for /// provided contract id. pub async fn get_deploy_auth_history( &self, contract_id: &ContractId, ) -> Result> { let rows = match self.wallet.query_multiple( &DEPLOY_HISTORY_TABLE, &[DEPLOY_HISTORY_COL_TX_HASH, DEPLOY_HISTORY_COL_TYPE, DEPLOY_HISTORY_COL_BLOCK_HEIGHT], convert_named_params! {(DEPLOY_HISTORY_COL_CONTRACT, serialize_async(contract_id).await)}, ) { Ok(r) => r, Err(e) => { return Err(Error::DatabaseError(format!( "[get_deploy_auth_history] Failed to retrieve deploy authority history records: {e}", ))) } }; let mut ret = Vec::with_capacity(rows.len()); for row in rows { let Value::Text(ref tx_hash) = row[0] else { return Err(Error::ParseFailed( "[get_deploy_auth_history] Transaction hash parsing failed", )) }; let Value::Text(ref tx_type) = row[1] else { return Err(Error::ParseFailed("[get_deploy_auth_history] Type parsing failed")) }; let Value::Integer(block_height) = row[2] else { return Err(Error::ParseFailed( "[get_deploy_auth_history] Block height parsing failed", )) }; let Ok(block_height) = u32::try_from(block_height) else { return Err(Error::ParseFailed( "[get_deploy_auth_history] Block height parsing failed", )) }; ret.push((tx_hash.clone(), tx_type.clone(), block_height)); } Ok(ret) } /// Retrieve deploy history record WASM bincode and deployed /// instruction, for provided transaction hash. pub async fn get_deploy_history_record_data( &self, tx_hash: &str, ) -> Result<(Option>, Option>)> { let row = match self.wallet.query_single( &DEPLOY_HISTORY_TABLE, &[DEPLOY_HISTORY_COL_WASM_BINCODE, DEPLOY_HISTORY_COL_DEPLOY_IX], convert_named_params! {(DEPLOY_HISTORY_COL_TX_HASH, tx_hash)}, ) { Ok(v) => v, Err(e) => { return Err(Error::DatabaseError(format!( "[get_deploy_history_record] Failed to retrieve deploy history record: {e}" ))) } }; let Value::Blob(ref wasm_bincode_bytes) = row[0] else { return Err(Error::ParseFailed( "[get_deploy_history_record] Failed to parse wasm bincode bytes", )) }; let wasm_bincode: Option> = deserialize_async(wasm_bincode_bytes).await?; let Value::Blob(ref deploy_ix_bytes) = row[1] else { return Err(Error::ParseFailed( "[get_deploy_history_record] Failed to parse deploy ix bytes", )) }; let deploy_ix: Option> = deserialize_async(deploy_ix_bytes).await?; Ok((wasm_bincode, deploy_ix)) } /// Auxiliary function to apply `DeployFunction::DeployV1` call /// data to the wallet. /// Returns a flag indicating if the provided call refers to our /// own wallet. fn apply_deploy_deploy_data( &self, scan_cache: &ScanCache, params: &DeployParamsV1, tx_hash: &TransactionHash, block_height: &u32, ) -> Result { // Check if we have the deploy authority key let Some(_) = scan_cache.own_deploy_auths.get(¶ms.public_key.to_bytes()) else { return Ok(false) }; // Create a new history record containing the deployment data if let Err(e) = self.put_deploy_history_record( tx_hash, &ContractId::derive_public(params.public_key), "DEPLOYMENT", block_height, &Some(params.wasm_bincode.clone()), &Some(params.ix.clone()), ) { return Err(Error::DatabaseError(format!( "[apply_deploy_deploy_data] Inserting deploy history recod failed: {e}" ))) } Ok(true) } /// Auxiliary function to apply `DeployFunction::LockV1` call /// data to the wallet. /// Returns a flag indicating if the provided call refers to our /// own wallet. async fn apply_deploy_lock_data( &self, scan_cache: &ScanCache, public_key: &PublicKey, tx_hash: &TransactionHash, lock_height: &u32, ) -> Result { // Check if we have the deploy authority key let Some(secret_key) = scan_cache.own_deploy_auths.get(&public_key.to_bytes()) else { return Ok(false) }; // Lock contract let secret_key = serialize_async(secret_key).await; let query = format!( "UPDATE {} SET {} = 1, {} = ?1 WHERE {} = ?2;", *DEPLOY_AUTH_TABLE, DEPLOY_AUTH_COL_IS_LOCKED, DEPLOY_AUTH_COL_LOCK_HEIGHT, DEPLOY_AUTH_COL_SECRET_KEY ); if let Err(e) = self.wallet.exec_sql(&query, rusqlite::params![Some(*lock_height), secret_key]) { return Err(Error::DatabaseError(format!( "[apply_deploy_lock_data] Lock deploy authority failed: {e}" ))) } // Create a new history record for the lock transaction if let Err(e) = self.put_deploy_history_record( tx_hash, &ContractId::derive_public(*public_key), "LOCK", lock_height, &None, &None, ) { return Err(Error::DatabaseError(format!( "[apply_deploy_lock_data] Inserting deploy history recod failed: {e}" ))) } Ok(true) } /// Append data related to DeployoOor contract transactions into /// the wallet database and update the provided scan cache. /// Returns a flag indicating if provided data refer to our own /// wallet. pub async fn apply_tx_deploy_data( &self, scan_cache: &mut ScanCache, data: &[u8], tx_hash: &TransactionHash, block_height: &u32, ) -> Result { // Run through the transaction call data and see what we got: match DeployFunction::try_from(data[0])? { DeployFunction::DeployV1 => { scan_cache.log(String::from("[apply_tx_deploy_data] Found Deploy::DeployV1 call")); let params: DeployParamsV1 = deserialize_async(&data[1..]).await?; self.apply_deploy_deploy_data(scan_cache, ¶ms, tx_hash, block_height) } DeployFunction::LockV1 => { scan_cache.log(String::from("[apply_tx_deploy_data] Found Deploy::LockV1 call")); let params: LockParamsV1 = deserialize_async(&data[1..]).await?; self.apply_deploy_lock_data(scan_cache, ¶ms.public_key, tx_hash, block_height) .await } } } /// Create a feeless contract deployment transaction. pub async fn deploy_contract( &self, deploy_auth: &ContractId, wasm_bincode: Vec, deploy_ix: Vec, ) -> Result { // Fetch the keypair and its status let (deploy_keypair, is_locked) = self.get_deploy_auth(deploy_auth).await?; // Check lock status if is_locked { return Err(Error::Custom("[deploy_contract] Contract is locked".to_string())) } // Now we need to do a lookup for the zkas proof bincodes, and create // the circuit objects and proving keys so we can build the transaction. // We also do this through the RPC. let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?; let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1) else { return Err(Error::Custom("[deploy_contract] Fee circuit not found".to_string())) }; let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?; let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin); // Creating Fee circuit proving keys let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit); // Create the contract call let deploy_call = DeployCallBuilder { deploy_keypair, wasm_bincode, deploy_ix }; let deploy_debris = deploy_call.build()?; // Encode the call let mut data = vec![DeployFunction::DeployV1 as u8]; deploy_debris.params.encode_async(&mut data).await?; let call = ContractCall { contract_id: *DEPLOYOOOR_CONTRACT_ID, data }; // Create the TransactionBuilder containing above call let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs: vec![] }, vec![])?; // We first have to execute the fee-less tx to gather its used gas, and then we feed // it into the fee-creating function. let mut tx = tx_builder.build()?; let sigs = tx.create_sigs(&[deploy_keypair.secret])?; tx.signatures.push(sigs); let tree = self.get_money_tree().await?; let (fee_call, fee_proofs, fee_secrets) = self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?; // Append the fee call to the transaction tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?; // Now build the actual transaction and sign it with all necessary keys. let mut tx = tx_builder.build()?; let sigs = tx.create_sigs(&[deploy_keypair.secret])?; tx.signatures.push(sigs); let sigs = tx.create_sigs(&fee_secrets)?; tx.signatures.push(sigs); Ok(tx) } /// Create a feeless contract redeployment lock transaction. pub async fn lock_contract(&self, deploy_auth: &ContractId) -> Result { // Fetch the keypair and its status let (deploy_keypair, is_locked) = self.get_deploy_auth(deploy_auth).await?; // Check lock status if is_locked { return Err(Error::Custom("[lock_contract] Contract is already locked".to_string())) } // Now we need to do a lookup for the zkas proof bincodes, and create // the circuit objects and proving keys so we can build the transaction. // We also do this through the RPC. let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?; let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1) else { return Err(Error::Custom("[lock_contract] Fee circuit not found".to_string())) }; let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?; let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin); // Creating Fee circuit proving keys let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit); // Create the contract call let lock_call = LockCallBuilder { deploy_keypair }; let lock_debris = lock_call.build()?; // Encode the call let mut data = vec![DeployFunction::LockV1 as u8]; lock_debris.params.encode_async(&mut data).await?; let call = ContractCall { contract_id: *DEPLOYOOOR_CONTRACT_ID, data }; // Create the TransactionBuilder containing above call let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs: vec![] }, vec![])?; // We first have to execute the fee-less tx to gather its used gas, and then we feed // it into the fee-creating function. let mut tx = tx_builder.build()?; let sigs = tx.create_sigs(&[deploy_keypair.secret])?; tx.signatures.push(sigs); let tree = self.get_money_tree().await?; let (fee_call, fee_proofs, fee_secrets) = self.append_fee_call(&tx, &tree, &fee_pk, &fee_zkbin, None).await?; // Append the fee call to the transaction tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?; // Now build the actual transaction and sign it with all necessary keys. let mut tx = tx_builder.build()?; let sigs = tx.create_sigs(&[deploy_keypair.secret])?; tx.signatures.push(sigs); let sigs = tx.create_sigs(&fee_secrets)?; tx.signatures.push(sigs); Ok(tx) } }